diff --git a/docs/content/docs/cli.mdx b/docs/content/docs/cli.mdx
index a6f75c6d3..bbe1da3c6 100644
--- a/docs/content/docs/cli.mdx
+++ b/docs/content/docs/cli.mdx
@@ -119,7 +119,7 @@ Verifies and loads the installed precompiled runtime objects and vendored librar
statements analyzed | Executable statements in application code, excluding dependency implementation code. |
compile statically | Statements that compile to native instructions. |
+ in dynamic tier | Of those, the statements in functions that operate on values typed only at runtime, such as any values and untyped JavaScript. Shown when there are any. See Dynamic tier. |
blockers | Unsupported operations with counts, diagnostic codes, reasons, locations, and hints. |
in unreached code | Blockers in code the entry never reaches. A build never compiles that code, so these cannot fail it. |
differs from Node | Sites that compile but can behave differently from Node.js. See Node divergences. |
@@ -51,6 +52,16 @@ If a function's signature can't compile, the statements in its body count as not
+
+
+## Dynamic tier
+
+Code that operates on values whose types are known only at runtime, such as `any` values, the result of an untyped `JSON.parse`, or untyped JavaScript, compiles into dynamic-tier functions. They are native code too, but they check types and look up properties at runtime, so they are slower than code with static types. The report's `in dynamic tier` line counts their statements.
+
+`--explain-dynamic` lists those functions and the operation that makes each one dynamic. It also lists the units an unsupported construct would move to the dynamic tier: the binding, field, class, function, or module that has no static representation for the construct, with the diagnostic behind each one. These are the constructs the dynamic tier is planned to compile; today they remain blockers in TypeScript and deferred runtime errors in JavaScript. Add a name, such as `--explain-dynamic=Point`, to show only the matching units. With `--print=diagnostics`, the document gains a `tiers` object with the same information.
+
+`--tier=static-only` keeps every unsupported construct a blocker instead of moving it to the dynamic tier; code that already operates on runtime-typed values compiles as before. Today `auto`, the default, behaves the same way.
+
## Diagnostic codes
Coverage and `scriptc build` report the same diagnostic codes. Common ones include:
@@ -160,7 +171,7 @@ For editors, CI annotations, and other tools, `scriptc coverage --print=diagnost
stats |
- Coverage only, absent after a preflight failure: the report's statement counts. statementsDynamic is always 0 and remains for schema compatibility. |
+ Coverage only, absent after a preflight failure: the report's statement counts. statementsStatic counts every statement that compiles to native code; statementsDynamic is the part of those in dynamic-tier functions. |
category |
diff --git a/docs/content/docs/dependencies.mdx b/docs/content/docs/dependencies.mdx
index 5ecc144c6..3c6958841 100644
--- a/docs/content/docs/dependencies.mdx
+++ b/docs/content/docs/dependencies.mdx
@@ -46,6 +46,8 @@ Your callback and the commander implementation both compile to native code. No e
scriptc finds packages in `node_modules` with Node.js's resolution rules, including the export conditions in `package.json`, and compiles their dependencies too. TypeScript workspace packages compile as part of your program, including `.js` import specifiers that resolve to `.ts` sources.
+Type-only imports follow Node.js's type stripping. `import type` and `export type … from` statements are erased, so a package or module named only by them supplies types and isn't compiled or loaded; it can even be a package with no JavaScript, such as `type-fest`. A clause whose specifiers are all marked `type`, such as `import { type Options } from "./config.ts"`, still loads its module, as it does under Node.js.
+
## Which packages compile
scriptc compiles a package automatically when it:
@@ -53,7 +55,7 @@ scriptc compiles a package automatically when it:
- ships its own type declarations, and
- ships readable, unminified JavaScript.
-A package that doesn't meet these requirements, or that uses something scriptc can't compile, gets a package-specific diagnostic that explains why. The usual fixes are to switch to a version or alternative package that compiles, or to replace the call with code you write yourself.
+A package that doesn't meet these requirements, or that uses something scriptc can't compile, gets a package-specific diagnostic that explains why. The usual fixes are to switch to a version or alternative package that compiles, or to replace the call with code you write yourself. A package whose declarations come from a separate `@types/` package doesn't ship its own; its diagnostic names the package and the `--npm-static` option that compiles it anyway.
@@ -67,7 +69,7 @@ Some packages rely on separately installed declarations, such as an `@types/` pa
$ scriptc build app.ts --npm-static=three -o app >/dev/null
```
-Naming a package skips the declaration requirement above, but the package still has to compile. `--npm-static=auto` uses the default automatic selection. The option accepts a comma-separated list and can be repeated.
+Naming a package skips the declaration requirement above, but the package still has to compile. The option selects only the packages it names: a dependency of a named package that doesn't ship its own declarations is still refused. Add a trailing `...` to select a package together with every package it depends on, for example `--npm-static=fs-extra...`. `--npm-static=auto` uses the default automatic selection. The option accepts a comma-separated list and can be repeated.
## CommonJS packages
diff --git a/docs/content/docs/limitations.mdx b/docs/content/docs/limitations.mdx
index 544bd2b2d..6f21e2ce3 100644
--- a/docs/content/docs/limitations.mdx
+++ b/docs/content/docs/limitations.mdx
@@ -76,6 +76,8 @@ title: "Limitations"
../limitations/builtin-apis/legacy-url-api.mdx
+../limitations/builtin-apis/deprecated-modules.mdx
+
../limitations/builtin-apis/filesystem-flags.mdx
../limitations/builtin-apis/copy.mdx
diff --git a/docs/content/limitations/builtin-apis/deprecated-modules.mdx b/docs/content/limitations/builtin-apis/deprecated-modules.mdx
new file mode 100644
index 000000000..bb630b8c3
--- /dev/null
+++ b/docs/content/limitations/builtin-apis/deprecated-modules.mdx
@@ -0,0 +1 @@
+- **Deprecated `punycode` and `constants` modules:** `punycode.encode()`, `decode()`, `toASCII()`, `toUnicode()`, `ucs2.decode()`, `ucs2.encode()`, and `version` compile, with the `DEP0040` warning Node.js reports when the module loads. A `require("punycode")` made from inside `node_modules` doesn't warn; a `require()` used inside a larger expression, such as `require("punycode").toASCII(name)`, doesn't warn either. The `punycode.ucs2` object itself is unsupported as a value. The `constants` module is the frozen object Node.js builds from `os.constants` and `fs.constants`, plus the `crypto.constants` of Node.js 24.22.0.
diff --git a/docs/src/generated/node-v24-compatibility-meta.json b/docs/src/generated/node-v24-compatibility-meta.json
index a64169933..f03a39e85 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": "a610bdd1ae56e1c4cd38",
+ "artifactVersion": "4421a7b808191ac7f410",
"rowCount": 3880
}
diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json
index d4b6839e7..7a9e0e2a2 100644
--- a/docs/src/generated/node-v24-compatibility.json
+++ b/docs/src/generated/node-v24-compatibility.json
@@ -584,9 +584,9 @@
"apiEntries": 9,
"static": {
"supported": 0,
- "partial": 0,
+ "partial": 8,
"refused": 0,
- "not-implemented": 9,
+ "not-implemented": 1,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -58489,7 +58489,7 @@
"inherited": false
},
"static": {
- "status": "not-implemented",
+ "status": "partial",
"detail": "Derived from the exact API rows in this chapter.",
"verification": "derived"
}
@@ -58512,9 +58512,9 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "partial",
+ "detail": "Implemented for the call shapes accepted by the compiler lowering.",
+ "verification": "test-backed"
}
},
{
@@ -58535,9 +58535,9 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "partial",
+ "detail": "Implemented for the call shapes accepted by the compiler lowering.",
+ "verification": "test-backed"
}
},
{
@@ -58558,9 +58558,9 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "partial",
+ "detail": "Implemented for the call shapes accepted by the compiler lowering.",
+ "verification": "test-backed"
}
},
{
@@ -58581,9 +58581,9 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "partial",
+ "detail": "Implemented for the call shapes accepted by the compiler lowering.",
+ "verification": "test-backed"
}
},
{
@@ -58627,9 +58627,9 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "partial",
+ "detail": "Implemented for the call shapes accepted by the compiler lowering.",
+ "verification": "test-backed"
}
},
{
@@ -58650,9 +58650,9 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "partial",
+ "detail": "Implemented for the call shapes accepted by the compiler lowering.",
+ "verification": "test-backed"
}
},
{
@@ -58673,9 +58673,9 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "partial",
+ "detail": "Implemented for the call shapes accepted by the compiler lowering.",
+ "verification": "test-backed"
}
},
{
diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json
index f36c865cf..0fba7781e 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": 1155,
- "verify-gap": 2174,
+ "audit-partial": 1162,
+ "verify-gap": 2167,
"replace-refusal": 40,
"classify": 2,
"implement": 85
@@ -58559,12 +58559,16 @@
"priority": "low",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:punycode",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "surface-manifest:node-builtin.punycode.decode",
+ "tests": [
+ "tests/corpus/6122-punycode-conversions.ts",
+ "tests/corpus/6124-punycode-require-warnings.cjs",
+ "tests/corpus/6126-punycode-dependency/main.cjs"
+ ]
}
}
},
@@ -58584,12 +58588,16 @@
"priority": "low",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:punycode",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "surface-manifest:node-builtin.punycode.encode",
+ "tests": [
+ "tests/corpus/6122-punycode-conversions.ts",
+ "tests/corpus/6124-punycode-require-warnings.cjs",
+ "tests/corpus/6126-punycode-dependency/main.cjs"
+ ]
}
}
},
@@ -58609,12 +58617,16 @@
"priority": "low",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:punycode",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "surface-manifest:node-builtin.punycode.toASCII",
+ "tests": [
+ "tests/corpus/6122-punycode-conversions.ts",
+ "tests/corpus/6124-punycode-require-warnings.cjs",
+ "tests/corpus/6126-punycode-dependency/main.cjs"
+ ]
}
}
},
@@ -58634,12 +58646,16 @@
"priority": "low",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:punycode",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "surface-manifest:node-builtin.punycode.toUnicode",
+ "tests": [
+ "tests/corpus/6122-punycode-conversions.ts",
+ "tests/corpus/6124-punycode-require-warnings.cjs",
+ "tests/corpus/6126-punycode-dependency/main.cjs"
+ ]
}
}
},
@@ -58684,12 +58700,16 @@
"priority": "low",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:punycode",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "surface-manifest:node-builtin.punycode.ucs2.decode",
+ "tests": [
+ "tests/corpus/6122-punycode-conversions.ts",
+ "tests/corpus/6124-punycode-require-warnings.cjs",
+ "tests/corpus/6126-punycode-dependency/main.cjs"
+ ]
}
}
},
@@ -58709,12 +58729,16 @@
"priority": "low",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:punycode",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "surface-manifest:node-builtin.punycode.ucs2.encode",
+ "tests": [
+ "tests/corpus/6122-punycode-conversions.ts",
+ "tests/corpus/6124-punycode-require-warnings.cjs",
+ "tests/corpus/6126-punycode-dependency/main.cjs"
+ ]
}
}
},
@@ -58734,12 +58758,16 @@
"priority": "low",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:punycode",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "surface-manifest:node-builtin.punycode.version",
+ "tests": [
+ "tests/corpus/6122-punycode-conversions.ts",
+ "tests/corpus/6124-punycode-require-warnings.cjs",
+ "tests/corpus/6126-punycode-dependency/main.cjs"
+ ]
}
}
},
diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json
index 514282a0a..b755a95e8 100644
--- a/internal/compatibility/generated/node-v24-internal.json
+++ b/internal/compatibility/generated/node-v24-internal.json
@@ -611,9 +611,9 @@
"apiEntries": 9,
"static": {
"supported": 0,
- "partial": 0,
+ "partial": 8,
"refused": 0,
- "not-implemented": 9,
+ "not-implemented": 1,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -81945,8 +81945,13 @@
"inherited": false
},
"static": {
- "status": "not-implemented",
- "evidence": "derived:descendants"
+ "status": "partial",
+ "evidence": "derived:descendants",
+ "tests": [
+ "tests/corpus/6122-punycode-conversions.ts",
+ "tests/corpus/6124-punycode-require-warnings.cjs",
+ "tests/corpus/6126-punycode-dependency/main.cjs"
+ ]
},
"anchorSource": "chapter"
},
@@ -81968,8 +81973,13 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:punycode"
+ "status": "partial",
+ "evidence": "surface-manifest:node-builtin.punycode.decode",
+ "tests": [
+ "tests/corpus/6122-punycode-conversions.ts",
+ "tests/corpus/6124-punycode-require-warnings.cjs",
+ "tests/corpus/6126-punycode-dependency/main.cjs"
+ ]
},
"anchorSource": "exact"
},
@@ -81991,8 +82001,13 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:punycode"
+ "status": "partial",
+ "evidence": "surface-manifest:node-builtin.punycode.encode",
+ "tests": [
+ "tests/corpus/6122-punycode-conversions.ts",
+ "tests/corpus/6124-punycode-require-warnings.cjs",
+ "tests/corpus/6126-punycode-dependency/main.cjs"
+ ]
},
"anchorSource": "exact"
},
@@ -82014,8 +82029,13 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:punycode"
+ "status": "partial",
+ "evidence": "surface-manifest:node-builtin.punycode.toASCII",
+ "tests": [
+ "tests/corpus/6122-punycode-conversions.ts",
+ "tests/corpus/6124-punycode-require-warnings.cjs",
+ "tests/corpus/6126-punycode-dependency/main.cjs"
+ ]
},
"anchorSource": "exact"
},
@@ -82037,8 +82057,13 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:punycode"
+ "status": "partial",
+ "evidence": "surface-manifest:node-builtin.punycode.toUnicode",
+ "tests": [
+ "tests/corpus/6122-punycode-conversions.ts",
+ "tests/corpus/6124-punycode-require-warnings.cjs",
+ "tests/corpus/6126-punycode-dependency/main.cjs"
+ ]
},
"anchorSource": "exact"
},
@@ -82083,8 +82108,13 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:punycode"
+ "status": "partial",
+ "evidence": "surface-manifest:node-builtin.punycode.ucs2.decode",
+ "tests": [
+ "tests/corpus/6122-punycode-conversions.ts",
+ "tests/corpus/6124-punycode-require-warnings.cjs",
+ "tests/corpus/6126-punycode-dependency/main.cjs"
+ ]
},
"anchorSource": "exact"
},
@@ -82106,8 +82136,13 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:punycode"
+ "status": "partial",
+ "evidence": "surface-manifest:node-builtin.punycode.ucs2.encode",
+ "tests": [
+ "tests/corpus/6122-punycode-conversions.ts",
+ "tests/corpus/6124-punycode-require-warnings.cjs",
+ "tests/corpus/6126-punycode-dependency/main.cjs"
+ ]
},
"anchorSource": "exact"
},
@@ -82129,8 +82164,13 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:punycode"
+ "status": "partial",
+ "evidence": "surface-manifest:node-builtin.punycode.version",
+ "tests": [
+ "tests/corpus/6122-punycode-conversions.ts",
+ "tests/corpus/6124-punycode-require-warnings.cjs",
+ "tests/corpus/6126-punycode-dependency/main.cjs"
+ ]
},
"anchorSource": "chapter"
},
diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json
index 45a946925..0adeb9882 100644
--- a/internal/compatibility/static-support.json
+++ b/internal/compatibility/static-support.json
@@ -147,6 +147,7 @@
"node-builtin.process.rows": ["tests/corpus/2992-stream-rows.ts", "tests/harness/terminal-geometry.test.ts"],
"node-builtin.process.columns": ["tests/corpus/1448-stream-columns.ts", "tests/harness/terminal-geometry.test.ts"],
"node-builtin.process.exitCode": ["tests/corpus/5809-process-exit-and-rejections.cjs", "tests/corpus/2941-process-exit-code.ts", "tests/corpus/2942-process-exit-code-override.ts", "tests/corpus/2943-process-exit-code-invalid.ts", "tests/corpus/2944-process-exit-code-wrap.ts"],
+ "node-builtin.punycode": ["tests/corpus/6122-punycode-conversions.ts", "tests/corpus/6124-punycode-require-warnings.cjs", "tests/corpus/6126-punycode-dependency/main.cjs"],
"node-builtin.querystring": ["tests/corpus/957-builtins-namespace.ts", "tests/corpus/2917-builtin-callable-widths.ts"],
"node-builtin.readline": ["tests/corpus/1475-readline-closed-stdin.ts"],
"node-builtin.stream": ["tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1746-stream-for-await.ts", "tests/corpus/1814-stream-pipeline.cjs", "tests/corpus/2629-stream-consumers.ts", "tests/corpus/2861-readable-iterator-close.ts"],
diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts
index d72ca2dcd..cc5de935c 100644
--- a/packages/compiler/ambient/scriptc-node-fallback.d.ts
+++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts
@@ -3762,6 +3762,39 @@ declare module "node:querystring" {
export * from "querystring";
}
+/* node:punycode (deprecated, DEP0040) — punycode.js 2.1.0: decode and
+ * toUnicode throw its RangeErrors on malformed input; ucs2 converts
+ * between strings and UTF-16 code point arrays. */
+declare module "punycode" {
+ export function decode(string: string): string;
+ export function encode(string: string): string;
+ export function toUnicode(domain: string): string;
+ export function toASCII(domain: string): string;
+ export namespace ucs2 {
+ function decode(string: string): number[];
+ function encode(codePoints: readonly number[]): string;
+ }
+ export const version: string;
+}
+declare module "node:punycode" {
+ export * from "punycode";
+}
+
+/* node:constants (deprecated, DEP0008) — one frozen object: os.constants'
+ * dlopen, errno, priority and signals groups, fs.constants, and
+ * crypto.constants. A platform's missing constant reads as undefined. */
+declare module "constants" {
+ const constants: { readonly [name: string]: number } & {
+ readonly defaultCoreCipherList: string;
+ readonly defaultCipherList: string;
+ };
+ export = constants;
+}
+declare module "node:constants" {
+ import constants = require("constants");
+ export = constants;
+}
+
/* node:readline — the question/close slice: createInterface over exactly
* { input: process.stdin, output: process.stdout }, question(query, cb)
* (the query writes to stdout, the callback gets the next line's text),
diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts
index bb35e70c2..86e97a374 100644
--- a/packages/compiler/src/backend/llvm/dyn.ts
+++ b/packages/compiler/src/backend/llvm/dyn.ts
@@ -1217,12 +1217,13 @@ export class LlDyn {
`llvm emitter bug: JSON schema of unknown shape ${t.shapeId}`,
);
const struct = mangleRecordStruct(t.shapeId);
- const index = new Map(shape.fields.map((f, i) => [f.name, i + 1]));
+ const base = host.nullableUnions.fieldBase(t.shapeId);
+ const index = new Map(shape.fields.map((f, i) => [f.name, i + base]));
// The runtime names kept-source keys by this index (record-sources.ts).
const order = recordSourceFieldOrder(shape);
const fieldTy = `{ ptr, ${S}, ${S}, ptr }`;
const entries = order.map((fieldName) => {
- const field = shape.fields[index.get(fieldName)! - 1]!;
+ const field = shape.fields[index.get(fieldName)! - base]!;
const sub = this.jsonSchema(field.type);
const offset = `ptrtoint (ptr getelementptr (%${struct}, ptr null, i64 0, i32 ${index.get(fieldName)}) to ${S})`;
return `${fieldTy} { ptr ${host.cstr(fieldName)}, ${S} ${Buffer.byteLength(fieldName, "utf8")}, ${S} ${offset}, ptr @${sub} }`;
@@ -1240,7 +1241,7 @@ export class LlDyn {
const overflow = shape.indexValue
? {
elem: `@${this.jsonSchema(shape.indexValue)}`,
- offset: `ptrtoint (ptr getelementptr (%${struct}, ptr null, i64 0, i32 ${shape.fields.length + 1}) to ${S})`,
+ offset: `ptrtoint (ptr getelementptr (%${struct}, ptr null, i64 0, i32 ${shape.fields.length + base}) to ${S})`,
}
: { elem: "null", offset: "0" };
body = `${S} 3, ${S} ${entries.length}, ptr ${fields}, ptr ${overflow.elem}, ptr @${mangleRecordNew(t.shapeId)}, ptr null, ptr ${vAdapters(host, t).release}, ptr null, ${S} ${overflow.offset}`;
@@ -1547,18 +1548,19 @@ export class LlDyn {
B.line(`call void @scr_arr_outline(ptr ${cached})`);
B.line(`call void @scr_arr_outline(ptr ${checked})`);
}
+ const base = t.kind === "record" ? host.nullableUnions.fieldBase(t.shapeId) : 1;
const members =
t.kind === "record"
? [
...shape!.fields.map((field, index) => ({
- index: index + 1,
+ index: index + base,
type: llFieldType(field.type, host.inlineUnions.record(field.type)),
name: field.name,
})),
...(shape!.indexValue
? [
{
- index: shape!.fields.length + 1,
+ index: shape!.fields.length + base,
type: "ptr" as const,
name: "[key: string] overflow",
},
@@ -1819,7 +1821,8 @@ export class LlDyn {
`llvm emitter bug: dynCheck of unknown shape ${t.shapeId}`,
);
const struct = mangleRecordStruct(t.shapeId);
- const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + 1]));
+ const base = host.nullableUnions.fieldBase(t.shapeId);
+ const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + base]));
const releaseR = (): void => {
B.line(`call void ${releaseSym(host, t)}(ptr %r0)`);
};
@@ -1996,7 +1999,7 @@ export class LlDyn {
const ovfp = B.tmp();
const ovf = B.tmp();
B.line(
- `${ovfp} = getelementptr inbounds %${struct}, ptr %r0, i64 0, i32 ${shape.fields.length + 1}`,
+ `${ovfp} = getelementptr inbounds %${struct}, ptr %r0, i64 0, i32 ${shape.fields.length + base}`,
);
B.line(`${ovf} = load ptr, ptr ${ovfp} ; overflow map`);
if (shape.fields.length === 0) {
@@ -2578,7 +2581,8 @@ export class LlDyn {
if (!shape)
throw new InternalCompilerError(`llvm emitter bug: to-dyn of unknown shape ${t.shapeId}`);
const struct = mangleRecordStruct(t.shapeId);
- const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + 1]));
+ const base = host.nullableUnions.fieldBase(t.shapeId);
+ const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + base]));
const loadFieldOf = (fname: string, ft: IrType): string => {
const p = B.tmp();
B.line(
@@ -2702,7 +2706,7 @@ export class LlDyn {
const ovfp = B.tmp();
const ovf = B.tmp();
B.line(
- `${ovfp} = getelementptr inbounds %${struct}, ptr %v, i64 0, i32 ${shape.fields.length + 1}`,
+ `${ovfp} = getelementptr inbounds %${struct}, ptr %v, i64 0, i32 ${shape.fields.length + base}`,
);
B.line(`${ovf} = load ptr, ptr ${ovfp} ; overflow map`);
const ks = B.tmp();
diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts
index 138a05fd2..e7137886f 100644
--- a/packages/compiler/src/backend/llvm/emitter.ts
+++ b/packages/compiler/src/backend/llvm/emitter.ts
@@ -394,6 +394,7 @@ import {
elemAccess,
emitInlineRcHelpers,
emitNullableRetainWrappers,
+ emitShapeUnionHelpers,
FN_ATTRS,
inlineRcDecls,
type InlineRcMode,
@@ -1186,7 +1187,7 @@ export class LlEmitter {
expr: (expr) => !(expr.kind === "libCall" && expr.fn === "error.stack"),
}),
);
- this.nullableUnions = new NullableUnions(mod);
+ this.nullableUnions = new NullableUnions(mod, options.debugSources === undefined);
this.mockTimers = moduleUsesMockTimers(mod);
this.debug =
options.debugSources === undefined
@@ -1285,8 +1286,10 @@ export class LlEmitter {
// Optional array reads also cover nullable unions: emitLocalArrayRead
// then yields the (borrowed or owned) element pointer, not a stack box.
// Arrays of such a union are read the same way.
- this.optionalArrayReads = new OptionalArrayReads(this.fnByName, this.unionsById, (id) =>
- this.nullableUnions.has(id),
+ this.optionalArrayReads = new OptionalArrayReads(
+ this.fnByName,
+ this.unionsById,
+ (id) => this.nullableUnions.single(id) !== null,
);
this.unionSources = {
unions: this.boxedUnionsById,
@@ -1339,7 +1342,8 @@ export class LlEmitter {
(type) =>
this.plainReference(type) &&
type.kind !== "string" &&
- (type.kind !== "union" || this.nullableUnions.get(type.unionId)?.arm.kind !== "string"),
+ (type.kind !== "union" ||
+ this.nullableUnions.single(type.unionId)?.arm.kind !== "string"),
this.walkBorrowsByFunction.get(fn.name) ?? new Set(),
);
if (rebound.size === 0) continue;
@@ -1392,6 +1396,27 @@ export class LlEmitter {
const traced = computeTraced(mod);
this.tracedShapes = traced.shapes;
this.tracedUnions = traced.unions;
+ // A shape union's value is whichever record it holds, and the collector
+ // visits it as that record: when one arm carries a collector header,
+ // every arm must (nullable-unions.ts). Unions that share an arm pass a
+ // header on to each other's arms, so repeat until no union adds one.
+ const shapeUnions = this.nullableUnions.allShapeUnions();
+ for (let changed = true; changed;) {
+ changed = false;
+ for (const union of shapeUnions) {
+ if (
+ !this.tracedUnions.has(union.unionId) &&
+ !union.arms.some((arm) => this.tracedShapes.has(`record:${arm.shapeId}`))
+ )
+ continue;
+ for (const arm of union.arms) {
+ const key = `record:${arm.shapeId}`;
+ if (this.tracedShapes.has(key)) continue;
+ this.tracedShapes.add(key);
+ changed = true;
+ }
+ }
+ }
this.shapeHost = {
sizeType: this.sizeType,
cycleBufOffset: this.cycleBufOffset,
@@ -3733,6 +3758,7 @@ export class LlEmitter {
return [
...late,
...emitNullableRetainWrappers(this.shapeHost),
+ ...emitShapeUnionHelpers(this.shapeHost),
...emitInlineRcHelpers(this.shapeHost),
];
}
@@ -5900,7 +5926,11 @@ export class LlEmitter {
const nullable = this.nullableUnions.get(unionId);
if (nullable) {
// The owned payload IS the nullable union's value.
- if (tag !== nullable.refTag)
+ if (
+ nullable.repr === "single"
+ ? tag !== nullable.refTag
+ : !nullable.arms.some((arm) => arm.tag === tag)
+ )
throw new InternalCompilerError(
`llvm emitter bug: payload wrap into unit arm ${tag} of ${unionId}`,
);
@@ -6005,7 +6035,7 @@ export class LlEmitter {
const index = shape.fields.findIndex((entry) => entry.name === field);
if (index < 0)
throw new InternalCompilerError(`llvm emitter bug: missing union field ${field}`);
- layout = "record:";
+ layout = this.nullableUnions.fieldBase(arm.shapeId) === 2 ? "record:tagged;" : "record:";
for (let i = 0; i <= index; i++) {
const type = shape.fields[i]!.type;
layout += `${llFieldType(type, this.inlineUnions.record(type))};`;
@@ -6041,7 +6071,7 @@ export class LlEmitter {
);
const p = this.B.tmp();
this.B.line(
- `${p} = getelementptr inbounds %${mangleRecordStruct(shapeId)}, ptr ${objName}, i64 0, i32 ${idx + 1}`,
+ `${p} = getelementptr inbounds %${mangleRecordStruct(shapeId)}, ptr ${objName}, i64 0, i32 ${idx + this.nullableUnions.fieldBase(shapeId)}`,
);
const type = shape.fields[idx]!.type;
return { ptr: p, type, inline: this.inlineUnions.record(type) };
@@ -6055,7 +6085,7 @@ export class LlEmitter {
const p = this.B.tmp();
const v = this.B.tmp();
this.B.line(
- `${p} = getelementptr inbounds %${mangleRecordStruct(shapeId)}, ptr ${objName}, i64 0, i32 ${shape.fields.length + 1}`,
+ `${p} = getelementptr inbounds %${mangleRecordStruct(shapeId)}, ptr ${objName}, i64 0, i32 ${shape.fields.length + this.nullableUnions.fieldBase(shapeId)}`,
);
this.B.line(`${v} = load ptr, ptr ${p}`);
return v;
@@ -10238,7 +10268,12 @@ export class LlEmitter {
unionsById: this.unionsById,
recordsById: this.recordsById,
inlineUnions: this.inlineUnions,
- nullableArm: (t) => this.nullableUnions.of(t)?.arm ?? null,
+ nullableArm: (t) => {
+ const nullable = this.nullableUnions.of(t);
+ return nullable?.repr === "single" ? nullable.arm : null;
+ },
+ shapeUnion: (t) => this.nullableUnions.of(t)?.repr === "shapes",
+ fieldBase: (shapeId) => this.nullableUnions.fieldBase(shapeId),
classInfo: (className) => {
const meta = this.classMetaOf(className);
return {
@@ -10959,7 +10994,11 @@ export class LlEmitter {
const nullable = this.nullableUnions.get(e.unionId);
if (!nullable) return null;
if (isUnitType(e.value.type)) return e.value.kind === "unitLit" ? "unit" : null;
- return e.tag === nullable.refTag && isRefCounted(e.value.type) ? "ref" : null;
+ const refArm =
+ nullable.repr === "single"
+ ? e.tag === nullable.refTag
+ : nullable.arms.some((arm) => arm.tag === e.tag);
+ return refArm && isRefCounted(e.value.type) ? "ref" : null;
}
materializeSplitLocal(localId: string): void {
diff --git a/packages/compiler/src/backend/llvm/expr-callbacks.ts b/packages/compiler/src/backend/llvm/expr-callbacks.ts
index e5560f14f..d71df333a 100644
--- a/packages/compiler/src/backend/llvm/expr-callbacks.ts
+++ b/packages/compiler/src/backend/llvm/expr-callbacks.ts
@@ -182,6 +182,7 @@ export function genResultThunkFor(
sym = mangleGenResThunk(host.resolveThunks.size);
host.resolveThunks.set(key, sym);
const shape = host.recordsById.get(recT.shapeId);
+ const base = host.nullableUnions.fieldBase(recT.shapeId);
const valueT = shape?.fields.find((f) => f.name === "value")?.type;
if (!shape || !valueT)
throw new InternalCompilerError("llvm emitter bug: genResume record lacks its value field");
@@ -242,7 +243,7 @@ export function genResultThunkFor(
` %r = call ptr @${mangleRecordNew(recT.shapeId)}()`,
` %d = call zeroext i1 @scr_gen_done(ptr %g)`,
` %dz = zext i1 %d to i8`,
- ` %dp = getelementptr inbounds %${mangleRecordStruct(recT.shapeId)}, ptr %r, i64 0, i32 ${doneIdxD + 1}`,
+ ` %dp = getelementptr inbounds %${mangleRecordStruct(recT.shapeId)}, ptr %r, i64 0, i32 ${doneIdxD + base}`,
` store i8 %dz, ptr %dp`,
` %has = call zeroext i1 @scr_gen_out_has(ptr %g)`,
` br i1 %has, label %take, label %undefv`,
@@ -259,7 +260,7 @@ export function genResultThunkFor(
` br label %join`,
`join:`,
` %v = load ptr, ptr %vslot`,
- ` %vp = getelementptr inbounds %${mangleRecordStruct(recT.shapeId)}, ptr %r, i64 0, i32 ${valueIdxD + 1}`,
+ ` %vp = getelementptr inbounds %${mangleRecordStruct(recT.shapeId)}, ptr %r, i64 0, i32 ${valueIdxD + base}`,
` store ptr %v, ptr %vp`,
` ret ptr %r`,
`}`,
@@ -293,7 +294,7 @@ export function genResultThunkFor(
` %r = call ptr @${mangleRecordNew(recT.shapeId)}()`,
` %d = call zeroext i1 @scr_gen_done(ptr %g)`,
` %dz = zext i1 %d to i8`,
- ` %dp = getelementptr inbounds %${mangleRecordStruct(recT.shapeId)}, ptr %r, i64 0, i32 ${doneIdx + 1}`,
+ ` %dp = getelementptr inbounds %${mangleRecordStruct(recT.shapeId)}, ptr %r, i64 0, i32 ${doneIdx + base}`,
` store i8 %dz, ptr %dp`,
` br i1 %d, label %doneb, label %susp`,
];
@@ -410,7 +411,7 @@ export function genResultThunkFor(
` br label %join`,
`join:`,
` %v = load ptr, ptr %vslot`,
- ` %vp = getelementptr inbounds %${mangleRecordStruct(recT.shapeId)}, ptr %r, i64 0, i32 ${valueIdx + 1}`,
+ ` %vp = getelementptr inbounds %${mangleRecordStruct(recT.shapeId)}, ptr %r, i64 0, i32 ${valueIdx + base}`,
// An inline value field copies the box's bits and drops the box.
...(host.inlineUnions.record(valueT)
? [
@@ -1092,7 +1093,8 @@ export function settledStoreFor(
return { tag, arm };
};
const fieldIndex = (arm: IrType & { kind: "record" }, field: string): number =>
- host.recordsById.get(arm.shapeId)!.fields.findIndex((f) => f.name === field) + 1;
+ host.recordsById.get(arm.shapeId)!.fields.findIndex((f) => f.name === field) +
+ host.nullableUnions.fieldBase(arm.shapeId);
const fulfilled = armOf("value");
const rejected = armOf("reason");
const valueT = host.recordsById
diff --git a/packages/compiler/src/backend/llvm/expr-control.ts b/packages/compiler/src/backend/llvm/expr-control.ts
index 298ea0199..fd4bc8787 100644
--- a/packages/compiler/src/backend/llvm/expr-control.ts
+++ b/packages/compiler/src/backend/llvm/expr-control.ts
@@ -330,7 +330,7 @@ export function emitControlExpr(
);
B.line(`store ${ty} ${t}, ptr ${slot}`);
if (host.mayThrow.has(e.retag)) host.emitPendingCheck();
- } else if (host.nullableUnions.has(e.left.type.unionId)) {
+ } else if (host.nullableUnions.single(e.left.type.unionId)) {
// The owned nullable pointer IS the owned arm value.
B.line(`store ${ty} ${l.name}, ptr ${slot}`);
} else {
@@ -440,7 +440,7 @@ export function emitControlExpr(
host.emitBranchInto(slot, e.right);
B.br(lj);
B.startBlock(lv);
- if (typeEquals(e.type, e.left.type) || host.nullableUnions.has(def.id)) {
+ if (typeEquals(e.type, e.left.type) || host.nullableUnions.single(def.id)) {
// Pass-through, or a nullable pointer that IS its owned arm value.
B.line(`store ${ty} ${l.name}, ptr ${slot}`);
} else {
diff --git a/packages/compiler/src/backend/llvm/expr-records.ts b/packages/compiler/src/backend/llvm/expr-records.ts
index fc9df6daf..3318ae774 100644
--- a/packages/compiler/src/backend/llvm/expr-records.ts
+++ b/packages/compiler/src/backend/llvm/expr-records.ts
@@ -62,9 +62,17 @@ export function emitUnionRetype(
const from = host.nullableUnions.get(e.fromId);
const to = host.nullableUnions.get(e.unionId);
if (from && to) {
- // Equal encodings make the pointer itself the target value.
- if (from.refTag === to.refTag && from.unitTag === to.unitTag && from.nullTag === to.nullTag)
- return host.own({ name: value.name, type: e.type });
+ // Equal encodings make the pointer itself the target value. A shape
+ // union derives its record arms from the record, so only the unit
+ // encodings must agree.
+ const same =
+ from.repr === "single" && to.repr === "single"
+ ? from.refTag === to.refTag && from.unitTag === to.unitTag && from.nullTag === to.nullTag
+ : from.repr === "shapes" &&
+ to.repr === "shapes" &&
+ from.unitTag === to.unitTag &&
+ from.nullTag === to.nullTag;
+ if (same) return host.own({ name: value.name, type: e.type });
return host.own({ name: rebuildRetype(host, value.name, e), type: e.type });
}
if (from || to) return host.own({ name: rebuildRetype(host, value.name, e), type: e.type });
diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts
index 55fa363cd..76ef35358 100644
--- a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts
+++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts
@@ -500,16 +500,17 @@ export function streamTypedRefCommitAdapter(
` br i1 %missing, label %done, label %swap`,
`swap:`,
];
+ const base = host.nullableUnions.fieldBase(t.shapeId);
const members = [
...shape.fields.map((field, index) => ({
- index: index + 1,
+ index: index + base,
type: llFieldType(field.type, host.inlineUnions.record(field.type)),
name: field.name,
})),
...(shape.indexValue
? [
{
- index: shape.fields.length + 1,
+ index: shape.fields.length + base,
type: "ptr" as const,
name: "[key: string] overflow",
},
@@ -943,7 +944,7 @@ export function streamTypedRefMaterializeAdapter(
B.line(`${out} = call ptr @scr_dyn_new_arr()`);
const fields = [...shape.fields].sort((a, b) => Number(a.name) - Number(b.name));
for (const field of fields) {
- const index = shape.fields.indexOf(field) + 1;
+ const index = shape.fields.indexOf(field) + host.nullableUnions.fieldBase(t.shapeId);
const fieldPtr = B.tmp();
B.line(
`${fieldPtr} = getelementptr inbounds %${mangleRecordStruct(t.shapeId)}, ptr %p, i64 0, i32 ${index}`,
@@ -980,7 +981,7 @@ export function streamTypedRefMaterializeAdapter(
liveRecordOps(host, t, snapshot, fields);
}
for (const field of fields) {
- const index = shape.fields.indexOf(field) + 1;
+ const index = shape.fields.indexOf(field) + host.nullableUnions.fieldBase(t.shapeId);
const fieldPtr = B.tmp();
B.line(
`${fieldPtr} = getelementptr inbounds %${mangleRecordStruct(t.shapeId)}, ptr %p, i64 0, i32 ${index}`,
@@ -1202,7 +1203,7 @@ function liveRecordOps(
const names = `${snapshot}_names`;
const ops = `${snapshot}_rops`;
const loadField = (B: BlockBuilder, field: IrRecordShape["fields"][number]): string => {
- const index = shape.fields.indexOf(field) + 1;
+ const index = shape.fields.indexOf(field) + host.nullableUnions.fieldBase(t.shapeId);
const fieldPtr = B.tmp();
B.line(`${fieldPtr} = getelementptr inbounds %${struct}, ptr %p, i64 0, i32 ${index}`);
return loadStructField(
diff --git a/packages/compiler/src/backend/llvm/lib-datagram.ts b/packages/compiler/src/backend/llvm/lib-datagram.ts
index c97744f64..3fae845e2 100644
--- a/packages/compiler/src/backend/llvm/lib-datagram.ts
+++ b/packages/compiler/src/backend/llvm/lib-datagram.ts
@@ -29,7 +29,7 @@ function messageAdapter(host: LlvmEmitterContext, type: IrType): string {
const isString = field === "address" || field === "family";
if (isString) lines.push(` %${field}.owned = call ptr @scr_str_retain_v(ptr %${field})`);
lines.push(
- ` %${field}.slot = getelementptr inbounds %${mangleRecordStruct(type.shapeId)}, ptr %record, i64 0, i32 ${index + 1}`,
+ ` %${field}.slot = getelementptr inbounds %${mangleRecordStruct(type.shapeId)}, ptr %record, i64 0, i32 ${index + host.nullableUnions.fieldBase(type.shapeId)}`,
` store ${isString ? "ptr" : "double"} %${field}${isString ? ".owned" : ""}, ptr %${field}.slot`,
);
}
@@ -169,7 +169,7 @@ export function emitDatagramLibCall(host: LlvmEmitterContext, e: LibCallExpr): L
);
const slot = host.B.tmp();
host.B.line(
- `${slot} = getelementptr inbounds %${mangleRecordStruct(type.shapeId)}, ptr ${record}, i64 0, i32 ${index + 1}`,
+ `${slot} = getelementptr inbounds %${mangleRecordStruct(type.shapeId)}, ptr ${record}, i64 0, i32 ${index + host.nullableUnions.fieldBase(type.shapeId)}`,
);
host.B.line(`store ${ty} ${value}, ptr ${slot}`);
}
diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts
index fdfe723d2..68d943ea1 100644
--- a/packages/compiler/src/backend/llvm/lib-dispatch.ts
+++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts
@@ -774,6 +774,8 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
case "blob":
case "record":
case "disposable":
+ case "punycode":
+ case "constants":
return host.emitGenericLibCall(e);
case "test":
return emitTestLibCall(host, e);
diff --git a/packages/compiler/src/backend/llvm/lib-filesystem.ts b/packages/compiler/src/backend/llvm/lib-filesystem.ts
index 0e7d02a15..74278789c 100644
--- a/packages/compiler/src/backend/llvm/lib-filesystem.ts
+++ b/packages/compiler/src/backend/llvm/lib-filesystem.ts
@@ -912,11 +912,8 @@ export function emitPathUrlLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
scopeid.inline,
);
}
- const rc = vAdapters(host.shapeHost, t);
- const rowU = B.tmp();
- B.line(
- `${rowU} = call ptr @scr_union_new_ref(i32 ${tag}, ptr ${r}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host.shapeHost, t)})`,
- );
+ // The row (+1) moves into its union arm (a shape union is the row).
+ const rowU = host.unionNewOwned(infoT.unionId, tag, { name: r, type: t });
B.line(`store ptr ${rowU}, ptr ${rowSlot}`);
B.br(lRow);
};
diff --git a/packages/compiler/src/backend/llvm/lib-lookup.ts b/packages/compiler/src/backend/llvm/lib-lookup.ts
index 9fc23cb94..a6e76e4e0 100644
--- a/packages/compiler/src/backend/llvm/lib-lookup.ts
+++ b/packages/compiler/src/backend/llvm/lib-lookup.ts
@@ -65,7 +65,7 @@ export function emitLookupConnect(host: LlvmEmitterContext, e: LibCallExpr): LlV
" br i1 %more, label %body, label %finish",
"body:",
" %record = call ptr @scr_arr_get_ref(ptr %addresses, double %index)",
- ` %addressp = getelementptr inbounds %${struct}, ptr %record, i64 0, i32 ${field + 1}`,
+ ` %addressp = getelementptr inbounds %${struct}, ptr %record, i64 0, i32 ${field + host.nullableUnions.fieldBase(addresses.elem.shapeId)}`,
" %address = load ptr, ptr %addressp",
" %owned.address = call ptr @scr_str_retain_v(ptr %address)",
" call double @scr_arr_push_ref(ptr %ips, ptr %owned.address)",
diff --git a/packages/compiler/src/backend/llvm/lib-network.ts b/packages/compiler/src/backend/llvm/lib-network.ts
index 3ac9fe050..322479a85 100644
--- a/packages/compiler/src/backend/llvm/lib-network.ts
+++ b/packages/compiler/src/backend/llvm/lib-network.ts
@@ -168,7 +168,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr)
const i = shape.fields.findIndex((f) => f.name === name);
if (i < 0)
throw new InternalCompilerError(`llvm emitter bug: net.serverAddress record lacks ${name}`);
- return i + 1;
+ return i + host.nullableUnions.fieldBase(recT.shapeId);
};
const store = (name: string, ty: string, v: string): void => {
const p = B.tmp();
diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts
index 02bf37192..6b2f739a8 100644
--- a/packages/compiler/src/backend/llvm/lib-shared.ts
+++ b/packages/compiler/src/backend/llvm/lib-shared.ts
@@ -194,6 +194,7 @@ export const LIB_FN_SYMS: Record = {
"os.cpusModel": "scr_os_cpus_model",
"os.cpusField": "scr_os_cpus_field",
"os.constants": "scr_os_constants",
+ "constants.object": "scr_node_constants",
"os.release": "scr_os_release",
"os.userName": "scr_os_user_name",
"os.userShell": "scr_os_user_shell",
@@ -638,6 +639,13 @@ export const LIB_FN_SYMS: Record = {
"url.legacyFormat": "scr_url_legacy_format",
"url.legacyResolve": "scr_url_legacy_resolve",
"url.domainToASCII": "scr_url_domain_to_ascii",
+ "punycode.decode": "scr_punycode_decode",
+ "punycode.encode": "scr_punycode_encode",
+ "punycode.toASCII": "scr_punycode_to_ascii",
+ "punycode.toUnicode": "scr_punycode_to_unicode",
+ "punycode.ucs2Decode": "scr_punycode_ucs2_decode",
+ "punycode.ucs2Encode": "scr_str_from_code_point",
+ "punycode.warn": "scr_punycode_warn",
"url.domainToUnicode": "scr_url_domain_to_unicode",
"url.pathToFileURL": "scr_url_from_path",
"url.pathToFileURLPlatform": "scr_url_from_path_platform",
diff --git a/packages/compiler/src/backend/llvm/nullable-unions.test.ts b/packages/compiler/src/backend/llvm/nullable-unions.test.ts
new file mode 100644
index 000000000..46bd688e7
--- /dev/null
+++ b/packages/compiler/src/backend/llvm/nullable-unions.test.ts
@@ -0,0 +1,83 @@
+import { expect, test } from "vitest";
+import {
+ F64,
+ NULL_T,
+ UNDEFINED_T,
+ type IrModule,
+ type IrType,
+ type IrUnionDef,
+} from "../../ir/ir.js";
+import { NullableUnions } from "./nullable-unions.js";
+
+const loc = { file: "nullable-unions.ts", start: 0, end: 0 };
+const a: IrType = { kind: "record", shapeId: "a" };
+const b: IrType = { kind: "record", shapeId: "b" };
+const c: IrType = { kind: "record", shapeId: "c" };
+
+function module(unions: IrUnionDef[], extra: Partial = {}): IrModule {
+ return {
+ irVersion: 16,
+ sourceFile: loc.file,
+ entry: "main",
+ records: ["a", "b", "c"].map((id) => ({ id, fields: [{ name: id, type: F64 }] })),
+ unions,
+ functions: [],
+ ...extra,
+ };
+}
+
+test("unions of two or more records become shape unions", () => {
+ const unions = new NullableUnions(
+ module([
+ { id: "ab", arms: [a, b] },
+ { id: "abu", arms: [a, UNDEFINED_T, b] },
+ { id: "abnu", arms: [a, b, NULL_T, UNDEFINED_T] },
+ { id: "af", arms: [a, F64] },
+ { id: "au", arms: [a, UNDEFINED_T] },
+ ]),
+ );
+ expect(unions.shapes("ab")).toMatchObject({ unitTag: -1, nullTag: -1 });
+ expect(unions.shapes("abu")).toMatchObject({ unitTag: 1, nullTag: -1 });
+ expect(unions.shapes("abnu")).toMatchObject({ unitTag: 3, nullTag: 2 });
+ expect(unions.shapes("abnu")?.arms).toEqual([
+ { tag: 0, shapeId: "a" },
+ { tag: 1, shapeId: "b" },
+ ]);
+ // A scalar arm keeps the box; one record arm is a single-arm nullable.
+ expect(unions.get("af")).toBeNull();
+ expect(unions.single("au")).not.toBeNull();
+ expect(unions.shapes("au")).toBeNull();
+ // Tagged shapes number from 2 and move their fields after the word.
+ expect([unions.shapeNumber("a"), unions.shapeNumber("b"), unions.shapeNumber("c")]).toEqual([
+ 2, 3, 0,
+ ]);
+ expect([unions.fieldBase("a"), unions.fieldBase("c")]).toEqual([2, 1]);
+ expect(unions.boxedUnions(new Map([["af", { id: "af", arms: [a, F64] }]])).size).toBe(1);
+});
+
+test("records of runtime-inspected unions keep every union over them boxed", () => {
+ const set: IrType = { kind: "set", elem: { kind: "union", unionId: "ab" } };
+ const unions = new NullableUnions(
+ module(
+ [
+ { id: "ab", arms: [a, b] },
+ { id: "abu", arms: [a, b, UNDEFINED_T] },
+ { id: "bc", arms: [b, c] },
+ { id: "ac", arms: [a, c] },
+ ],
+ { globals: [{ id: "%g.s", name: "s", type: set, mutable: false }] },
+ ),
+ );
+ expect(unions.get("ab")).toBeNull();
+ expect(unions.get("abu")).toBeNull();
+ expect(unions.get("bc")).toBeNull();
+ expect(unions.get("ac")).toBeNull();
+ expect(unions.shapeNumber("c")).toBe(0);
+});
+
+test("library, lifetime-region and debug builds keep record unions boxed", () => {
+ const unions = [{ id: "ab", arms: [a, b] }];
+ expect(new NullableUnions(module(unions), false).get("ab")).toBeNull();
+ expect(new NullableUnions(module(unions, { lifetimeRegions: true })).get("ab")).toBeNull();
+ expect(new NullableUnions(module(unions)).shapes("ab")).not.toBeNull();
+});
diff --git a/packages/compiler/src/backend/llvm/nullable-unions.ts b/packages/compiler/src/backend/llvm/nullable-unions.ts
index b1032f9d4..8729e9649 100644
--- a/packages/compiler/src/backend/llvm/nullable-unions.ts
+++ b/packages/compiler/src/backend/llvm/nullable-unions.ts
@@ -17,6 +17,7 @@ import { everyModuleNode } from "../../ir/traverse.js";
* every container, field, capture, parameter, and return slot holds the
* plain pointer. Every other union keeps the tagged `%ScrUnion` box. */
export interface NullableUnion {
+ repr: "single";
unionId: string;
refTag: number;
/** The unit arm NULL encodes. */
@@ -24,7 +25,57 @@ export interface NullableUnion {
/** The `null` arm of a two-unit-arm union, encoded by NULLABLE_NULL;
* -1 when the union has one unit arm. */
nullTag: number;
- arm: NullableRefArm;
+ /** One of NullableRefArm. Typed as the plain IrType so the self-hosted
+ * frontend keeps IrType's structurally identical arms one record shape
+ * (a PointerUnion arm holding the narrowed intersection would split them). */
+ arm: IrType;
+}
+
+/** A union of two or more record arms and at most two unit arms (`A | B`,
+ * `A | B | undefined`, discriminated unions of object literals) whose
+ * VALUES are the record pointer itself, like a NullableUnion: NULL is the
+ * unit arm (`undefined` when both exist, NULLABLE_NULL is then `null`).
+ * Every record shape that is an arm of such a union is a TAGGED shape: its
+ * struct carries the shape's module-wide number in a hidden `i32` word
+ * right after the reference count (struct index 1), stored by every
+ * allocation, so the arm of a pointer is that word mapped through the
+ * union's arm table (the per-union `@sc_stag.` helper). Records share
+ * one retain; release and trace dispatch on the arm. */
+export interface ShapeUnion {
+ repr: "shapes";
+ unionId: string;
+ /** The unit arm NULL encodes; -1 when the union has no unit arm. */
+ unitTag: number;
+ /** The `null` arm of a two-unit-arm union, encoded by NULLABLE_NULL;
+ * -1 otherwise. */
+ nullTag: number;
+ /** The record arms in tag order. */
+ arms: readonly { tag: number; shapeId: string }[];
+}
+
+/** A union whose values are plain pointers (no `%ScrUnion` box). */
+export type PointerUnion = NullableUnion | ShapeUnion;
+
+/** The struct index of a tagged record shape's hidden shape-number word. */
+export const SHAPE_WORD_INDEX = 1;
+
+/** The LLVM type through which any tagged record's shape word is read
+ * (the reference count, then the word). */
+export function shapeWordHeader(sizeType: "i32" | "i64"): string {
+ return `{ ${sizeType}, i32 }`;
+}
+
+/** The per-union helper mapping a shape-union value to its tag. */
+export function shapeTagFn(unionId: string): string {
+ return `@sc_stag.${unionId.replace(/[^A-Za-z0-9_.]/g, "_")}`;
+}
+
+/** The per-union release and trace dispatchers of a shape union. */
+export function shapeReleaseFn(unionId: string): string {
+ return `@sc_srelease.${unionId.replace(/[^A-Za-z0-9_.]/g, "_")}`;
+}
+export function shapeTraceFn(unionId: string): string {
+ return `@sc_strace.${unionId.replace(/[^A-Za-z0-9_.]/g, "_")}`;
}
/** The module-scope immortal sentinel standing for `null` in a
@@ -48,44 +99,98 @@ export type NullableRefArm =
| (IrType & { kind: "array" | "map" | "set" });
/** The arm is a JS object: always truthy, compared by identity. */
-export function isObjectArm(nullable: NullableUnion): boolean {
- return nullable.arm.kind !== "string";
+export function isObjectArm(nullable: PointerUnion): boolean {
+ return nullable.repr === "shapes" || nullable.arm.kind !== "string";
}
/** The representation decision for every union of one module. It is a
* pure function of the module, so serialized-IR and in-memory emission
* agree. */
export class NullableUnions {
- private readonly byId = new Map();
+ private readonly byId = new Map();
+ /** Tagged record shapes (arms of a shape union) by their module-wide
+ * shape number (2 and up: 0 and 1 are the unit encodings' pseudo
+ * numbers, never stored). */
+ private readonly shapeNumbers = new Map();
/** NULL-skipping retain wrappers requested so far: symbol → inner retain
* call target. The emitter defines each once. */
readonly retainWrappers = new Map();
/** Some nullable union encodes `null` with NULLABLE_NULL. */
readonly usesNullSentinel: boolean = false;
- constructor(mod: IrModule) {
+ /** `shapes`: allow shape unions (debug builds keep boxes, whose
+ * debug-info layouts describe every arm). */
+ constructor(mod: IrModule, shapes = true) {
const emitted = new Set();
for (const cls of mod.classes ?? []) if (!cls.runtime) emitted.add(cls.name);
const records = new Set();
for (const shape of mod.records ?? []) records.add(shape.id);
const boxed = boxedUnionIds(mod);
+ // Shape unions stay out of library builds (their host ABI marshals
+ // boxes) and of programs with lifetime regions (whose walks report a
+ // reference slot by its type's release entry point).
+ const allowShapes = shapes && mod.lib === undefined && mod.lifetimeRegions !== true;
+ // A record that is an arm of a boxed union never becomes a shape
+ // union's arm: conversions between unions over the same records then
+ // keep one representation.
+ const boxedRecords = new Set();
+ for (const def of mod.unions ?? [])
+ if (boxed.has(def.id))
+ for (const arm of def.arms) if (arm.kind === "record") boxedRecords.add(arm.shapeId);
for (const def of mod.unions ?? []) {
if (boxed.has(def.id)) continue;
- const nullable = nullableShape(def, emitted, records);
+ const nullable =
+ nullableShape(def, emitted, records) ??
+ (allowShapes ? shapeUnion(def, records, boxedRecords) : null);
if (!nullable) continue;
this.byId.set(def.id, nullable);
if (nullable.nullTag >= 0) this.usesNullSentinel = true;
+ if (nullable.repr === "shapes")
+ for (const arm of nullable.arms)
+ if (!this.shapeNumbers.has(arm.shapeId))
+ this.shapeNumbers.set(arm.shapeId, this.shapeNumbers.size + 2);
}
}
- get(unionId: string): NullableUnion | null {
+ get(unionId: string): PointerUnion | null {
return this.byId.get(unionId) ?? null;
}
- of(type: IrType): NullableUnion | null {
+ of(type: IrType): PointerUnion | null {
return type.kind === "union" ? this.get(type.unionId) : null;
}
+ /** The single-reference-arm form only (shape unions answer null). */
+ single(unionId: string): NullableUnion | null {
+ const nullable = this.byId.get(unionId);
+ return nullable?.repr === "single" ? nullable : null;
+ }
+
+ /** The shape-union form only. */
+ shapes(unionId: string): ShapeUnion | null {
+ const nullable = this.byId.get(unionId);
+ return nullable?.repr === "shapes" ? nullable : null;
+ }
+
+ /** Every shape union, in module order. */
+ allShapeUnions(): ShapeUnion[] {
+ const out: ShapeUnion[] = [];
+ for (const nullable of this.byId.values()) if (nullable.repr === "shapes") out.push(nullable);
+ return out;
+ }
+
+ /** The module-wide number a tagged record shape stores in its shape
+ * word, or 0 for shapes that carry none. */
+ shapeNumber(shapeId: string): number {
+ return this.shapeNumbers.get(shapeId) ?? 0;
+ }
+
+ /** The struct index of a record shape's first declared field: 2 for
+ * tagged shapes (after the shape word), 1 otherwise. */
+ fieldBase(shapeId: string): number {
+ return this.shapeNumbers.has(shapeId) ? 2 : 1;
+ }
+
has(unionId: string): boolean {
return this.byId.has(unionId);
}
@@ -94,10 +199,11 @@ export class NullableUnions {
return this.byId.size;
}
- /** The reference arm of every nullable union, by union id. */
+ /** The reference arm of every single-arm nullable union, by union id. */
armTypes(): Map {
const result = new Map();
- for (const [id, nullable] of this.byId) result.set(id, nullable.arm);
+ for (const [id, nullable] of this.byId)
+ if (nullable.repr === "single") result.set(id, nullable.arm);
return result;
}
@@ -129,7 +235,7 @@ function nullableShape(
if (unitTag < 0 || nullTag < 0) return null;
}
const arm = def.arms[refTag]!;
- const base = { unionId: def.id, refTag, unitTag, nullTag };
+ const base = { repr: "single" as const, unionId: def.id, refTag, unitTag, nullTag };
if (arm.kind === "object" && emittedClasses.has(arm.className))
return { ...base, arm: { kind: "object", className: arm.className } };
if (arm.kind === "record" && records.has(arm.shapeId))
@@ -140,6 +246,34 @@ function nullableShape(
return null;
}
+/** Two or more emitted record arms and at most two unit arms (both:
+ * undefined and null). */
+function shapeUnion(
+ def: IrUnionDef,
+ records: ReadonlySet,
+ boxedRecords: ReadonlySet,
+): ShapeUnion | null {
+ const units = def.arms.filter(isUnitType).length;
+ if (units > 2 || def.arms.length - units < 2) return null;
+ const arms: { tag: number; shapeId: string }[] = [];
+ for (let tag = 0; tag < def.arms.length; tag++) {
+ const arm = def.arms[tag]!;
+ if (isUnitType(arm)) continue;
+ if (arm.kind !== "record" || !records.has(arm.shapeId) || boxedRecords.has(arm.shapeId))
+ return null;
+ arms.push({ tag, shapeId: arm.shapeId });
+ }
+ let unitTag = -1,
+ nullTag = -1;
+ if (units === 1) unitTag = def.arms.findIndex(isUnitType);
+ else if (units === 2) {
+ unitTag = def.arms.findIndex((arm) => arm.kind === "undefinedT");
+ nullTag = def.arms.findIndex((arm) => arm.kind === "nullT");
+ if (unitTag < 0 || nullTag < 0) return null;
+ }
+ return { repr: "shapes", unionId: def.id, unitTag, nullTag, arms };
+}
+
/** Unions the runtime itself inspects as tagged boxes keep that
* representation: Map keys and Set elements hash and compare union keys by
* tag and payload inside the C map. */
diff --git a/packages/compiler/src/backend/llvm/publish.ts b/packages/compiler/src/backend/llvm/publish.ts
index c884339af..4f807424b 100644
--- a/packages/compiler/src/backend/llvm/publish.ts
+++ b/packages/compiler/src/backend/llvm/publish.ts
@@ -25,6 +25,7 @@ import type { IrClassDef, IrRecordShape, IrType, IrUnionDef, SrcLoc } from "../.
import { mapOf, STRING, typeKey } from "../../ir/ir.js";
import { mangleClassStruct, mangleRecordStruct } from "../mangle.js";
import { FN_ATTRS } from "./shapes.js";
+import { shapeTagFn } from "./nullable-unions.js";
import type { InlineUnionFields } from "./inline-unions.js";
import { LlvmUnsupportedError } from "./unsupported.js";
@@ -44,6 +45,10 @@ export interface PublishHost {
readonly inlineUnions: InlineUnionFields;
/** The nullable-pointer representation of a union, when it has one. */
nullableArm(t: IrType): IrType | null;
+ /** The union's values are tagged record pointers (nullable-unions.ts). */
+ shapeUnion(t: IrType): boolean;
+ /** The struct index of a record shape's first field. */
+ fieldBase(shapeId: string): number;
/** Class layout: hierarchy membership, preorder interval, field slots
* (with their storage types), runtime roots. */
classInfo(className: string): PublishClassInfo;
@@ -210,6 +215,19 @@ export class PublishWalkers {
if (def.arms.some((arm) => scalarRefusal(arm)))
throw new LlvmUnsupportedError(`publish of ${typeKey(t)}`, loc);
const live = arms.flatMap((a) => (a.fn === null ? [] : [{ tag: a.tag, fn: a.fn }]));
+ if (this.host.shapeUnion(t)) {
+ // The value is the record itself (or a unit encoding): no box to
+ // mark; the arm's walker marks and walks the record.
+ const lines = [head, `entry:`];
+ lines.push(
+ ` %tag = call i32 ${shapeTagFn(t.unionId)}(ptr %o)`,
+ ` switch i32 %tag, label %done [ ${live.map((a) => `i32 ${a.tag}, label %arm${a.tag}`).join(" ")} ]`,
+ );
+ for (const a of live)
+ lines.push(`arm${a.tag}:`, ` call void ${a.fn}(ptr %o, ptr %ctx)`, ` br label %done`);
+ lines.push(`done:`, ` ret void`, `}`);
+ return lines;
+ }
const lines = [
head,
`entry:`,
@@ -240,17 +258,18 @@ export class PublishWalkers {
const shape = this.host.recordsById.get(t.shapeId);
if (!shape)
throw new LlvmUnsupportedError(`publish of an unknown record ${t.shapeId}`, loc);
+ const base = this.host.fieldBase(shape.id);
const members = [
...shape.fields.flatMap((f, i) =>
this.host.inlineUnions.record(f.type)
? []
- : [{ name: f.name, index: i + 1, type: f.type }],
+ : [{ name: f.name, index: i + base, type: f.type }],
),
...(shape.indexValue
? [
{
name: "[key: string]",
- index: shape.fields.length + 1,
+ index: shape.fields.length + base,
type: mapOf(STRING, shape.indexValue),
},
]
diff --git a/packages/compiler/src/backend/llvm/regions.ts b/packages/compiler/src/backend/llvm/regions.ts
index fbd41a3f7..875f554eb 100644
--- a/packages/compiler/src/backend/llvm/regions.ts
+++ b/packages/compiler/src/backend/llvm/regions.ts
@@ -101,8 +101,8 @@ export class LifetimeRegions {
/** Can a value of type `t` hold (or own something that holds) a
* reference to a ProgramLifetime instance the walk can explain? */
private reaches(t: IrType): boolean {
- const arm = this.host.nullableUnions.of(t)?.arm;
- if (arm !== undefined) return this.reaches(arm);
+ const nullable = this.host.nullableUnions.of(t);
+ if (nullable?.repr === "single") return this.reaches(nullable.arm);
switch (t.kind) {
case "object":
return this.isLifetime(t.className) || this.reach.get(`object:${t.className}`) === true;
@@ -287,7 +287,8 @@ export class LifetimeRegions {
* ProgramLifetime instance). */
private reportLines(struct: string, f: Field, i: number): string[] {
if (!isRefCounted(f.type) || !this.reaches(f.type)) return [];
- const arm = this.host.nullableUnions.of(f.type)?.arm ?? f.type;
+ const nullable = this.host.nullableUnions.of(f.type);
+ const arm = nullable?.repr === "single" ? nullable.arm : f.type;
const load = [
` %f${i} = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${f.index}`,
` %v${i} = load ptr, ptr %f${i}`,
@@ -411,7 +412,7 @@ export class LifetimeRegions {
...this.reportBody(
`define internal void ${walk}(ptr %o, ptr %w) ${FN_ATTRS} { ; region walk record ${shape.id}`,
mangleRecordStruct(shape.id),
- rcMembers(shape, this.host.inlineUnions),
+ rcMembers(shape, this.host.inlineUnions, this.host.nullableUnions.fieldBase(shape.id)),
),
);
rows.push(`{ ptr, ptr } { ptr @${mangleRecordRelease(shape.id)}, ptr ${walk} }`);
diff --git a/packages/compiler/src/backend/llvm/shapes.ts b/packages/compiler/src/backend/llvm/shapes.ts
index 23565eff4..162b36425 100644
--- a/packages/compiler/src/backend/llvm/shapes.ts
+++ b/packages/compiler/src/backend/llvm/shapes.ts
@@ -31,7 +31,16 @@ import {
import { cycTypeDescriptor, emitObjectAlloc, emitObjectFree } from "./alloc.js";
import type { BlockBuilder } from "./blocks.js";
import { SCR_ARR_FIELD, llvmCommentText } from "./common.js";
-import type { NullableUnions } from "./nullable-unions.js";
+import {
+ NULLABLE_NULL,
+ SHAPE_WORD_INDEX,
+ shapeReleaseFn,
+ shapeTagFn,
+ shapeTraceFn,
+ shapeWordHeader,
+ type NullableUnions,
+ type ShapeUnion,
+} from "./nullable-unions.js";
import {
INLINE_UNION_FIELD,
loadInlineUnion,
@@ -129,6 +138,7 @@ export { computeTraced } from "../cycle-analysis.js";
* `_v`-shaped. */
export function vAdapters(host: ShapeHost, t: IrType): { retain: string; release: string } {
const nullable = host.nullableUnions.of(t);
+ if (nullable?.repr === "shapes") return shapeUnionRc(nullable);
if (nullable) {
const arm = vAdapters(host, nullable.arm);
return { retain: nullableRetainSym(host, nullable.arm), release: arm.release };
@@ -184,6 +194,7 @@ export function vAdapters(host: ShapeHost, t: IrType): { retain: string; release
* Families with a mirrored fast path call the inline helper instead. */
export function retainSym(host: ShapeHost, t: IrType): string {
const nullable = host.nullableUnions.of(t);
+ if (nullable?.repr === "shapes") return shapeUnionRc(nullable).retain;
if (nullable) return nullableRetainSym(host, nullable.arm);
const stem = runtimeRcStem(t);
if (host.rcHelpers !== null && stem !== null && inlineRcFamily(host, stem, "retain") !== null) {
@@ -195,6 +206,99 @@ export function retainSym(host: ShapeHost, t: IrType): string {
return vAdapters(host, t).retain;
}
+/** A shape union's RC entry points: every record retain is the same
+ * NULL- and immortal-skipping increment (the null sentinel is immortal),
+ * so the first arm's serves; release dispatches on the arm. */
+function shapeUnionRc(union: ShapeUnion): { retain: string; release: string } {
+ return {
+ retain: `@${mangleRecordRetain(union.arms[0]!.shapeId)}`,
+ release: shapeReleaseFn(union.unionId),
+ };
+}
+
+/** The tag, release and trace helpers of every shape union
+ * (nullable-unions.ts). The tag helper maps a value to its arm: NULL and
+ * the null sentinel are the unit arms, anything else a tagged record whose
+ * shape word names its arm (the switch folds to a lookup table). */
+export function emitShapeUnionHelpers(host: ShapeHost): string[] {
+ const out: string[] = [];
+ const S = host.sizeType;
+ for (const union of host.nullableUnions.allShapeUnions()) {
+ const header = shapeWordHeader(S);
+ const tagFn = shapeTagFn(union.unionId);
+ const cases = union.arms
+ .map((arm) => `i32 ${host.nullableUnions.shapeNumber(arm.shapeId)}, label %a${arm.tag}`)
+ .join(" ");
+ out.push(
+ `define internal i32 ${tagFn}(ptr %u) alwaysinline ${FN_ATTRS} { ; shape union tag`,
+ `entry:`,
+ ` %isnull = icmp eq ptr %u, null`,
+ ` br i1 %isnull, label %unit, label %ref`,
+ `unit:`,
+ ` ret i32 ${union.unitTag}`,
+ `ref:`,
+ );
+ if (union.nullTag >= 0)
+ out.push(
+ ` %isnullarm = icmp eq ptr %u, ${NULLABLE_NULL}`,
+ ` br i1 %isnullarm, label %nullarm, label %rec`,
+ `nullarm:`,
+ ` ret i32 ${union.nullTag}`,
+ `rec:`,
+ );
+ out.push(
+ ` %wp = getelementptr inbounds ${header}, ptr %u, i32 0, i32 ${SHAPE_WORD_INDEX}`,
+ ` %w = load i32, ptr %wp`,
+ ` switch i32 %w, label %bad [ ${cases} ]`,
+ ...union.arms.flatMap((arm) => [`a${arm.tag}:`, ` ret i32 ${arm.tag}`]),
+ `bad:`,
+ ` ret i32 -1`,
+ `}`,
+ ``,
+ );
+ // release: unit encodings are NULL or immortal; each arm releases
+ // through its own (NULL- and immortal-tolerant) helper.
+ out.push(
+ `define internal void ${shapeReleaseFn(union.unionId)}(ptr %u) ${FN_ATTRS} { ; shape union release`,
+ `entry:`,
+ ` %t = call i32 ${tagFn}(ptr %u)`,
+ ` switch i32 %t, label %done [ ${union.arms.map((arm) => `i32 ${arm.tag}, label %a${arm.tag}`).join(" ")} ]`,
+ ...union.arms.flatMap((arm) => [
+ `a${arm.tag}:`,
+ ` call void @${mangleRecordRelease(arm.shapeId)}(ptr %u)`,
+ ` br label %done`,
+ ]),
+ `done:`,
+ ` ret void`,
+ `}`,
+ ``,
+ );
+ if (shapeUnionTraced(host, union))
+ out.push(
+ `define internal void ${shapeTraceFn(union.unionId)}(ptr %u, ptr %visit, ptr %ctx) ${FN_ATTRS} { ; shape union trace`,
+ `entry:`,
+ ` %t = call i32 ${tagFn}(ptr %u)`,
+ ` switch i32 %t, label %done [ ${union.arms.map((arm) => `i32 ${arm.tag}, label %a${arm.tag}`).join(" ")} ]`,
+ ...union.arms.flatMap((arm) => [
+ `a${arm.tag}:`,
+ ` call void @${mangleRecordTrace(arm.shapeId)}(ptr %u, ptr %visit, ptr %ctx)`,
+ ` br label %done`,
+ ]),
+ `done:`,
+ ` ret void`,
+ `}`,
+ ``,
+ );
+ }
+ return out;
+}
+
+/** A shape union's arms carry collector headers (all or none: the emitter
+ * traces every arm of a union with one traced arm). */
+function shapeUnionTraced(host: ShapeHost, union: ShapeUnion): boolean {
+ return union.arms.some((arm) => host.tracedShapes.has(`record:${arm.shapeId}`));
+}
+
/** The retain entry point of a nullable union's value: the arm's own when
* it tolerates NULL (emitted class and record helpers), otherwise a
* NULL-skipping wrapper around it (the null sentinel and literals are
@@ -233,6 +337,7 @@ export function emitNullableRetainWrappers(host: ShapeHost): string[] {
* Families with a mirrored fast path call the inline helper instead. */
export function releaseSym(host: ShapeHost, t: IrType): string {
const nullable = host.nullableUnions.of(t);
+ if (nullable?.repr === "shapes") return shapeUnionRc(nullable).release;
if (nullable) return releaseSym(host, nullable.arm);
const stem = runtimeRcStem(t);
if (host.rcHelpers !== null && stem !== null && inlineRcFamily(host, stem, "release") !== null) {
@@ -643,6 +748,8 @@ export function traceAdapter(host: ShapeHost, t: IrType): string | null {
return "@scr_promise_trace_v";
case "union": {
const nullable = host.nullableUnions.get(t.unionId);
+ if (nullable?.repr === "shapes")
+ return shapeUnionTraced(host, nullable) ? shapeTraceFn(nullable.unionId) : null;
if (nullable) return traceAdapter(host, nullable.arm);
if (!host.tracedUnions.has(t.unionId)) return null;
host.declare(`declare void @scr_union_trace_v(ptr, ptr, ptr)`);
@@ -945,15 +1052,17 @@ export function mapValKindNum(value: IrType): number {
export function rcMembers(
shape: IrRecordShape,
inline: InlineUnionFields,
+ /** The struct index of the first field (2 on tagged shapes). */
+ base = 1,
): { index: number; type: IrType; name: string }[] {
return [
...shape.fields.flatMap((f, i) =>
- inline.record(f.type) ? [] : [{ index: i + 1, type: f.type, name: f.name }],
+ inline.record(f.type) ? [] : [{ index: i + base, type: f.type, name: f.name }],
),
...(shape.indexValue
? [
{
- index: shape.fields.length + 1,
+ index: shape.fields.length + base,
type: mapOf(STRING, shape.indexValue),
name: "[key: string] overflow",
},
@@ -1107,7 +1216,12 @@ export function directDestroyTarget(
}
function directDestroyKey(host: ShapeHost, type: IrType): string | null {
- const t = host.nullableUnions.of(type)?.arm ?? type;
+ const nullable = host.nullableUnions.of(type);
+ let t = type;
+ if (nullable !== null) {
+ if (nullable.repr === "shapes") return null;
+ t = nullable.arm;
+ }
if (t.kind === "record") return `record:${t.shapeId}`;
return t.kind === "object" ? `object:${t.className}` : null;
}
@@ -1338,8 +1452,12 @@ export function emitRecordShapes(
for (const shape of records) {
const struct = mangleRecordStruct(shape.id);
- const fieldTys = shape.fields.map((f) => llFieldType(f.type, host.inlineUnions.record(f.type)));
+ const fieldTys: string[] = shape.fields.map((f) =>
+ llFieldType(f.type, host.inlineUnions.record(f.type)),
+ );
if (shape.indexValue) fieldTys.push("ptr"); // the overflow ScrMap *
+ // A tagged shape (an arm of a shape union) carries its shape number.
+ if (host.nullableUnions.shapeNumber(shape.id) !== 0) fieldTys.unshift("i32");
typeDefs.push(
`%${struct} = type { ${host.sizeType}${fieldTys.length ? ", " + fieldTys.join(", ") : ""} } ` +
`; record ${shape.id} { ${shape.fields.map((f) => llvmCommentText(f.name)).join("; ")}${shape.indexValue ? "; [key: string]" : ""} }`,
@@ -1352,7 +1470,7 @@ export function emitRecordShapes(
const candidates: [string, DirectDestroy][] = [];
const memberTypes: IrType[] = [];
for (const shape of records) {
- const members = rcMembers(shape, host.inlineUnions);
+ const members = rcMembers(shape, host.inlineUnions, host.nullableUnions.fieldBase(shape.id));
for (const m of members) memberTypes.push(m.type);
if (!members.some((m) => isRefCounted(m.type) && needsBoundedRelease(m.type))) continue;
const release = mangleRecordRelease(shape.id);
@@ -1370,7 +1488,9 @@ export function emitRecordShapes(
for (const shape of records) {
const struct = mangleRecordStruct(shape.id);
const traced = host.tracedShapes.has(`record:${shape.id}`);
- const members = rcMembers(shape, host.inlineUnions);
+ const base = host.nullableUnions.fieldBase(shape.id);
+ const shapeNumber = host.nullableUnions.shapeNumber(shape.id);
+ const members = rcMembers(shape, host.inlineUnions, base);
const refMembers = members.filter((m) => isRefCounted(m.type));
const sizeOf = `ptrtoint (ptr getelementptr (%${struct}, ptr null, i32 1) to ${host.sizeType})`;
@@ -1439,6 +1559,11 @@ export function emitRecordShapes(
);
nw.push(...emitObjectAlloc(host, sizeOf, traced ? { type: cycType } : null));
nw.push(` store ${host.sizeType} 1, ptr %o`);
+ if (shapeNumber !== 0)
+ nw.push(
+ ` %shp = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${SHAPE_WORD_INDEX}`,
+ ` store i32 ${shapeNumber}, ptr %shp ; shape number`,
+ );
if (shape.indexValue) {
// The overflow map (string-keyed): value handling is type-directed
// exactly like shapes.ts's overflowNewC.
@@ -1449,7 +1574,7 @@ export function emitRecordShapes(
const trace = valKind === 2 ? traceArg(host, v) : "null";
nw.push(
` %ovf = call ptr @scr_map_new(i32 1, i32 ${valKind}, ptr ${host.mapOps(["null", "null", "null", rc.retain, rc.release, trace, "null"])})`,
- ` %ovfp = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${shape.fields.length + 1}`,
+ ` %ovfp = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${shape.fields.length + base}`,
` store ptr %ovf, ptr %ovfp`,
);
}
@@ -1465,7 +1590,7 @@ export function emitRecordShapes(
];
let i = 0;
for (const field of shape.fields) {
- const index = i + 1;
+ const index = i + base;
const inline = host.inlineUnions.record(field.type);
const fieldTy = llFieldType(field.type, inline);
clone.push(
diff --git a/packages/compiler/src/backend/llvm/stack-objects.ts b/packages/compiler/src/backend/llvm/stack-objects.ts
index 051e7bf39..cbbf0cc29 100644
--- a/packages/compiler/src/backend/llvm/stack-objects.ts
+++ b/packages/compiler/src/backend/llvm/stack-objects.ts
@@ -21,6 +21,7 @@ import type { CallLifetimes } from "./call-lifetimes.js";
import { emitCallExpr, emitConstruction } from "./expr-calls.js";
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
import { emitRecordLitFields } from "./expr-primitives.js";
+import { SHAPE_WORD_INDEX } from "./nullable-unions.js";
import { llFieldType } from "./shapes.js";
export type StackObjectSource = IrExpr & { kind: "recordLit" | "recordClone" | "new" | "call" };
@@ -200,12 +201,13 @@ export function stackObjectFields(
const cached = host.stackObjectFieldPtrs.get(slot);
if (cached) return cached;
const struct = stackObjectStruct(type);
+ const base = host.nullableUnions.fieldBase(type.shapeId);
const fields: StackObjectField[] = [];
host.recordShape(type.shapeId).fields.forEach((field, index) => {
if (!isRefCounted(field.type) || host.inlineUnions.record(field.type)) return;
const ptr = host.B.slot();
host.B.entryAllocas.push(
- `${ptr} = getelementptr inbounds ${struct}, ptr ${slot}, i64 0, i32 ${index + 1}`,
+ `${ptr} = getelementptr inbounds ${struct}, ptr ${slot}, i64 0, i32 ${index + base}`,
);
fields.push({ name: field.name, ptr, type: field.type });
});
@@ -300,6 +302,17 @@ export function emitStackObject(
}
}
B.line(`store ${host.sizeType} -1, ptr ${slot}`);
+ if (type.kind === "record") {
+ // A tagged shape's word names it to every shape union's tag read.
+ const shapeNumber = host.nullableUnions.shapeNumber(type.shapeId);
+ if (shapeNumber !== 0) {
+ const p = B.tmp();
+ B.line(
+ `${p} = getelementptr inbounds ${struct}, ptr ${slot}, i64 0, i32 ${SHAPE_WORD_INDEX}`,
+ );
+ B.line(`store i32 ${shapeNumber}, ptr ${p}`);
+ }
+ }
if (e.kind === "call") {
// The entry writes the result into the slot it receives last.
emitCallExpr(
diff --git a/packages/compiler/src/backend/llvm/union-repr.ts b/packages/compiler/src/backend/llvm/union-repr.ts
index 415aeb851..959f9e51e 100644
--- a/packages/compiler/src/backend/llvm/union-repr.ts
+++ b/packages/compiler/src/backend/llvm/union-repr.ts
@@ -2,14 +2,19 @@
* helper generators (walkers, dyn). A tagged union is a `%ScrUnion` box; a
* nullable union (nullable-unions.ts) is the arm pointer itself: NULL for
* its NULL-encoded unit arm and, in `C | null | undefined`, the immortal
- * NULLABLE_NULL sentinel for `null`. Every structural read of a union value
+ * NULLABLE_NULL sentinel for `null`. A shape union's value is likewise the
+ * record pointer; its tag comes from the record's shape word. Every structural read of a union value
* goes through here. */
import type { BlockBuilder } from "./blocks.js";
-import { NULLABLE_NULL, type NullableUnion } from "./nullable-unions.js";
+import { NULLABLE_NULL, shapeTagFn, type PointerUnion } from "./nullable-unions.js";
/** The i32 tag of a union value. */
-export function emitUnionTag(B: BlockBuilder, nullable: NullableUnion | null, u: string): string {
+export function emitUnionTag(B: BlockBuilder, nullable: PointerUnion | null, u: string): string {
const t = B.tmp();
+ if (nullable?.repr === "shapes") {
+ B.line(`${t} = call i32 ${shapeTagFn(nullable.unionId)}(ptr ${u})`);
+ return t;
+ }
if (nullable) {
const isNull = B.tmp();
B.line(`${isNull} = icmp eq ptr ${u}, null`);
@@ -32,8 +37,13 @@ export function emitUnionTag(B: BlockBuilder, nullable: NullableUnion | null, u:
/** i1: a nullable union value holds its reference arm (neither NULL nor
* the `null` sentinel). */
-export function emitNullablePresent(B: BlockBuilder, nullable: NullableUnion, u: string): string {
+export function emitNullablePresent(B: BlockBuilder, nullable: PointerUnion, u: string): string {
const present = B.tmp();
+ if (nullable.repr === "shapes" && nullable.unitTag < 0) {
+ // No unit arm: every value is a record (NULL only as a pending dummy).
+ B.line(`${present} = icmp ne ptr ${u}, null`);
+ return present;
+ }
B.line(`${present} = icmp ne ptr ${u}, null`);
if (nullable.nullTag < 0) return present;
const notNullArm = B.tmp(),
@@ -46,12 +56,18 @@ export function emitNullablePresent(B: BlockBuilder, nullable: NullableUnion, u:
/** i1: a nullable union value holds the arm `tag` (`negated`: does not). */
export function emitNullableIsTag(
B: BlockBuilder,
- nullable: NullableUnion,
+ nullable: PointerUnion,
u: string,
tag: number,
negated: boolean,
): string {
- if (tag === nullable.refTag) {
+ if (nullable.repr === "shapes" && tag !== nullable.unitTag && tag !== nullable.nullTag) {
+ const actual = emitUnionTag(B, nullable, u),
+ t = B.tmp();
+ B.line(`${t} = icmp ${negated ? "ne" : "eq"} i32 ${actual}, ${tag}`);
+ return t;
+ }
+ if (nullable.repr === "single" && tag === nullable.refTag) {
const present = emitNullablePresent(B, nullable, u);
if (!negated) return present;
const t = B.tmp();
@@ -65,7 +81,7 @@ export function emitNullableIsTag(
}
/** The BORROWED payload pointer of a reference arm. */
-export function emitUnionPeek(B: BlockBuilder, nullable: NullableUnion | null, u: string): string {
+export function emitUnionPeek(B: BlockBuilder, nullable: PointerUnion | null, u: string): string {
if (nullable) return u;
const p = B.tmp();
const t = B.tmp();
@@ -76,7 +92,9 @@ export function emitUnionPeek(B: BlockBuilder, nullable: NullableUnion | null, u
/** Text-template forms for helpers written as raw LLVM lines: `%out`
* receives the tag / payload of `u`. */
-export function unionTagLines(nullable: NullableUnion | null, u: string, out: string): string[] {
+export function unionTagLines(nullable: PointerUnion | null, u: string, out: string): string[] {
+ if (nullable?.repr === "shapes")
+ return [` ${out} = call i32 ${shapeTagFn(nullable.unionId)}(ptr ${u})`];
if (nullable && nullable.nullTag < 0)
return [
` ${out}.isnull = icmp eq ptr ${u}, null`,
@@ -95,7 +113,7 @@ export function unionTagLines(nullable: NullableUnion | null, u: string, out: st
];
}
-export function unionPeekLines(nullable: NullableUnion | null, u: string, out: string): string[] {
+export function unionPeekLines(nullable: PointerUnion | null, u: string, out: string): string[] {
// A plain copy of the pointer (NULL included) under the template's name.
if (nullable) return [` ${out} = getelementptr i8, ptr ${u}, i64 0`];
return [
diff --git a/packages/compiler/src/backend/llvm/union-retype.test.ts b/packages/compiler/src/backend/llvm/union-retype.test.ts
index eca6adcd6..0f0abdb68 100644
--- a/packages/compiler/src/backend/llvm/union-retype.test.ts
+++ b/packages/compiler/src/backend/llvm/union-retype.test.ts
@@ -183,7 +183,9 @@ test("a nullable-pointer source rebuilds the target box", () => {
expect(retag).not.toContain("retype.share");
});
-async function llvmFor(source: string): Promise<{ module: IrModule; ir: string }> {
+/** `boxed`: keep every union a tagged box (record unions otherwise become
+ * shape unions, which retype without any box). */
+async function llvmFor(source: string, boxed = true): Promise<{ module: IrModule; ir: string }> {
const dir = await mkdtemp(join(tmpdir(), "scriptc-union-retype-"));
try {
const entry = join(dir, "main.ts"),
@@ -194,7 +196,7 @@ async function llvmFor(source: string): Promise<{ module: IrModule; ir: string }
throw new Error(result.diagnostics.map((item) => `${item.code}: ${item.message}`).join("\n"));
const module = deserializeModule(await readFile(output, "utf8"));
expect(validateModule(module)).toEqual([]);
- return { module, ir: emitLlvmModule(module) };
+ return { module, ir: emitLlvmModule(boxed ? keepUnionsBoxed(module) : module) };
} finally {
await rm(dir, { recursive: true, force: true });
}
@@ -253,3 +255,16 @@ console.log(leafValue(parsed));
expect(stored).not.toBeNull();
expect(ir).toContain(`load i8, ptr ${stored![1]}`);
});
+
+test("record unions retype as the record pointer itself", async () => {
+ const { module, ir } = await llvmFor(VISITOR, false);
+ const helpers = module.functions.filter((fn) => fn.name.startsWith("%union.retag."));
+ expect(helpers.length).toBeGreaterThan(0);
+ for (const helper of helpers) {
+ const symbol = `sc_bf__x25_union_retag_${helper.name.slice("%union.retag.".length)}`;
+ const retag = body(ir, symbol);
+ expect(retag).not.toMatch(/@scr_union_new|retype\.share|sc_retype_guard/);
+ }
+ // No record of the visitor is ever boxed.
+ expect(ir).not.toMatch(/call ptr @scr_union_new_ref/);
+});
diff --git a/packages/compiler/src/backend/llvm/walkers.ts b/packages/compiler/src/backend/llvm/walkers.ts
index 9b8d80b87..2f69dd5ec 100644
--- a/packages/compiler/src/backend/llvm/walkers.ts
+++ b/packages/compiler/src/backend/llvm/walkers.ts
@@ -284,7 +284,8 @@ export class LlWalkers {
throw new InternalCompilerError(
`llvm emitter bug: jsonStringify of unknown shape ${shapeId}`,
);
- const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + 1]));
+ const fieldBase = this.host.nullableUnions.fieldBase(shapeId);
+ const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + fieldBase]));
if (recordMayKeepSource(shape)) this.emitRecordSourceWriter(B, shapeId);
// CYCLE-CAPABLE shapes bracket the walk with the circular-detection
// stack; edge labels stamp before members whose walk can re-enter —
@@ -437,7 +438,8 @@ export class LlWalkers {
throw new InternalCompilerError(`llvm emitter bug: field writer of unknown shape ${shapeId}`);
const name = `sc_jwf_${this.recordFieldWriters.size}`;
this.recordFieldWriters.set(shapeId, name);
- const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + 1]));
+ const fieldBase = this.host.nullableUnions.fieldBase(shapeId);
+ const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + fieldBase]));
const byName = new Map(shape.fields.map((f) => [f.name, f]));
const cyclic = traceAdapter(this.host, { kind: "record", shapeId }) !== null;
const order = recordSourceFieldOrder(shape);
@@ -534,7 +536,7 @@ export class LlWalkers {
const ovfp = B.tmp();
const ovf = B.tmp();
B.line(
- `${ovfp} = getelementptr inbounds %${mangleRecordStruct(shape.id)}, ptr %v, i64 0, i32 ${shape.fields.length + 1}`,
+ `${ovfp} = getelementptr inbounds %${mangleRecordStruct(shape.id)}, ptr %v, i64 0, i32 ${shape.fields.length + this.host.nullableUnions.fieldBase(shape.id)}`,
);
B.line(`${ovf} = load ptr, ptr ${ovfp} ; overflow map`);
if (iv.kind === "dyn") {
diff --git a/packages/compiler/src/cli/command.ts b/packages/compiler/src/cli/command.ts
index d729de866..6810cc8fb 100644
--- a/packages/compiler/src/cli/command.ts
+++ b/packages/compiler/src/cli/command.ts
@@ -7,8 +7,10 @@ import {
coverageEnvelope,
coveragePasses,
renderCoverage,
+ renderExplainDynamic,
type CoverageFailOn,
} from "../coverage/report.js";
+import { TIER_MODES, type TierMode } from "../frontend/tier-analysis.js";
import { buildEnvelope } from "../diagnostics/envelope.js";
import { renderWarnings } from "../diagnostics/render.js";
import { isNativeOptimization } from "../backend/optimization.js";
@@ -45,8 +47,15 @@ function parseCli(
): ReturnType<
typeof parseArgs<{ options: typeof CLI_OPTIONS; allowPositionals: true; allowNegative: true }>
> {
+ // `--explain-dynamic` takes an optional value: alone it lists every unit.
+ const normalized = args.map((arg) => (arg === "--explain-dynamic" ? "--explain-dynamic=" : arg));
try {
- return parseArgs({ args, options: CLI_OPTIONS, allowPositionals: true, allowNegative: true });
+ return parseArgs({
+ args: normalized,
+ options: CLI_OPTIONS,
+ allowPositionals: true,
+ allowNegative: true,
+ });
} catch (err) {
// parseArgs appends a paragraph about `--` and positionals to the
// unknown-option message; the first sentence is the part that names
@@ -91,6 +100,8 @@ async function main(args: string[], host: CliHost): Promise {
externalTypeArgs.length > 0 ||
values.out !== undefined ||
values["emit-ir"] ||
+ values.tier !== undefined ||
+ values["explain-dynamic"] !== undefined ||
!values["keep-llvm"]
) {
fail(
@@ -152,10 +163,12 @@ async function main(args: string[], host: CliHost): Promise {
values["windows-subsystem"] !== undefined ||
values["detached-exit"] ||
(values["npm-static"] ?? []).length > 0 ||
- externalTypeArgs.length > 0
+ externalTypeArgs.length > 0 ||
+ values.tier !== undefined ||
+ values["explain-dynamic"] !== undefined
) {
fail(
- "scriptc build --lib takes no --backend/--emit/--print/--optimization/--strip/--windows-subsystem/--detached-exit/--npm-static/--ffi/--external-types: the profile pins the emission and optimization, npm imports are judged automatically, outbound FFI belongs to executable builds, and external type mappings belong to coverage",
+ "scriptc build --lib takes no --backend/--emit/--print/--optimization/--strip/--windows-subsystem/--detached-exit/--npm-static/--ffi/--external-types/--tier/--explain-dynamic: the profile pins the emission and optimization, npm imports are judged automatically, outbound FFI belongs to executable builds, external type mappings belong to coverage, and library artifacts are static-tier only",
);
}
const profilePath = resolve(profileArg);
@@ -223,6 +236,15 @@ async function main(args: string[], host: CliHost): Promise {
fail(`--fail-on is a coverage option (a build already fails on blockers)\n\n${USAGE}`);
}
const failOn = failOnRaw as CoverageFailOn | undefined;
+ const tierRaw = values.tier;
+ if (tierRaw !== undefined && !(TIER_MODES as readonly string[]).includes(tierRaw)) {
+ fail(`unknown --tier value "${tierRaw}" (supported: ${TIER_MODES.join(", ")})\n\n${USAGE}`);
+ }
+ const tier = tierRaw as TierMode | undefined;
+ const explainDynamic = values["explain-dynamic"];
+ if (explainDynamic !== undefined && command !== "coverage") {
+ fail(`--explain-dynamic is a coverage option\n\n${USAGE}`);
+ }
if (printNativeLinkInfo && command !== "build") {
fail(`--print=native-link-info is a build option\n\n${USAGE}`);
}
@@ -340,6 +362,8 @@ async function main(args: string[], host: CliHost): Promise {
...(npmStatic !== undefined ? { npmStatic } : {}),
...(ffiProfilePath !== undefined ? { ffiProfilePath } : {}),
...(Object.keys(externalTypes).length > 0 ? { externalTypes } : {}),
+ ...(tier !== undefined ? { tier } : {}),
+ ...(explainDynamic !== undefined ? { explainDynamic: true } : {}),
});
if (printDiagnostics) {
// Keep stdout pure JSON for tooling.
@@ -354,6 +378,18 @@ async function main(args: string[], host: CliHost): Promise {
process.stdout.write(
renderCoverage(coverage, { color, sourceTexts, root: process.cwd() }) + "\n",
);
+ if (explainDynamic !== undefined && coverage.tiers !== undefined) {
+ process.stdout.write(
+ "\n" +
+ renderExplainDynamic(coverage.tiers, {
+ color,
+ sourceTexts,
+ root: process.cwd(),
+ query: explainDynamic,
+ }) +
+ "\n",
+ );
+ }
}
return coveragePasses(coverage, failOn) ? 0 : 1;
}
@@ -386,6 +422,7 @@ async function main(args: string[], host: CliHost): Promise {
...(npmStatic !== undefined ? { npmStatic } : {}),
...(ffiProfilePath !== undefined ? { ffiProfilePath } : {}),
...(printNativeLinkInfo ? { nativeLinkInfo: true } : {}),
+ ...(tier !== undefined ? { tier } : {}),
});
if (!result.ok) {
if (printDiagnostics) {
diff --git a/packages/compiler/src/cli/usage.ts b/packages/compiler/src/cli/usage.ts
index a0388374b..b5dcec332 100644
--- a/packages/compiler/src/cli/usage.ts
+++ b/packages/compiler/src/cli/usage.ts
@@ -57,7 +57,8 @@ Options:
compile the named npm packages' shipped JS statically as
program modules (repeatable; "auto" opts in every eligible
direct import: own .d.ts, unminified JS, no build-transform
- markers). Packages that fail preflight are refused with a
+ markers; "pkg..." also names every package pkg depends
+ on). Packages that fail preflight are refused with a
diagnostic. Eligible dependencies compile automatically.
--provenance-sources
EXPERIMENTAL: compile npm dependencies from their
@@ -69,6 +70,16 @@ Options:
local declaration file. The declaration supplies types
for analysis; the host module remains an explicit
external-boundary blocker (repeatable)
+ --tier
+ tier policy (default: auto). static-only keeps every
+ construct without a static lowering a compile error, so
+ the program has no dynamic-tier demotions. Both modes
+ currently compile identically
+ --explain-dynamic[=]
+ coverage only: list the functions that operate on dynamic
+ values and the bindings, fields, functions, classes and
+ modules the dynamic tier would take over, with the
+ refusal behind each; a name shows only that unit's chain
-h, --help show this help
-v, --version print the version
`;
@@ -90,6 +101,8 @@ export const CLI_OPTIONS = {
"npm-static": { type: "string", multiple: true },
"provenance-sources": { type: "boolean", default: false },
"external-types": { type: "string", multiple: true },
+ tier: { type: "string" },
+ "explain-dynamic": { type: "string" },
lib: { type: "boolean", default: false },
profile: { type: "string" },
help: { type: "boolean", short: "h", default: false },
diff --git a/packages/compiler/src/compile-types.ts b/packages/compiler/src/compile-types.ts
index de2c1a335..f069df247 100644
--- a/packages/compiler/src/compile-types.ts
+++ b/packages/compiler/src/compile-types.ts
@@ -3,6 +3,7 @@ import type { NativeLinkInfo } from "./backend/native-link-info.js";
import type { NativeOptimization } from "./backend/optimization.js";
import type { ScrDiagnostic } from "./diagnostics/diagnostic.js";
import type { CoverageInput } from "./coverage/report.js";
+import type { TierMode } from "./frontend/tier-analysis.js";
export type CompileOutputKind = "ir" | "llvm" | "asm" | "obj" | "exe";
@@ -45,6 +46,10 @@ export interface CompileBaseOptions {
/** Attach the machine-readable external link recipe to an object result.
* Valid only with outputKind "obj"; it never invokes a linker. */
nativeLinkInfo?: boolean;
+ /** `--tier` (frontend/tier-analysis.ts). "static-only" keeps every demotion
+ * request a refusal (design invariant I2). Until demotion lands both modes
+ * compile exactly as before. */
+ tier?: TierMode;
}
/** Executable compile options. This remains the compatibility type for the
@@ -161,6 +166,11 @@ export interface AnalyzeOptions {
* to analyze project code, but imported runtime values remain explicit
* SC1010 blockers rather than being counted as executable. */
externalTypes?: Readonly>;
+ /** `--tier` (see CompileBaseOptions.tier). */
+ tier?: TierMode;
+ /** `--explain-dynamic`: attach the tier-assignment report
+ * (CoverageInput.tiers). Such analyses bypass the verdict cache. */
+ explainDynamic?: boolean;
}
export interface AnalyzeResult {
diff --git a/packages/compiler/src/coverage/report.ts b/packages/compiler/src/coverage/report.ts
index 9d0776d9a..8b03f221d 100644
--- a/packages/compiler/src/coverage/report.ts
+++ b/packages/compiler/src/coverage/report.ts
@@ -17,10 +17,13 @@ import {
type DiagnosticsEnvelope,
type LocatedDiagnostic,
type SourcePosition,
+ type TierUnitPosition,
+ type TiersEnvelope,
} from "../diagnostics/envelope.js";
import { renderDiagnostics } from "../diagnostics/render.js";
import type { LowerStats } from "../frontend/lowering/lowerer.js";
import type { ProvenanceSources } from "../frontend/provenance-registry.js";
+import type { DemotionUnit, TierReport } from "../frontend/tier-analysis.js";
export interface CoverageInput {
file: string;
@@ -51,6 +54,9 @@ export interface CoverageInput {
/** --provenance-sources: diagnostics of elided pure-annotated dead
* consts (off the build; the provenance section names them). */
provenanceElided?: ScrDiagnostic[];
+ /** --explain-dynamic: the report-only tier assignment (which bindings,
+ * fields, functions, classes and modules the dynamic tier would take). */
+ tiers?: TierReport;
/** True when tsc preflight failed — no coverage can be computed. */
preflightFailed: boolean;
}
@@ -61,6 +67,8 @@ export interface NpmStaticStatus {
status: "static" | "refused";
/** The first refusal reason for a refused package. */
detail?: string;
+ /** How to act on the refusal, when there is a specific remedy. */
+ hint?: string;
}
const GREEN = "\x1b[32m";
@@ -180,6 +188,12 @@ export function renderCoverage(input: CoverageInput, opts: CoverageRenderOptions
out.push(` statements analyzed ${total}`);
out.push(` compile statically ${ok} ${c(pctColor, c(BOLD, `(${pct}%)`))}`);
+ const dynamic = dynamicStatements(input);
+ if (dynamic > 0) {
+ out.push(
+ ` in dynamic tier ${dynamic} ${c(DIM, "(operate on values typed only at runtime; --explain-dynamic lists them)")}`,
+ );
+ }
if (skipped > 0) {
out.push(
@@ -432,13 +446,147 @@ export function coverageEnvelope(
stats: {
statementsTotal: total,
statementsStatic: ok,
- statementsDynamic: 0,
+ statementsDynamic: dynamicStatements(input),
statementsFailed: failed,
functionsSkipped: skipped,
percentStatic: total === 0 ? 100 : Math.floor((ok / total) * 100),
},
}),
+ ...(input.tiers !== undefined ? { tiers: tiersEnvelope(input.tiers, locator) } : {}),
groups,
diagnostics: located,
};
}
+
+/** Reached statements in dynamic-tier functions, never more than the
+ * compiled statements they are part of. */
+function dynamicStatements(input: CoverageInput): number {
+ const compiled = input.stats.statementsTotal - input.stats.statementsFailed;
+ return Math.max(0, Math.min(input.stats.statementsDynamic ?? 0, compiled));
+}
+
+function tiersEnvelope(tiers: TierReport, locator: SourceLocator): TiersEnvelope {
+ const at = (loc: { file: string; start: number }): SourcePosition => ({
+ file: loc.file,
+ ...locator.position(loc.file, loc.start),
+ });
+ const unit = (u: DemotionUnit): TierUnitPosition => ({
+ kind: u.kind,
+ name: u.name,
+ ...(u.owner !== undefined ? { owner: u.owner } : {}),
+ ...at(u.loc),
+ });
+ return {
+ mode: tiers.mode,
+ statementsDynamic: tiers.statementsDynamic,
+ dynamicFunctions: tiers.dynamicFunctions.map((fn) => ({
+ name: fn.name,
+ ...at(fn.loc),
+ operation: fn.operation,
+ operationAt: at(fn.operationLoc),
+ statements: fn.statements,
+ })),
+ requests: tiers.requests.map((r) => ({
+ unit: unit(r.unit),
+ enclosing: unit(r.enclosing),
+ seed: r.seed,
+ scope: r.scope,
+ code: r.code,
+ message: r.message,
+ ...at(r.loc),
+ })),
+ units: tiers.units.map((u) => ({ ...unit(u), requests: u.requests, sites: u.sites })),
+ };
+}
+
+/** IR function names as a user reads them: `%init.3` is a module's top
+ * level, `%Point.norm` the method `Point.norm`; generated helpers keep
+ * their `%`-prefixed names. */
+function functionDisplayName(name: string): string {
+ if (name.startsWith("%init.")) return "";
+ if (/^%[A-Za-z_$][\w$]*\.[A-Za-z_$#][\w$]*$/.test(name)) return name.slice(1);
+ return name;
+}
+
+function unitLabel(u: DemotionUnit, root: string | undefined): string {
+ if (u.kind === "module") return `module ${displayPath(u.name, root)}`;
+ if (u.kind === "binding" && u.owner !== undefined) return `binding ${u.name} in ${u.owner}`;
+ return `${u.kind} ${u.name}`;
+}
+
+function unitMatches(u: DemotionUnit, query: string): boolean {
+ return (
+ u.name === query ||
+ u.name.endsWith(`.${query}`) ||
+ u.name.split(" ")[0] === query ||
+ `${u.owner ?? ""}:${u.name}` === query
+ );
+}
+
+/** `scriptc coverage --explain-dynamic[=]`: the dynamic tier today
+ * and the demotion chains the report-only analysis computed. With a query,
+ * only units whose name matches it (`x`, `Point.x`, `parse:x`). */
+export function renderExplainDynamic(
+ tiers: TierReport,
+ opts: CoverageRenderOptions & { query?: string } = {},
+): string {
+ const c = (code: string, s: string) => (opts.color ? code + s + RESET : s);
+ const locator = new SourceLocator(opts.sourceTexts);
+ const where = (loc: { file: string; start: number }) => {
+ const p = locator.position(loc.file, loc.start);
+ return `${displayPath(loc.file, opts.root)}:${p.line}:${p.column}`;
+ };
+ const query = opts.query !== undefined && opts.query !== "" ? opts.query : undefined;
+ const out: string[] = [];
+ out.push(
+ ` ${c(BOLD, "dynamic tier")} ${c(DIM, `(tier assignment, report-only; --tier=${tiers.mode})`)}`,
+ );
+ if (query === undefined) {
+ const fns = tiers.dynamicFunctions;
+ out.push(
+ ` ${fns.length} function${fns.length === 1 ? "" : "s"} already operate${fns.length === 1 ? "s" : ""} on dynamic values ${c(DIM, `(${tiers.statementsDynamic} statement${tiers.statementsDynamic === 1 ? "" : "s"})`)}`,
+ );
+ for (const fn of fns) {
+ out.push(
+ ` ${functionDisplayName(fn.name)} ${c(DIM, `${where(fn.loc)}: ${fn.operation} at ${where(fn.operationLoc)}`)}`,
+ );
+ }
+ }
+ const live = tiers.requests.filter((r) => r.scope !== "unreached" && r.seed !== "excluded");
+ const excluded = tiers.requests.filter((r) => r.seed === "excluded").length;
+ const unreached = tiers.requests.filter((r) => r.scope === "unreached").length;
+ if (query === undefined) {
+ out.push("");
+ out.push(
+ ` ${live.length} demotion request${live.length === 1 ? "" : "s"} ${c(DIM, "(refusals and fences the dynamic tier would replace)")}` +
+ (unreached > 0 ? c(DIM, `, ${unreached} more in unreached code`) : "") +
+ (excluded > 0 ? c(DIM, `, ${excluded} excluded by policy`) : ""),
+ );
+ }
+ const units = tiers.units.filter((u) => query === undefined || unitMatches(u, query));
+ if (query !== undefined && units.length === 0) {
+ out.push(` no demoted unit matches '${query}'`);
+ return out.join("\n");
+ }
+ for (const u of units) {
+ const counts = [
+ u.requests > 0 ? `${u.requests} request${u.requests === 1 ? "" : "s"}` : "",
+ u.sites > 0 ? `${u.sites} site${u.sites === 1 ? "" : "s"} inside` : "",
+ ]
+ .filter((s) => s !== "")
+ .join(", ");
+ out.push(` ${unitLabel(u, opts.root)} ${c(DIM, `${where(u.loc)} (${counts})`)}`);
+ if (query === undefined && u.requests === 0) continue;
+ for (const r of live) {
+ if (r.unit.kind !== u.kind || r.unit.name !== u.name || r.unit.loc.start !== u.loc.start)
+ continue;
+ if (r.unit.loc.file !== u.loc.file) continue;
+ out.push(
+ ` ${c(DIM, "<-")} ${r.code} ${r.seed} at ${where(r.loc)}: ${capMessage(r.message)}`,
+ );
+ if (r.enclosing.name !== r.unit.name || r.enclosing.kind !== r.unit.kind)
+ out.push(` ${c(DIM, "->")} ${unitLabel(r.enclosing, opts.root)} turns dynamic`);
+ }
+ }
+ return out.join("\n");
+}
diff --git a/packages/compiler/src/coverage/verdict-cache.ts b/packages/compiler/src/coverage/verdict-cache.ts
index 841e2b57b..250259685 100644
--- a/packages/compiler/src/coverage/verdict-cache.ts
+++ b/packages/compiler/src/coverage/verdict-cache.ts
@@ -89,6 +89,9 @@ export function verdictKey(
opts: AnalyzeOptions,
cache: VerdictCacheOptions,
): string | null {
+ // The tier report carries source locations the semantic replay does not
+ // move; explaining analyses always lower.
+ if (opts.explainDynamic === true) return null;
let ffi = "";
if (opts.ffiProfilePath !== undefined) {
try {
@@ -120,6 +123,8 @@ export function verdictKey(
opts.ffiProfilePath ?? "",
ffi,
external.join("\n"),
+ // Demotion (stage S6) makes the tier mode part of the verdict.
+ ...(opts.tier === "static-only" ? ["tier=static-only"] : []),
].join("\0"),
);
}
diff --git a/packages/compiler/src/diagnostics/diagnostic.ts b/packages/compiler/src/diagnostics/diagnostic.ts
index eaae1b531..e6ed50ae1 100644
--- a/packages/compiler/src/diagnostics/diagnostic.ts
+++ b/packages/compiler/src/diagnostics/diagnostic.ts
@@ -877,13 +877,20 @@ export function unsupportedPackageValueDiag(pkg: string, loc: SrcLoc): ScrDiagno
);
}
-export function npmStaticRefusalDiag(pkg: string, detail: string, loc: SrcLoc): ScrDiagnostic {
+export function npmStaticRefusalDiag(
+ pkg: string,
+ detail: string,
+ loc: SrcLoc,
+ hint?: string,
+): ScrDiagnostic {
return {
code: "SC2013",
message: `cannot compile npm package '${pkg}' statically: ${detail}`,
loc,
milestone: "later",
- hint: "use a dependency with supported source, or replace the unsupported operation; scriptc has no interpreter fallback",
+ hint:
+ hint ??
+ "use a dependency with supported source, or replace the unsupported operation; scriptc has no interpreter fallback",
};
}
diff --git a/packages/compiler/src/diagnostics/envelope.ts b/packages/compiler/src/diagnostics/envelope.ts
index 39b193dd0..4c7c158d9 100644
--- a/packages/compiler/src/diagnostics/envelope.ts
+++ b/packages/compiler/src/diagnostics/envelope.ts
@@ -81,6 +81,34 @@ export interface DiagnosticGroup {
sites: SourcePosition[];
}
+export interface TierUnitPosition extends SourcePosition {
+ kind: "binding" | "field" | "function" | "class" | "module";
+ name: string;
+ owner?: string;
+}
+
+/** `scriptc coverage --explain-dynamic --print=diagnostics`: the tier
+ * assignment of frontend/tier-analysis.ts with source positions. */
+export interface TiersEnvelope {
+ mode: "auto" | "static-only";
+ statementsDynamic: number;
+ dynamicFunctions: (SourcePosition & {
+ name: string;
+ operation: string;
+ operationAt: SourcePosition;
+ statements: number;
+ })[];
+ requests: (SourcePosition & {
+ unit: TierUnitPosition;
+ enclosing: TierUnitPosition;
+ seed: string;
+ scope: string;
+ code: string;
+ message: string;
+ })[];
+ units: (TierUnitPosition & { requests: number; sites: number })[];
+}
+
export interface DiagnosticsEnvelope {
schema: typeof DIAGNOSTICS_SCHEMA;
schemaVersion: typeof DIAGNOSTICS_SCHEMA_VERSION;
@@ -97,8 +125,10 @@ export interface DiagnosticsEnvelope {
stats?: {
statementsTotal: number;
statementsStatic: number;
- /** Always 0: every compiled statement is native. Kept so schema
- * version 1 consumers keep reading the same fields. */
+ /** The compiled statements (counted in statementsStatic, which covers
+ * every statement that compiles to native code) that belong to
+ * dynamic-tier functions: code operating on values whose types are only
+ * known at runtime. */
statementsDynamic: number;
statementsFailed: number;
functionsSkipped: number;
@@ -106,6 +136,8 @@ export interface DiagnosticsEnvelope {
};
/** build only: the produced artifact path. */
artifact?: string;
+ /** coverage --explain-dynamic only: the report-only tier assignment. */
+ tiers?: TiersEnvelope;
groups: DiagnosticGroup[];
diagnostics: LocatedDiagnostic[];
}
diff --git a/packages/compiler/src/executable/prepare.ts b/packages/compiler/src/executable/prepare.ts
index f6012dd2a..d97497c70 100644
--- a/packages/compiler/src/executable/prepare.ts
+++ b/packages/compiler/src/executable/prepare.ts
@@ -59,6 +59,7 @@ export function prepareExecutableModule(
entry.package,
entry.detail ?? "its static compilation was refused",
fe.npmImportSites.get(entry.package) ?? { file: entryPath, start: 0, end: 0 },
+ entry.hint,
),
),
);
@@ -69,6 +70,7 @@ export function prepareExecutableModule(
lowered = fe.lower({
targetPlatform: buildPlatform,
...(ffi !== null ? { ffiImports: ffi.functions } : {}),
+ ...(opts.tier !== undefined ? { tierMode: opts.tier } : {}),
});
timing("lower");
} catch (e) {
diff --git a/packages/compiler/src/frontend/analysis.ts b/packages/compiler/src/frontend/analysis.ts
index b38a267fa..f17f6af6c 100644
--- a/packages/compiler/src/frontend/analysis.ts
+++ b/packages/compiler/src/frontend/analysis.ts
@@ -58,6 +58,7 @@ export function analyzeWithFrontend(
entry.package,
entry.detail ?? "its static compilation was refused",
fe.npmImportSites.get(entry.package) ?? { file: entryPath, start: 0, end: 0 },
+ entry.hint,
),
),
];
@@ -90,6 +91,8 @@ export function analyzeWithFrontend(
coverage: true,
targetPlatform: buildPlatform,
...(ffi !== null ? { ffiImports: ffi.functions } : {}),
+ ...(opts.tier !== undefined ? { tierMode: opts.tier } : {}),
+ ...(opts.explainDynamic === true ? { tierReport: { preflight } } : {}),
});
const provenance = provenanceSources();
return {
@@ -109,6 +112,7 @@ export function analyzeWithFrontend(
...(provenance !== null ? { provenance } : {}),
...(lowered.statsByFile ? { statsByFile: lowered.statsByFile } : {}),
...(lowered.provenanceElided ? { provenanceElided: lowered.provenanceElided } : {}),
+ ...(lowered.tiers !== undefined ? { tiers: lowered.tiers } : {}),
preflightFailed: false,
},
sourceTexts: fe.sourceTexts(),
diff --git a/packages/compiler/src/frontend/builtin-modules.ts b/packages/compiler/src/frontend/builtin-modules.ts
index b45dce6d7..3a1acb934 100644
--- a/packages/compiler/src/frontend/builtin-modules.ts
+++ b/packages/compiler/src/frontend/builtin-modules.ts
@@ -42,6 +42,8 @@ export const SUPPORTED_BUILTIN_MODULES = [
"module",
"console",
"process",
+ "punycode",
+ "constants",
] as const;
/** Builtins Node itself serves ONLY under the node: prefix —
diff --git a/packages/compiler/src/frontend/lowering/builtins/class-values.ts b/packages/compiler/src/frontend/lowering/builtins/class-values.ts
new file mode 100644
index 000000000..f24db09cf
--- /dev/null
+++ b/packages/compiler/src/frontend/lowering/builtins/class-values.ts
@@ -0,0 +1,159 @@
+import * as ts from "../../ts7/adapter.js";
+import { type Lowerer } from "../lowerer.js";
+import { type ClassInfo } from "../lower-classes.js";
+import { isJsSourceFile } from "../../program.js";
+import { builtinMemberRequireDecl } from "./module-bindings.js";
+import { stripTypeCasts } from "./arguments.js";
+import { streamClassAliasDecl } from "../lower-stream.js";
+import { zlibClassName, zlibStreamModeOf } from "./compression.js";
+import { stdlibGlobalNameOf } from "../surfaces.js";
+
+/** A builtin class an expression names at compile time: the error
+ * classes, node:events EventEmitter, the node:stream classes (each a
+ * runtime ClassInfo), the node:zlib stream classes (by node_zlib_mode),
+ * and the Map and Set constructors. `name` is the class's `name`
+ * property. */
+export type BuiltinClassRef =
+ | { kind: "error" | "emitter" | "stream"; info: ClassInfo; name: string }
+ | { kind: "zlib"; mode: number; name: string }
+ | { kind: "collection"; which: "Map" | "Set"; name: string };
+
+/** The builtin class `expr` names: a direct spelling (`EventEmitter`,
+ * `stream.Readable`, `zlib.Gzip`, `Map`, `globalThis.TypeError`) or a
+ * `const` binding of one (`const E = EventEmitter`, an alias of an alias,
+ * an imported alias), through parentheses and type assertions. Null for
+ * anything else. */
+export function builtinClassRefOf(lowerer: Lowerer, expr: ts.Expression): BuiltinClassRef | null {
+ const e = stripTypeCasts(expr);
+ // Checker lookups only: answering must not run the fences and deferred
+ // diagnostics of a lowered reference (resolveValueSymbol).
+ let symbol: ts.Symbol | undefined;
+ if (ts.isIdentifier(e)) {
+ symbol = lowerer.checker.getSymbolAtLocation(e);
+ } else if (ts.isPropertyAccessExpression(e) && !e.questionDotToken) {
+ symbol = lowerer.checker.getSymbolAtLocation(e.name);
+ } else {
+ return null;
+ }
+ if (!symbol) return null;
+ const alias = builtinClassAliasOf(lowerer, symbol);
+ if (alias !== null) return alias;
+ if (symbol.flags & ts.SymbolFlags.Alias) symbol = lowerer.checker.getAliasedSymbol(symbol);
+ const which = stdlibGlobalNameOf(lowerer, e);
+ if (which === "Map" || which === "Set") return { kind: "collection", which, name: which };
+ const error = lowerer.builtinErrorInfoOf(symbol);
+ if (error) return { kind: "error", info: error, name: runtimeClassName(error) };
+ const emitter = lowerer.builtinEmitterInfoOf(symbol);
+ if (emitter) return { kind: "emitter", info: emitter, name: runtimeClassName(emitter) };
+ const stream = lowerer.builtinStreamInfoOf(symbol);
+ if (stream) return { kind: "stream", info: stream, name: runtimeClassName(stream) };
+ const mode = zlibStreamModeOf(lowerer, symbol);
+ if (mode !== null) return { kind: "zlib", mode, name: zlibClassName(mode) };
+ return null;
+}
+
+/** The builtin class a `const` binding names (`const E = EventEmitter`,
+ * `const M = Map`, `const C = zlib.Gzip`, or an alias of such a binding),
+ * or null. `let` and `var` bindings could be reassigned, and annotated
+ * bindings give the value another type, so they are not aliases. Resolved from the declaration, so uses lowered before the
+ * declaration statement (hoisted functions) see the same answer. */
+export function builtinClassAliasOf(lowerer: Lowerer, symbol: ts.Symbol): BuiltinClassRef | null {
+ const cached = lowerer.builtinClassAliases.get(symbol);
+ if (cached !== undefined) return cached;
+ // An import of an exported alias names the exporting binding.
+ if (symbol.flags & ts.SymbolFlags.Alias) {
+ lowerer.builtinClassAliases.set(symbol, null);
+ const target = lowerer.checker.getAliasedSymbol(symbol);
+ const ref = target === symbol ? null : builtinClassAliasOf(lowerer, target);
+ lowerer.builtinClassAliases.set(symbol, ref);
+ return ref;
+ }
+ const init = constInitializerOf(lowerer, symbol);
+ if (init === null) {
+ lowerer.builtinClassAliases.set(symbol, null);
+ return null;
+ }
+ // Marked first: a cycle through other bindings answers null.
+ lowerer.builtinClassAliases.set(symbol, null);
+ const ref = builtinClassRefOf(lowerer, init);
+ lowerer.builtinClassAliases.set(symbol, ref);
+ return ref;
+}
+
+/** True when `symbol` is a builtin class alias whose declaration owns no
+ * runtime slot: new, instanceof, typeof, .name and === lower at each use,
+ * and other uses report SC1090. In JavaScript files, a binding whose
+ * initializer has a value (`const E = EventEmitter`, `const M =
+ * globalThis.Map`, both identity tokens) keeps its slot; only builtin
+ * module members (`zlib.Gzip`) and copies of slotless aliases own none.
+ * The `require("events").EventEmitter` and `stream.Writable` spellings
+ * keep their own declaration handling. */
+export function builtinClassAliasOwnsNoSlot(lowerer: Lowerer, symbol: ts.Symbol): boolean {
+ if (builtinClassAliasOf(lowerer, symbol) === null) return false;
+ const binding =
+ symbol.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(symbol) : symbol;
+ const decl = lowerer.checker.valueDeclarationOf(binding);
+ if (decl === undefined || !ts.isVariableDeclaration(decl)) return false;
+ if (builtinMemberRequireDecl(decl.name, decl.initializer)) return false;
+ if (streamClassAliasDecl(lowerer, decl.name, decl.initializer)) return false;
+ if (!isJsSourceFile(decl.getSourceFile())) return true;
+ let init = decl.initializer;
+ while (init !== undefined && ts.isParenthesizedExpression(init)) init = init.expression;
+ if (init === undefined) return true;
+ let source: ts.Symbol | undefined;
+ if (ts.isPropertyAccessExpression(init)) {
+ if (lowerer.builtinNamespaceModuleOf(init.expression) !== null) return true;
+ source = lowerer.checker.getSymbolAtLocation(init.name);
+ } else {
+ source = lowerer.checker.getSymbolAtLocation(init);
+ }
+ return source !== undefined && builtinClassAliasOwnsNoSlot(lowerer, source);
+}
+
+/** True for a `const` declaration that is a builtin class alias owning no
+ * runtime slot (see builtinClassAliasOwnsNoSlot): both declaration walks
+ * skip it. */
+export function registerBuiltinClassAlias(
+ lowerer: Lowerer,
+ nameNode: ts.Node,
+ init: ts.Expression | undefined,
+): boolean {
+ if (!ts.isIdentifier(nameNode) || init === undefined) return false;
+ const symbol = lowerer.checker.getSymbolAtLocation(nameNode);
+ return symbol !== undefined && builtinClassAliasOwnsNoSlot(lowerer, symbol);
+}
+
+/** True when two references name the same builtin class. */
+export function sameBuiltinClass(a: BuiltinClassRef, b: BuiltinClassRef): boolean {
+ if (a.kind === "zlib") return b.kind === "zlib" && a.mode === b.mode;
+ if (a.kind === "collection") return b.kind === "collection" && a.which === b.which;
+ return b.kind === a.kind && b.info === a.info;
+}
+
+/** The initializer of an unannotated `const` binding whose initializer
+ * can name a class (an identifier or a property access), or null. */
+function constInitializerOf(lowerer: Lowerer, symbol: ts.Symbol): ts.Expression | null {
+ if ((symbol.flags & ts.SymbolFlags.BlockScopedVariable) === 0) return null;
+ const decl = lowerer.checker.valueDeclarationOf(symbol);
+ if (
+ decl === undefined ||
+ !ts.isVariableDeclaration(decl) ||
+ !ts.isIdentifier(decl.name) ||
+ decl.type !== undefined
+ ) {
+ return null;
+ }
+ const list = decl.parent;
+ if (!ts.isVariableDeclarationList(list) || (list.flags & ts.NodeFlags.Const) === 0) return null;
+ let init = decl.initializer;
+ while (init !== undefined && ts.isParenthesizedExpression(init)) init = init.expression;
+ if (init === undefined) return null;
+ if (ts.isIdentifier(init)) return init;
+ if (ts.isPropertyAccessExpression(init) && !init.questionDotToken) return init;
+ return null;
+}
+
+/** Node's constructor name of a runtime class (`%TypeError` → TypeError). */
+function runtimeClassName(info: ClassInfo): string {
+ return info.def.name.startsWith("%") ? info.def.name.slice(1) : info.def.name;
+}
diff --git a/packages/compiler/src/frontend/lowering/builtins/compression.ts b/packages/compiler/src/frontend/lowering/builtins/compression.ts
index c9553c2f9..6de345dfe 100644
--- a/packages/compiler/src/frontend/lowering/builtins/compression.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/compression.ts
@@ -132,8 +132,8 @@ export function zlibStreamModeOf(
symbol: ts.Symbol | null | undefined,
): number | null {
if (!symbol) return null;
- const aliasMode = zlibClassAliasModeOf(lowerer, symbol, 0);
- if (aliasMode !== undefined) return aliasMode;
+ const alias = lowerer.builtinClassAliasOf(symbol);
+ if (alias !== null) return alias.kind === "zlib" ? alias.mode : null;
const target =
symbol.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(symbol) : symbol;
const mode = ZLIB_STREAM_MODES.get(target.name);
@@ -499,80 +499,12 @@ export function lowerZlibModuleCall(
throw new InternalCompilerError(`unhandled lowered zlib member ${bi.member}`);
}
-/** The zlib stream class an expression names (`zlib.Gzip`, an imported
- * `Gzip`, a `const C = zlib.Gzip` alias), as its node_zlib_mode, or null. */
-export function zlibClassModeOfExpr(lowerer: Lowerer, expr: ts.Expression): number | null {
- let e = expr;
- while (ts.isParenthesizedExpression(e)) e = e.expression;
- if (ts.isIdentifier(e)) return zlibStreamModeOf(lowerer, lowerer.checker.getSymbolAtLocation(e));
- if (ts.isPropertyAccessExpression(e) && !e.questionDotToken) {
- return zlibStreamModeOf(lowerer, lowerer.checker.getSymbolAtLocation(e.name));
- }
- return null;
-}
-
/** The class name of a zlib stream mode (Node's constructor name). */
export function zlibClassName(mode: number): string {
for (const [name, m] of ZLIB_STREAM_MODES) if (m === mode) return name;
return "Zlib";
}
-/** The mode of a `const C = zlib.Gzip` binding (or an alias of such a
- * binding), null when the symbol is a const binding of something else,
- * undefined when it is not a const binding. Resolved from the declaration,
- * so uses lowered before the declaration statement see the same answer. */
-function zlibClassAliasModeOf(
- lowerer: Lowerer,
- symbol: ts.Symbol,
- depth: number,
-): number | null | undefined {
- const cached = lowerer.zlibClassAliases.get(symbol);
- if (cached !== undefined) return cached;
- if ((symbol.flags & ts.SymbolFlags.BlockScopedVariable) === 0) return undefined;
- const decl = lowerer.checker.valueDeclarationOf(symbol);
- if (decl === undefined || !ts.isVariableDeclaration(decl) || !ts.isIdentifier(decl.name)) {
- return undefined;
- }
- // const only: a var or let binding could be reassigned
- const list = decl.parent;
- if (!ts.isVariableDeclarationList(list) || (list.flags & ts.NodeFlags.Const) === 0) {
- return undefined;
- }
- let init = decl.initializer;
- while (init !== undefined && ts.isParenthesizedExpression(init)) init = init.expression;
- if (init === undefined || depth > 16) return null;
- let mode: number | null = null;
- if (ts.isIdentifier(init)) {
- const next = lowerer.checker.getSymbolAtLocation(init);
- if (next !== undefined) {
- const nested = zlibClassAliasModeOf(lowerer, next, depth + 1);
- mode = nested === undefined ? zlibStreamModeOf(lowerer, next) : nested;
- }
- } else if (ts.isPropertyAccessExpression(init) && !init.questionDotToken) {
- mode = zlibStreamModeOf(lowerer, lowerer.checker.getSymbolAtLocation(init.name));
- }
- if (mode !== null) lowerer.zlibClassAliases.set(symbol, mode);
- return mode;
-}
-
-/** True for `const C = zlib.Gzip` (or an alias of an alias): a
- * compile-time zlib class alias that owns no runtime slot, so the
- * statement lowering skips it. */
-export function registerZlibClassAlias(
- lowerer: Lowerer,
- nameNode: ts.Node,
- init: ts.Expression | undefined,
-): boolean {
- if (!ts.isIdentifier(nameNode) || init === undefined) return false;
- const symbol = lowerer.checker.getSymbolAtLocation(nameNode);
- return symbol !== undefined && zlibClassAliasOf(lowerer, symbol) !== null;
-}
-
-/** The mode of a `const C = zlib.Gzip` alias symbol, or null. */
-export function zlibClassAliasOf(lowerer: Lowerer, symbol: ts.Symbol): number | null {
- return zlibClassAliasModeOf(lowerer, symbol, 0) ?? null;
-}
-
/** `value instanceof zlib.Gzip` (any spelling of the class, aliases
* included). zlib streams are runtime Transforms: a Transform operand asks
* the runtime for the stream's mode, and an operand typed as one of
diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts
index 444a9cf23..120032bf5 100644
--- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts
+++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts
@@ -1159,7 +1159,9 @@ export class ClassDynamicDispatch {
// Runtime-owned roots keep their dedicated property contracts. A
// family whose fields all exist from construction and are never
// deleted keeps a fixed layout, as TypeScript classes do.
- this.deletedNames ??= deletedPropertyNames(lowerer.moduleOrder);
+ // null is a result too; undefined means not computed yet.
+ if (this.deletedNames === undefined)
+ this.deletedNames = deletedPropertyNames(lowerer.moduleOrder);
if (!root.base?.def.runtime && !fixedFieldFamily(root, this.deletedNames)) track(root);
}
const loc = this.propertyBag.loc;
diff --git a/packages/compiler/src/frontend/lowering/function-returns.ts b/packages/compiler/src/frontend/lowering/function-returns.ts
index d3c5eb1ba..5ba63be26 100644
--- a/packages/compiler/src/frontend/lowering/function-returns.ts
+++ b/packages/compiler/src/frontend/lowering/function-returns.ts
@@ -429,11 +429,14 @@ export function resolveInferredReturn(
if (ice.length > 0) lowerer.diags.push(...ice);
lowerer.runtimeFences.push(...captured.filter((d) => d.code !== "SC9001"));
const first = captured.find((d) => d.code !== "SC9001");
+ // Property writes, not literal element writes: the class method
+ // census reads `x["kind"] = …` as a possible write to every method
+ // named `kind`, which would make those methods dynamic in native builds.
const mutable = e.stmt as unknown as Record;
- delete mutable["value"];
- mutable["kind"] = "runtimeFence";
- mutable["code"] = first?.code ?? "SC1090";
- mutable["message"] = first
+ delete mutable.value;
+ mutable.kind = "runtimeFence";
+ mutable.code = first?.code ?? "SC1090";
+ mutable.message = first
? `${first.message} [${first.code}]`
: "this return value has no lowering onto the instance's settled return type [SC1090]";
}
diff --git a/packages/compiler/src/frontend/lowering/lower-builtin-values.ts b/packages/compiler/src/frontend/lowering/lower-builtin-values.ts
index a9df9e662..c05430ad3 100644
--- a/packages/compiler/src/frontend/lowering/lower-builtin-values.ts
+++ b/packages/compiler/src/frontend/lowering/lower-builtin-values.ts
@@ -23,6 +23,7 @@ import {
builtinModulesArrayLit,
stdlibGlobalNameOf,
} from "./surfaces.js";
+import { CRYPTO_CONSTANTS } from "./crypto-tables.js";
const PROTOTYPE_METHODS: Record = {
Date: ["getTime", "valueOf", "toISOString", "toJSON"],
@@ -680,3 +681,34 @@ export function lowerBuiltinLoaderValue(lowerer: Lowerer, loc: SrcLoc): IrExpr {
loc,
};
}
+
+/** The deprecated node:constants module object: the runtime merges
+ * os.constants' groups and fs.constants with crypto.constants, which a
+ * lifted function answers from the baked table the first time the object
+ * is built. */
+export function builtinConstantsObject(lowerer: Lowerer, loc: SrcLoc): IrExpr {
+ const name = "%builtin.constants.crypto";
+ if (!lowerer.builtinCallableValueFns.has(name)) {
+ lowerer.builtinCallableValueFns.set(name, name);
+ const fields = Object.entries(CRYPTO_CONSTANTS).map(([key, value]) => ({
+ key: strLit(key, loc),
+ value: box(builtinConstLit(value, loc)),
+ }));
+ lowerer.liftedFns.push({
+ name,
+ params: [],
+ returnType: DYN,
+ locals: [],
+ body: [{ kind: "return", value: { kind: "dynObjLit", fields, type: DYN, loc }, loc }],
+ loc,
+ });
+ }
+ const table: IrExpr = {
+ kind: "closure",
+ fnName: name,
+ captures: [],
+ type: { kind: "func", params: [], ret: DYN },
+ loc,
+ };
+ return { kind: "libCall", fn: "constants.object", args: [box(table)], type: DYN, loc };
+}
diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts
index 730c0567f..394458981 100644
--- a/packages/compiler/src/frontend/lowering/lower-classes.ts
+++ b/packages/compiler/src/frontend/lowering/lower-classes.ts
@@ -12,6 +12,11 @@ import { InternalCompilerError } from "../../errors.js";
import { fsStreamClassOf, lowerFsStreamNew } from "./builtins/fs-streams.js";
import * as ts from "../ts7/adapter.js";
import { lowerZlibStreamNew, zlibClassName, zlibStreamModeOf } from "./builtins/compression.js";
+import {
+ type BuiltinClassRef,
+ builtinClassAliasOf,
+ builtinClassRefOf,
+} from "./builtins/class-values.js";
import type { FnCtx, Lowerer } from "./lowerer.js";
import {
BOOL,
@@ -927,12 +932,15 @@ export function registerBuiltinStreamClasses(lowerer: Lowerer): void {
* "stream" ambient module, EXCLUDING @types/node's (whose stream.Readable
* also types child stdio — under @types/node the childStream mapping
* keeps priority and the static stream classes stand down; the shipped
- * fallback declarations are the supported surface). */
+ * fallback declarations are the supported surface). A const binding of
+ * one (`const R = Readable`) names the same class (builtinClassAliasOf). */
export function builtinStreamInfoOf(
lowerer: Lowerer,
symbol: ts.Symbol | null | undefined,
): ClassInfo | null {
if (!symbol) return null;
+ const alias = builtinClassAliasOf(lowerer, symbol);
+ if (alias !== null) return alias.kind === "stream" ? alias.info : null;
if (!lowerer.isStdlibSymbol(symbol)) {
// A const ALIAS of a namespace member (`const Writable =
// stream.Writable` — the two-step spelling; the one-step
@@ -1287,12 +1295,16 @@ function undefArmedFieldType(lowerer: Lowerer, p: ts.Symbol): IrType | null {
* `extends EventEmitter`, `x instanceof EventEmitter`) — any import
* spelling (named/default/namespace member, CJS require) resolves to
* the ambient class. Provenance-checked like the error classes: only a
- * stdlib-file declaration inside the "events" ambient module counts. */
+ * stdlib-file declaration inside the "events" ambient module counts. A
+ * const binding of it (`const E = EventEmitter`) names the same class
+ * (builtinClassAliasOf). */
export function builtinEmitterInfoOf(
lowerer: Lowerer,
symbol: ts.Symbol | null | undefined,
): ClassInfo | null {
if (!symbol) return null;
+ const alias = builtinClassAliasOf(lowerer, symbol);
+ if (alias !== null) return alias.kind === "emitter" ? alias.info : null;
if (!lowerer.isStdlibSymbol(symbol)) {
// A const ALIAS of the emitter class member (`const EventEmitter =
// require('node:events').EventEmitter` — commander's spelling; the
@@ -1333,12 +1345,17 @@ export function builtinEmitterInfoOf(
/** The builtin error ClassInfo a VALUE symbol refers to (`new Error`,
* `extends TypeError`, `x instanceof RangeError`), or null. Provenance-
* checked: only the standard library's declarations count — a user's own
- * `class Error` resolves through classBySymbol instead. */
+ * `class Error` resolves through classBySymbol instead. A const binding
+ * of one (`const E = TypeError`) names the same class
+ * (builtinClassAliasOf). */
export function builtinErrorInfoOf(
lowerer: Lowerer,
symbol: ts.Symbol | null | undefined,
): ClassInfo | null {
- if (!symbol || !lowerer.isStdlibSymbol(symbol)) return null;
+ if (!symbol) return null;
+ const alias = builtinClassAliasOf(lowerer, symbol);
+ if (alias !== null) return alias.kind === "error" ? alias.info : null;
+ if (!lowerer.isStdlibSymbol(symbol)) return null;
for (const [irName, rec] of RUNTIME_ERROR_CLASSES) {
if (rec.lib === symbol.name) return lowerer.classes.get(irName) ?? null;
}
@@ -4362,6 +4379,7 @@ export function lowerStaticFieldInits(lowerer: Lowerer, info: ClassInfo): IrStmt
// instantiation's bindings/context (a no-op for everything else —
// generic FAMILIES own their statics and carry no genericInstance).
const diagsBefore = lowerer.diags.length;
+ const sinkMark = lowerer.diagSinkMark();
try {
return withInstanceBindings(lowerer, info, () => lowerStaticFieldInitsInner(lowerer, info));
} catch (error) {
@@ -4369,8 +4387,11 @@ export function lowerStaticFieldInits(lowerer: Lowerer, info: ClassInfo): IrStmt
// Keep their refusal attached to the class declaration instead of
// letting a poisoned initializer escape the module lowering pass.
if (!(error instanceof PoisonError)) throw error;
- if (info.decl && isJsSourceFile(info.decl.getSourceFile()) && lowerer.diagSink === null) {
- const fence = lowerer.deferToRuntimeFence(diagsBefore, info.decl, { kind: "statement" });
+ if (info.decl && isJsSourceFile(info.decl.getSourceFile())) {
+ const fence =
+ lowerer.diagSink === null
+ ? lowerer.deferToRuntimeFence(diagsBefore, info.decl, { kind: "statement" })
+ : lowerer.capturedRuntimeFence(sinkMark, info.decl);
if (fence) return [fence];
}
return [];
@@ -4507,6 +4528,7 @@ function lowerStaticFieldInitsInner(lowerer: Lowerer, info: ClassInfo): IrStmt[]
lowerer.stats.statementsTotal++;
lowerer.bumpFileStat(locOf(f.initializer).file, "total");
const diagsBefore = lowerer.diags.length;
+ const sinkMark = lowerer.diagSinkMark();
try {
const previousThis = lowerer.ctx.thisLocal;
const receiver =
@@ -4542,10 +4564,11 @@ function lowerStaticFieldInitsInner(lowerer: Lowerer, info: ClassInfo): IrStmt[]
if (!(e instanceof PoisonError)) throw e;
lowerer.stats.statementsFailed++;
lowerer.bumpFileStat(locOf(f.initializer).file, "failed");
- if (isJsSourceFile(f.initializer.getSourceFile()) && lowerer.diagSink === null) {
- const fence = lowerer.deferToRuntimeFence(diagsBefore, f.initializer, {
- kind: "statement",
- });
+ if (isJsSourceFile(f.initializer.getSourceFile())) {
+ const fence =
+ lowerer.diagSink === null
+ ? lowerer.deferToRuntimeFence(diagsBefore, f.initializer, { kind: "statement" })
+ : lowerer.capturedRuntimeFence(sinkMark, f.initializer);
if (fence) out.push(fence);
}
}
@@ -6418,7 +6441,7 @@ export function collectInheritedJsFieldWrites(
* hierarchy identifies the original declarations through aliases as well. */
export function collectVirtualJsMethods(lowerer: Lowerer, files: readonly ts.SourceFile[]): void {
const families: [ts.MethodDeclaration, ts.MethodDeclaration][] = [];
- collectClassMethodMutations(lowerer, files);
+ lowerer.speculativeTypeQueries(() => collectClassMethodMutations(lowerer, files));
const visit = (node: ts.Node): void => {
if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) {
const accessors = node.members.filter(
@@ -7619,6 +7642,7 @@ export function fieldInitStmts(lowerer: Lowerer, info: ClassInfo, thisLocal: IrL
lowerer.stats.statementsTotal++;
lowerer.bumpFileStat(locOf(f.initializer).file, "total");
const diagsBefore = lowerer.diags.length;
+ const sinkMark = lowerer.diagSinkMark();
try {
const value = lowerer.lowerExprExpecting(f.initializer, f.type);
out.push({
@@ -7633,10 +7657,11 @@ export function fieldInitStmts(lowerer: Lowerer, info: ClassInfo, thisLocal: IrL
if (!(e instanceof PoisonError)) throw e;
lowerer.stats.statementsFailed++;
lowerer.bumpFileStat(locOf(f.initializer).file, "failed");
- if (isJsSourceFile(f.initializer.getSourceFile()) && lowerer.diagSink === null) {
- const fence = lowerer.deferToRuntimeFence(diagsBefore, f.initializer, {
- kind: "statement",
- });
+ if (isJsSourceFile(f.initializer.getSourceFile())) {
+ const fence =
+ lowerer.diagSink === null
+ ? lowerer.deferToRuntimeFence(diagsBefore, f.initializer, { kind: "statement" })
+ : lowerer.capturedRuntimeFence(sinkMark, f.initializer);
if (fence) out.push(fence);
}
}
@@ -8438,6 +8463,12 @@ function assignedThisFieldType(lowerer: Lowerer, expr: ts.NewExpression): IrType
return lowerer.currentClass?.fields.get(parent.left.name.text) ?? null;
}
+/** Map, Set and the error classes construct in lowerNew's identifier
+ * branch, which a module member alias of one (`new mod.M()`) also takes. */
+function isCollectionOrErrorClass(ref: BuiltinClassRef | null): boolean {
+ return ref?.kind === "collection" || ref?.kind === "error";
+}
+
export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
const loc = locOf(expr);
const worker = lowerWorkerNew(lowerer, expr);
@@ -8732,7 +8763,10 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
ts.isIdentifier(expr.expression) ||
(ts.isPropertyAccessExpression(expr.expression) &&
ts.isIdentifier(expr.expression.name) &&
- lowerer.isStdlibGlobal(expr.expression.expression, "globalThis"))
+ (lowerer.isStdlibGlobal(expr.expression.expression, "globalThis") ||
+ // `new mod.M()` over an exported `const M = Map` or `const E =
+ // TypeError`
+ isCollectionOrErrorClass(builtinClassRefOf(lowerer, expr.expression))))
) {
// `import C = N.C; new C()` — the alias's own source-order guards
// (a no-op for every non-import= binding).
@@ -9097,7 +9131,8 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
if (symbol?.name === "Array" && lowerer.isStdlibSymbol(symbol)) {
return lowerArrayConstructor(lowerer, expr, expr.arguments ?? []);
}
- if (symbol?.name === "Map" && lowerer.isStdlibSymbol(symbol)) {
+ // A const alias (`const M = Map`) constructs the same Map.
+ if (ts.isIdentifier(identifier) && lowerer.isStdlibGlobal(identifier, "Map")) {
const seedArg = (expr.arguments?.length ?? 0) === 1 ? expr.arguments![0]! : null;
const isPairLit = (el: ts.Expression): el is ts.ArrayLiteralExpression =>
ts.isArrayLiteralExpression(el) &&
@@ -9229,7 +9264,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
}
// `new Set()`: construct + bulk add from supported values,
// including live collection cursors. Duplicates collapse in insertion order.
- if (symbol?.name === "Set" && lowerer.isStdlibSymbol(symbol)) {
+ if (ts.isIdentifier(identifier) && lowerer.isStdlibGlobal(identifier, "Set")) {
const tsType = lowerer.typeOf(expr);
let mapped = lowerer.mapTypeOf(tsType);
const fieldType = assignedThisFieldType(lowerer, expr);
diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts
index 1644424e7..5c5313215 100644
--- a/packages/compiler/src/frontend/lowering/lower-exprs.ts
+++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts
@@ -36,10 +36,12 @@ import {
isZlibStreamMember,
lowerZlibInstanceOf,
lowerZlibStreamBytesWritten,
- zlibClassAliasOf,
- zlibClassModeOfExpr,
- zlibClassName,
} from "./builtins/compression.js";
+import {
+ builtinClassAliasOwnsNoSlot,
+ builtinClassRefOf,
+ sameBuiltinClass,
+} from "./builtins/class-values.js";
import { constituentTypes } from "../ts7/checker.js";
import { dirname } from "node:path";
import * as posix from "node:path/posix";
@@ -204,6 +206,7 @@ import { tryLowerExpression } from "./expressions/try-lower-expression.js";
import {
fenceNodeModuleMutation,
isNodeModuleValue,
+ lowerCjsTopLevelThis,
lowerNodeModuleIdentifier,
lowerNodeModuleProperty,
lowerRequireCacheElement,
@@ -228,6 +231,7 @@ import {
} from "./class-prototypes.js";
import { isClassCallback } from "./class-callbacks.js";
import {
+ builtinConstantsObject,
builtinPrototypeMethod,
lowerArrayIsArrayValue,
lowerCheckedPredicateValue,
@@ -616,8 +620,9 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
if (isNodeModuleValue(lowerer, expr.expression)) {
return { kind: "strLit", value: "object", type: STRING, loc };
}
- // `typeof zlib.Gzip`, `typeof C` over a zlib class alias.
- if (zlibClassModeOfExpr(lowerer, expr.expression) !== null) {
+ // `typeof EventEmitter`, `typeof zlib.Gzip`, `typeof C` over a
+ // builtin class alias.
+ if (builtinClassRefOf(lowerer, expr.expression) !== null) {
return { kind: "strLit", value: "function", type: STRING, loc };
}
if (ts.isPropertyAccessExpression(expr.expression)) {
@@ -691,6 +696,30 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
if (!sym) {
return { kind: "strLit", value: "undefined", type: STRING, loc };
}
+ // `const { inspect } = require("util")` / `import { inspect } from
+ // "node:util"`: a binding of a builtin module's exported function,
+ // class or object. JavaScript sources represent such a value without
+ // its own lowering as an identity token (a string), so the operand's
+ // representation cannot answer. Node's answer follows the export's
+ // kind, and the binding read has no effects.
+ const imported = isJsSourceFile(tested.getSourceFile())
+ ? lowerer.builtinImportOf(tested)
+ : null;
+ if (
+ imported &&
+ builtinModuleConstOf(lowerer, imported.module, imported.member) === undefined
+ ) {
+ const t = lowerer.typeOf(tested);
+ if (
+ lowerer.checker.getCallSignatures(t).length > 0 ||
+ lowerer.checker.getConstructSignatures(t).length > 0
+ ) {
+ return { kind: "strLit", value: "function", type: STRING, loc };
+ }
+ if ((t.flags & ts.TypeFlags.Object) !== 0) {
+ return { kind: "strLit", value: "object", type: STRING, loc };
+ }
+ }
if (lowerer.isStdlibSymbol(sym)) {
const t = lowerer.typeOf(expr.expression);
if (
@@ -878,13 +907,13 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
`a builtin function alias as an escaping value (call '${expr.text}' directly)`,
);
}
- // `const C = zlib.Gzip` owns no runtime slot: construction,
+ // `const E = EventEmitter` owns no runtime slot: construction,
// instanceof, typeof and .name lower at their use sites.
- if (sym && zlibClassAliasOf(lowerer, sym) !== null) {
+ if (sym && builtinClassAliasOwnsNoSlot(lowerer, sym)) {
lowerer.unsupported(
"SC1090",
expr,
- `a zlib class alias as an escaping value (use '${expr.text}' with new, instanceof, typeof or .name)`,
+ `a builtin class alias as an escaping value (use '${expr.text}' with new, instanceof, typeof or .name)`,
);
}
}
@@ -1235,6 +1264,17 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
if (bi.module === "os" && bi.member === "constants") {
return { kind: "libCall", fn: "os.constants", args: [], type: DYN, loc };
}
+ // `import { O_RDONLY } from "constants"`: the member of the frozen
+ // module object.
+ if (bi.module === "constants") {
+ return {
+ kind: "dynKeyGet",
+ value: builtinConstantsObject(lowerer, loc),
+ key: { kind: "strLit", value: bi.member, type: STRING, loc },
+ type: DYN,
+ loc,
+ };
+ }
// tls.rootCertificates: a runtime-valued module constant (the
// cached bundled-CA array) — the one member read that lowers
// to a libCall instead of a baked literal.
@@ -1277,6 +1317,7 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
// their access sites only. The CommonJS namespace binding
// (`const lib = require("./lib.js")`) gets the same fence.
const builtinNamespace = lowerer.builtinNamespaceModuleOf(expr);
+ if (builtinNamespace === "constants") return builtinConstantsObject(lowerer, loc);
if (builtinNamespace !== null) {
const type: IrType = { kind: "moduleNs", moduleId: `builtin:${builtinNamespace}` };
return { kind: "moduleNsRef", moduleId: type.moduleId, type, loc };
@@ -1566,6 +1607,7 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
return { kind: "moduleNsRef", moduleId: moduleNs.moduleId, type: moduleNs, loc };
}
const builtinNs = lowerer.builtinNamespaceModuleOf(expr);
+ if (builtinNs === "constants") return builtinConstantsObject(lowerer, loc);
if (builtinNs !== null) {
const type: IrType = { kind: "moduleNs", moduleId: `builtin:${builtinNs}` };
return { kind: "moduleNsRef", moduleId: type.moduleId, type, loc };
@@ -1668,6 +1710,9 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
// constant. TypeScript keeps the fence (noImplicitThis makes it a
// compile-time story there).
if (isJsSourceFile(expr.getSourceFile())) {
+ // Module scope of a CommonJS file: `this` is module.exports.
+ const exportsThis = lowerCjsTopLevelThis(lowerer, expr);
+ if (exportsThis) return exportsThis;
return { kind: "libCall", fn: "dyn.this", args: [], type: DYN, loc };
}
lowerer.unsupported("SC1080", expr);
@@ -1829,10 +1874,11 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
if (table) return table;
}
lowerThreadSlotsProperty(lowerer, expr);
- // `zlib.Gzip.name`, `C.name` over a zlib class alias.
+ // `EventEmitter.name`, `zlib.Gzip.name`, `C.name` over a builtin
+ // class alias.
if (expr.name.text === "name" && !expr.questionDotToken) {
- const mode = zlibClassModeOfExpr(lowerer, expr.expression);
- if (mode !== null) return { kind: "strLit", value: zlibClassName(mode), type: STRING, loc };
+ const ref = builtinClassRefOf(lowerer, expr.expression);
+ if (ref !== null) return { kind: "strLit", value: ref.name, type: STRING, loc };
}
// Expando statics may still be absent even when JS inference sees
// the eventual assignment's scalar type. Preserve the checked value.
@@ -10246,17 +10292,18 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
lowerUnionTypeofTest(lowerer, expr, loc);
if (test) return test;
}
- // `C === zlib.Gzip` between two zlib class references: the classes
- // are compile-time values, so the answer folds.
+ // `C === EventEmitter` between two builtin class references: the
+ // classes are compile-time values, so the answer folds.
if (
op === ts.SyntaxKind.EqualsEqualsEqualsToken ||
op === ts.SyntaxKind.ExclamationEqualsEqualsToken
) {
- const leftClass = zlibClassModeOfExpr(lowerer, expr.left);
- const rightClass = leftClass === null ? null : zlibClassModeOfExpr(lowerer, expr.right);
+ const leftClass = builtinClassRefOf(lowerer, expr.left);
+ const rightClass = leftClass === null ? null : builtinClassRefOf(lowerer, expr.right);
if (leftClass !== null && rightClass !== null) {
const negated = op === ts.SyntaxKind.ExclamationEqualsEqualsToken;
- return { kind: "boolLit", value: (leftClass === rightClass) !== negated, type: BOOL, loc };
+ const same = sameBuiltinClass(leftClass, rightClass);
+ return { kind: "boolLit", value: same !== negated, type: BOOL, loc };
}
}
@@ -12332,11 +12379,11 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
// h2 compat 'connect' narrowing (lower-server.ts): a union tag test.
const sockTest = lowerSocketInstanceOf(lowerer, expr, loc);
if (sockTest !== null) return sockTest;
- const zlibMode = zlibClassModeOfExpr(lowerer, expr.right);
- if (zlibMode !== null) return lowerZlibInstanceOf(lowerer, expr, zlibMode, loc);
+ const rightClass = builtinClassRefOf(lowerer, expr.right);
+ if (rightClass?.kind === "zlib") return lowerZlibInstanceOf(lowerer, expr, rightClass.mode, loc);
- if (lowerer.isStdlibGlobal(expr.right, "Set") || lowerer.isStdlibGlobal(expr.right, "Map")) {
- const collection = lowerer.isStdlibGlobal(expr.right, "Map") ? "map" : "set";
+ if (rightClass?.kind === "collection") {
+ const collection = rightClass.which === "Map" ? "map" : "set";
const value = lowerer.lowerExpr(expr.left);
if (value.type.kind === "union") {
const union = lowerer.unions.get(value.type.unionId);
diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts
index c5802b07e..3c035cd45 100644
--- a/packages/compiler/src/frontend/lowering/lower-modules.ts
+++ b/packages/compiler/src/frontend/lowering/lower-modules.ts
@@ -80,7 +80,7 @@ import {
createRequireProgramModuleDecl,
} from "./builtins/module-bindings.js";
import { isPromisifyCall, registerBuiltinCallableAlias } from "./builtins/callable-aliases.js";
-import { registerZlibClassAlias } from "./builtins/compression.js";
+import { registerBuiltinClassAlias } from "./builtins/class-values.js";
import { stripTypeCasts } from "./builtins/arguments.js";
import {
bindingContextualGenericFnNodeOf,
@@ -1701,7 +1701,7 @@ export function collectGlobals(
// resolution and value reads reuse the source's interned closure.
if (isConst && registerOverloadedCallableAlias(lowerer, decl)) continue;
if (isConst && registerBuiltinCallableAlias(lowerer, decl.name, decl.initializer)) continue;
- if (registerZlibClassAlias(lowerer, decl.name, decl.initializer)) continue;
+ if (registerBuiltinClassAlias(lowerer, decl.name, decl.initializer)) continue;
// `const f = (x) => ...` at file scope of an npm-static JS file
// whose params are implicit-any: the implicit-monomorphization
// twin of the generic binding above — no global exists; calls and
@@ -2664,6 +2664,18 @@ export function lowerFileInit(
loc: loc0,
});
}
+ if (sf === lowerer.entry) header.push(...lowerer.punycodeLinkWarning());
+ // Top-level `this` of a CommonJS module: its object exists before the
+ // module's first statement (lowerCjsTopLevelThis).
+ const exportsThis = lowerer.cjsExportsThis.get(sf);
+ if (exportsThis) {
+ header.push({
+ kind: "assign",
+ localId: exportsThis.id,
+ value: { kind: "dynObjLit", fields: [], type: DYN, loc: loc0 },
+ loc: loc0,
+ });
+ }
for (const { stmt, dep } of orderedImportsOf(lowerer.program, sf)) {
const npm = lowerer.npmInitActions.get(sf)?.get(stmt);
if (npm !== undefined && npm.length > 0) {
diff --git a/packages/compiler/src/frontend/lowering/lower-node-module.ts b/packages/compiler/src/frontend/lowering/lower-node-module.ts
index ad3f86dee..c15749601 100644
--- a/packages/compiler/src/frontend/lowering/lower-node-module.ts
+++ b/packages/compiler/src/frontend/lowering/lower-node-module.ts
@@ -8,10 +8,11 @@ import * as win32 from "node:path/win32";
import * as ts from "../ts7/adapter.js";
import type { Lowerer } from "./lowerer.js";
import type { FileParts } from "./lower-modules.js";
-import { isNodeEsmFile, locOf } from "../program.js";
+import { isCjsJsFile, isNodeEsmFile, locOf } from "../program.js";
import { runtimePathForTarget } from "../runtime-resolve.js";
import {
BOOL,
+ DYN,
F64,
type IrExpr,
type IrStmt,
@@ -224,6 +225,135 @@ function nodeModuleType(lowerer: Lowerer, node: ts.Expression): boolean {
return rendered === "ScriptcModule" || rendered === "NodeModule" || rendered === "NodeJS.Module";
}
+/** `module.exports` used only for its truthiness (the UMD test `typeof
+ * module !== "undefined" && module.exports`): the exports object starts as
+ * a fresh object, so it stays truthy unless the module replaces it with a
+ * value that can be falsy. True when every `module.exports = value` in the
+ * file assigns an object or function. */
+function moduleExportsAlwaysTruthy(lowerer: Lowerer, expr: ts.PropertyAccessExpression): boolean {
+ if (!truthinessOnly(expr)) return false;
+ const sf = expr.getSourceFile();
+ if (isNodeEsmFile(sf, lowerer.program)) return false;
+ let truthy = true;
+ ts.walkPreorder(sf, (node) => {
+ if (
+ !ts.isBinaryExpression(node) ||
+ node.operatorToken.kind < ts.SyntaxKind.FirstAssignment ||
+ node.operatorToken.kind > ts.SyntaxKind.LastAssignment
+ )
+ return undefined;
+ let target = node.left;
+ while (ts.isParenthesizedExpression(target)) target = target.expression;
+ if (
+ !ts.isPropertyAccessExpression(target) ||
+ target.name.text !== "exports" ||
+ !isCjsModuleGlobal(lowerer, target.expression)
+ )
+ return undefined;
+ const type = lowerer.typeOf(node.right);
+ const objectLike = (t: ts.Type): boolean =>
+ t.isUnionType()
+ ? ts.constituentTypes(t).every(objectLike)
+ : (t.flags & ts.TypeFlags.Object) !== 0 || (t.flags & ts.TypeFlags.NonPrimitive) !== 0;
+ if (node.operatorToken.kind !== ts.SyntaxKind.EqualsToken || !objectLike(type)) {
+ truthy = false;
+ return "stop";
+ }
+ return undefined;
+ });
+ return truthy;
+}
+
+/** Whether only the truthiness of `node` can be observed: a condition, a
+ * `!` operand, or a `&&`/`||` operand of an expression that is itself used
+ * only for its truthiness. */
+function truthinessOnly(node: ts.Expression): boolean {
+ let child: ts.Node = node;
+ for (let parent = node.parent; ; child = parent, parent = parent.parent) {
+ if (ts.isParenthesizedExpression(parent)) continue;
+ if (ts.isPrefixUnaryExpression(parent))
+ return parent.operator === ts.SyntaxKind.ExclamationToken;
+ if (
+ ts.isBinaryExpression(parent) &&
+ (parent.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ||
+ parent.operatorToken.kind === ts.SyntaxKind.BarBarToken)
+ )
+ continue;
+ if (ts.isIfStatement(parent) || ts.isWhileStatement(parent) || ts.isDoStatement(parent))
+ return parent.expression === child;
+ if (ts.isForStatement(parent)) return parent.condition === child;
+ if (ts.isConditionalExpression(parent)) return parent.condition === child;
+ return false;
+ }
+}
+
+/** `this` at the top level of a CommonJS module (directly, or inside
+ * arrow functions there): Node calls the module wrapper with `this` bound
+ * to the module's original exports object. */
+export function isCjsTopLevelThis(lowerer: Lowerer, node: ts.Node): boolean {
+ if (node.kind !== ts.SyntaxKind.ThisKeyword) return false;
+ for (let p: ts.Node | undefined = node.parent; p && !ts.isSourceFile(p); p = p.parent) {
+ if (ts.isArrowFunction(p)) continue;
+ if (
+ ts.isFunctionLike(p) ||
+ ts.isClassDeclaration(p) ||
+ ts.isClassExpression(p) ||
+ ts.isClassStaticBlockDeclaration(p)
+ )
+ return false;
+ }
+ return isCjsJsFile(node.getSourceFile(), lowerer.program);
+}
+
+export function usesCjsTopLevelThis(lowerer: Lowerer, sf: ts.SourceFile): boolean {
+ if (!isCjsJsFile(sf, lowerer.program)) return false;
+ let found = false;
+ ts.walkPreorder(sf, (node) => {
+ if (
+ node !== sf &&
+ !ts.isArrowFunction(node) &&
+ (ts.isFunctionLike(node) ||
+ ts.isClassDeclaration(node) ||
+ ts.isClassExpression(node) ||
+ ts.isClassStaticBlockDeclaration(node))
+ )
+ return "skip";
+ if (node.kind === ts.SyntaxKind.ThisKeyword) {
+ found = true;
+ return "stop";
+ }
+ return undefined;
+ });
+ return found;
+}
+
+/** The value of top-level `this` in a CommonJS module: the module's own
+ * object standing for its original exports, created when the module starts
+ * (lowerFileInit). Exports themselves are the static export table, so a
+ * property access through `this` would read or write an object no requirer
+ * sees; it keeps a named refusal instead. Typeof, truthiness, identity and
+ * passing the value along (the UMD wrapper `(function (root) { ... })(this)`)
+ * behave as in Node. */
+export function lowerCjsTopLevelThis(lowerer: Lowerer, expr: ts.Expression): IrExpr | null {
+ if (!isCjsTopLevelThis(lowerer, expr)) return null;
+ const global = lowerer.cjsExportsThis.get(expr.getSourceFile());
+ if (!global) return null;
+ let receiver: ts.Node = expr;
+ while (ts.isParenthesizedExpression(receiver.parent)) receiver = receiver.parent;
+ const access = receiver.parent;
+ if (
+ (ts.isPropertyAccessExpression(access) || ts.isElementAccessExpression(access)) &&
+ access.expression === receiver
+ ) {
+ lowerer.unsupported(
+ "SC1090",
+ access,
+ "property access through top-level 'this' in a CommonJS module (it is module.exports; spell exports.)",
+ );
+ }
+ return varRef(global.id, DYN, locOf(expr));
+}
+
function optionalModule(
lowerer: Lowerer,
node: ts.Expression,
@@ -309,6 +439,8 @@ export function lowerNodeModuleProperty(
if (!direct && !nodeModuleType(lowerer, expr.expression)) return null;
if (expr.questionDotToken) return null;
const member = expr.name.text;
+ if (member === "exports" && direct && moduleExportsAlwaysTruthy(lowerer, expr))
+ return boolLit(true, locOf(expr));
if (member === "exports" || member === "require") return null;
let moduleRef = direct
? cjsModuleRef(lowerer, expr.expression)
diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts
index 9eb512332..57e6c2311 100644
--- a/packages/compiler/src/frontend/lowering/lower-stmts.ts
+++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts
@@ -148,7 +148,7 @@ import {
} from "./builtins/module-bindings.js";
import { lowerNodeModuleCall } from "./builtins/module-resolution.js";
import { registerBuiltinCallableAlias } from "./builtins/callable-aliases.js";
-import { registerZlibClassAlias } from "./builtins/compression.js";
+import { builtinClassAliasOwnsNoSlot, registerBuiltinClassAlias } from "./builtins/class-values.js";
import { lowerEnumDeclaration } from "./lower-enums.js";
import {
abstractPropertyDeclOf,
@@ -672,6 +672,7 @@ export function lowerStmts(lowerer: Lowerer, stmts: readonly ts.Statement[]): Ir
if (sf !== undefined) lowerer.bumpFileStat(sf.fileName, "total");
}
const diagsBefore = lowerer.diags.length;
+ const sinkMark = lowerer.diagSinkMark();
try {
let lowered: IrStmt | IrStmt[] | null;
let appended = false;
@@ -756,6 +757,11 @@ export function lowerStmts(lowerer: Lowerer, stmts: readonly ts.Statement[]): Ir
},
});
if (fence) out.push(fence);
+ } else if (deferFences) {
+ // Inside a capture buffer the owner keeps the diagnostics, but
+ // the statement still must not vanish from a result it keeps.
+ const fence = lowerer.capturedRuntimeFence(sinkMark, stmt);
+ if (fence) out.push(fence);
}
}
}
@@ -847,6 +853,9 @@ export function predeclareForwardCapture(lowerer: Lowerer, symbol: ts.Symbol): b
if (!decl.initializer) return false;
const flags = ts.getCombinedNodeFlags(decl);
if ((flags & ts.NodeFlags.BlockScoped) === 0 || (flags & ts.NodeFlags.Using) !== 0) return false;
+ // `const E = EventEmitter` has no storage to box: uses resolve at
+ // compile time (builtinClassAliasOf).
+ if (builtinClassAliasOwnsNoSlot(lowerer, symbol)) return false;
// MODULE-scope bindings are pre-registered globals (collectGlobals):
// references resolve through globalOf after the local search fails, so
// a TDZ box here would SHADOW the global and never fill (the top-level
@@ -1577,7 +1586,9 @@ function lowerVarDeclarator(
// Relative requires keep the fence: their module-init-at-position
// semantics needs storage this model doesn't represent.
if (canonicalBuiltinModule(spec) !== null) {
- return [];
+ return canonicalBuiltinModule(spec) === "punycode"
+ ? [lowerer.punycodeRequireWarning(decl)]
+ : [];
}
lowerer.unsupported(
"SC1090",
@@ -4423,7 +4434,7 @@ export function lowerVarDecl(
// reads reuse its interned closure identity.
if (!isLet && registerOverloadedCallableAlias(lowerer, decl)) return null;
if (!isLet && registerBuiltinCallableAlias(lowerer, decl.name, decl.initializer)) return null;
- if (registerZlibClassAlias(lowerer, decl.name, decl.initializer)) return null;
+ if (registerBuiltinClassAlias(lowerer, decl.name, decl.initializer)) return null;
// `const knownBy = (cmd) => ...` — an IMPLICIT-ANY function-value
// binding in npm-static JS (function-scope bindings register here, in
@@ -4512,16 +4523,21 @@ export function lowerVarDecl(
if (wrapped) return { kind: "assign", localId: g.id, value: wrapped, loc: locOf(decl) };
return null;
}
- const raw = lowerVariableInitializer(
- lowerer,
- decl.initializer,
- g.type.kind === "dyn" &&
- (isNativeBuiltinValueInitializer(lowerer, decl.initializer) ||
- ts.isArrayLiteralExpression(decl.initializer) ||
- ts.isObjectLiteralExpression(decl.initializer))
- ? DYN
- : undefined,
- );
+ const guardedRead = isGuardedRecordRead(lowerer, decl)
+ ? lowerAbsenceProbe(lowerer, decl.initializer)
+ : null;
+ const raw =
+ guardedRead ??
+ lowerVariableInitializer(
+ lowerer,
+ decl.initializer,
+ g.type.kind === "dyn" &&
+ (isNativeBuiltinValueInitializer(lowerer, decl.initializer) ||
+ ts.isArrayLiteralExpression(decl.initializer) ||
+ ts.isObjectLiteralExpression(decl.initializer))
+ ? DYN
+ : undefined,
+ );
if (
isJsSourceFile(decl.getSourceFile()) &&
!decl.type &&
@@ -5317,29 +5333,111 @@ export function jsBindingHasOpenWrites(lowerer: Lowerer, decl: ts.VariableDeclar
}
function immediatelyGuardedAbsenceProbe(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean {
- if (decl.type || !ts.isIdentifier(decl.name) || !decl.initializer) return false;
+ const guard = immediateAbsenceGuard(lowerer, decl);
+ // `if (!x)`: the falsy test sees a missing value.
+ if (guard === "falsy") return true;
+ // `if (x === undefined)` and its spellings compare the binding with the
+ // missing value itself, so the read must be able to produce it. Only
+ // index-signature record reads take this form: array reads already
+ // carry their undefined arm, and their ordinary lowering keeps its
+ // bounds proofs.
+ return guard === "compare" && isIndexSignatureRecordRead(lowerer, decl);
+}
+
+/** A file-scope or block-scope `let e = map[k]` over an index-signature
+ * record whose next statement guards `e` for absence (`if (e ===
+ * undefined)`, `if (!e)`, ...): the get-or-create idiom. Its read keeps the
+ * missing key as undefined even without noUncheckedIndexedAccess. */
+export function isGuardedRecordRead(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean {
+ return immediateAbsenceGuard(lowerer, decl) !== null && isIndexSignatureRecordRead(lowerer, decl);
+}
+
+function isIndexSignatureRecordRead(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean {
+ if (!decl.initializer) return false;
+ let init = decl.initializer;
+ while (ts.isParenthesizedExpression(init)) init = init.expression;
+ if (!ts.isElementAccessExpression(init)) return false;
+ // Under noUncheckedIndexedAccess the checker already spells the missing
+ // arm, and the ordinary read carries it.
+ const read = lowerer.typeOf(init);
+ if (
+ (read.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0 ||
+ (read.isUnionType() &&
+ ts.constituentTypes(read).some((t) => (t.flags & ts.TypeFlags.Undefined) !== 0))
+ )
+ return false;
+ const receiver = lowerer.mapTypeOf(lowerer.typeOf(init.expression));
+ const shape = receiver?.kind === "record" ? lowerer.shapes.get(receiver.shapeId) : undefined;
+ return !!shape && !shape.tuple && !!shape.indexValue && shape.indexValue.kind !== "dyn";
+}
+
+/** How the statement right after an unannotated declaration tests the
+ * declared binding for absence: `if (!x)` ("falsy"), or a comparison with
+ * the missing value — `x === undefined`, `x == null`, `x !== void 0`,
+ * `typeof x === "undefined"` and their mirrored and negated spellings
+ * ("compare"). Null for anything else. */
+function immediateAbsenceGuard(
+ lowerer: Lowerer,
+ decl: ts.VariableDeclaration,
+): "falsy" | "compare" | null {
+ if (decl.type || !ts.isIdentifier(decl.name) || !decl.initializer) return null;
const statement = decl.parent?.parent;
- if (!statement || !ts.isVariableStatement(statement)) return false;
+ if (!statement || !ts.isVariableStatement(statement)) return null;
const container = statement.parent;
const statements =
ts.isBlock(container) || ts.isSourceFile(container) ? container.statements : null;
- if (!statements) return false;
+ if (!statements) return null;
const index = statements.indexOf(statement);
const next = index >= 0 ? statements[index + 1] : undefined;
- if (!next || !ts.isIfStatement(next)) return false;
- let condition = next.expression;
- while (ts.isParenthesizedExpression(condition)) condition = condition.expression;
- if (
- !ts.isPrefixUnaryExpression(condition) ||
- condition.operator !== ts.SyntaxKind.ExclamationToken
- )
- return false;
- let operand = condition.operand;
- while (ts.isParenthesizedExpression(operand)) operand = operand.expression;
- return (
- ts.isIdentifier(operand) &&
- lowerer.resolveValueSymbol(operand) === lowerer.resolveValueSymbol(decl.name)
- );
+ if (!next || !ts.isIfStatement(next)) return null;
+ const binding = lowerer.resolveValueSymbol(decl.name);
+ const peel = (node: ts.Expression): ts.Expression => {
+ while (ts.isParenthesizedExpression(node)) node = node.expression;
+ return node;
+ };
+ const isBinding = (node: ts.Expression): boolean => {
+ const operand = peel(node);
+ return ts.isIdentifier(operand) && lowerer.resolveValueSymbol(operand) === binding;
+ };
+ const condition = peel(next.expression);
+ if (ts.isPrefixUnaryExpression(condition))
+ return condition.operator === ts.SyntaxKind.ExclamationToken && isBinding(condition.operand)
+ ? "falsy"
+ : null;
+ if (!ts.isBinaryExpression(condition)) return null;
+ const op = condition.operatorToken.kind;
+ const strict =
+ op === ts.SyntaxKind.EqualsEqualsEqualsToken ||
+ op === ts.SyntaxKind.ExclamationEqualsEqualsToken;
+ const loose =
+ op === ts.SyntaxKind.EqualsEqualsToken || op === ts.SyntaxKind.ExclamationEqualsToken;
+ if (!strict && !loose) return null;
+ const isUndefined = (node: ts.Expression): boolean => {
+ const operand = peel(node);
+ if (ts.isVoidExpression(operand)) return true;
+ return (
+ ts.isIdentifier(operand) &&
+ operand.text === "undefined" &&
+ (lowerer.typeOf(operand).flags & ts.TypeFlags.Undefined) !== 0
+ );
+ };
+ const isNull = (node: ts.Expression): boolean => peel(node).kind === ts.SyntaxKind.NullKeyword;
+ const missing = (node: ts.Expression): boolean => isUndefined(node) || (loose && isNull(node));
+ const isTypeofBinding = (node: ts.Expression): boolean => {
+ const operand = peel(node);
+ return ts.isTypeOfExpression(operand) && isBinding(operand.expression);
+ };
+ const isUndefinedTag = (node: ts.Expression): boolean => {
+ const operand = peel(node);
+ return ts.isStringLiteralLike(operand) && operand.text === "undefined";
+ };
+ const { left, right } = condition;
+ return (isBinding(left) && missing(right)) ||
+ (missing(left) && isBinding(right)) ||
+ (isTypeofBinding(left) && isUndefinedTag(right)) ||
+ (isUndefinedTag(left) && isTypeofBinding(right))
+ ? "compare"
+ : null;
}
function falsyExitRuntimeOptionalLocals(lowerer: Lowerer, stmt: ts.IfStatement): IrLocal[] {
diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts
index 4e04702b9..ac38afe26 100644
--- a/packages/compiler/src/frontend/lowering/lowerer.ts
+++ b/packages/compiler/src/frontend/lowering/lowerer.ts
@@ -112,6 +112,13 @@ import { ClassDynamicDispatch, trackClassFieldCreation } from "./class-dynamic-d
import { ClassProtocols } from "./class-protocols.js";
import { earlyReadVarSymbols } from "./var-hoisting.js";
import { recordTextCodecClass } from "../../ir/ir.js";
+import { assignLegacyTiers, type DynamicFunction } from "../../ir/tiers.js";
+import {
+ analyzeTiers,
+ dynamicStatementCount,
+ type TierMode,
+ type TierReport,
+} from "../tier-analysis.js";
import { DeferredModuleInitializers } from "./deferred-module-initializers.js";
import { finalizeClassMethodValues } from "./class-method-values.js";
import type { ClassSymbolKey } from "./symbol-fields.js";
@@ -136,6 +143,7 @@ import { resolve } from "node:path";
import { bindingSource } from "../binding-source.js";
import { tsgoPath } from "../dts-paths.js";
import * as ts from "../ts7/adapter.js";
+import { erasedTypeOnlyImport } from "../type-only-imports.js";
import { literalValues } from "../literal-values.js";
import { discriminantOwners, literalUnionArm, type UnionLiteral } from "../union-discriminants.js";
import type { ScrDiagnostic } from "../../diagnostics/diagnostic.js";
@@ -255,6 +263,7 @@ import {
stdlibGlobalMember,
nodeTypesOnlySymbol,
} from "./surfaces.js";
+import { type BuiltinClassRef, builtinClassAliasOf } from "./builtins/class-values.js";
import {
type FileParts,
splitFiles,
@@ -272,7 +281,7 @@ import {
appendForkModules,
appendWorkerModules,
} from "./lower-modules.js";
-import { prepareCjsModuleGraph } from "./lower-node-module.js";
+import { prepareCjsModuleGraph, usesCjsTopLevelThis } from "./lower-node-module.js";
import { registerBuiltinEventTargetClasses } from "./lower-event-target.js";
import {
type ClassInfo,
@@ -534,6 +543,7 @@ import {
lowerForOf,
lowerForStatement,
matchGroupsPatternSource,
+ isGuardedRecordRead,
} from "./lower-stmts.js";
import {
type FieldTarget,
@@ -631,6 +641,9 @@ export interface LowerStats {
/** Functions whose signature couldn't be analyzed (bodies not counted). */
functionsSkipped: number;
+ /** Coverage only: reached statements that compile into dynamic-tier
+ * functions (tier-analysis.ts), a subset of the compiled statements. */
+ statementsDynamic?: number;
}
/** Every identifier a binding name binds: the identifier itself, or all
@@ -705,6 +718,11 @@ export interface LowerResult {
/** Compile-time-detectable behavior differences from Node (SC6xxx
* divergence warnings) in the program's own modules. Never failures. */
divergences?: ScrDiagnostic[];
+ /** Functions lowering placed in the dynamic tier (ir/tiers.ts), present
+ * when there are any. */
+ dynamicFunctions?: DynamicFunction[];
+ /** LowerOptions.tierReport: the report-only tier assignment. */
+ tiers?: TierReport;
}
export interface LowerOptions {
@@ -754,6 +772,12 @@ export interface LowerOptions {
/** The mapped entries plus relative declaration dependencies, attributed
* to their owning external specifier. */
externalTypeSpecifiersByFile?: ReadonlyMap;
+ /** `--tier` (tier-analysis.ts); "auto" when absent. */
+ tierMode?: TierMode;
+ /** Coverage only: compute the tier-assignment report (`--explain-dynamic`).
+ * `preflight` holds the import and package refusals coverage reports
+ * beside the lowering's own blockers; they request module demotions. */
+ tierReport?: { preflight: readonly ScrDiagnostic[] };
}
interface RuntimeFenceFallback {
@@ -829,6 +853,8 @@ export interface LowererMode {
externalTypes?: ReadonlyMap;
/** LowerOptions.externalTypeSpecifiersByFile, shared by every pass. */
externalTypeSpecifiersByFile?: ReadonlyMap;
+ /** LowerOptions.tierMode, shared by every pass. */
+ tierMode?: TierMode;
}
function directExternalTypeSpecifiersByFile(
@@ -920,6 +946,7 @@ export function lowerToIr(
const externalTypes = options.externalTypes ?? new Map();
const externalTypeSpecifiersByFile =
options.externalTypeSpecifiersByFile ?? directExternalTypeSpecifiersByFile(externalTypes);
+ const tierMode = options.tierMode ?? "auto";
const validation = new Lowerer(program, entry, moduleOrder, {
frontendServices: options.frontendServices,
projectCheckJs: options.projectCheckJs ?? false,
@@ -931,6 +958,7 @@ export function lowerToIr(
workerTargets,
externalTypes,
externalTypeSpecifiersByFile,
+ tierMode,
});
const ffiValidation = validateFfiImports(validation);
timing("ffi-validate");
@@ -953,6 +981,7 @@ export function lowerToIr(
workerTargets,
externalTypes,
externalTypeSpecifiersByFile,
+ tierMode,
ffiBindingSymbols: ffiValidation.symbolsByName,
});
for (const d of extraModuleCycleDiags) reachableEmit.pushDiag(d);
@@ -983,6 +1012,7 @@ export function lowerToIr(
ffiBindingSymbols: ffiValidation.symbolsByName,
externalTypes,
externalTypeSpecifiersByFile,
+ tierMode,
});
for (const d of extraModuleCycleDiags) emit.pushDiag(d);
for (const d of ffiValidation.diagnostics) emit.pushDiag(d);
@@ -1041,19 +1071,44 @@ export function lowerToIr(
ffiBindingSymbols: ffiValidation.symbolsByName,
externalTypes,
externalTypeSpecifiersByFile,
+ tierMode,
});
const rem = remainder.run();
+ timing("coverage-remainder");
+ // Tiers (tier-analysis.ts): reached statements in dynamic-tier functions,
+ // and with tierReport the report-only demotion analysis.
+ const dynamicFunctions = resultLowerer.dynamicBodies;
+ const tiers =
+ options.tierReport === undefined
+ ? undefined
+ : analyzeTiers(program, {
+ mode: tierMode,
+ dynamicFunctions,
+ reached: [...options.tierReport.preflight, ...result.diagnostics],
+ deferred: result.runtimeFences,
+ unreached: [...rem.diagnostics, ...rem.runtimeFences],
+ });
+ // A dynamic function's own statements can include ones that failed.
+ const statementsDynamic = Math.min(
+ tiers?.statementsDynamic ?? dynamicStatementCount(program, dynamicFunctions),
+ result.stats.statementsTotal - result.stats.statementsFailed,
+ );
+ if (tiers !== undefined) tiers.statementsDynamic = statementsDynamic;
+ timing("tier-analysis", { dynamic: dynamicFunctions.length });
// Unreached JS statements whose fences deferred to runtime count as
// failed statements in the remainder's stats, so their diagnostics must
// travel with them: a failed statement is never left unexplained.
- return {
+ const covered: LowerResult = {
...result,
+ stats: { ...result.stats, statementsDynamic },
unreached: {
diagnostics: rem.diagnostics,
stats: rem.stats,
...(rem.runtimeFences.length > 0 ? { runtimeFences: rem.runtimeFences } : {}),
},
};
+ if (tiers !== undefined) covered.tiers = tiers;
+ return covered;
}
/** The native namespace type produced by a literal static import() of a
@@ -1806,11 +1861,11 @@ export class Lowerer {
* typeParamBindings — the checker has no `T` to substitute, so the
* binding rides the node-type accessor instead of mapType. */
implicitParamTypes: Map | null = null;
- /** Immutable bindings that name a zlib stream class (`const C =
- * zlib.Gzip`), keyed by the binding's symbol: the class's
- * node_zlib_mode. No runtime slot exists: new, typeof, .name and ===
- * resolve at compile time, and instanceof checks the stream's mode. */
- readonly zlibClassAliases = new Map();
+ /** The builtin class each binding names (`const E = EventEmitter`,
+ * `const C = zlib.Gzip`), keyed by the binding's symbol; null for
+ * bindings that name none (builtinClassAliasOf). new, instanceof,
+ * typeof, .name and === resolve at compile time. */
+ readonly builtinClassAliases = new Map();
/** Whether some net.Socket in this program may get an encoding: a
* setEncoding call whose receiver is a socket or an untyped value.
* Computed once on first use (null until then). */
@@ -1860,6 +1915,9 @@ export class Lowerer {
* carry undefined at runtime. Identifier reads keep the union tag so
* typeof and later boundaries observe the real value. */
readonly runtimeOptionalGlobals = new Set();
+ /** Per CommonJS file using top-level `this`: the global holding the
+ * object that stands for its original exports (lowerCjsTopLevelThis). */
+ readonly cjsExportsThis = new Map();
/** Bindings whose unchecked string arithmetic can produce either a number
* (NaN for an absent read) or a string. Preserve that result union at bare
* reads instead of applying the checker-bare string narrowing adapter. */
@@ -2703,6 +2761,14 @@ export class Lowerer {
/** Coverage remainder mode: the reachability gate inverts (see wantBody)
* and no module is built. */
readonly remainder: boolean;
+ /** `--tier` (tier-analysis.ts). Stage S2 is report-only: both modes
+ * refuse and fence exactly as before. Stage S6 demotes the units of
+ * committed refusals in "auto" and keeps them refusals in "static-only". */
+ readonly tierMode: TierMode;
+ /** Functions finishModule placed in the dynamic tier (ir/tiers.ts). */
+ dynamicFunctions: DynamicFunction[] = [];
+ /** Their IR, for coverage's dynamic statement count. */
+ dynamicBodies: IrFunction[] = [];
/** Deferred collection diagnostics (failed signatures/class shapes) by
* declaration symbol: an unreached declaration must not fail the build,
* so its diagnostics wait until a reference makes them relevant. */
@@ -2789,6 +2855,7 @@ export class Lowerer {
this.frontendServices = mode.frontendServices;
this.reachable = mode.reachable ?? null;
this.remainder = mode.remainder ?? false;
+ this.tierMode = mode.tierMode ?? "auto";
this.alreadyFlushed = mode.alreadyFlushed ?? new Set();
this.targetPlatform = mode.targetPlatform ?? process.platform;
this.startupCrash = mode.startupCrash ?? null;
@@ -2878,7 +2945,8 @@ export class Lowerer {
return null;
}
const className = this.classNamer(decl);
- if (!this.collectingExprClasses.has(decl)) this.lowerClassExpressionInfo(decl);
+ if (!this.collectingExprClasses.has(decl) && !this.collectLocalClassForType(decl))
+ return null;
return { kind: "object", className };
},
mixinIntersectionInstance: (widened) => mixinIntersectionInstanceType(this, widened),
@@ -3021,6 +3089,10 @@ export class Lowerer {
return builtinStreamInfoOf(this, symbol);
}
+ builtinClassAliasOf(symbol: ts.Symbol): BuiltinClassRef | null {
+ return builtinClassAliasOf(this, symbol);
+ }
+
/** Program-wide qualified name for a top-level declaration. */
qualify(sf: ts.SourceFile, name: string): string {
return `${this.fileTag.get(sf) ?? ""}${name}`;
@@ -3715,6 +3787,16 @@ export class Lowerer {
const id = `%g.${tag}%loaded`;
this.moduleGuardOf.set(fp.sf, id);
this.globalsList.push({ id, name: "%loaded", type: BOOL, mutable: true });
+ if (usesCjsTopLevelThis(this, fp.sf)) {
+ const exportsThis: IrGlobal = {
+ id: `%g.${tag}%exportsThis`,
+ name: "%exportsThis",
+ type: DYN,
+ mutable: true,
+ };
+ this.globalsList.push(exportsThis);
+ this.cjsExportsThis.set(fp.sf, exportsThis);
+ }
}
// A module is intrinsically async when an await/for-await occurs
@@ -3834,6 +3916,65 @@ export class Lowerer {
for (const fp of parts) if (!indexOf.has(fp.sf)) visit(fp.sf);
}
+ /** node:punycode's load at a require() call: the module body reports
+ * DEP0040 when it first runs, unless isInsideNodeModules() finds the
+ * requiring frame in a dependency, and process.emitWarning delivers the
+ * report on the next tick. Later loads are cache hits either way. */
+ punycodeRequireWarning(node: ts.Node): IrStmt {
+ const loc = locOf(node);
+ const fromDependency = /[\\/]node_modules[\\/]/.test(node.getSourceFile().fileName);
+ return {
+ kind: "exprStmt",
+ expr: {
+ kind: "libCall",
+ fn: "punycode.warn",
+ args: [
+ { kind: "boolLit", value: false, type: BOOL, loc },
+ { kind: "boolLit", value: !fromDependency, type: BOOL, loc },
+ ],
+ type: VOID,
+ loc,
+ },
+ loc,
+ };
+ }
+
+ /** node:punycode's DEP0040 for a static ES import anywhere in the module
+ * graph: Node links builtins before any module evaluates, so the report
+ * comes first, before the program can register a 'warning' listener —
+ * whichever file imports it (a dependency's ES import has no requiring
+ * frame either). Prepended to the entry's %init. */
+ punycodeLinkWarning(): IrStmt[] {
+ const imports = this.moduleOrder.some((sf) =>
+ sf.statements.some(
+ (stmt) =>
+ ((ts.isImportDeclaration(stmt) && !erasedTypeOnlyImport(stmt)) ||
+ (ts.isExportDeclaration(stmt) && !stmt.isTypeOnly)) &&
+ stmt.moduleSpecifier !== undefined &&
+ ts.isStringLiteral(stmt.moduleSpecifier) &&
+ canonicalBuiltinModule(stmt.moduleSpecifier.text) === "punycode",
+ ),
+ );
+ if (!imports) return [];
+ const loc: SrcLoc = { file: this.entry.fileName, start: 0, end: 0 };
+ return [
+ {
+ kind: "exprStmt",
+ expr: {
+ kind: "libCall",
+ fn: "punycode.warn",
+ args: [
+ { kind: "boolLit", value: true, type: BOOL, loc },
+ { kind: "boolLit", value: true, type: BOOL, loc },
+ ],
+ type: VOID,
+ loc,
+ },
+ loc,
+ },
+ ];
+ }
+
/** The lowering of a CommonJS `require("./local")` occurrence: a call of
* the required module's run-once %init at exactly this statement's
* position — Node's inline evaluation, with the guard supplying the
@@ -3843,6 +3984,7 @@ export class Lowerer {
* their external dependencies by name). Null for everything else
* (builtins load nothing; the rest kept its preflight fence). */
requireInitStmt(spec: string, node: ts.Node): IrStmt | null {
+ if (canonicalBuiltinModule(spec) === "punycode") return this.punycodeRequireWarning(node);
// Project requires resolve within the program; another bare require can
// be a program-module edge when it names an opted-in --npm-static package
// (one package requiring another — the resolution answered its shipped
@@ -3969,6 +4111,33 @@ export class Lowerer {
}
}
+ /** A file-scope get-or-create read (`let e = map[k]; if (e ===
+ * undefined) ...` over an index-signature record) keeps a missing key as
+ * undefined (isGuardedRecordRead). Its pre-registered global takes the
+ * undefined arm before any body reads it; the declaration lowers its
+ * initializer as the matching absence probe. */
+ analyzeGuardedRecordReadGlobals(parts: FileParts[]): void {
+ for (const { sf } of parts) {
+ for (const statement of sf.statements) {
+ if (!ts.isVariableStatement(statement)) continue;
+ for (const decl of statement.declarationList.declarations) {
+ if (!ts.isIdentifier(decl.name) || !isGuardedRecordRead(this, decl)) continue;
+ const global = this.globalOf(decl.name);
+ const symbol = this.resolveValueSymbol(decl.name);
+ if (!global || !symbol || global.tdz || this.runtimeOptionalBindingTypes.has(symbol))
+ continue;
+ const widened =
+ global.type.kind === "union"
+ ? this.withUndefinedArmOf(global.type)
+ : this.withUndefinedArm(global.type);
+ if (!widened || typeEquals(widened, global.type)) continue;
+ global.type = widened;
+ this.runtimeOptionalBindingTypes.set(symbol, widened);
+ }
+ }
+ }
+ }
+
analyzeRuntimeOptionalArrayReads(parts: FileParts[]): void {
const provenance = RuntimeOptionalProvenance.fromEnvironment();
// The site whose value the current promotion follows (debug only).
@@ -6211,8 +6380,11 @@ export class Lowerer {
for (const fp of parts) this.checker.prefetchSourceFile(fp.sf);
}
this.collectProgram(parts);
- this.analyzeRuntimeOptionalArrayReads(parts);
- this.analyzeDateJsonResults(parts);
+ this.speculativeTypeQueries(() => {
+ this.analyzeRuntimeOptionalArrayReads(parts);
+ this.analyzeDateJsonResults(parts);
+ this.analyzeGuardedRecordReadGlobals(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
@@ -6466,11 +6638,18 @@ export class Lowerer {
};
if (module) sanitizeUnregisteredClassTypes(module, (name) => this.classes.has(name));
if (module) checkCycleBindingAccesses(this, module);
+ // Execution tiers from the final bodies (ir/tiers.ts). Assigned with or
+ // without a module: coverage reports the dynamic tier of programs that
+ // still have blockers.
+ this.dynamicFunctions = assignLegacyTiers(functions);
+ if (this.dynamicFunctions.length > 0)
+ this.dynamicBodies = functions.filter((fn) => fn.tier === "dynamic");
return {
module,
diagnostics: this.diags,
runtimeFences: this.runtimeFences,
stats: this.stats,
+ ...(this.dynamicFunctions.length > 0 ? { dynamicFunctions: this.dynamicFunctions } : {}),
...(this.statsByFile.size > 0 ? { statsByFile: this.statsByFile } : {}),
...(this.provenanceElided.length > 0 ? { provenanceElided: this.provenanceElided } : {}),
};
@@ -6600,8 +6779,11 @@ export class Lowerer {
collectionEdges.add(name);
};
this.collectProgram(parts);
- this.analyzeRuntimeOptionalArrayReads(parts);
- this.analyzeDateJsonResults(parts);
+ this.speculativeTypeQueries(() => {
+ this.analyzeRuntimeOptionalArrayReads(parts);
+ this.analyzeDateJsonResults(parts);
+ this.analyzeGuardedRecordReadGlobals(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.
@@ -7095,23 +7277,8 @@ export class Lowerer {
}
this.runtimeFences.push(...captured);
- const first = captured[0];
+ const fence = this.runtimeFenceStmt(captured[0], node, target.fallback, target.bareMessage);
const loc = locOf(node);
- const code = first?.code ?? target.fallback!.code;
- const baseMessage = first?.message ?? target.fallback!.message;
- const messageLoc = first?.loc ?? loc;
- const source = first
- ? (this.program.getSourceFile(messageLoc.file) ?? node.getSourceFile())
- : node.getSourceFile();
- const pos = ts.getLineAndCharacterOfPosition(source, messageLoc.start);
- const fence: IrStmt = {
- kind: "runtimeFence",
- code,
- message: target.bareMessage
- ? baseMessage
- : `${baseMessage} [${code} at ${messageLoc.file}:${pos.line + 1}]`,
- loc,
- };
if (target.kind === "statement") return fence;
const params = target.params ?? [];
@@ -7139,6 +7306,54 @@ export class Lowerer {
return { kind: "closure", fnName: fn.name, captures: [], type: target.type, loc };
}
+ /** The runtimeFence statement that throws `first`'s message (or the
+ * fallback's) at `node`. */
+ private runtimeFenceStmt(
+ first: ScrDiagnostic | undefined,
+ node: ts.Node,
+ fallback: { code: string; message: string } | undefined,
+ bareMessage = false,
+ ): IrStmt & { kind: "runtimeFence" } {
+ const loc = locOf(node);
+ const code = first?.code ?? fallback?.code ?? "SC1090";
+ const baseMessage =
+ first?.message ??
+ fallback?.message ??
+ "this statement uses a construct with no static lowering";
+ const messageLoc = first?.loc ?? loc;
+ const source = first
+ ? (this.program.getSourceFile(messageLoc.file) ?? node.getSourceFile())
+ : node.getSourceFile();
+ const pos = ts.getLineAndCharacterOfPosition(source, messageLoc.start);
+ return {
+ kind: "runtimeFence",
+ code,
+ message: bareMessage
+ ? baseMessage
+ : `${baseMessage} [${code} at ${messageLoc.file}:${pos.line + 1}]`,
+ loc,
+ };
+ }
+
+ /** The current length of the capture buffer (diagSink), or 0 without one:
+ * the mark capturedRuntimeFence reads the refusal from. */
+ diagSinkMark(): number {
+ return this.diagSink?.length ?? 0;
+ }
+
+ /** The runtimeFence for a JavaScript statement refused while a capture
+ * buffer (diagSink) is active. The buffer's owner decides whether its
+ * diagnostics report, and some owners keep the lowered result (a
+ * successful tryLowerExpression candidate, a computed class base whose
+ * diagnostics defer to the class). Without a fence the refused statement
+ * would vanish from that result; with one it throws the refusal when it
+ * runs, exactly like the fence lowerStmts emits outside a buffer. The
+ * diagnostics stay in the buffer for its owner. Null without a buffer. */
+ capturedRuntimeFence(sinkMark: number, node: ts.Node): IrStmt | null {
+ if (this.diagSink === null) return null;
+ return this.runtimeFenceStmt(this.diagSink[sinkMark], node, undefined);
+ }
+
/** All diagnostics land here; while a generic instance body is lowering,
* the instantiation context is appended so the user knows which concrete
* types made the (source-anchored) construct fail. */
@@ -9753,6 +9968,43 @@ export class Lowerer {
return lowerClassExpressionInfo(this, expr);
}
+ /** Depth of the whole-program analyses that query types across every
+ * file before reachability is known (speculativeTypeQueries). */
+ private speculativeTypeQueryDepth = 0;
+
+ /** Runs a whole-program analysis whose type queries are speculative: they
+ * visit every file, reached or not, and no statement or declaration owns
+ * a refusal they trigger (collectLocalClassForType). */
+ speculativeTypeQueries(analysis: () => void): void {
+ this.speculativeTypeQueryDepth++;
+ try {
+ analysis();
+ } finally {
+ this.speculativeTypeQueryDepth--;
+ }
+ }
+
+ /** Collects a function-local class because a type query named its
+ * instance type. Collection and body lowering own a refusal of that
+ * class: the poison reaches the declaration or statement that asked, as
+ * before. A speculative whole-program analysis owns none, so there the
+ * refusal stays reported (the build fails with it, as it did where such
+ * an analysis already caught the poison) and the class is unmappable for
+ * the query instead of the poison escaping the compiler. */
+ collectLocalClassForType(decl: ts.ClassLikeDeclaration): boolean {
+ if (this.speculativeTypeQueryDepth === 0 || this.fnStack.length > 0) {
+ this.lowerClassExpressionInfo(decl);
+ return true;
+ }
+ try {
+ this.lowerClassExpressionInfo(decl);
+ return true;
+ } catch (e) {
+ if (e instanceof PoisonError) return false;
+ throw e;
+ }
+ }
+
lowerClassExpression(expr: ts.ClassLikeDeclaration): IrExpr {
return lowerClassExpression(this, expr);
}
@@ -11559,7 +11811,23 @@ export class Lowerer {
return { module: "timers/promises", member: `scheduler.${access.name.text}` };
}
}
+ // punycode.ucs2.decode / encode: methods of the module's ucs2 object,
+ // dispatched as module members.
+ if (access.name.text === "decode" || access.name.text === "encode") {
+ const owner = access.expression;
+ const imported = ts.isIdentifier(owner)
+ ? this.builtinImportOf(owner)
+ : ts.isPropertyAccessExpression(owner)
+ ? this.builtinMemberOf(owner)
+ : null;
+ if (imported?.module === "punycode" && imported.member === "ucs2") {
+ return { module: "punycode", member: `ucs2.${access.name.text}` };
+ }
+ }
const module = this.builtinNamespaceModuleOf(access.expression);
+ // node:constants is one checked-dynamic object: its members read
+ // through the object itself (builtinConstantsObject), not the tables.
+ if (module === "constants") return null;
if (module !== null) return { module, member: access.name.text };
// The events module object IS the EventEmitter class, so the class's
// defaultMaxListeners is the module property.
diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts
index d394c1960..44ed37e3c 100644
--- a/packages/compiler/src/frontend/lowering/surfaces.ts
+++ b/packages/compiler/src/frontend/lowering/surfaces.ts
@@ -1822,6 +1822,22 @@ export const BUILTIN_MODULE_FNS: Record<
valueParams: [{ mode: "required", type: STRING }],
},
},
+ // node:punycode (deprecated): punycode.js 2.1.0's conversions over
+ // strings (scr_url.c). ucs2.decode/encode are members of the module's
+ // ucs2 object, dispatched as module members (builtinMemberOf). The
+ // DEP0040 report rides the module's load sites (punycodeLoadWarning).
+ punycode: {
+ decode: { fn: "punycode.decode", params: [STRING], result: STRING },
+ encode: { fn: "punycode.encode", params: [STRING], result: STRING },
+ toASCII: { fn: "punycode.toASCII", params: [STRING], result: STRING },
+ toUnicode: { fn: "punycode.toUnicode", params: [STRING], result: STRING },
+ "ucs2.decode": { fn: "punycode.ucs2Decode", params: [STRING], result: arrayOf(F64) },
+ "ucs2.encode": { fn: "punycode.ucs2Encode", params: [arrayOf(F64)], result: STRING },
+ },
+ // node:constants (deprecated) is one frozen object (Object.assign of
+ // os.constants' groups, fs.constants and crypto.constants): bindings of
+ // the module read it as a checked-dynamic value (builtinConstantsObject).
+ constants: {},
// node:readline: createInterface's options are entirely special-cased
// (exactly { input: process.stdin, output?: process.stdout } — see
// lowerBuiltinModuleCall); the entry carries the canonical shape. The
@@ -1951,6 +1967,7 @@ export const BUILTIN_MODULE_CONSTS: Record<
os: { EOL: "\n", devNull: "/dev/null" },
worker_threads: { isMainThread: true, threadId: 0 },
cluster: { isPrimary: true, isMaster: true, isWorker: false },
+ punycode: { version: "2.1.0" },
// zlib's module-level copies of its constants (every member outside
// BROTLI_*, Node's own loop over zlib.constants).
zlib: Object.fromEntries(
@@ -3207,7 +3224,9 @@ export function isStdlibSymbol(lowerer: Lowerer, symbol: ts.Symbol | undefined):
* `global.process`) — `declare var` globals ARE properties of
* `typeof globalThis`, same symbol either way;
* - a local alias binding (`const process = globalThis.process`, the
- * tamper-guard prologue) registered in stdlibGlobalAliases. */
+ * tamper-guard prologue) registered in stdlibGlobalAliases;
+ * - a const binding of the Map or Set constructor (`const M = Map`,
+ * builtinClassAliasOf). */
export function stdlibGlobalNameOf(lowerer: Lowerer, expr: ts.Expression): string | null {
if (ts.isParenthesizedExpression(expr)) return stdlibGlobalNameOf(lowerer, expr.expression);
if (ts.isIdentifier(expr)) {
@@ -3225,6 +3244,8 @@ export function stdlibGlobalNameOf(lowerer: Lowerer, expr: ts.Expression): strin
if (!symbol) return null;
const alias = lowerer.stdlibGlobalAliases.get(symbol);
if (alias !== undefined) return alias;
+ const classAlias = lowerer.builtinClassAliasOf(symbol);
+ if (classAlias?.kind === "collection") return classAlias.which;
const decl = lowerer.checker.declarationsOf(symbol)[0];
if (decl !== undefined && ts.isImportClause(decl) && decl.name !== undefined) {
const importDecl = decl.parent;
diff --git a/packages/compiler/src/frontend/npm-static-prune.ts b/packages/compiler/src/frontend/npm-static-prune.ts
index c6c1f9caf..b71d6ca33 100644
--- a/packages/compiler/src/frontend/npm-static-prune.ts
+++ b/packages/compiler/src/frontend/npm-static-prune.ts
@@ -10,12 +10,27 @@ import { nearestPackageType, resolveBareModule } from "./resolve.js";
import { npmStaticPackageOfPath } from "./npm-static.js";
import { trackedReadFile } from "./input-tracker.js";
import { npmPackageNameOf } from "./workspace-registry.js";
+import { typeOnlyNamedImport } from "./type-only-imports.js";
export function isPrunedNpmReexport(program: ts.Program, stmt: ts.ExportDeclaration): boolean {
return program.analysis.prunedNpmReexports.has(stmt);
}
-function packageJson(path: string): Record | null {
+type PackageJson = Record | null;
+
+/** Parsed package.json files by path, kept for one plan: every purity check
+ * reads the metadata of its file's package again. */
+type PackageJsonCache = Map;
+
+function packageJson(path: string, cache: PackageJsonCache): PackageJson {
+ const cached = cache.get(path);
+ if (cached !== undefined) return cached;
+ const json = readPackageJson(path);
+ cache.set(path, json);
+ return json;
+}
+
+function readPackageJson(path: string): PackageJson {
const source = trackedReadFile(path);
if (source === null) return null;
try {
@@ -58,12 +73,16 @@ function sideEffectPattern(pattern: string): RegExp {
/** A nested format scope such as dist/esm/package.json is not the package's
* published metadata. Stay inside the resolved package while finding its
* named root, including pnpm's realpath and workspace-linked installs. */
-function packageRootJsonPath(fromFile: string, packageName: string): string | null {
+function packageRootJsonPath(
+ fromFile: string,
+ packageName: string,
+ cache: PackageJsonCache,
+): string | null {
let dir = dirname(resolve(fromFile));
let root: string | null = null;
while (npmPackageNameOf(dir.replaceAll("\\", "/")) === packageName) {
const path = join(dir, "package.json");
- if (packageJson(path)?.["name"] === packageName) root = path;
+ if (packageJson(path, cache)?.["name"] === packageName) root = path;
const parent = dirname(dir);
if (parent === dir) break;
dir = parent;
@@ -73,9 +92,13 @@ function packageRootJsonPath(fromFile: string, packageName: string): string | nu
// An external package's executable sources may not be in this program.
// Preserve the conservative whole-dependency promise in that case.
-function pureExternalPackage(path: string, visiting = new Set()): boolean {
+function pureExternalPackage(
+ path: string,
+ cache: PackageJsonCache,
+ visiting = new Set(),
+): boolean {
if (visiting.has(path)) return true;
- const json = packageJson(path);
+ const json = packageJson(path, cache);
const value = json?.["sideEffects"];
if (json === null || !(value === false || (Array.isArray(value) && value.length === 0)))
return false;
@@ -85,8 +108,8 @@ function pureExternalPackage(path: string, visiting = new Set()): boolea
if (!dependencies || typeof dependencies !== "object" || Array.isArray(dependencies)) continue;
for (const name of Object.keys(dependencies)) {
const resolved = resolveBareModule(path, name, "js-only");
- const root = resolved === null ? null : packageRootJsonPath(resolved.typesFile, name);
- if (root === null || !pureExternalPackage(root, visiting)) return false;
+ const root = resolved === null ? null : packageRootJsonPath(resolved.typesFile, name, cache);
+ if (root === null || !pureExternalPackage(root, cache, visiting)) return false;
}
}
return true;
@@ -135,10 +158,11 @@ export function planNpmStaticReexports(
for (const path of extraRoots) request(program.getSourceFile(path) ?? null, null);
const pureMemo = new Map();
+ const packageJsons: PackageJsonCache = new Map();
const filePure = (sf: ts.SourceFile): boolean => {
const pkg = npmPackageNameOf(sf.fileName) ?? npmStaticPackageOfPath(sf.fileName);
- const path = pkg === null ? null : packageRootJsonPath(sf.fileName, pkg);
- const json = path === null ? null : packageJson(path);
+ const path = pkg === null ? null : packageRootJsonPath(sf.fileName, pkg, packageJsons);
+ const json = path === null ? null : packageJson(path, packageJsons);
const value = json?.["sideEffects"];
if (value === false) return true;
if (
@@ -195,8 +219,10 @@ export function planNpmStaticReexports(
const resolved = resolveBareModule(sf.fileName, edge.spec, "js-only");
const pkg = resolved === null ? null : npmPackageNameOf(resolved.typesFile);
const root =
- pkg === null || resolved === null ? null : packageRootJsonPath(resolved.typesFile, pkg);
- return root !== null && pureExternalPackage(root);
+ pkg === null || resolved === null
+ ? null
+ : packageRootJsonPath(resolved.typesFile, pkg, packageJsons);
+ return root !== null && pureExternalPackage(root, packageJsons);
});
};
const pure = visit(root);
@@ -232,7 +258,9 @@ export function planNpmStaticReexports(
(clause.namedBindings !== undefined &&
(ts.isNamespaceImport(clause.namedBindings) ||
(ts.isNamedImports(clause.namedBindings) &&
- clause.namedBindings.elements.length === 0)))
+ clause.namedBindings.elements.length === 0))) ||
+ // `import { type A }` still loads the module, like `import {}`.
+ typeOnlyNamedImport(stmt)
) {
request(dep, null);
} else {
@@ -244,12 +272,10 @@ export function planNpmStaticReexports(
}
}
} else if (ts.isExportDeclaration(stmt)) {
+ // `export { type A } from` still loads the module under Node's
+ // type stripping; only `export type … from` erases.
if (
stmt.isTypeOnly ||
- (stmt.exportClause !== undefined &&
- ts.isNamedExports(stmt.exportClause) &&
- stmt.exportClause.elements.length > 0 &&
- stmt.exportClause.elements.every((element) => element.isTypeOnly)) ||
stmt.moduleSpecifier === undefined ||
!ts.isStringLiteral(stmt.moduleSpecifier)
)
diff --git a/packages/compiler/src/frontend/pipeline.ts b/packages/compiler/src/frontend/pipeline.ts
index 2ec9553f6..c769ed99e 100644
--- a/packages/compiler/src/frontend/pipeline.ts
+++ b/packages/compiler/src/frontend/pipeline.ts
@@ -25,9 +25,18 @@ import { isJsSourceFileName } from "./tsc-codes.js";
import { isRelativeSpecifier, packageNameOfSpecifier } from "./workspace-registry.js";
import { lowerToIr, type LowerOptions, type LowerResult } from "./lowering/lowerer.js";
import type { FrontendServices } from "./services.js";
-import { isImportDeclaration, isExportDeclaration, isStringLiteral } from "./ts7/adapter.js";
+import {
+ isIdentifier,
+ isImportDeclaration,
+ isExportDeclaration,
+ isStringLiteral,
+ isStringLiteralLike,
+ SyntaxKind,
+} from "./ts7/adapter.js";
+import { moduleSourceCandidates } from "./module-source-candidates.js";
import { CONTRACT_TYPES_MODULE } from "./contract-types.js";
import { THREADS_MODULE } from "./threads-import.js";
+import { erasedTypeOnlyImport } from "./type-only-imports.js";
import { EXPERIMENTAL_MODULE } from "./experimental-import.js";
export interface ProgramLoadOptions {
@@ -91,15 +100,36 @@ function detectAutoPackages(
mode: "auto" | "lib" = "auto",
judged?: Set,
sites?: Map,
+ transitive: Set = new Set(),
): string[] {
+ const chosen: 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,
// or a package visible only to a nested package.json realm (a pnpm
// monorepo's packages/*/node_modules, unreachable from the entry's own
// walk-up) answers "no runtime JS" for perfectly ordinary installs.
- const seen = new Map();
- for (const sf of [...load.moduleOrder, load.entry]) {
+ const seen = new Map<
+ string,
+ { typesFile: string; fromFile: string; spec: string; owner: string | null }
+ >();
+ // A transitive package's files outside the module order (reached through
+ // nested require() calls the graph walk does not model) still name its
+ // dependencies.
+ const scanned = new Set([...load.moduleOrder, load.entry]);
+ const transitiveFiles =
+ transitive.size === 0
+ ? []
+ : load.program.getImplementationSourceFiles().filter((sf) => {
+ const pkg = npmStaticPackageOfPath(sf.fileName);
+ return !scanned.has(sf) && pkg !== null && transitive.has(pkg);
+ });
+ for (const sf of [...scanned, ...transitiveFiles]) {
if (mode === "auto" && sf.fileName.includes("/node_modules/")) continue;
+ // `--npm-static=pkg...` opts pkg's whole dependency closure in: every
+ // package its compiled files import is selected the same explicit way,
+ // without the declaration eligibility bar.
+ const owner = npmStaticPackageOfPath(sf.fileName);
+ const viaTransitive = owner !== null && transitive.has(owner);
const edges: { spec: string; loc: SrcLoc }[] = [];
for (const stmt of sf.statements) {
if (
@@ -107,13 +137,31 @@ function detectAutoPackages(
stmt.moduleSpecifier &&
isStringLiteral(stmt.moduleSpecifier)
) {
+ // `import type` and `export type … from` erase under Node's type
+ // stripping: Node never loads the package, so it is no dependency
+ // to compile (its declarations still type the program).
+ if (isImportDeclaration(stmt) ? erasedTypeOnlyImport(stmt) : stmt.isTypeOnly) continue;
edges.push({ spec: stmt.moduleSpecifier.text, loc: locOf(stmt) });
- } else if (mode === "lib") {
+ } else if (mode === "lib" && !viaTransitive) {
// CJS packages spell their dep edges as top-level requires; the
// import-statement scan alone would miss every one of them.
for (const req of requiresOf(stmt)) edges.push({ spec: req.spec, loc: locOf(req.node) });
}
}
+ // Transitive selection counts every literal require() and import()
+ // anywhere in the package, nested ones included.
+ if (viaTransitive) {
+ for (const call of moduleSourceCandidates(load.program, sf).calls) {
+ const arg = call.arguments[0];
+ if (
+ arg !== undefined &&
+ isStringLiteralLike(arg) &&
+ (call.expression.kind === SyntaxKind.ImportKeyword ||
+ (isIdentifier(call.expression) && call.expression.text === "require"))
+ )
+ edges.push({ spec: arg.text, loc: locOf(call) });
+ }
+ }
for (const { spec, loc } of edges) {
if (load.externalTypes?.has(spec)) continue;
// scriptc's own packages are not npm dependencies: contract types are
@@ -137,28 +185,51 @@ function detectAutoPackages(
if (judged?.has(packageNameOfSpecifier(spec))) continue;
const npm = resolveNpmImport(sf.fileName, spec);
if (npm !== null && isNodeTypesPath(npm.typesFile)) continue;
- if (npm === null) {
- if (mode !== "lib") continue;
- const js = resolveBareModule(sf.fileName, spec, "js-only");
- if (js === null || judged!.has(js.packageName)) continue;
- judged!.add(js.packageName);
- sites!.set(js.packageName, loc);
+ // The runtime package Node loads. Types-first resolution answers an
+ // @types twin's declarations for a package that ships none, but
+ // selection and every refusal name the package that has the code.
+ const js =
+ mode === "lib" && (npm === null || npm.packageName.startsWith("@types/"))
+ ? resolveBareModule(sf.fileName, spec, "js-only")
+ : null;
+ const runtimePkg = js !== null && js.packageName !== npm?.packageName ? js.packageName : null;
+ if (npm === null || runtimePkg !== null) {
+ if (mode !== "lib" || runtimePkg === null || judged!.has(runtimePkg)) continue;
+ judged!.add(runtimePkg);
+ sites!.set(runtimePkg, loc);
+ if (viaTransitive) {
+ transitive.add(runtimePkg);
+ chosen.push(runtimePkg);
+ continue;
+ }
statuses.push({
- package: js.packageName,
+ package: runtimePkg,
status: "refused",
- detail: "it ships no own .d.ts declaration surface",
+ detail:
+ npm === null
+ ? "it ships no own .d.ts declaration surface"
+ : `its declared types come from the separate ${npm.packageName} package, not the package itself`,
+ hint: npmStaticOptInHint(runtimePkg, owner),
});
continue;
}
if (judged?.has(npm.packageName)) continue;
+ // An @types twin with no runtime package installed has nothing to
+ // compile (Node cannot load it either).
+ if (viaTransitive && !npm.packageName.startsWith("@types/")) {
+ judged!.add(npm.packageName);
+ sites!.set(npm.packageName, loc);
+ transitive.add(npm.packageName);
+ chosen.push(npm.packageName);
+ continue;
+ }
if (!seen.has(npm.packageName)) {
- seen.set(npm.packageName, { typesFile: npm.typesFile, fromFile: sf.fileName, spec });
+ seen.set(npm.packageName, { typesFile: npm.typesFile, fromFile: sf.fileName, spec, owner });
sites?.set(npm.packageName, loc);
}
}
}
- const chosen: string[] = [];
- for (const [pkg, { typesFile, fromFile, spec }] of seen) {
+ for (const [pkg, { typesFile, fromFile, spec, owner }] of seen) {
judged?.add(pkg);
// A package without a root entry (no "main", "exports" or index file)
// can still serve the subpath the program imports.
@@ -175,11 +246,23 @@ function detectAutoPackages(
package: pkg,
status: "refused",
detail: mode === "lib" ? reason : `auto: ${reason}`,
+ // Naming a package bypasses the declaration and shipped-JS
+ // heuristics; it cannot supply JavaScript that does not resolve.
+ ...(mode === "lib" && jsEntry !== null ? { hint: npmStaticOptInHint(pkg, owner) } : {}),
});
}
return chosen;
}
+/** How to compile a package the declaration bar refused anyway: name it,
+ * or (for a dependency of an opted-in package) opt in that package's
+ * whole dependency closure. */
+function npmStaticOptInHint(pkg: string, importer: string | null): string {
+ return importer === null
+ ? `to compile it anyway, name it with --npm-static=${pkg} (or --npm-static=${pkg}... to include its dependencies)`
+ : `to compile it anyway, name it with --npm-static=${pkg}, or use --npm-static=${importer}... to include all of ${importer}'s dependencies`;
+}
+
/** The opted-in packages a consumer-anchored tsc message NAMES: module
* specifiers in `Module '"spec"'` phrasings, and resolved file paths in
* `import("…")` type spellings — the two ways the checker points at an
@@ -227,9 +310,23 @@ export function runFrontend(
// selections bypass eligibility heuristics, but never compilation checks.
const explicit: readonly string[] =
npmStatic !== undefined && typeof npmStatic !== "string" ? npmStatic : [];
- return loadFrontend(entryPath, trackedLoader, "lib", externalTypes, [
- ...new Set([...libraryNpmStatic, ...explicit]),
- ]);
+ // A trailing "..." (`--npm-static=fs-extra...`) selects the package and
+ // its whole dependency closure, which the library fixpoint discovers.
+ const transitive = new Set();
+ const names = [...libraryNpmStatic, ...explicit].map((name) => {
+ if (!name.endsWith("...") || name.length === 3) return name;
+ const base = name.slice(0, -3);
+ transitive.add(base);
+ return base;
+ });
+ return loadFrontend(
+ entryPath,
+ trackedLoader,
+ "lib",
+ externalTypes,
+ [...new Set(names)],
+ transitive,
+ );
} catch (error) {
for (const load of active) {
// Preserve the original failure and attempt every remaining cleanup.
@@ -249,6 +346,7 @@ function loadFrontend(
npmStatic?: readonly string[] | "auto" | "lib",
externalTypes?: Readonly>,
libraryNpmStatic: readonly string[] = [],
+ transitive: Set = new Set(),
): Frontend {
// Resolver package/workspace metadata is intentionally shared across the
// several load attempts of ONE auto-detection fixpoint, but never across
@@ -270,7 +368,10 @@ function loadFrontend(
const scoutPreflight = checkPreflight(scout);
requested =
npmStatic === "lib"
- ? [...libraryNpmStatic, ...detectAutoPackages(scout, statuses, "lib", judged, npmSites)]
+ ? [
+ ...libraryNpmStatic,
+ ...detectAutoPackages(scout, statuses, "lib", judged, npmSites, transitive),
+ ]
: detectAutoPackages(scout, statuses);
// With no package to opt in, the scout already IS the final frontend:
// same roots, resolution posture, preflight, and module order. Retain it
@@ -328,7 +429,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, transitive);
if (grown.length === 0) break;
requested = [...requested, ...grown];
load.dispose();
diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts
index df0cb673e..dcdaf9040 100644
--- a/packages/compiler/src/frontend/program.ts
+++ b/packages/compiler/src/frontend/program.ts
@@ -119,6 +119,11 @@ import {
import { trackedFileExists, trackedReadFile, trackedRealpath } from "./input-tracker.js";
import { forkTargetPaths } from "./fork-target.js";
import { workerTargetPaths } from "./worker-target.js";
+import {
+ erasedTypeOnlyImport,
+ typeOnlyNamedImport,
+ typeOnlyReachedFiles,
+} from "./type-only-imports.js";
import { moduleSourceCandidates } from "./module-source-candidates.js";
import { inferredJsDiagnosticSuppressed } from "./inferred-js-diagnostics.js";
@@ -2146,7 +2151,7 @@ function insideFunctionLike7(node: ts.Node): boolean {
function nodeEsmSyntaxMarker7(sf: ts.SourceFile): ts.Node | null {
for (const stmt of sf.statements) {
if (ts.isImportDeclaration(stmt) && !erasedTypeOnlyImport(stmt)) return stmt;
- if (ts.isExportDeclaration(stmt) && !stmt.isTypeOnly && !erasedTypeOnlyReexport(stmt)) {
+ if (ts.isExportDeclaration(stmt) && !stmt.isTypeOnly) {
return stmt;
}
if (ts.isExportAssignment(stmt)) return stmt;
@@ -2829,6 +2834,7 @@ export function makeCycleAdmission(
const clause = e.stmt.importClause;
if (
clause === undefined ||
+ typeOnlyNamedImport(e.stmt) ||
(clause.name === undefined &&
(clause.namedBindings === undefined ||
(ts.isNamespaceImport(clause.namedBindings) &&
@@ -4299,21 +4305,41 @@ function preflight7(load: LoadResult): {
npmStaticPackageOfPath(sf.fileName) !== null) &&
!unselectedPackageJsFile(sf.fileName),
);
+ // Files reached only through `import type` / `export type … from` are
+ // checker input, never program modules: Node does not load them, so
+ // their imports and syntax must not fence the program.
+ const resolveRuntimeEdge = (
+ sf: ts.SourceFile,
+ spec: string,
+ resolutionKind?: "import" | "require",
+ ): ts.SourceFile | null =>
+ resolveImport7(program, sf, spec, resolutionKind) ??
+ npmStaticDepSf7(program, sf, spec, resolutionKind);
+ const typeOnlyFiles = typeOnlyReachedFiles(
+ program,
+ entry,
+ programFiles,
+ () => [
+ ...forkTargetPaths(program, program.getImplementationSourceFiles()),
+ ...workerTargetPaths(program, program.getImplementationSourceFiles()),
+ ],
+ resolveRuntimeEdge,
+ );
+ const runtimeFiles =
+ typeOnlyFiles.size === 0 ? programFiles : programFiles.filter((sf) => !typeOnlyFiles.has(sf));
const userFiles = npmStaticActive()
? planNpmStaticReexports(
program,
entry,
- programFiles,
+ runtimeFiles,
[
...createRequireProgramRoots7(program),
...forkTargetPaths(program, program.getImplementationSourceFiles()),
...workerTargetPaths(program, program.getImplementationSourceFiles()),
],
- (sf, spec, resolutionKind) =>
- resolveImport7(program, sf, spec, resolutionKind) ??
- npmStaticDepSf7(program, sf, spec, resolutionKind),
+ resolveRuntimeEdge,
)
- : programFiles;
+ : runtimeFiles;
program.getTypeChecker().prefetchSourceFileStructures(userFiles);
// Unselected node_modules JS is not native program source even when
@@ -4438,8 +4464,7 @@ function preflight7(load: LoadResult): {
continue;
}
if (ts.isExportDeclaration(stmt)) {
- if (stmt.isTypeOnly || erasedTypeOnlyReexport(stmt) || isPrunedNpmReexport(program, stmt))
- continue;
+ if (stmt.isTypeOnly || isPrunedNpmReexport(program, stmt)) continue;
if (!stmt.moduleSpecifier) continue;
const fromSpec = ts.isStringLiteral(stmt.moduleSpecifier) ? stmt.moduleSpecifier.text : "";
if (load.externalTypes.has(fromSpec)) {
@@ -5708,42 +5733,6 @@ export function isCheckedJsSourceFile(sf: ts.SourceFile, projectCheckJs: boolean
return checked;
}
-/** `import type …` declarations and named clauses whose every element is
- * `type`-qualified are ERASED by tsc at emit — Node's module graph has no
- * edge there, so module order, cycle detection, init calls, and the
- * startup-refusal probe must not count them either (zod v4's core modules
- * are "cyclic" only through `import type * as ns` namespaces — 27 phantom
- * SC1016 sites against 1 real value-level cycle; and a type-only import
- * of a module Node cannot resolve is NOT a startup crash — Node never
- * resolves it). Originated as the provenance-sources pilot's flag-gated
- * prototype (SCRIPTC_TYPE_ONLY_EDGES), promoted to always-on with the
- * full-suite differential green. Side-effect imports (no clause) stay
- * real edges. */
-function erasedTypeOnlyImport(stmt: ts.ImportDeclaration): boolean {
- const clause = stmt.importClause;
- if (clause === undefined) return false; // side-effect import: a real edge
- if (clause.phaseModifier === ts.SyntaxKind.TypeKeyword) return true;
- return (
- clause.name === undefined &&
- clause.namedBindings !== undefined &&
- ts.isNamedImports(clause.namedBindings) &&
- clause.namedBindings.elements.length > 0 &&
- clause.namedBindings.elements.every((e) => e.isTypeOnly)
- );
-}
-
-/** The re-export twin: `export { type X } from "./m"` with every element
- * `type`-qualified erases at emit exactly like `export type { X } from`
- * (which stmt.isTypeOnly already covers) — no module edge. */
-function erasedTypeOnlyReexport(stmt: ts.ExportDeclaration): boolean {
- return (
- stmt.exportClause !== undefined &&
- ts.isNamedExports(stmt.exportClause) &&
- stmt.exportClause.elements.length > 0 &&
- stmt.exportClause.elements.every((e) => e.isTypeOnly)
- );
-}
-
export function orderedImportsOf(
program: ts.Program,
sf: ts.SourceFile,
@@ -5751,11 +5740,7 @@ export function orderedImportsOf(
const out: { stmt: ts.Statement; dep: ts.SourceFile | null }[] = [];
for (const stmt of sf.statements) {
if (ts.isExportDeclaration(stmt) && isPrunedNpmReexport(program, stmt)) continue;
- if (
- ts.isExportDeclaration(stmt) &&
- (stmt.isTypeOnly || erasedTypeOnlyReexport(stmt) || !stmt.moduleSpecifier)
- )
- continue;
+ if (ts.isExportDeclaration(stmt) && (stmt.isTypeOnly || !stmt.moduleSpecifier)) continue;
if (!ts.isImportDeclaration(stmt) && !ts.isExportDeclaration(stmt)) continue;
if (ts.isImportDeclaration(stmt) && erasedTypeOnlyImport(stmt)) continue;
if (!stmt.moduleSpecifier || !ts.isStringLiteral(stmt.moduleSpecifier)) continue;
diff --git a/packages/compiler/src/frontend/tier-analysis.ts b/packages/compiler/src/frontend/tier-analysis.ts
new file mode 100644
index 000000000..ded7c5225
--- /dev/null
+++ b/packages/compiler/src/frontend/tier-analysis.ts
@@ -0,0 +1,629 @@
+/* Tier assignment, report-only (two-tier design, stage S2).
+ *
+ * The design assigns every binding, field, function instance, class and
+ * module a tier. Static is today's compiler; dynamic code operates on
+ * dynamic values. Demotion from static to dynamic is monotone and is seeded
+ * by `any`, untyped JavaScript, types and operations without a static
+ * lowering, reflection a static layout cannot answer, and dynamic modules
+ * (DESIGN.md §6).
+ *
+ * Stage S2 computes what WOULD be demoted without demoting anything:
+ *
+ * 1. Existing dynamic code. Functions whose bodies already use the `dyn`
+ * vocabulary are the dynamic tier today (ir/tiers.ts assigns them). The
+ * source statements whose lowering performs a dynamic operation are the
+ * coverage report's statementsDynamic.
+ * 2. Demotion requests. Every refusal or runtime fence the lowerer commits
+ * (today's poison sites: the SC1xxx/SC2xxx families) and every refused
+ * import or package becomes a request, attributed to the unit whose
+ * static representation could not express the construct. The lowerer
+ * still refuses or fences exactly as before; the requests are recorded
+ * from the committed diagnostics, so speculative probes that roll back
+ * record nothing. Under `--tier=static-only` every request stays today's
+ * refusal; stage S6 turns requests into demotions in the default mode.
+ * 3. Demoted units. A request demotes its unit, and the function instance
+ * containing the site turns dynamic (it would hold dynamic operations,
+ * invariant I1). Units are deduplicated; each keeps its requests, which
+ * is the chain `--explain-dynamic` prints.
+ *
+ * Attribution is syntactic and approximate: the site's innermost node, the
+ * operand it acts on, and that operand's declaration. Stage S6 replaces it
+ * with requests the failing lowering names exactly. */
+import type { ScrDiagnostic } from "../diagnostics/diagnostic.js";
+import type { IrFunction, SrcLoc } from "../ir/ir.js";
+import { dynamicOperationSites, type DynamicFunction } from "../ir/tiers.js";
+import * as ts from "./ts7/adapter.js";
+
+/** `--tier`: "auto" (the default) will demote; "static-only" keeps every
+ * demotion request a refusal (design invariant I2). Identical in S2. */
+export type TierMode = "auto" | "static-only";
+
+export const TIER_MODES: readonly TierMode[] = ["auto", "static-only"];
+
+export type DemotionUnitKind = "binding" | "field" | "function" | "class" | "module";
+
+export interface DemotionUnit {
+ kind: DemotionUnitKind;
+ /** Display name: `x`, `Point.x`, `parse`, `Parser.next`, `Point`, or the
+ * module's file path. */
+ name: string;
+ /** Bindings: the function declaring a local, absent for module scope. */
+ owner?: string;
+ /** The unit's declaration (the site itself when none is known). */
+ loc: SrcLoc;
+}
+
+/** Why a unit is demoted (DESIGN.md §6.2): a type with no static
+ * representation, an operation with no static lowering, reflection on a
+ * class, a use of a binding whose declaration was refused, a module or
+ * package that cannot be static, or a construct excluded by policy (no
+ * demotion: it stays a refusal). */
+export type DemotionSeed = "type" | "operation" | "reflection" | "cascade" | "module" | "excluded";
+
+export type DemotionScope = "reached" | "deferred" | "unreached";
+
+export interface DemotionRequest {
+ unit: DemotionUnit;
+ /** The function instance (or module top level) containing the site. */
+ enclosing: DemotionUnit;
+ seed: DemotionSeed;
+ scope: DemotionScope;
+ code: string;
+ message: string;
+ loc: SrcLoc;
+}
+
+export interface DemotedUnit extends DemotionUnit {
+ /** Requests naming this unit directly. */
+ requests: number;
+ /** Requests whose sites this function instance or module contains. */
+ sites: number;
+}
+
+export interface TierDynamicFunction extends DynamicFunction {
+ /** Source statements with a dynamic operation in this function. */
+ statements: number;
+}
+
+export interface TierReport {
+ mode: TierMode;
+ /** Reached source statements that perform a dynamic operation. */
+ statementsDynamic: number;
+ dynamicFunctions: TierDynamicFunction[];
+ requests: DemotionRequest[];
+ /** Units the requests would demote (reached and deferred scopes),
+ * deduplicated, by kind then source order. */
+ units: DemotedUnit[];
+}
+
+/* ── statements of dynamic functions ─────────────────────────────────── */
+
+function isClassLike(node: ts.Node): boolean {
+ return ts.isClassDeclaration(node) || ts.isClassExpression(node);
+}
+
+/** The innermost node spanning exactly or containing [start, end). */
+function nodeAt(sf: ts.SourceFile, start: number, end: number): ts.Node {
+ let node: ts.Node = sf;
+ for (;;) {
+ let next: ts.Node | undefined;
+ node.forEachChild((child) => {
+ if (child.getStart(sf) <= start && end <= child.getEnd()) {
+ next = child;
+ return true;
+ }
+ return undefined;
+ });
+ if (next === undefined) return node;
+ node = next;
+ }
+}
+
+/** The source statement an IR location belongs to: the innermost
+ * statement containing it, or an arrow function's expression body. Null
+ * for synthetic locations. */
+function statementAt(program: ts.Program, loc: SrcLoc): ts.Node | null {
+ if (loc.start === 0 && loc.end === 0) return null;
+ const sf = program.getSourceFile(loc.file);
+ if (sf === undefined) return null;
+ let node: ts.Node | undefined = nodeAt(sf, loc.start, loc.end);
+ while (node !== undefined && !ts.isSourceFile(node)) {
+ const parent: ts.Node | undefined = node.parent;
+ if (parent !== undefined && ts.isArrowFunction(parent) && parent.body === node) return node;
+ if (ts.isStatement(node) && !ts.isBlock(node)) return node;
+ node = parent;
+ }
+ return null;
+}
+
+/** Dynamic statements are the source statements whose lowering performs a
+ * dynamic operation (in the function itself or in a generated helper it
+ * calls). They count once however many functions and operations they
+ * involve. A function's tier is the unit of I1, but static operations
+ * inside a dynamic function stay static, so the statement is the honest
+ * measure of how much code is dynamic. */
+function dynamicFunctionReports(
+ program: ts.Program,
+ bodies: readonly IrFunction[],
+): { functions: TierDynamicFunction[]; statements: number } {
+ const byLoc = new Map();
+ const all = new Set();
+ const functions: TierDynamicFunction[] = [];
+ for (const fn of bodies) {
+ const sites = dynamicOperationSites(fn);
+ if (sites.length === 0) continue;
+ const own = new Set();
+ for (const site of sites) {
+ const key = `${site.loc.file}\0${site.loc.start}\0${site.loc.end}`;
+ let statement = byLoc.get(key);
+ if (statement === undefined) {
+ statement = statementAt(program, site.loc);
+ byLoc.set(key, statement);
+ }
+ if (statement === null) continue;
+ own.add(statement);
+ all.add(statement);
+ }
+ functions.push({
+ name: fn.name,
+ loc: fn.loc,
+ operation: sites[0]!.operation,
+ operationLoc: sites[0]!.loc,
+ statements: own.size,
+ });
+ }
+ return { functions, statements: all.size };
+}
+
+/* ── demotion requests ───────────────────────────────────────────────── */
+
+const TYPE_CODES = new Set([
+ "SC2001",
+ "SC2002",
+ "SC2003",
+ "SC2005",
+ "SC2006",
+ "SC2007",
+ "SC2008",
+ "SC2009",
+ "SC2011",
+ "SC1063",
+ "SC1100",
+ "SC1101",
+]);
+const MODULE_CODES = new Set([
+ "SC1010",
+ "SC1012",
+ "SC1013",
+ "SC1014",
+ "SC1015",
+ "SC1016",
+ "SC2013",
+]);
+/** Runtime code generation (`eval`, `new Function`): excluded by policy,
+ * since the dynamic tier compiles ahead of time (DESIGN.md §12 class X). */
+const EXCLUDED_CODES = new Set(["SC2010"]);
+const EXCLUDED_SURFACE = /^'(?:new Function|Function|eval)'/;
+
+/** Refusals and fences that name a static-tier gap. Type errors,
+ * environment and target refusals, divergences and internal errors are
+ * not demotion requests. */
+export function isDemotionCode(code: string): boolean {
+ return /^SC[12]\d{3}$/.test(code);
+}
+
+function isFunctionLikeOrFile(node: ts.Node): boolean {
+ return (
+ ts.isSourceFile(node) ||
+ ts.isFunctionLikeDeclaration(node) ||
+ ts.isClassStaticBlockDeclaration(node)
+ );
+}
+
+function nameText(name: ts.Node | undefined): string | null {
+ if (name === undefined) return null;
+ if (ts.isIdentifier(name) || ts.isPrivateIdentifier(name)) return name.text;
+ if (ts.isStringLiteral(name) || ts.isNumericLiteral(name)) return name.text;
+ return null;
+}
+
+function className(node: ts.Node | undefined): string {
+ if (node === undefined) return "";
+ if ((ts.isClassDeclaration(node) || ts.isClassExpression(node)) && node.name !== undefined)
+ return node.name.text;
+ const parent = node.parent;
+ if (parent !== undefined && ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name))
+ return parent.name.text;
+ return "";
+}
+
+/** The display name of a function-like node, as a user would look it up. */
+function functionName(node: ts.Node): string {
+ if (ts.isSourceFile(node)) return "";
+ if (ts.isClassStaticBlockDeclaration(node)) return `${className(node.parent)}.`;
+ if (ts.isConstructorDeclaration(node)) return `${className(node.parent)}.constructor`;
+ const own = ts.isFunctionLikeDeclaration(node) ? nameText(node.name) : null;
+ const parent = node.parent;
+ if (parent !== undefined && isClassLike(parent) && own !== null)
+ return `${className(parent)}.${own}`;
+ if (own !== null) return own;
+ if (parent !== undefined) {
+ if (ts.isVariableDeclaration(parent) || ts.isPropertyAssignment(parent)) {
+ const bound = nameText(parent.name);
+ if (bound !== null) return bound;
+ }
+ if (
+ ts.isPropertyDeclaration(parent) &&
+ parent.parent !== undefined &&
+ isClassLike(parent.parent)
+ ) {
+ const field = nameText(parent.name);
+ if (field !== null) return `${className(parent.parent)}.${field}`;
+ }
+ if (ts.isBinaryExpression(parent) && parent.right === node) {
+ const left = parent.left;
+ if (ts.isIdentifier(left)) return left.text;
+ if (ts.isPropertyAccessExpression(left)) return left.getText();
+ }
+ }
+ const sf = node.getSourceFile();
+ const pos = ts.getLineAndCharacterOfPosition(sf, node.getStart(sf));
+ return ``;
+}
+
+function locOfNode(node: ts.Node): SrcLoc {
+ const sf = node.getSourceFile();
+ return { file: sf.fileName, start: node.getStart(sf), end: node.getEnd() };
+}
+
+function enclosingFunction(node: ts.Node): ts.Node {
+ let current: ts.Node | undefined = node.parent;
+ while (current !== undefined && !isFunctionLikeOrFile(current)) current = current.parent;
+ return current ?? node.getSourceFile();
+}
+
+function functionUnit(node: ts.Node, instance: string | null): DemotionUnit {
+ // Top-level code runs in the module's initializer function.
+ if (ts.isSourceFile(node))
+ return {
+ kind: "function",
+ name: "",
+ loc: { file: node.fileName, start: 0, end: 0 },
+ };
+ const name = functionName(node);
+ return {
+ kind: "function",
+ name: instance === null ? name : `${name} ${instance}`,
+ loc: locOfNode(node),
+ };
+}
+
+/** Generic instances append their instantiation context to diagnostics
+ * (Lowerer.pushDiag: ` (instantiating 'f' with )`); the instance, not
+ * the source function, is the unit. Returns the type-argument text. */
+function instanceOf(message: string): string | null {
+ const at = message.lastIndexOf(" (instantiating ");
+ if (at < 0 || !message.endsWith(")")) return null;
+ const context = message.slice(at + 2, -1);
+ const withAt = context.indexOf(" with ");
+ return withAt < 0 ? context : context.slice(withAt + 6);
+}
+
+/** Strip wrappers and walk receivers down to the value an operation acts
+ * on: `a.b.c(…)` → `a`, `(x as T)[k]` → `x`. Records whether the chain
+ * passes through `.prototype`. */
+function rootOf(expr: ts.Node): { root: ts.Node; viaPrototype: boolean } {
+ let node = expr;
+ let viaPrototype = false;
+ for (;;) {
+ if (
+ ts.isParenthesizedExpression(node) ||
+ ts.isAsExpression(node) ||
+ ts.isNonNullExpression(node) ||
+ ts.isAssertionExpression(node) ||
+ ts.isSatisfiesExpression(node) ||
+ ts.isSpreadElement(node) ||
+ ts.isAwaitExpression(node)
+ ) {
+ node = node.expression!;
+ } else if (ts.isPropertyAccessExpression(node)) {
+ if (node.name.text === "prototype") viaPrototype = true;
+ node = node.expression;
+ } else if (ts.isElementAccessExpression(node) || ts.isCallExpression(node)) {
+ node = node.expression;
+ } else if (
+ ts.isDeleteExpression(node) ||
+ ts.isTypeOfExpression(node) ||
+ ts.isVoidExpression(node)
+ ) {
+ node = node.expression;
+ } else {
+ return { root: node, viaPrototype };
+ }
+ }
+}
+
+const REFLECTION_NAMESPACES = new Set(["Object", "Reflect"]);
+
+/** The expression an operation at `node` acts on. Reflection calls
+ * (`Object.defineProperty(x, …)`, `Reflect.set(x, …)`) act on their first
+ * argument; `new C()` and `x instanceof C` on the class value. */
+function subjectOf(node: ts.Node): ts.Node {
+ let current = node;
+ // A refused callee or member name: the whole call or access is the site.
+ const parent = current.parent;
+ if (parent !== undefined && ts.isPropertyAccessExpression(parent) && parent.name === current)
+ current = parent;
+ if (ts.isCallExpression(current)) {
+ const callee = current.expression;
+ if (
+ ts.isPropertyAccessExpression(callee) &&
+ ts.isIdentifier(callee.expression) &&
+ REFLECTION_NAMESPACES.has(callee.expression.text) &&
+ current.arguments.length > 0
+ ) {
+ return current.arguments[0]!;
+ }
+ return callee;
+ }
+ if (ts.isNewExpression(current)) return current.expression;
+ if (ts.isBinaryExpression(current)) {
+ if (current.operatorToken.kind === ts.SyntaxKind.InstanceOfKeyword) return current.right;
+ if (current.operatorToken.kind === ts.SyntaxKind.EqualsToken) return current.left;
+ }
+ return current;
+}
+
+/** The declaration owning a type-refused node: the binding, parameter,
+ * field or function whose declared type has no static representation. */
+function typeOwnerOf(node: ts.Node): ts.Node | null {
+ let current: ts.Node | undefined = node;
+ while (current !== undefined && !ts.isSourceFile(current)) {
+ if (
+ ts.isVariableDeclaration(current) ||
+ ts.isParameter(current) ||
+ ts.isBindingElement(current) ||
+ ts.isPropertyDeclaration(current) ||
+ ts.isPropertySignature(current) ||
+ ts.isCatchClause(current)
+ ) {
+ return current;
+ }
+ if (ts.isFunctionLikeDeclaration(current) || isClassLike(current)) return current;
+ if (ts.isStatement(current)) return null;
+ current = current.parent;
+ }
+ return null;
+}
+
+function isModuleObjectName(node: ts.Node): boolean {
+ return ts.isIdentifier(node) && (node.text === "exports" || node.text === "module");
+}
+
+class UnitResolver {
+ constructor(private readonly checker: ts.TypeChecker) {}
+
+ /** The unit a declaration node denotes. */
+ declarationUnit(decl: ts.Node): DemotionUnit | null {
+ if (isClassLike(decl)) return { kind: "class", name: className(decl), loc: locOfNode(decl) };
+ if (ts.isFunctionLikeDeclaration(decl)) return functionUnit(decl, null);
+ if (ts.isPropertyDeclaration(decl) || ts.isPropertySignature(decl)) {
+ const field = nameText(decl.name) ?? "";
+ const owner = decl.parent;
+ const ownerName = owner !== undefined && isClassLike(owner) ? className(owner) : null;
+ return {
+ kind: ownerName === null ? "binding" : "field",
+ name: ownerName === null ? field : `${ownerName}.${field}`,
+ loc: locOfNode(decl),
+ };
+ }
+ if (ts.isCatchClause(decl)) {
+ const binding = decl.variableDeclaration;
+ if (binding === undefined) return null;
+ return this.declarationUnit(binding);
+ }
+ if (ts.isVariableDeclaration(decl) || ts.isParameter(decl) || ts.isBindingElement(decl)) {
+ const name = nameText(decl.name) ?? decl.name?.getText() ?? "";
+ const scope = enclosingFunction(decl);
+ const unit: DemotionUnit = { kind: "binding", name, loc: locOfNode(decl) };
+ if (ts.isParameter(decl) && ts.isFunctionLikeDeclaration(decl.parent))
+ unit.owner = functionName(decl.parent);
+ else if (!ts.isSourceFile(scope)) unit.owner = functionName(scope);
+ return unit;
+ }
+ return null;
+ }
+
+ /** The unit an operand expression denotes. */
+ valueUnit(expr: ts.Node): { unit: DemotionUnit; reflection: boolean } | null {
+ const { root, viaPrototype } = rootOf(expr);
+ const sf = root.getSourceFile();
+ if (root.kind === ts.SyntaxKind.ThisKeyword) {
+ const owner = enclosingFunction(root);
+ if (ts.isSourceFile(owner))
+ return {
+ unit: { kind: "module", name: sf.fileName, loc: locOfNode(sf) },
+ reflection: false,
+ };
+ const cls = owner.parent;
+ if (cls !== undefined && isClassLike(cls)) {
+ // `this.f` names a field; `this` itself is the instance.
+ const access = root.parent;
+ if (
+ access !== undefined &&
+ ts.isPropertyAccessExpression(access) &&
+ access.expression === root
+ ) {
+ return {
+ unit: {
+ kind: "field",
+ name: `${className(cls)}.${access.name.text}`,
+ loc: locOfNode(access),
+ },
+ reflection: false,
+ };
+ }
+ return {
+ unit: { kind: "class", name: className(cls), loc: locOfNode(cls) },
+ reflection: viaPrototype,
+ };
+ }
+ return null;
+ }
+ if (!ts.isIdentifier(root)) return null;
+ let symbol = this.checker.getSymbolAtLocation(root);
+ if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0)
+ symbol = this.checker.getAliasedSymbol(symbol);
+ const decls = symbol === undefined ? [] : this.checker.declarationsOf(symbol);
+ const decl = decls.find((d) => !d.getSourceFile().isDeclarationFile);
+ if (decl === undefined) {
+ // CommonJS `exports`/`module` as values: the module object.
+ if (isModuleObjectName(root))
+ return {
+ unit: { kind: "module", name: sf.fileName, loc: locOfNode(sf) },
+ reflection: false,
+ };
+ return null;
+ }
+ if (ts.isSourceFile(decl))
+ return {
+ unit: { kind: "module", name: decl.fileName, loc: locOfNode(decl) },
+ reflection: false,
+ };
+ const unit = this.declarationUnit(decl);
+ if (unit === null) return null;
+ // `C.prototype.m = …`, `Object.defineProperty(C.prototype, …)`: the
+ // class (or the constructor function's prototype object).
+ if (viaPrototype && (unit.kind === "class" || unit.kind === "function"))
+ return { unit: { ...unit, kind: "class" }, reflection: true };
+ return { unit, reflection: unit.kind === "class" };
+ }
+}
+
+function seedOf(code: string, message: string): DemotionSeed {
+ if (EXCLUDED_CODES.has(code) || EXCLUDED_SURFACE.test(message)) return "excluded";
+ if (code === "SC2004") return "cascade";
+ if (MODULE_CODES.has(code)) return "module";
+ if (TYPE_CODES.has(code)) return "type";
+ return "operation";
+}
+
+/** One request for a committed refusal or fence diagnostic. */
+function requestFor(
+ program: ts.Program,
+ resolver: UnitResolver,
+ diag: ScrDiagnostic,
+ scope: DemotionScope,
+): DemotionRequest | null {
+ if (!isDemotionCode(diag.code)) return null;
+ let seed = seedOf(diag.code, diag.message);
+ const base = { code: diag.code, message: diag.message, loc: diag.loc, scope };
+ const sf = program.getSourceFile(diag.loc.file);
+ if (sf === undefined) {
+ const unit: DemotionUnit = { kind: "module", name: diag.loc.file, loc: diag.loc };
+ return { ...base, unit, enclosing: unit, seed };
+ }
+ const moduleUnit: DemotionUnit = { kind: "module", name: sf.fileName, loc: locOfNode(sf) };
+ const node = nodeAt(sf, diag.loc.start, diag.loc.end);
+ const instance = instanceOf(diag.message);
+ const owner = ts.isSourceFile(node) ? sf : enclosingFunction(node);
+ const enclosing = functionUnit(owner, instance);
+ if (seed === "module" || ts.isSourceFile(node))
+ return { ...base, unit: moduleUnit, enclosing, seed };
+ let unit: DemotionUnit | null = null;
+ if (seed === "type") {
+ const decl = typeOwnerOf(node);
+ if (decl !== null) unit = resolver.declarationUnit(decl);
+ }
+ if (unit === null) {
+ const value = resolver.valueUnit(subjectOf(node));
+ if (value !== null) {
+ unit = value.unit;
+ if (value.reflection && seed === "operation") seed = "reflection";
+ }
+ }
+ return { ...base, unit: unit ?? enclosing, enclosing, seed };
+}
+
+function unitKey(unit: DemotionUnit): string {
+ return `${unit.kind}\0${unit.name}\0${unit.owner ?? ""}\0${unit.loc.file}\0${unit.loc.start}`;
+}
+
+const KIND_ORDER: readonly DemotionUnitKind[] = ["module", "class", "field", "function", "binding"];
+
+function demotedUnits(requests: readonly DemotionRequest[]): DemotedUnit[] {
+ const units = new Map();
+ const entry = (unit: DemotionUnit): DemotedUnit => {
+ const key = unitKey(unit);
+ let found = units.get(key);
+ if (found === undefined) units.set(key, (found = { ...unit, requests: 0, sites: 0 }));
+ return found;
+ };
+ for (const request of requests) {
+ if (request.scope === "unreached" || request.seed === "excluded") continue;
+ entry(request.unit).requests++;
+ entry(request.enclosing).sites++;
+ }
+ return [...units.values()].sort(
+ (a, b) =>
+ KIND_ORDER.indexOf(a.kind) - KIND_ORDER.indexOf(b.kind) ||
+ a.loc.file.localeCompare(b.loc.file) ||
+ a.loc.start - b.loc.start ||
+ a.name.localeCompare(b.name),
+ );
+}
+
+export interface TierAnalysisInput {
+ mode: TierMode;
+ /** The dynamic-tier IR functions of the reached program. */
+ dynamicFunctions: readonly IrFunction[];
+ reached: readonly ScrDiagnostic[];
+ deferred: readonly ScrDiagnostic[];
+ unreached: readonly ScrDiagnostic[];
+}
+
+/** Coverage's statementsDynamic (see dynamicFunctionReports). */
+export function dynamicStatementCount(
+ program: ts.Program,
+ dynamicFunctions: readonly IrFunction[],
+): number {
+ return dynamicFunctionReports(program, dynamicFunctions).statements;
+}
+
+export function analyzeTiers(program: ts.Program, input: TierAnalysisInput): TierReport {
+ const resolver = new UnitResolver(program.getTypeChecker());
+ const requests: DemotionRequest[] = [];
+ const add = (diags: readonly ScrDiagnostic[], scope: DemotionScope): void => {
+ for (const diag of diags) {
+ let request: DemotionRequest | null;
+ try {
+ request = requestFor(program, resolver, diag, scope);
+ } catch {
+ // Attribution is best effort; a report never fails an analysis.
+ if (!isDemotionCode(diag.code)) continue;
+ const unit: DemotionUnit = { kind: "module", name: diag.loc.file, loc: diag.loc };
+ request = {
+ unit,
+ enclosing: unit,
+ seed: seedOf(diag.code, diag.message),
+ scope,
+ code: diag.code,
+ message: diag.message,
+ loc: diag.loc,
+ };
+ }
+ if (request !== null) requests.push(request);
+ }
+ };
+ add(input.reached, "reached");
+ add(input.deferred, "deferred");
+ add(input.unreached, "unreached");
+ const dynamic = dynamicFunctionReports(program, input.dynamicFunctions);
+ return {
+ mode: input.mode,
+ statementsDynamic: dynamic.statements,
+ dynamicFunctions: dynamic.functions,
+ requests,
+ units: demotedUnits(requests),
+ };
+}
diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts
index 3ba97097c..260297671 100644
--- a/packages/compiler/src/frontend/type-mapper.ts
+++ b/packages/compiler/src/frontend/type-mapper.ts
@@ -1709,16 +1709,38 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
// Declaration-only package types have no compiled implementation.
// Primitive and container types retain their structural mappings; types
// owned by the standard library or explicit host mappings are handled
- // by their dedicated native rules.
- const npmSym = widened.getAliasSymbol() ?? widened.getSymbol();
- const npmDecls = npmSym ? checker.declarationsOf(npmSym) : undefined;
+ // by their dedicated native rules. Pure type shapes — interfaces, type
+ // aliases, and object or mapped type literals with no value declaration
+ // merged in — describe data, not an implementation: a program value of
+ // such a type is one the program built itself (`import type` from a
+ // types-only package), so it maps structurally like a local declaration.
+ // Values the package itself would produce stay refused at their import.
+ const packageOnly = (sym: ts.Symbol | undefined): readonly ts.Node[] | null => {
+ const decls = sym ? checker.declarationsOf(sym) : undefined;
+ return decls &&
+ decls.length > 0 &&
+ decls.every((d) => {
+ const sf = d.getSourceFile();
+ return sf.isDeclarationFile && !ctx.isStdlibFile(sf) && !ctx.isExternalTypeFile(sf);
+ })
+ ? decls
+ : null;
+ };
+ const pureTypeShape = (decls: readonly ts.Node[] | null): boolean =>
+ decls === null ||
+ decls.every(
+ (d) =>
+ packageTypeFile(d.getSourceFile(), ctx) &&
+ (ts.isInterfaceDeclaration(d) ||
+ ts.isTypeAliasDeclaration(d) ||
+ ts.isTypeLiteralNode(d) ||
+ ts.isMappedTypeNode(d)),
+ );
+ const aliasDecls = packageOnly(widened.getAliasSymbol());
+ const ownDecls = packageOnly(widened.getSymbol());
if (
- npmDecls &&
- npmDecls.length > 0 &&
- npmDecls.every((d) => {
- const sf = d.getSourceFile();
- return sf.isDeclarationFile && !ctx.isStdlibFile(sf) && !ctx.isExternalTypeFile(sf);
- })
+ (aliasDecls !== null || (widened.getAliasSymbol() === undefined && ownDecls !== null)) &&
+ !(pureTypeShape(aliasDecls) && pureTypeShape(ownDecls))
) {
return null;
}
@@ -5429,6 +5451,17 @@ function isMappedShape(t: ts.Type): boolean {
);
}
+/** A declaration file an installed package ships (its own or an @types
+ * twin), outside the standard library and @types/node: the type surface
+ * of a package the program does not compile. Its pure type shapes —
+ * interfaces, aliases, type literals — describe data a program can build
+ * itself (`import type` from a types-only package such as type-fest). */
+function packageTypeFile(sf: ts.SourceFile, ctx: Pick): boolean {
+ return (
+ sf.isDeclarationFile && /[\\/]node_modules[\\/]/.test(sf.fileName) && !ctx.isStdlibFile(sf)
+ );
+}
+
/** The record path's provenance fence. Declared shapes (object literals,
* interfaces, type literals) must come from user code or an explicitly
* mapped external type surface, never an arbitrary .d.ts — the empty
@@ -5467,13 +5500,14 @@ function recordProvenanceOk(t: ts.Type, ctx: TypeMapperCtx): boolean {
.every(
(decl) =>
!decl.getSourceFile().isDeclarationFile ||
- ctx.isExternalTypeFile(decl.getSourceFile()),
+ ctx.isExternalTypeFile(decl.getSourceFile()) ||
+ packageTypeFile(decl.getSourceFile(), ctx),
),
);
}
return !decls.some((d) => {
const sf = d.getSourceFile();
- return sf.isDeclarationFile && !ctx.isExternalTypeFile(sf);
+ return sf.isDeclarationFile && !ctx.isExternalTypeFile(sf) && !packageTypeFile(sf, ctx);
});
}
@@ -5930,7 +5964,7 @@ function mapRecordTypeInner(
computed &&
checker.declarationsOf(p).some((d) => {
const sf = d.getSourceFile();
- return sf.isDeclarationFile && !ctx.isExternalTypeFile(sf);
+ return sf.isDeclarationFile && !ctx.isExternalTypeFile(sf) && !packageTypeFile(sf, ctx);
})
) {
return null;
@@ -6257,7 +6291,7 @@ export function describeRecordMemberBlocker(widened: ts.Type, ctx: TypeMapperCtx
computed &&
checker.declarationsOf(p).some((d) => {
const sf = d.getSourceFile();
- return sf.isDeclarationFile && !ctx.isExternalTypeFile(sf);
+ return sf.isDeclarationFile && !ctx.isExternalTypeFile(sf) && !packageTypeFile(sf, ctx);
})
) {
return null;
diff --git a/packages/compiler/src/frontend/type-only-imports.ts b/packages/compiler/src/frontend/type-only-imports.ts
new file mode 100644
index 000000000..003d4ccf7
--- /dev/null
+++ b/packages/compiler/src/frontend/type-only-imports.ts
@@ -0,0 +1,143 @@
+/** Type-only module edges under Node's type stripping.
+ *
+ * Node runs TypeScript by erasing types in place, with the semantics of
+ * `verbatimModuleSyntax`: `import type …` and `export type … from` vanish,
+ * but `import { type A } from "./m"` and `export { type A } from "./m"`
+ * keep an empty clause (`import {} from "./m"`) and still load and
+ * evaluate `./m`. Only the erased forms are absent from Node's module
+ * graph. */
+import * as ts from "./ts7/adapter.js";
+import { moduleSourceCandidates } from "./module-source-candidates.js";
+
+/** `import type …` (default, named, or namespace): erased by Node, so
+ * module order, cycle detection, init calls, package selection, and the
+ * startup-refusal probe must not count it (zod v4's core modules are
+ * "cyclic" only through `import type * as ns` namespaces, and a
+ * type-only import of a module Node cannot resolve is not a startup
+ * crash). Side-effect imports and inline `type` specifiers stay real
+ * edges. */
+export function erasedTypeOnlyImport(stmt: ts.ImportDeclaration): boolean {
+ return stmt.importClause?.phaseModifier === ts.SyntaxKind.TypeKeyword;
+}
+
+/** `import { type A, type B } from "./m"`: a real module edge that binds
+ * nothing at runtime, exactly like a side-effect import. */
+export function typeOnlyNamedImport(stmt: ts.ImportDeclaration): boolean {
+ const clause = stmt.importClause;
+ return (
+ clause !== undefined &&
+ clause.phaseModifier !== ts.SyntaxKind.TypeKeyword &&
+ clause.name === undefined &&
+ clause.namedBindings !== undefined &&
+ ts.isNamedImports(clause.namedBindings) &&
+ clause.namedBindings.elements.length > 0 &&
+ clause.namedBindings.elements.every((e) => e.isTypeOnly)
+ );
+}
+
+/** One module reference of a program file: its specifier and whether
+ * Node erases it. */
+interface ModuleReference {
+ spec: string;
+ erased: boolean;
+ kind?: "import" | "require";
+}
+
+function declarationReferencesOf(sf: ts.SourceFile): ModuleReference[] {
+ const refs: ModuleReference[] = [];
+ for (const stmt of sf.statements) {
+ if (ts.isImportDeclaration(stmt)) {
+ if (ts.isStringLiteral(stmt.moduleSpecifier))
+ refs.push({ spec: stmt.moduleSpecifier.text, erased: erasedTypeOnlyImport(stmt) });
+ } else if (ts.isExportDeclaration(stmt)) {
+ if (stmt.moduleSpecifier !== undefined && ts.isStringLiteral(stmt.moduleSpecifier))
+ refs.push({ spec: stmt.moduleSpecifier.text, erased: stmt.isTypeOnly });
+ } else if (
+ ts.isImportEqualsDeclaration(stmt) &&
+ ts.isExternalModuleReference(stmt.moduleReference) &&
+ ts.isStringLiteralLike(stmt.moduleReference.expression)
+ ) {
+ refs.push({
+ spec: stmt.moduleReference.expression.text,
+ erased: stmt.isTypeOnly,
+ kind: "require",
+ });
+ }
+ }
+ return refs;
+}
+
+/** Every call with a leading string literal counts as a loading edge —
+ * require(), import(), createRequire'd require functions, and loader
+ * helpers alike. Over-counting only keeps a file in the graph. */
+function callReferencesOf(program: ts.Program, sf: ts.SourceFile): ModuleReference[] {
+ const refs: ModuleReference[] = [];
+ for (const node of moduleSourceCandidates(program, sf).calls) {
+ const arg = node.arguments[0];
+ if (arg === undefined || !ts.isStringLiteralLike(arg)) continue;
+ refs.push({
+ spec: arg.text,
+ erased: false,
+ kind: node.expression.kind === ts.SyntaxKind.ImportKeyword ? "import" : "require",
+ });
+ }
+ return refs;
+}
+
+/** The program files Node never loads because the only references that
+ * reach them are erased type-only forms (directly or through other such
+ * files). A file referenced by nothing at all (a program root, or a file
+ * reached through a reference this walk does not model, such as an
+ * `import("./m")` type query) keeps its place, as do `extraRoots` (fork
+ * and worker targets). Empty when no erased reference resolves to a
+ * program file — the common case pays one statement scan per file. */
+export function typeOnlyReachedFiles(
+ program: ts.Program,
+ entry: ts.SourceFile,
+ files: readonly ts.SourceFile[],
+ extraRoots: () => readonly string[],
+ resolveEdge: (
+ from: ts.SourceFile,
+ spec: string,
+ resolutionKind?: "import" | "require",
+ ) => ts.SourceFile | null,
+): Set {
+ const available = new Set(files);
+ const declarationRefs = new Map(files.map((sf) => [sf, declarationReferencesOf(sf)] as const));
+ const target = (sf: ts.SourceFile, ref: ModuleReference): ts.SourceFile | null => {
+ const dep = resolveEdge(sf, ref.spec, ref.kind);
+ return dep === null || dep === sf || !available.has(dep) ? null : dep;
+ };
+ if (
+ !files.some((sf) =>
+ declarationRefs.get(sf)!.some((ref) => ref.erased && target(sf, ref) !== null),
+ )
+ )
+ return new Set();
+ const valueEdges = new Map();
+ const referenced = new Set();
+ for (const sf of files) {
+ const deps: ts.SourceFile[] = [];
+ valueEdges.set(sf, deps);
+ for (const ref of [...declarationRefs.get(sf)!, ...callReferencesOf(program, sf)]) {
+ const dep = target(sf, ref);
+ if (dep === null) continue;
+ referenced.add(dep);
+ if (!ref.erased) deps.push(dep);
+ }
+ }
+ const roots = files.filter((sf) => sf === entry || !referenced.has(sf));
+ for (const path of extraRoots()) {
+ const sf = program.getSourceFile(path);
+ if (sf !== undefined && available.has(sf)) roots.push(sf);
+ }
+ const reached = new Set();
+ const queue = [...roots];
+ while (queue.length > 0) {
+ const sf = queue.pop()!;
+ if (reached.has(sf)) continue;
+ reached.add(sf);
+ for (const dep of valueEdges.get(sf) ?? []) if (!reached.has(dep)) queue.push(dep);
+ }
+ return new Set(files.filter((sf) => !reached.has(sf)));
+}
diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts
index c3009665a..56b787e18 100644
--- a/packages/compiler/src/index.ts
+++ b/packages/compiler/src/index.ts
@@ -172,11 +172,22 @@ export {
} from "./diagnostics/render.js";
export {
renderCoverage,
+ renderExplainDynamic,
coverageEnvelope,
coveragePasses,
type CoverageInput,
type CoverageFailOn,
} from "./coverage/report.js";
+export {
+ TIER_MODES,
+ type DemotedUnit,
+ type DemotionRequest,
+ type DemotionSeed,
+ type DemotionUnit,
+ type DemotionUnitKind,
+ type TierMode,
+ type TierReport,
+} from "./frontend/tier-analysis.js";
export {
buildEnvelope,
DIAGNOSTICS_SCHEMA_VERSION,
@@ -208,6 +219,13 @@ export {
} from "./compat/fetch-profile.js";
export { LIB_FN_SIGS } from "./ir/builtin-signatures.js";
export { validateModule } from "./ir/validate.js";
+export {
+ assignLegacyTiers,
+ dynamicOperationOf,
+ functionTier,
+ type DynamicFunction,
+ type IrTier,
+} from "./ir/tiers.js";
export {
deserializeModule,
deserializeModuleBytes,
diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts
index 1bb9cbd14..155c097d5 100644
--- a/packages/compiler/src/ir/builtin-effects.ts
+++ b/packages/compiler/src/ir/builtin-effects.ts
@@ -489,6 +489,13 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([
"str.encodeUri",
"qs.escape",
"qs.stringify",
+ // node:punycode's RangeErrors (encode/toASCII only past 32-bit deltas)
+ // and String.fromCodePoint's for ucs2.encode.
+ "punycode.decode",
+ "punycode.encode",
+ "punycode.toASCII",
+ "punycode.toUnicode",
+ "punycode.ucs2Encode",
// The base64 globals: atob/btoa throw the catchable DOMException
// InvalidCharacterError on malformed input; the zero-argument form
// always throws Node's TypeError [ERR_MISSING_ARGS].
diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts
index 08c493ae4..8015c104a 100644
--- a/packages/compiler/src/ir/builtin-signatures.ts
+++ b/packages/compiler/src/ir/builtin-signatures.ts
@@ -563,6 +563,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
/** os.constants: the per-thread null-prototype constants object (dlopen,
* errno, signals, priority, UV_UDP_REUSEADDR). Never throws. */
"os.constants": { argTypes: [], result: DYN },
+ /** The deprecated node:constants object (scr_os.c): the os groups and
+ * fs.constants merged with the borrowed baked crypto.constants table,
+ * frozen, one per thread. */
+ "constants.object": { argTypes: [DYN], result: DYN },
/** umask(2): arg < 0 reads without setting (umask has no read-only form
* — set 0, restore); otherwise sets and answers the PREVIOUS mask.
* Never throws. */
@@ -861,6 +865,19 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"url.legacyResolve": { argTypes: [DYN, DYN, BOOL], result: STRING },
"url.domainToASCII": { argTypes: [DYN, BOOL], result: STRING },
"url.domainToUnicode": { argTypes: [DYN, BOOL], result: STRING },
+ /** node:punycode (scr_url.c): punycode.js 2.1.0's string conversions;
+ * decode/toUnicode (and, past 32-bit deltas, encode/toASCII) throw the
+ * module's RangeErrors, ucs2Encode String.fromCodePoint's. warn marks the
+ * module's first load (scr_async_dyn.c) and reports DEP0040 then unless
+ * the second argument is false: at once for a static ES import, on the
+ * next tick for a require() call. */
+ "punycode.decode": { argTypes: [STRING], result: STRING },
+ "punycode.encode": { argTypes: [STRING], result: STRING },
+ "punycode.toASCII": { argTypes: [STRING], result: STRING },
+ "punycode.toUnicode": { argTypes: [STRING], result: STRING },
+ "punycode.ucs2Decode": { argTypes: [STRING], result: arrayOf(F64) },
+ "punycode.ucs2Encode": { argTypes: [arrayOf(F64)], result: STRING },
+ "punycode.warn": { argTypes: [BOOL, BOOL], result: VOID },
"url.fileURLToPathBuffer": { argTypes: [DYN, DYN], result: BYTES_U8 },
"url.pathToFileURLChecked": { argTypes: [DYN, DYN], result: URL_T },
"url.fileURLToPathStr": { argTypes: [STRING], result: STRING },
diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts
index 2d8aed0e8..ef54f9d2b 100644
--- a/packages/compiler/src/ir/ir.ts
+++ b/packages/compiler/src/ir/ir.ts
@@ -1660,6 +1660,14 @@ export interface IrFunction {
sourceName?: string;
/** Ordinary authored function declarations/expressions own a prototype. */
ownsPrototype?: true;
+ /** The function's execution tier (ir/tiers.ts). Absent means static: the
+ * function may use dynamic values only through boundary operations
+ * (dynFrom, dynCheck, kind tests) and moves, which the validator enforces
+ * (invariant I1). "dynamic" admits the dynamic operation vocabulary
+ * (dynKeyGet, dynCall, dyn.* builtins, ...). Lowering assigns the field
+ * from the body (assignLegacyTiers); stage S2 of the two-tier design
+ * keeps it out of serialized IR, so loaders derive it again. */
+ tier?: "dynamic";
body: IrStmt[];
loc: SrcLoc;
}
diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts
index d16edfabd..7df85c738 100644
--- a/packages/compiler/src/ir/runtime-features.ts
+++ b/packages/compiler/src/ir/runtime-features.ts
@@ -46,6 +46,8 @@ export interface RuntimeFeatures {
}
const DYN_ASYNC_LIB_FNS: ReadonlySet = new Set([
+ // DEP0040 rides the dyn-async unit's warning machinery.
+ "punycode.warn",
"fs.callbackValue",
"fs.callbackCall",
"fs.callbackGlob",
diff --git a/packages/compiler/src/ir/serialize.ts b/packages/compiler/src/ir/serialize.ts
index 54e210ea3..1d0e5364e 100644
--- a/packages/compiler/src/ir/serialize.ts
+++ b/packages/compiler/src/ir/serialize.ts
@@ -7,6 +7,7 @@
* text parts and decoded from bytes one function at a time; neither direction
* materializes the complete document as a single string. */
import type { IrExpr, IrFunction, IrModule, IrStmt } from "./ir.js";
+import { deriveModuleTiers } from "./tiers.js";
// Version 16 removes embedded-engine types, operations, and module metadata.
// Older modules can require runtime support that no longer exists.
@@ -42,6 +43,19 @@ function encodeNumber(_key: string, value: unknown): unknown {
return value;
}
+/** Stage S2 of the two-tier design keeps function tiers out of IR
+ * artifacts, so a report-only release changes no serialized byte; loaders
+ * derive them again (deriveModuleTiers). No IR node other than a function
+ * carries a `tier` key. */
+function encodeWithoutTier(key: string, value: unknown): unknown {
+ return key === "tier" ? undefined : encodeNumber(key, value);
+}
+
+function encodeFunction(fn: IrFunction): string | undefined {
+ if (fn.tier !== undefined) return JSON.stringify(fn, encodeWithoutTier);
+ return ordinaryNumbers(fn) ? JSON.stringify(fn) : JSON.stringify(fn, encodeNumber);
+}
+
/** The compact encoding as ordered parts: the module header up to the
* function array, one part per function (with separate comma parts), and the
* header's remainder. Concatenating the parts yields exactly
@@ -58,9 +72,7 @@ export function serializeModuleParts(mod: IrModule): string[] {
for (let index = 0; index < mod.functions.length; index++) {
const fn = mod.functions[index]!;
if (index > 0) parts.push(",");
- parts.push(
- (ordinaryNumbers(fn) ? JSON.stringify(fn) : JSON.stringify(fn, encodeNumber)) ?? "null",
- );
+ parts.push(encodeFunction(fn) ?? "null");
}
parts.push("]" + header.slice(offset + slot.length));
return parts;
@@ -70,7 +82,7 @@ export function serializeModuleParts(mod: IrModule): string[] {
* readable default. Both return one string, so modules whose encoding
* exceeds the host's string limit must use `serializeModuleParts`. */
export function serializeModule(mod: IrModule, compact = false): string {
- if (!compact) return JSON.stringify(mod, encodeNumber, 2);
+ if (!compact) return JSON.stringify(mod, encodeWithoutTier, 2);
return serializeModuleParts(mod).join("");
}
@@ -96,7 +108,7 @@ function checkedVersion(mod: IrModule): IrModule {
}
export function deserializeModule(json: string): IrModule {
- return checkedVersion(JSON.parse(json, decodeNumber) as IrModule);
+ return deriveModuleTiers(checkedVersion(JSON.parse(json, decodeNumber) as IrModule));
}
/** A node as it reads back from the compact encoding: a fresh object, as
@@ -249,5 +261,5 @@ export function deserializeModuleBytes(bytes: Buffer): IrModule {
);
}
mod.functions = functions;
- return mod;
+ return deriveModuleTiers(mod);
}
diff --git a/packages/compiler/src/ir/tiers.test.ts b/packages/compiler/src/ir/tiers.test.ts
new file mode 100644
index 000000000..c5f1ad4b8
--- /dev/null
+++ b/packages/compiler/src/ir/tiers.test.ts
@@ -0,0 +1,142 @@
+import { expect, test } from "vitest";
+import { BOOL, DYN, F64, STRING, VOID, type IrExpr, type IrFunction, type IrModule } from "./ir.js";
+import {
+ deserializeModule,
+ deserializeModuleBytes,
+ serializeModule,
+ serializeModuleParts,
+} from "./serialize.js";
+import {
+ assignLegacyTiers,
+ dynamicOperationOf,
+ firstDynamicOperation,
+ functionTier,
+} from "./tiers.js";
+import { validateModule } from "./validate.js";
+
+const loc = { file: "tiers.ts", start: 0, end: 0 };
+const dynParam = { kind: "varRef", localId: "v", type: DYN, loc } as const satisfies IrExpr;
+
+function fn(name: string, value: IrExpr): IrFunction {
+ return {
+ name,
+ params: [{ localId: "v", name: "v", type: DYN }],
+ returnType: value.type,
+ locals: [{ id: "v", name: "v", type: DYN, mutable: false }],
+ body: [{ kind: "return", value, loc }],
+ loc,
+ };
+}
+
+function moduleOf(...functions: IrFunction[]): IrModule {
+ const main: IrFunction = {
+ name: "%main",
+ params: [],
+ returnType: VOID,
+ locals: [],
+ body: [],
+ loc,
+ };
+ return { irVersion: 16, sourceFile: "tiers.ts", functions: [main, ...functions], entry: "%main" };
+}
+
+const keyGet: IrExpr = {
+ kind: "dynKeyGet",
+ key: { kind: "strLit", value: "name", type: STRING, loc },
+ value: dynParam,
+ type: DYN,
+ loc,
+};
+
+test("dynamic operations and boundary operations are told apart", () => {
+ expect(dynamicOperationOf(keyGet)).toBe("dynKeyGet");
+ expect(
+ dynamicOperationOf({
+ kind: "libCall",
+ fn: "dyn.add",
+ args: [dynParam, dynParam],
+ type: DYN,
+ loc,
+ }),
+ ).toBe("dyn.add");
+ expect(
+ dynamicOperationOf({
+ kind: "call",
+ callee: "%dyn.class.property.12",
+ args: [dynParam],
+ type: DYN,
+ loc,
+ }),
+ ).toBe("%dyn.class.property");
+ // Boundary operations: conversions and kind tests.
+ expect(dynamicOperationOf({ kind: "dynCheck", value: dynParam, type: F64, loc })).toBeNull();
+ expect(
+ dynamicOperationOf({
+ kind: "dynFrom",
+ value: { kind: "numLit", value: 1, type: F64, loc },
+ type: DYN,
+ loc,
+ }),
+ ).toBeNull();
+ expect(
+ dynamicOperationOf({ kind: "dynTest", test: "string", value: dynParam, type: BOOL, loc }),
+ ).toBeNull();
+ expect(
+ dynamicOperationOf({ kind: "libCall", fn: "dyn.typedRefIs", args: [dynParam], type: DYN, loc }),
+ ).toBeNull();
+ // Moves and calls of ordinary functions.
+ expect(dynamicOperationOf(dynParam)).toBeNull();
+ expect(
+ dynamicOperationOf({ kind: "call", callee: "helper", args: [dynParam], type: DYN, loc }),
+ ).toBeNull();
+});
+
+test("lowering's classification puts exactly the functions with dynamic operations in the dynamic tier", () => {
+ const reader = fn("reader", keyGet);
+ const mover = fn("mover", dynParam);
+ const dynamic = assignLegacyTiers([reader, mover]);
+ expect(dynamic.map((d) => [d.name, d.operation])).toEqual([["reader", "dynKeyGet"]]);
+ expect(functionTier(reader)).toBe("dynamic");
+ expect(functionTier(mover)).toBe("static");
+ expect(firstDynamicOperation(mover)).toBeNull();
+});
+
+test("the validator enforces I1: static functions perform no dynamic operation", () => {
+ const reader = fn("reader", keyGet);
+ const errors = validateModule(moduleOf(reader));
+ expect(errors.map((e) => e.message)).toEqual([
+ "in reader: static-tier function performs the dynamic operation dynKeyGet",
+ ]);
+ reader.tier = "dynamic";
+ expect(validateModule(moduleOf(reader))).toEqual([]);
+ // A logical operator over dynamic values applies ToBoolean to them.
+ const either = fn("either", {
+ kind: "logical",
+ op: "||",
+ left: dynParam,
+ right: dynParam,
+ type: DYN,
+ loc,
+ });
+ expect(validateModule(moduleOf(either)).map((e) => e.message)).toEqual([
+ "in either: static-tier function performs the dynamic operation logical",
+ ]);
+});
+
+test("serialized IR carries no tiers in stage S2, and loading derives them again", () => {
+ const reader = fn("reader", keyGet);
+ const mod = moduleOf(reader);
+ const untiered = serializeModule(mod, true);
+ assignLegacyTiers(mod.functions);
+ expect(reader.tier).toBe("dynamic");
+ expect(serializeModule(mod, true)).toBe(untiered);
+ expect(serializeModuleParts(mod).join("")).toBe(untiered);
+ expect(serializeModule(mod)).not.toContain('"tier"');
+ for (const loaded of [
+ deserializeModule(untiered),
+ deserializeModuleBytes(Buffer.from(untiered)),
+ ]) {
+ expect(loaded.functions.map(functionTier)).toEqual(["static", "dynamic"]);
+ expect(validateModule(loaded)).toEqual([]);
+ }
+});
diff --git a/packages/compiler/src/ir/tiers.ts b/packages/compiler/src/ir/tiers.ts
new file mode 100644
index 000000000..201bc2a58
--- /dev/null
+++ b/packages/compiler/src/ir/tiers.ts
@@ -0,0 +1,157 @@
+/* Execution tiers (two-tier design, stage S2: report-only).
+ *
+ * Every IR function belongs to one of two tiers:
+ *
+ * - static: today's compiler. Exact layouts, direct calls, unboxed values.
+ * A static function may hold dynamic (`dyn`) values, pass them to calls,
+ * return them and store them, and it may cross the boundary with the
+ * boundary operations below, but it never operates on a dynamic value.
+ * - dynamic: code that operates on dynamic values with JavaScript semantics
+ * (property reads and writes, generic operators, calls of dynamic values,
+ * dynamic object and array construction).
+ *
+ * Invariant I1 (validate.ts): a static function contains no dynamic
+ * operation. Only boundary operations and moves may mix the tiers. Calls
+ * of generated `%dyn.*` helpers count as dynamic operations: they are the
+ * dynamic dispatch the lowering placed out of line. Calls of other dynamic
+ * functions are boundaries (the design's "call to a dynamic function").
+ *
+ * Boundary operations:
+ * - `dynFrom` (static to dynamic, the design's `toJs`) and `caughtToDyn`;
+ * - `dynCheck` (dynamic to static, the design's `fromJs`);
+ * - kind tests that only inspect a dynamic value's tag (`dynTest`, the
+ * `dyn.*Is` and `dyn.typeof` builtins): the tag test of a conversion;
+ * - calls of non-`dyn.*` builtins whose signatures take or return `dyn`
+ * (typed APIs implemented through dyn, leak L6 of the design). They stay
+ * boundaries until stage S1 gives them typed signatures.
+ *
+ * Classification of existing code. The `dyn` vocabulary is today's
+ * dynamic tier (`ScrDyn`): `any`, untyped JavaScript, unschematized
+ * `JSON.parse`, and the generated dynamic dispatch helpers already lower to
+ * it. Lowering therefore assigns tiers from function bodies: a function
+ * containing a dynamic operation is dynamic, every other function is static
+ * (`assignLegacyTiers`). Demotion (stage S6) will add tier decisions that
+ * do not follow from a body; until then the classification is exact, and
+ * the validator guards it against later passes and hand-written IR.
+ *
+ * Serialization. Stage S2 must not change any artifact, so serialized IR
+ * omits `tier` (serialize.ts) and loaders derive it again with the same
+ * classification. The next IR version serializes the field. */
+import type { IrExpr, IrFunction, IrModule, SrcLoc } from "./ir.js";
+import { everyStmtList, type IrVisitor } from "./traverse.js";
+
+export type IrTier = "static" | "dynamic";
+
+export function functionTier(fn: IrFunction): IrTier {
+ return fn.tier === "dynamic" ? "dynamic" : "static";
+}
+
+/** `dyn.*` builtins that only inspect a dynamic value's kind: the tag test
+ * half of a dynamic-to-static conversion. */
+const DYN_KIND_TESTS: ReadonlySet = new Set([
+ "dyn.typedRefIs",
+ "dyn.typedRefKeyIs",
+ "dyn.classIs",
+ "dyn.nativeSetIs",
+ "dyn.nativeMapIs",
+ "dyn.nativeUrlIs",
+ "dyn.nativeDateIs",
+ "dyn.nativeRegexIs",
+ "dyn.dataViewIs",
+ "dyn.typeof",
+]);
+
+const DYN_HELPER_PREFIX = "%dyn.";
+
+/** `%dyn.class.property.12` → `%dyn.class.property`. */
+function dynHelperFamily(name: string): string {
+ return name.replace(/(\.\d+)+$/, "");
+}
+
+/** The dynamic operation an expression performs, named for reports
+ * (`dynKeyGet`, `dyn.keySet`, ...), or null for static operations, boundary
+ * operations and moves. Only the node itself is inspected. */
+export function dynamicOperationOf(e: IrExpr): string | null {
+ switch (e.kind) {
+ case "dynCall":
+ case "dynInvoke":
+ case "dynObjLit":
+ case "dynArrLit":
+ case "dynKeyGet":
+ case "dynHasKey":
+ case "dynScalarEq":
+ case "dynDestrCheck":
+ case "dynIterN":
+ return e.kind;
+ case "logical":
+ // `a || b` over dyn operands applies ToBoolean to a dynamic value.
+ return e.type.kind === "dyn" ? "logical" : null;
+ case "libCall":
+ return e.fn.startsWith("dyn.") && !DYN_KIND_TESTS.has(e.fn) ? e.fn : null;
+ case "call":
+ // Generated `%dyn.*` helpers are dynamic operations lowered out of
+ // line (class dynamic dispatch, dynamic HOF adapters).
+ return e.callee.startsWith(DYN_HELPER_PREFIX) ? dynHelperFamily(e.callee) : null;
+ default:
+ return null;
+ }
+}
+
+export interface DynamicOperationSite {
+ operation: string;
+ loc: SrcLoc;
+}
+
+function collectDynamicOperations(fn: IrFunction, first: boolean): DynamicOperationSite[] {
+ const found: DynamicOperationSite[] = [];
+ const visitor: IrVisitor = {
+ expr: (e) => {
+ const operation = dynamicOperationOf(e);
+ if (operation === null) return true;
+ found.push({ operation, loc: e.loc });
+ return !first;
+ },
+ stmt: () => true,
+ };
+ everyStmtList(fn.body, visitor);
+ return found;
+}
+
+/** The first dynamic operation in a function body, in traversal order. */
+export function firstDynamicOperation(fn: IrFunction): DynamicOperationSite | null {
+ return collectDynamicOperations(fn, true)[0] ?? null;
+}
+
+/** Every dynamic operation in a function body (coverage reports). */
+export function dynamicOperationSites(fn: IrFunction): DynamicOperationSite[] {
+ return collectDynamicOperations(fn, false);
+}
+
+/** A function the classification placed in the dynamic tier, with the
+ * operation that put it there. */
+export interface DynamicFunction {
+ name: string;
+ loc: SrcLoc;
+ operation: string;
+ operationLoc: SrcLoc;
+}
+
+/** Assign every function its tier from its body (see the header). Returns
+ * the dynamic functions in module order. */
+export function assignLegacyTiers(functions: readonly IrFunction[]): DynamicFunction[] {
+ const dynamic: DynamicFunction[] = [];
+ for (const fn of functions) {
+ const site = firstDynamicOperation(fn);
+ if (site === null) continue;
+ fn.tier = "dynamic";
+ dynamic.push({ name: fn.name, loc: fn.loc, operation: site.operation, operationLoc: site.loc });
+ }
+ return dynamic;
+}
+
+/** Loaders call this on deserialized modules (stage S2 serializes no
+ * tiers). */
+export function deriveModuleTiers(mod: IrModule): IrModule {
+ assignLegacyTiers(mod.functions);
+ return mod;
+}
diff --git a/packages/compiler/src/ir/validate.test.ts b/packages/compiler/src/ir/validate.test.ts
index b7caf8c41..8086cc176 100644
--- a/packages/compiler/src/ir/validate.test.ts
+++ b/packages/compiler/src/ir/validate.test.ts
@@ -20,6 +20,7 @@ import {
type IrUnionDef,
} from "./ir.js";
import { deserializeModule, serializeModule } from "./serialize.js";
+import { assignLegacyTiers } from "./tiers.js";
import { validateModule } from "./validate.js";
const loc = { file: "numeric-read.ts", start: 0, end: 0 };
@@ -399,7 +400,7 @@ test.each(["sortPrimitive", "toSortedPrimitive"] as const)(
);
function expressionModule(expr: IrExpr, unions: IrUnionDef[]): IrModule {
- return {
+ const mod: IrModule = {
irVersion: 16,
sourceFile: loc.file,
entry: "main",
@@ -415,6 +416,9 @@ function expressionModule(expr: IrExpr, unions: IrUnionDef[]): IrModule {
},
],
};
+ // Lowering assigns tiers from function bodies (ir/tiers.ts).
+ assignLegacyTiers(mod.functions);
+ return mod;
}
test("numeric byte tokens and DataView stores validate their storage before emission", () => {
diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts
index 9c211e527..cecacc01c 100644
--- a/packages/compiler/src/ir/validate.ts
+++ b/packages/compiler/src/ir/validate.ts
@@ -59,6 +59,7 @@ import {
import { DYN_CLASS_PROPERTIES } from "./ir.js";
import { unionWideningTags } from "./analysis.js";
import { alwaysReturns } from "./control-flow.js";
+import { dynamicOperationOf, functionTier } from "./tiers.js";
import { LIB_FN_SIGS } from "./builtin-signatures.js";
/** The promisified crypto calls' value kinds besides checked-dynamic. */
@@ -1021,6 +1022,14 @@ function validateFunction(
const locals = new Map(fn.locals.map((l) => [l.id, l]));
const err = (message: string, loc: SrcLoc) =>
errors.push({ message: `in ${fn.name}: ${message}`, loc });
+ // Invariant I1 (ir/tiers.ts): a static function reaches dynamic values
+ // only through boundary operations and moves.
+ const staticTier = functionTier(fn) === "static";
+ const checkTier = (e: IrExpr): void => {
+ const operation = dynamicOperationOf(e);
+ if (operation !== null)
+ err(`static-tier function performs the dynamic operation ${operation}`, e.loc);
+ };
const asyncCaches = [
["asyncCacheGlobal", fn.asyncCacheGlobal],
@@ -1265,6 +1274,8 @@ function validateFunction(
continue;
}
if (e.kind === "logical") {
+ // checkExpr already checked the root's tier.
+ if (staticTier && e !== root) checkTier(e);
if (
e.type.kind !== "f64" &&
e.type.kind !== "string" &&
@@ -1308,6 +1319,7 @@ function validateFunction(
}
function checkExpr(e: IrExpr): void {
+ if (staticTier) checkTier(e);
switch (e.kind) {
case "numLit":
case "strLit":
diff --git a/packages/compiler/src/library/prepare.ts b/packages/compiler/src/library/prepare.ts
index 94f20198a..298bb43bb 100644
--- a/packages/compiler/src/library/prepare.ts
+++ b/packages/compiler/src/library/prepare.ts
@@ -553,6 +553,8 @@ export function prepareLibrary(
try {
lowered = fe.lower({
targetPlatform: buildPlatform,
+ // Library artifacts are static-tier only (two-tier design I2).
+ tierMode: "static-only",
...(cbImports.length > 0 ? { ffiImports: cbImports } : {}),
...(cbImports.length > 0 ? { libraryCallbacks: true } : {}),
// The profile-mapped exports are called from OUTSIDE the graph:
diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json
index f5313f12d..6dc871976 100644
--- a/packages/compiler/surface-manifest.json
+++ b/packages/compiler/surface-manifest.json
@@ -466,6 +466,13 @@
"status": "static",
"note": "recognized module (bare and node:-prefixed specifiers)"
},
+ {
+ "id": "node-builtin.constants",
+ "kind": "node-builtin",
+ "name": "constants",
+ "status": "static",
+ "note": "recognized module (bare and node:-prefixed specifiers)"
+ },
{
"id": "node-builtin.crypto",
"kind": "node-builtin",
@@ -2107,6 +2114,56 @@
"status": "static",
"note": "shared version dictionary containing node and openssl; other components are absent unless defined by the program"
},
+ {
+ "id": "node-builtin.punycode",
+ "kind": "node-builtin",
+ "name": "punycode",
+ "status": "static",
+ "note": "recognized module (bare and node:-prefixed specifiers)"
+ },
+ {
+ "id": "node-builtin.punycode.decode",
+ "kind": "node-builtin",
+ "name": "punycode.decode",
+ "status": "static"
+ },
+ {
+ "id": "node-builtin.punycode.encode",
+ "kind": "node-builtin",
+ "name": "punycode.encode",
+ "status": "static"
+ },
+ {
+ "id": "node-builtin.punycode.toASCII",
+ "kind": "node-builtin",
+ "name": "punycode.toASCII",
+ "status": "static"
+ },
+ {
+ "id": "node-builtin.punycode.toUnicode",
+ "kind": "node-builtin",
+ "name": "punycode.toUnicode",
+ "status": "static"
+ },
+ {
+ "id": "node-builtin.punycode.ucs2.decode",
+ "kind": "node-builtin",
+ "name": "punycode.ucs2.decode",
+ "status": "static"
+ },
+ {
+ "id": "node-builtin.punycode.ucs2.encode",
+ "kind": "node-builtin",
+ "name": "punycode.ucs2.encode",
+ "status": "static"
+ },
+ {
+ "id": "node-builtin.punycode.version",
+ "kind": "node-builtin",
+ "name": "punycode.version",
+ "status": "static",
+ "note": "constant value read"
+ },
{
"id": "node-builtin.querystring",
"kind": "node-builtin",
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5633-zlib-typed-options.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5633-zlib-typed-options.ts.json
new file mode 100644
index 000000000..f4d9673d5
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5633-zlib-typed-options.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/5633-zlib-typed-options.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5683-date-local-fields-tz.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5683-date-local-fields-tz.ts.json
new file mode 100644
index 000000000..7ac133650
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5683-date-local-fields-tz.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/5683-date-local-fields-tz.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5919-record-unions-shared-arms.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5919-record-unions-shared-arms.ts.json
new file mode 100644
index 000000000..bfb23e115
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5919-record-unions-shared-arms.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/5919-record-unions-shared-arms.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5930-record-union-pointers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5930-record-union-pointers.ts.json
new file mode 100644
index 000000000..e6e3632f5
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5930-record-union-pointers.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/5930-record-union-pointers.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5931-record-union-unit-arms.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5931-record-union-unit-arms.ts.json
new file mode 100644
index 000000000..53d531568
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5931-record-union-unit-arms.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/5931-record-union-unit-arms.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5932-record-union-trees.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5932-record-union-trees.ts.json
new file mode 100644
index 000000000..ee4281193
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5932-record-union-trees.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/5932-record-union-trees.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5933-record-union-conversions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5933-record-union-conversions.ts.json
new file mode 100644
index 000000000..a1fcec317
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5933-record-union-conversions.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/5933-record-union-conversions.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5934-record-union-set-members.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5934-record-union-set-members.ts.json
new file mode 100644
index 000000000..98b0ac1f4
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5934-record-union-set-members.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/5934-record-union-set-members.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5976-builtin-class-aliases.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5976-builtin-class-aliases.ts.json
new file mode 100644
index 000000000..f83d23fc7
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5976-builtin-class-aliases.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/5976-builtin-class-aliases.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5977-builtin-class-aliases.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5977-builtin-class-aliases.cjs.json
new file mode 100644
index 000000000..99d07e4cd
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5977-builtin-class-aliases.cjs.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/5977-builtin-class-aliases.cjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5978-builtin-class-alias-modules/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5978-builtin-class-alias-modules/main.ts.json
new file mode 100644
index 000000000..fd6bb94f5
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5978-builtin-class-alias-modules/main.ts.json
@@ -0,0 +1,7 @@
+{
+ "order": [
+ "/tests/corpus/5978-builtin-class-alias-modules/classes.ts",
+ "/tests/corpus/5978-builtin-class-alias-modules/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6100-record-get-or-create.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6100-record-get-or-create.ts.json
new file mode 100644
index 000000000..b7c18a8a4
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6100-record-get-or-create.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6100-record-get-or-create.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6101-record-get-or-create-unchecked/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6101-record-get-or-create-unchecked/main.ts.json
new file mode 100644
index 000000000..dcb5b192f
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6101-record-get-or-create-unchecked/main.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6101-record-get-or-create-unchecked/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6103-typeof-builtin-bindings.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6103-typeof-builtin-bindings.cjs.json
new file mode 100644
index 000000000..008914d85
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6103-typeof-builtin-bindings.cjs.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6103-typeof-builtin-bindings.cjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6104-typeof-builtin-bindings.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6104-typeof-builtin-bindings.mjs.json
new file mode 100644
index 000000000..b6a8a48e2
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6104-typeof-builtin-bindings.mjs.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6104-typeof-builtin-bindings.mjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6105-cjs-top-level-this/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6105-cjs-top-level-this/main.js.json
new file mode 100644
index 000000000..ef768b4ce
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6105-cjs-top-level-this/main.js.json
@@ -0,0 +1,7 @@
+{
+ "order": [
+ "/tests/corpus/6105-cjs-top-level-this/lib.js",
+ "/tests/corpus/6105-cjs-top-level-this/main.js"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6106-esm-top-level-this.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6106-esm-top-level-this.mjs.json
new file mode 100644
index 000000000..ad427bd7f
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6106-esm-top-level-this.mjs.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6106-esm-top-level-this.mjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6110-cjs-speculative-wrapper-fences.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6110-cjs-speculative-wrapper-fences.cjs.json
new file mode 100644
index 000000000..0a5ce66b0
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6110-cjs-speculative-wrapper-fences.cjs.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6110-cjs-speculative-wrapper-fences.cjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6120-type-only-import-graph/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6120-type-only-import-graph/main.ts.json
new file mode 100644
index 000000000..979391c1c
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6120-type-only-import-graph/main.ts.json
@@ -0,0 +1,8 @@
+{
+ "order": [
+ "/tests/corpus/6120-type-only-import-graph/mixed.ts",
+ "/tests/corpus/6120-type-only-import-graph/flags.ts",
+ "/tests/corpus/6120-type-only-import-graph/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6121-type-specifier-edges/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6121-type-specifier-edges/main.ts.json
new file mode 100644
index 000000000..c0f3f653c
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6121-type-specifier-edges/main.ts.json
@@ -0,0 +1,10 @@
+{
+ "order": [
+ "/tests/corpus/6121-type-specifier-edges/config.ts",
+ "/tests/corpus/6121-type-specifier-edges/config-barrel.ts",
+ "/tests/corpus/6121-type-specifier-edges/b.ts",
+ "/tests/corpus/6121-type-specifier-edges/a.ts",
+ "/tests/corpus/6121-type-specifier-edges/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6122-punycode-conversions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6122-punycode-conversions.ts.json
new file mode 100644
index 000000000..b15e4fe13
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6122-punycode-conversions.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6122-punycode-conversions.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6124-punycode-require-warnings.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6124-punycode-require-warnings.cjs.json
new file mode 100644
index 000000000..dfd4aa912
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6124-punycode-require-warnings.cjs.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6124-punycode-require-warnings.cjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6125-constants-module.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6125-constants-module.cjs.json
new file mode 100644
index 000000000..ef8c7f2bf
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6125-constants-module.cjs.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6125-constants-module.cjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6126-punycode-dependency/main.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6126-punycode-dependency/main.cjs.json
new file mode 100644
index 000000000..5de83361a
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6126-punycode-dependency/main.cjs.json
@@ -0,0 +1,18 @@
+{
+ "order": [
+ "/tests/corpus/6126-punycode-dependency/main.cjs"
+ ],
+ "diags": [
+ {
+ "code": "SC1010",
+ "message": "the 'puny-dep' module is not supported yet",
+ "loc": {
+ "file": "/tests/corpus/6126-punycode-dependency/main.cjs",
+ "start": 347,
+ "end": 376
+ },
+ "milestone": "M4",
+ "hint": "relative imports (./file, ../dir/file), package.json-mediated project imports (#alias via the imports field, self-name references via exports), statically compiled npm packages, and the built-in fs, fs/promises, path, os, url, crypto, zlib, child_process, net, http, tls, https, http2, dgram, dns, util, util/types, string_decoder, querystring, readline, events, stream, stream/promises, stream/consumers, buffer, assert, assert/strict, worker_threads, cluster, tty, async_hooks, timers, timers/promises, diagnostics_channel, perf_hooks, module, and console modules (bare or node:-prefixed) and node:test (node:-prefixed only, like in Node) are supported"
+ }
+ ]
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/builtin-class-alias-escape.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/builtin-class-alias-escape.ts.json
new file mode 100644
index 000000000..f56d058f1
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/builtin-class-alias-escape.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/diagnostics/builtin-class-alias-escape.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/local-class-type-query-brand.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/local-class-type-query-brand.ts.json
new file mode 100644
index 000000000..e17ea3bf5
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/local-class-type-query-brand.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/diagnostics/local-class-type-query-brand.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/local-class-type-query-js.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/local-class-type-query-js.js.json
new file mode 100644
index 000000000..6bf774cd8
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/local-class-type-query-js.js.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/diagnostics/local-class-type-query-js.js"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/local-class-type-query.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/local-class-type-query.ts.json
new file mode 100644
index 000000000..3c89fba9f
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/local-class-type-query.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/diagnostics/local-class-type-query.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/speculative-function-refusal.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/speculative-function-refusal.ts.json
new file mode 100644
index 000000000..e24349766
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/speculative-function-refusal.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/diagnostics/speculative-function-refusal.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/runtime/src/scr_async.h b/packages/runtime/src/scr_async.h
index 7f294ca29..efa98e942 100644
--- a/packages/runtime/src/scr_async.h
+++ b/packages/runtime/src/scr_async.h
@@ -212,6 +212,11 @@ void scr_process_on_warning(ScrDyn *fn);
void scr_process_off_warning(ScrDyn *fn);
void scr_process_emit_warning(ScrDyn *args);
void scr_emit_warning(const char *name, const char *code, ScrStr *message);
+/* node:punycode's first load (scr_async_dyn.c): later calls are module
+ * cache hits. The first reports DEP0040 when `report` (false for a require
+ * made from node_modules) — at once for a static ES import, which Node
+ * links before any module evaluates, otherwise on the next tick. */
+void scr_punycode_warn(bool immediate, bool report);
/* scr_emit_warning once a 'warning' listener exists (scr_lib.c; set by the
* listener registration): units that cannot link the dyn-async unit route
* their warnings through it, and print the default report otherwise. */
diff --git a/packages/runtime/src/scr_async_dyn.c b/packages/runtime/src/scr_async_dyn.c
index b7387c13e..95771d457 100644
--- a/packages/runtime/src/scr_async_dyn.c
+++ b/packages/runtime/src/scr_async_dyn.c
@@ -904,6 +904,27 @@ void scr_warning_emit_deferred(ScrDyn *w) {
scr_warning_defer(w, scr_warning_tick);
}
+static SCR_TL bool scr_punycode_loaded = false;
+
+void scr_punycode_warn(bool immediate, bool report) {
+ if (scr_punycode_loaded) return;
+ scr_punycode_loaded = true;
+ if (!report) return;
+ static const char text[] =
+ "The `punycode` module is deprecated. Please use a userland alternative instead.";
+ ScrStr *message = scr_str_new(text, sizeof text - 1);
+ ScrStr *code = scr_str_new("DEP0040", 7);
+ ScrDyn *w = scr_warning_new(message, "DeprecationWarning", code);
+ if (immediate) {
+ if (!scr_process_deprecation_flag_on(0)) scr_warning_dispatch(w);
+ } else {
+ scr_warning_emit_deferred(w);
+ }
+ scr_dyn_release(w);
+ scr_str_release(code);
+ scr_str_release(message);
+}
+
/* The console members' warnings. Node's emitWarning emits on
* process.nextTick, so these queue in order and each rides one raw tick
* on the shared tick queue; any left at exit are dropped with it. */
diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c
index 9307d9b01..fd51e9c78 100644
--- a/packages/runtime/src/scr_lib.c
+++ b/packages/runtime/src/scr_lib.c
@@ -824,6 +824,26 @@ static void scr_node_debug_capture(const ScrStr *name) {
if (name->len == 10 && memcmp(name->data, "NODE_DEBUG", 10) == 0) (void)scr_node_debug_initial();
}
+/* Counts writes and deletes of process.env.TZ. The local-time getters keep
+ * their last conversion for the generation it was made in. */
+#ifdef SCR_WORKERS
+static atomic_uint scr_date_tz_generation;
+#else
+static unsigned scr_date_tz_generation;
+#endif
+
+/* After a write or delete of the process's TZ, local time follows the new
+ * value, as in Node: tzset(3) rereads it for later localtime calls. */
+static void scr_env_tz_changed(const ScrStr *name) {
+ /* Compared by character: a string literal would join the merged
+ * read-only strings of every program that links this file. */
+ if (name->len != 2 || name->data[0] != 'T' || name->data[1] != 'Z') return;
+#if !defined(_WIN32) && !defined(__wasi__)
+ tzset();
+#endif
+ scr_date_tz_generation++;
+}
+
/* process.env.NAME = v — setenv(3): later scr_env_get reads and spawned
* children (posix_spawn inherits environ) observe the write, like Node.
* Both args borrowed (NUL-terminated ScrStr data); setenv copies. */
@@ -842,6 +862,7 @@ void scr_env_set(const ScrStr *name, const ScrStr *value) {
#else
setenv(k, v, 1);
#endif
+ scr_env_tz_changed(name);
#ifdef SCR_WORKERS
done:
#endif
@@ -861,6 +882,7 @@ void scr_env_unset(const ScrStr *name) {
#else
unsetenv(name->data);
#endif
+ scr_env_tz_changed(name);
}
static ScrStr *scr_fs_read_file_core(ScrStr *path, bool env_file, size_t *ascii);
@@ -10475,9 +10497,14 @@ typedef struct ScrDateParts {
* endpoint. The checked wrapper is the public UTC-getter gate; the inner
* walk accepts those bounded local-time values too. */
static void scr_date_utc_parts_unchecked(double t, ScrDateParts *out) {
- double dayd = floor(t / 86400000.0);
- long long msday = (long long)(t - dayd * 86400000.0);
- long long z = (long long)dayd + 719468;
+ /* t is an integral number of milliseconds within about 2^53. */
+ long long tms = (long long)t;
+ long long day = tms / 86400000, msday = tms % 86400000;
+ if (msday < 0) {
+ msday += 86400000;
+ day--;
+ }
+ long long z = day + 719468;
long long era = (z >= 0 ? z : z - 146096) / 146097;
unsigned long long doe = (unsigned long long)(z - era * 146097);
unsigned long long yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
@@ -10487,7 +10514,7 @@ static void scr_date_utc_parts_unchecked(double t, ScrDateParts *out) {
unsigned long long d = doy - (153 * mp + 2) / 5 + 1;
unsigned long long m = mp < 10 ? mp + 3 : mp - 9;
if (m <= 2) y++;
- long long wday = ((long long)dayd + 4) % 7;
+ long long wday = (day + 4) % 7;
if (wday < 0) wday += 7;
out->year = y;
out->month = (int)m - 1;
@@ -10516,10 +10543,39 @@ static bool scr_date_localtime(double secd, struct tm *out) {
#endif
}
+/* The thread's last local conversion: reading several local fields of one
+ * Date (getFullYear, getMonth, getDate, getHours, ...) converts it once.
+ * A change of process.env.TZ starts a new generation. */
+typedef struct {
+ bool valid;
+ unsigned generation;
+ double ms;
+ ScrDateParts parts;
+} ScrDateLocalCache;
+static SCR_TL ScrDateLocalCache scr_date_local_cache;
+
+static bool scr_date_local_parts_uncached(double clipped, ScrDateParts *out);
+
static bool scr_date_local_parts(double ms, ScrDateParts *out) {
if (!isfinite(ms) || fabs(ms) > 8640000000000000.0) return false;
double clipped = trunc(ms);
- double secd = floor(clipped / 1000.0);
+ ScrDateLocalCache *c = &scr_date_local_cache;
+ unsigned generation = scr_date_tz_generation;
+ if (c->valid && c->ms == clipped && c->generation == generation) {
+ *out = c->parts;
+ return true;
+ }
+ if (!scr_date_local_parts_uncached(clipped, out)) return false;
+ c->valid = true;
+ c->generation = generation;
+ c->ms = clipped;
+ c->parts = *out;
+ return true;
+}
+
+static bool scr_date_local_parts_uncached(double clipped, ScrDateParts *out) {
+ long long cms = (long long)clipped;
+ double secd = (double)(cms / 1000 - (cms % 1000 < 0));
struct tm tmv;
double basis_secd = secd;
if (!scr_date_localtime(basis_secd, &tmv)) {
diff --git a/packages/runtime/src/scr_os.c b/packages/runtime/src/scr_os.c
index 76f69588e..2e70d0d85 100644
--- a/packages/runtime/src/scr_os.c
+++ b/packages/runtime/src/scr_os.c
@@ -27,6 +27,8 @@
#include
#include
#endif
+#include
+#include
#ifdef __linux__
#include
#endif
@@ -1332,3 +1334,195 @@ ScrDyn *scr_os_constants(void) {
scr_atexit(scr_os_constants_clear);
return scr_dyn_retain(os);
}
+
+/* ── node:constants (deprecated, DEP0008) ──────────────────────────────
+ * Node's lib/constants.js: Object.assign({}, os.constants.dlopen, errno,
+ * priority, signals, fs.constants, crypto.constants), frozen, with an
+ * ordinary prototype. The fs group follows node_constants.cc's
+ * DefineFsConstants order and guards (libuv's UV_* values are fixed on
+ * POSIX); crypto_table (borrowed) is a compiler-emitted function that
+ * answers the baked crypto.constants table, called only on first use. One
+ * object per thread. */
+#define SCR_FS_CONSTANT(o, name) scr_os_constant((o), #name, (double)(name))
+
+static void scr_fs_constants(ScrDyn *o) {
+ scr_os_constant(o, "UV_FS_SYMLINK_DIR", 1);
+ scr_os_constant(o, "UV_FS_SYMLINK_JUNCTION", 2);
+#ifdef O_RDONLY
+ SCR_FS_CONSTANT(o, O_RDONLY);
+#endif
+#ifdef O_WRONLY
+ SCR_FS_CONSTANT(o, O_WRONLY);
+#endif
+#ifdef O_RDWR
+ SCR_FS_CONSTANT(o, O_RDWR);
+#endif
+ scr_os_constant(o, "UV_DIRENT_UNKNOWN", 0);
+ scr_os_constant(o, "UV_DIRENT_FILE", 1);
+ scr_os_constant(o, "UV_DIRENT_DIR", 2);
+ scr_os_constant(o, "UV_DIRENT_LINK", 3);
+ scr_os_constant(o, "UV_DIRENT_FIFO", 4);
+ scr_os_constant(o, "UV_DIRENT_SOCKET", 5);
+ scr_os_constant(o, "UV_DIRENT_CHAR", 6);
+ scr_os_constant(o, "UV_DIRENT_BLOCK", 7);
+#ifdef S_IFMT
+ SCR_FS_CONSTANT(o, S_IFMT);
+#endif
+#ifdef S_IFREG
+ SCR_FS_CONSTANT(o, S_IFREG);
+#endif
+#ifdef S_IFDIR
+ SCR_FS_CONSTANT(o, S_IFDIR);
+#endif
+#ifdef S_IFCHR
+ SCR_FS_CONSTANT(o, S_IFCHR);
+#endif
+#ifdef S_IFBLK
+ SCR_FS_CONSTANT(o, S_IFBLK);
+#endif
+#ifdef S_IFIFO
+ SCR_FS_CONSTANT(o, S_IFIFO);
+#endif
+#ifdef S_IFLNK
+ SCR_FS_CONSTANT(o, S_IFLNK);
+#endif
+#ifdef S_IFSOCK
+ SCR_FS_CONSTANT(o, S_IFSOCK);
+#endif
+#ifdef O_CREAT
+ SCR_FS_CONSTANT(o, O_CREAT);
+#endif
+#ifdef O_EXCL
+ SCR_FS_CONSTANT(o, O_EXCL);
+#endif
+ /* libuv's Windows-only open flags are 0 everywhere else. */
+ scr_os_constant(o, "UV_FS_O_FILEMAP", 0);
+ scr_os_constant(o, "UV_FS_O_TEMPORARY", 0);
+ scr_os_constant(o, "UV_FS_O_SHORT_LIVED", 0);
+ scr_os_constant(o, "UV_FS_O_SEQUENTIAL", 0);
+ scr_os_constant(o, "UV_FS_O_RANDOM", 0);
+#ifdef O_NOCTTY
+ SCR_FS_CONSTANT(o, O_NOCTTY);
+#endif
+#ifdef O_TRUNC
+ SCR_FS_CONSTANT(o, O_TRUNC);
+#endif
+#ifdef O_APPEND
+ SCR_FS_CONSTANT(o, O_APPEND);
+#endif
+#ifdef O_DIRECTORY
+ SCR_FS_CONSTANT(o, O_DIRECTORY);
+#endif
+#ifdef O_NOATIME
+ SCR_FS_CONSTANT(o, O_NOATIME);
+#endif
+#ifdef O_NOFOLLOW
+ SCR_FS_CONSTANT(o, O_NOFOLLOW);
+#endif
+#ifdef O_SYNC
+ SCR_FS_CONSTANT(o, O_SYNC);
+#endif
+#ifdef O_DSYNC
+ SCR_FS_CONSTANT(o, O_DSYNC);
+#endif
+#ifdef O_SYMLINK
+ SCR_FS_CONSTANT(o, O_SYMLINK);
+#endif
+#ifdef O_DIRECT
+ SCR_FS_CONSTANT(o, O_DIRECT);
+#endif
+#ifdef O_NONBLOCK
+ SCR_FS_CONSTANT(o, O_NONBLOCK);
+#endif
+#ifdef S_IRWXU
+ SCR_FS_CONSTANT(o, S_IRWXU);
+#endif
+#ifdef S_IRUSR
+ SCR_FS_CONSTANT(o, S_IRUSR);
+#endif
+#ifdef S_IWUSR
+ SCR_FS_CONSTANT(o, S_IWUSR);
+#endif
+#ifdef S_IXUSR
+ SCR_FS_CONSTANT(o, S_IXUSR);
+#endif
+#ifdef S_IRWXG
+ SCR_FS_CONSTANT(o, S_IRWXG);
+#endif
+#ifdef S_IRGRP
+ SCR_FS_CONSTANT(o, S_IRGRP);
+#endif
+#ifdef S_IWGRP
+ SCR_FS_CONSTANT(o, S_IWGRP);
+#endif
+#ifdef S_IXGRP
+ SCR_FS_CONSTANT(o, S_IXGRP);
+#endif
+#ifdef S_IRWXO
+ SCR_FS_CONSTANT(o, S_IRWXO);
+#endif
+#ifdef S_IROTH
+ SCR_FS_CONSTANT(o, S_IROTH);
+#endif
+#ifdef S_IWOTH
+ SCR_FS_CONSTANT(o, S_IWOTH);
+#endif
+#ifdef S_IXOTH
+ SCR_FS_CONSTANT(o, S_IXOTH);
+#endif
+#ifdef F_OK
+ SCR_FS_CONSTANT(o, F_OK);
+#endif
+#ifdef R_OK
+ SCR_FS_CONSTANT(o, R_OK);
+#endif
+#ifdef W_OK
+ SCR_FS_CONSTANT(o, W_OK);
+#endif
+#ifdef X_OK
+ SCR_FS_CONSTANT(o, X_OK);
+#endif
+ scr_os_constant(o, "UV_FS_COPYFILE_EXCL", 1);
+ scr_os_constant(o, "COPYFILE_EXCL", 1);
+ scr_os_constant(o, "UV_FS_COPYFILE_FICLONE", 2);
+ scr_os_constant(o, "COPYFILE_FICLONE", 2);
+ scr_os_constant(o, "UV_FS_COPYFILE_FICLONE_FORCE", 4);
+ scr_os_constant(o, "COPYFILE_FICLONE_FORCE", 4);
+}
+#undef SCR_FS_CONSTANT
+
+static SCR_TL ScrDyn *scr_node_constants_value;
+static void scr_node_constants_clear(void) {
+ ScrDyn *value = scr_node_constants_value;
+ scr_node_constants_value = NULL;
+ scr_dyn_release(value);
+}
+
+ScrDyn *scr_node_constants(const ScrDyn *crypto_table) {
+ if (scr_node_constants_value) return scr_dyn_retain(scr_node_constants_value);
+ ScrDyn *crypto = scr_dyn_call(crypto_table, NULL, 0, "constants");
+ if (!crypto) return NULL;
+ ScrDyn *o = scr_dyn_new_obj();
+ scr_os_dlopen_constants(o);
+ scr_os_errno_constants(o);
+ scr_os_constant(o, "PRIORITY_LOW", SCR_OS_PRIORITY_LOW);
+ scr_os_constant(o, "PRIORITY_BELOW_NORMAL", SCR_OS_PRIORITY_BELOW_NORMAL);
+ scr_os_constant(o, "PRIORITY_NORMAL", SCR_OS_PRIORITY_NORMAL);
+ scr_os_constant(o, "PRIORITY_ABOVE_NORMAL", SCR_OS_PRIORITY_ABOVE_NORMAL);
+ scr_os_constant(o, "PRIORITY_HIGH", SCR_OS_PRIORITY_HIGH);
+ scr_os_constant(o, "PRIORITY_HIGHEST", SCR_OS_PRIORITY_HIGHEST);
+ scr_os_signal_constants(o);
+ scr_fs_constants(o);
+ if (crypto && crypto->kind == SCR_DYN_OBJ) {
+ for (size_t i = 0; i < crypto->v.obj.len; i++) {
+ const ScrDynEntry *entry = &crypto->v.obj.entries[i];
+ if (entry->accessor || !entry->value) continue;
+ scr_dyn_obj_set(o, entry->key, entry->key_len, scr_dyn_retain(entry->value));
+ }
+ }
+ scr_dyn_release(crypto);
+ scr_dyn_release(scr_dyn_freeze(o));
+ scr_node_constants_value = o;
+ scr_atexit(scr_node_constants_clear);
+ return scr_dyn_retain(o);
+}
diff --git a/packages/runtime/src/scr_os.h b/packages/runtime/src/scr_os.h
index 3c5009fc5..219726660 100644
--- a/packages/runtime/src/scr_os.h
+++ b/packages/runtime/src/scr_os.h
@@ -38,6 +38,10 @@ ScrStr *scr_os_cpus_model(double i);
double scr_os_cpus_field(double i, double field);
/* os.constants: the per-thread null-prototype constants object (+1). */
ScrDyn *scr_os_constants(void);
+/* The deprecated node:constants module object (scr_os.c): the os groups,
+ * fs.constants and the crypto table the borrowed function answers, merged
+ * and frozen (+1). */
+ScrDyn *scr_node_constants(const ScrDyn *crypto_table);
double scr_os_available_parallelism(void); /* uv_available_parallelism: CPUs this process may use, >= 1 */
/* os.userInfo()'s field trio (pw_name / pw_shell / pw_dir — the passwd
* home, not the $HOME cascade). +1 fresh; abort on lookup failure. */
diff --git a/packages/runtime/src/scr_url.c b/packages/runtime/src/scr_url.c
index eeadba98f..67de82471 100644
--- a/packages/runtime/src/scr_url.c
+++ b/packages/runtime/src/scr_url.c
@@ -1989,3 +1989,342 @@ ScrBytes *scr_url_checked_to_path_buffer(const ScrDyn *value, const ScrDyn *opti
scr_url_release(url);
return result;
}
+
+/* ── node:punycode (deprecated, DEP0040) ───────────────────────────────
+ * A port of the punycode.js 2.1.0 that Node 24 bundles as lib/punycode.js.
+ * It works on UTF-16 code units: ucs2.decode pairs surrogates and keeps
+ * lone ones, which is exactly a code-point walk over WTF-8 storage.
+ * Errors throw the module's RangeErrors ("Overflow: ...", "Illegal input
+ * >= 0x80 ...", "Invalid input"); decoded code points past U+10FFFF throw
+ * String.fromCodePoint's "Invalid code point N". All inputs are borrowed;
+ * results are +1, or NULL with the error pending. The DEP0040 report is
+ * scr_punycode_warn (scr_async_dyn.c). */
+
+enum {
+ PC_MAX_INT = 2147483647,
+ PC_BASE = 36,
+ PC_TMIN = 1,
+ PC_TMAX = 26,
+ PC_SKEW = 38,
+ PC_DAMP = 700,
+ PC_INITIAL_BIAS = 72,
+ PC_INITIAL_N = 128
+};
+
+static void pc_error(const char *message) {
+ scr_throw_error_msg(SCR_ERR_RANGE, message, strlen(message));
+}
+#define PC_OVERFLOW "Overflow: input needs wider integers to process"
+#define PC_NOT_BASIC "Illegal input >= 0x80 (not a basic code point)"
+#define PC_INVALID "Invalid input"
+
+typedef struct {
+ uint32_t *data;
+ size_t len, cap;
+} PcPoints;
+
+static void pc_push(PcPoints *p, uint32_t cp) {
+ if (p->len == p->cap) {
+ size_t cap = p->cap ? p->cap * 2 : 16;
+ uint32_t *grown = realloc(p->data, cap * sizeof *grown);
+ if (!grown) scr_trap("scriptc: out of memory\n");
+ p->data = grown;
+ p->cap = cap;
+ }
+ p->data[p->len++] = cp;
+}
+
+/* ucs2decode over WTF-8 storage: a pair is one 4-byte sequence, a lone
+ * surrogate one 3-byte sequence carrying its own value. */
+static void pc_code_points(const char *s, size_t n, PcPoints *out) {
+ const unsigned char *b = (const unsigned char *)s;
+ for (size_t i = 0; i < n;) {
+ uint32_t c = b[i];
+ if (c < 0x80) {
+ i += 1;
+ } else if (c < 0xE0 && i + 1 < n) {
+ c = ((c & 0x1F) << 6) | (b[i + 1] & 0x3F);
+ i += 2;
+ } else if (c < 0xF0 && i + 2 < n) {
+ c = ((c & 0x0F) << 12) | ((uint32_t)(b[i + 1] & 0x3F) << 6) | (b[i + 2] & 0x3F);
+ i += 3;
+ } else if (i + 3 < n) {
+ c = ((c & 0x07) << 18) | ((uint32_t)(b[i + 1] & 0x3F) << 12) |
+ ((uint32_t)(b[i + 2] & 0x3F) << 6) | (b[i + 3] & 0x3F);
+ i += 4;
+ } else {
+ i += 1;
+ }
+ pc_push(out, c);
+ }
+}
+
+static int64_t pc_adapt(int64_t delta, int64_t points, bool first) {
+ int64_t k = 0;
+ delta = first ? delta / PC_DAMP : delta >> 1;
+ delta += delta / points;
+ for (; delta > ((PC_BASE - PC_TMIN) * PC_TMAX) >> 1; k += PC_BASE) delta /= PC_BASE - PC_TMIN;
+ return k + (PC_BASE - PC_TMIN + 1) * delta / (delta + PC_SKEW);
+}
+
+static int64_t pc_basic_to_digit(unsigned char c) {
+ if (c >= 0x30 && c < 0x3A) return 26 + (c - 0x30);
+ if (c >= 0x41 && c < 0x5B) return c - 0x41;
+ if (c >= 0x61 && c < 0x7B) return c - 0x61;
+ return PC_BASE;
+}
+
+static char pc_digit_to_basic(int64_t digit) {
+ return (char)(digit + 22 + 75 * (digit < 26));
+}
+
+/* String.fromCodePoint(...points). */
+static ScrStr *pc_from_points(const PcPoints *p) {
+ for (size_t i = 0; i < p->len; i++) {
+ if (p->data[i] > 0x10FFFF) {
+ char message[48];
+ int size = snprintf(message, sizeof message, "Invalid code point %u", (unsigned)p->data[i]);
+ scr_throw_error_msg(SCR_ERR_RANGE, message, (size_t)size);
+ return NULL;
+ }
+ }
+ char *out = malloc(p->len * 4 + 1);
+ if (!out) scr_trap("scriptc: out of memory\n");
+ size_t offset = 0;
+ for (size_t i = 0; i < p->len; i++) {
+ uint32_t point = p->data[i];
+ if (point >= 0xD800 && point <= 0xDBFF && i + 1 < p->len && p->data[i + 1] >= 0xDC00 &&
+ p->data[i + 1] <= 0xDFFF) {
+ point = 0x10000 + ((point - 0xD800) << 10) + (p->data[i + 1] - 0xDC00);
+ i++;
+ }
+ offset += scr_wtf8_put(out + offset, point);
+ }
+ ScrStr *result = scr_str_new(out, offset);
+ free(out);
+ return result;
+}
+
+/* punycode.decode over the bytes of one label. Any non-ASCII byte fails
+ * where its first code unit would: before the last delimiter it is not
+ * basic, after it it is no digit — so byte positions stand in for code
+ * unit positions until the first error. */
+static ScrStr *pc_decode(const char *s, size_t n) {
+ PcPoints output = {0};
+ size_t basic = 0;
+ for (size_t k = n; k > 0; k--) {
+ if (s[k - 1] == '-') {
+ basic = k - 1;
+ break;
+ }
+ }
+ ScrStr *result = NULL;
+ for (size_t j = 0; j < basic; j++) {
+ if ((unsigned char)s[j] >= 0x80) {
+ pc_error(PC_NOT_BASIC);
+ goto done;
+ }
+ pc_push(&output, (unsigned char)s[j]);
+ }
+ int64_t i = 0, cp = PC_INITIAL_N, bias = PC_INITIAL_BIAS;
+ for (size_t index = basic > 0 ? basic + 1 : 0; index < n;) {
+ int64_t old = i;
+ for (int64_t w = 1, k = PC_BASE;; k += PC_BASE) {
+ if (index >= n) {
+ pc_error(PC_INVALID);
+ goto done;
+ }
+ int64_t digit = pc_basic_to_digit((unsigned char)s[index++]);
+ if (digit >= PC_BASE) {
+ pc_error(PC_INVALID);
+ goto done;
+ }
+ if (digit > (PC_MAX_INT - i) / w) {
+ pc_error(PC_OVERFLOW);
+ goto done;
+ }
+ i += digit * w;
+ int64_t t = k <= bias ? PC_TMIN : (k >= bias + PC_TMAX ? PC_TMAX : k - bias);
+ if (digit < t) break;
+ if (w > PC_MAX_INT / (PC_BASE - t)) {
+ pc_error(PC_OVERFLOW);
+ goto done;
+ }
+ w *= PC_BASE - t;
+ }
+ int64_t out = (int64_t)output.len + 1;
+ bias = pc_adapt(i - old, out, old == 0);
+ if (i / out > PC_MAX_INT - cp) {
+ pc_error(PC_OVERFLOW);
+ goto done;
+ }
+ cp += i / out;
+ i %= out;
+ pc_push(&output, 0);
+ memmove(output.data + i + 1, output.data + i, (output.len - 1 - (size_t)i) * sizeof *output.data);
+ output.data[i++] = (uint32_t)cp;
+ }
+ result = pc_from_points(&output);
+done:
+ free(output.data);
+ return result;
+}
+
+/* punycode.encode of one label's code points, appended to buf. */
+static bool pc_encode(const PcPoints *input, ScrJsonBuf *buf) {
+ int64_t cp = PC_INITIAL_N, delta = 0, bias = PC_INITIAL_BIAS;
+ size_t basic_length = 0;
+ for (size_t k = 0; k < input->len; k++) {
+ if (input->data[k] < 0x80) {
+ scr_jb_putc(buf, (char)input->data[k]);
+ basic_length++;
+ }
+ }
+ size_t handled = basic_length;
+ if (basic_length) scr_jb_putc(buf, '-');
+ while (handled < input->len) {
+ int64_t m = PC_MAX_INT;
+ for (size_t k = 0; k < input->len; k++)
+ if (input->data[k] >= cp && input->data[k] < m) m = input->data[k];
+ int64_t handled_plus_one = (int64_t)handled + 1;
+ if (m - cp > (PC_MAX_INT - delta) / handled_plus_one) {
+ pc_error(PC_OVERFLOW);
+ return false;
+ }
+ delta += (m - cp) * handled_plus_one;
+ cp = m;
+ for (size_t k = 0; k < input->len; k++) {
+ int64_t current = input->data[k];
+ if (current < cp && ++delta > PC_MAX_INT) {
+ pc_error(PC_OVERFLOW);
+ return false;
+ }
+ if (current == cp) {
+ int64_t q = delta;
+ for (int64_t j = PC_BASE;; j += PC_BASE) {
+ int64_t t = j <= bias ? PC_TMIN : (j >= bias + PC_TMAX ? PC_TMAX : j - bias);
+ if (q < t) break;
+ scr_jb_putc(buf, pc_digit_to_basic(t + (q - t) % (PC_BASE - t)));
+ q = (q - t) / (PC_BASE - t);
+ }
+ scr_jb_putc(buf, pc_digit_to_basic(q));
+ bias = pc_adapt(delta, handled_plus_one, handled == basic_length);
+ delta = 0;
+ handled++;
+ }
+ }
+ delta++;
+ cp++;
+ }
+ return true;
+}
+
+ScrStr *scr_punycode_decode(const ScrStr *input) { return pc_decode(input->data, input->len); }
+
+ScrStr *scr_punycode_encode(const ScrStr *input) {
+ PcPoints points = {0};
+ pc_code_points(input->data, input->len, &points);
+ ScrJsonBuf buf;
+ scr_jb_init(&buf);
+ bool ok = pc_encode(&points, &buf);
+ free(points.data);
+ ScrStr *result = scr_jb_finish(&buf);
+ if (ok) return result;
+ scr_str_release(result);
+ return NULL;
+}
+
+ScrArr *scr_punycode_ucs2_decode(const ScrStr *input) {
+ PcPoints points = {0};
+ pc_code_points(input->data, input->len, &points);
+ ScrArr *out = scr_arr_new(SCR_ELEM_F64, points.len);
+ for (size_t i = 0; i < points.len; i++) scr_arr_push_f64(out, (double)points.data[i]);
+ free(points.data);
+ return out;
+}
+
+/* mapDomain: only the part after the first '@' maps (and only up to a
+ * second one — punycode.js keeps parts[0] and parts[1]); the RFC 3490
+ * separators U+002E, U+3002, U+FF0E and U+FF61 all split labels, and the
+ * labels rejoin with '.'. */
+static ScrStr *pc_map_domain(const ScrStr *input, bool to_ascii) {
+ const char *s = input->data;
+ size_t n = input->len;
+ ScrJsonBuf buf;
+ scr_jb_init(&buf);
+ const char *at = memchr(s, '@', n);
+ if (at) {
+ scr_jb_write(&buf, s, (size_t)(at - s) + 1);
+ const char *rest = at + 1;
+ const char *second = memchr(rest, '@', n - (size_t)(rest - s));
+ n = (second ? (size_t)(second - rest) : n - (size_t)(rest - s));
+ s = rest;
+ }
+ size_t start = 0;
+ bool ok = true;
+ for (size_t i = 0; ok && i <= n;) {
+ size_t sep = 0;
+ if (i == n) sep = 1;
+ else if (s[i] == '.') sep = 1;
+ else if (i + 2 < n && (unsigned char)s[i] == 0xE3 && (unsigned char)s[i + 1] == 0x80 &&
+ (unsigned char)s[i + 2] == 0x82)
+ sep = 3;
+ else if (i + 2 < n && (unsigned char)s[i] == 0xEF &&
+ (((unsigned char)s[i + 1] == 0xBC && (unsigned char)s[i + 2] == 0x8E) ||
+ ((unsigned char)s[i + 1] == 0xBD && (unsigned char)s[i + 2] == 0xA1)))
+ sep = 3;
+ if (!sep) {
+ i++;
+ continue;
+ }
+ const char *label = s + start;
+ size_t len = i - start;
+ bool ascii = true;
+ for (size_t k = 0; k < len; k++) ascii = ascii && (unsigned char)label[k] < 0x80;
+ if (to_ascii && !ascii) {
+ PcPoints points = {0};
+ pc_code_points(label, len, &points);
+ scr_jb_puts(&buf, "xn--");
+ ok = pc_encode(&points, &buf);
+ free(points.data);
+ } else if (!to_ascii && len >= 4 && memcmp(label, "xn--", 4) == 0) {
+ /* string.slice(4).toLowerCase(): ASCII letters fold; the one other
+ * code point whose lowercase is ASCII is KELVIN SIGN (U+212A → k).
+ * Any other non-ASCII stays non-ASCII and fails in decode either
+ * way, at the same position. */
+ ScrJsonBuf lower;
+ scr_jb_init(&lower);
+ for (size_t k = 4; k < len; k++) {
+ unsigned char c = (unsigned char)label[k];
+ if (c == 0xE2 && k + 2 < len && (unsigned char)label[k + 1] == 0x84 &&
+ (unsigned char)label[k + 2] == 0xAA) {
+ scr_jb_putc(&lower, 'k');
+ k += 2;
+ } else {
+ scr_jb_putc(&lower, (char)(c >= 'A' && c <= 'Z' ? c + 32 : c));
+ }
+ }
+ ScrStr *folded = scr_jb_finish(&lower);
+ ScrStr *decoded = pc_decode(folded->data, folded->len);
+ scr_str_release(folded);
+ if (decoded) {
+ scr_jb_put_str(&buf, decoded);
+ scr_str_release(decoded);
+ } else {
+ ok = false;
+ }
+ } else {
+ scr_jb_write(&buf, label, len);
+ }
+ if (ok && i < n) scr_jb_putc(&buf, '.');
+ i += sep;
+ start = i;
+ }
+ ScrStr *result = scr_jb_finish(&buf);
+ if (ok) return result;
+ scr_str_release(result);
+ return NULL;
+}
+
+ScrStr *scr_punycode_to_ascii(const ScrStr *input) { return pc_map_domain(input, true); }
+ScrStr *scr_punycode_to_unicode(const ScrStr *input) { return pc_map_domain(input, false); }
diff --git a/packages/runtime/src/scr_url.h b/packages/runtime/src/scr_url.h
index 134893054..05e5702bb 100644
--- a/packages/runtime/src/scr_url.h
+++ b/packages/runtime/src/scr_url.h
@@ -53,6 +53,15 @@ ScrDyn *scr_url_legacy_parse(const ScrDyn *url, const ScrDyn *parse_query, const
ScrStr *scr_url_legacy_format(const ScrDyn *object, const ScrDyn *options, bool warn);
ScrStr *scr_url_legacy_resolve(const ScrDyn *from, const ScrDyn *to, bool warn);
ScrStr *scr_url_domain_to_ascii(const ScrDyn *domain, bool missing);
+/* node:punycode (scr_url.c): punycode.js 2.1.0's encode/decode, toASCII/
+ * toUnicode (mapDomain over RFC 3490 separators) and ucs2.decode (UTF-16
+ * code points as an f64 array). Borrowed input; +1, or NULL with the
+ * module's RangeError pending. */
+ScrStr *scr_punycode_decode(const ScrStr *input);
+ScrStr *scr_punycode_encode(const ScrStr *input);
+ScrStr *scr_punycode_to_ascii(const ScrStr *input);
+ScrStr *scr_punycode_to_unicode(const ScrStr *input);
+ScrArr *scr_punycode_ucs2_decode(const ScrStr *input);
ScrStr *scr_url_domain_to_unicode(const ScrDyn *domain, bool missing);
ScrStr *scr_url_to_path(ScrUrl *u); /* +1, or throws */
ScrStr *scr_url_str_to_path(ScrStr *s); /* +1, or throws */
diff --git a/packages/runtime/src/scr_zlib.c b/packages/runtime/src/scr_zlib.c
index b61cdc1cb..ff09e7ea1 100644
--- a/packages/runtime/src/scr_zlib.c
+++ b/packages/runtime/src/scr_zlib.c
@@ -420,7 +420,22 @@ void scr_zlib_options_drop(ScrZlibOptions *o) {
o->dict = NULL;
}
+static bool scr_zlib_options_parse_object(int mode, const ScrDyn *opts, ScrZlibOptions *out);
+
+/* A typed options object (a record that crossed into a checked-dynamic
+ * slot by reference) is read through its materialized view, as a plain
+ * object is. */
bool scr_zlib_options_parse(int mode, const ScrDyn *opts, ScrZlibOptions *out) {
+ if (opts == NULL || opts->kind != SCR_DYN_TYPED_REF) return scr_zlib_options_parse_object(mode, opts, out);
+ ScrDyn *view = scr_dyn_typed_ref_materialize(opts);
+ bool ok = false;
+ if (scr_exc_pending()) memset(out, 0, sizeof *out);
+ else ok = scr_zlib_options_parse_object(mode, view, out);
+ scr_dyn_release(view);
+ return ok;
+}
+
+static bool scr_zlib_options_parse_object(int mode, const ScrDyn *opts, ScrZlibOptions *out) {
memset(out, 0, sizeof *out);
out->window_bits = 15;
out->level = Z_DEFAULT_COMPRESSION;
diff --git a/packages/runtime/src/scr_zlib_stream.c b/packages/runtime/src/scr_zlib_stream.c
index 14d8747e8..4ed717d53 100644
--- a/packages/runtime/src/scr_zlib_stream.c
+++ b/packages/runtime/src/scr_zlib_stream.c
@@ -431,6 +431,13 @@ static const char *scr_zs_class_name(int mode) {
}
ScrStream *scr_zlib_stream_new(double mode_d, ScrDyn *opts) {
+ /* A typed options object is read through its materialized view. */
+ if (opts != NULL && opts->kind == SCR_DYN_TYPED_REF) {
+ ScrDyn *view = scr_dyn_typed_ref_materialize(opts);
+ ScrStream *stream = scr_exc_pending() ? NULL : scr_zlib_stream_new(mode_d, view);
+ scr_dyn_release(view);
+ return stream;
+ }
int mode = (int)mode_d;
const ScrDyn *o = opts != NULL && scr_dyn_truthy(opts) ? opts : NULL;
ScrZlibOptions zo;
diff --git a/tests/corpus/5633-zlib-typed-options.ts b/tests/corpus/5633-zlib-typed-options.ts
new file mode 100644
index 000000000..da657ca14
--- /dev/null
+++ b/tests/corpus/5633-zlib-typed-options.ts
@@ -0,0 +1,58 @@
+// zlib options held in a typed object (a cast object literal that crosses
+// into the options slot by reference) are read like a plain options
+// object: levels, windowBits and strategies apply, and out-of-range values
+// throw Node's ERR_OUT_OF_RANGE from the sync, callback, promisified and
+// stream forms.
+import * as zlib from "node:zlib";
+import { promisify } from "node:util";
+
+const data = Buffer.from("hello hello hello hello hello");
+const stored = { level: 0 } as zlib.ZlibOptions;
+const fast = { level: 1 } as zlib.ZlibOptions;
+const huffman = { strategy: zlib.constants.Z_HUFFMAN_ONLY } as zlib.ZlibOptions;
+const smallWindow = { windowBits: 9, memLevel: 1 } as zlib.ZlibOptions;
+const badLevel = { level: 99 } as zlib.ZlibOptions;
+const badWindow = { windowBits: 3 } as zlib.ZlibOptions;
+
+function attempt(label: string, run: () => void): void {
+ try {
+ run();
+ console.log(label, "ok");
+ } catch (e) {
+ const err = e as NodeJS.ErrnoException;
+ console.log(label, err.name, err.code, err.message);
+ }
+}
+
+console.log("sync stored", zlib.deflateSync(data, stored).toString("hex"));
+console.log("sync fast", zlib.deflateSync(data, fast).toString("hex"));
+console.log("sync huffman", zlib.deflateSync(data, huffman).toString("hex"));
+console.log("raw small window", zlib.deflateRawSync(data, smallWindow).toString("hex"));
+console.log("gzip stored", zlib.gzipSync(data, stored).subarray(10).toString("hex"));
+attempt("sync bad level", () => zlib.deflateSync(data, badLevel));
+attempt("sync bad window", () => zlib.inflateSync(zlib.deflateSync(data), badWindow));
+attempt("callback bad level", () => zlib.gzip(data, badLevel, () => console.log("not called")));
+attempt("stream bad level", () => zlib.createDeflate(badLevel));
+
+const deflateAsync = promisify(zlib.deflate);
+
+async function main(): Promise {
+ const viaCallback = await new Promise((resolve, reject) =>
+ zlib.deflate(data, stored, (err, out) => (err ? reject(err) : resolve(out))),
+ );
+ console.log("callback stored", viaCallback.toString("hex"));
+ console.log("promise stored", (await deflateAsync(data, stored)).toString("hex"));
+ try {
+ await deflateAsync(data, badLevel);
+ console.log("promise bad level fulfilled");
+ } catch (e) {
+ console.log("promise bad level", (e as NodeJS.ErrnoException).code);
+ }
+ const stream = zlib.createDeflate(fast);
+ const parts: Buffer[] = [];
+ stream.on("data", (chunk: Buffer) => parts.push(chunk));
+ stream.on("end", () => console.log("stream fast", Buffer.concat(parts).toString("hex")));
+ stream.end(data);
+}
+
+main().catch((e) => console.log("unexpected", String(e)));
diff --git a/tests/corpus/5683-date-local-fields-tz.ts b/tests/corpus/5683-date-local-fields-tz.ts
new file mode 100644
index 000000000..dc75011ed
--- /dev/null
+++ b/tests/corpus/5683-date-local-fields-tz.ts
@@ -0,0 +1,95 @@
+// Local-time getters read repeatedly on one Date and alternately on two,
+// on Dates parsed from strings, and across process.env.TZ writes and
+// deletes: every read answers the fields of the zone in effect at that
+// moment, as in Node.
+
+function fields(d: Date): string {
+ return [
+ d.getFullYear(),
+ d.getMonth(),
+ d.getDate(),
+ d.getDay(),
+ d.getHours(),
+ d.getMinutes(),
+ d.getSeconds(),
+ d.getMilliseconds(),
+ d.getTimezoneOffset(),
+ ].join(",");
+}
+
+const a = new Date(Date.UTC(2024, 2, 10, 6, 59, 59, 999));
+const b = new Date(Date.UTC(2024, 2, 10, 7, 0, 0, 0));
+const c = new Date(Date.UTC(2024, 10, 3, 5, 30, 0, 250));
+
+function report(label: string): void {
+ console.log(label, fields(a), fields(a));
+ console.log(label, fields(b), fields(c), fields(b));
+ let mixed = 0;
+ for (let i = 0; i < 6; i++) mixed += a.getHours() * 100 + b.getHours() + c.getDate();
+ console.log(label, "alternating", mixed);
+}
+
+report("start");
+process.env.TZ = "America/New_York";
+report("new york");
+process.env.TZ = "Asia/Kolkata";
+report("kolkata");
+process.env.TZ = "Australia/Lord_Howe";
+report("lord howe");
+
+// Dates parsed from strings with an explicit offset, read in local time.
+const d = new Date("2024-06-01T23:45:00+05:30");
+console.log("offset string", fields(d), d.getTime());
+const e = new Date("2024-07-11T00:15:30.001Z");
+console.log("offset string", fields(e), e.getTime());
+console.log("first again", fields(d));
+
+delete process.env.TZ;
+report("deleted");
+process.env.TZ = "UTC";
+report("utc");
+
+// Many instants read field by field.
+process.env.TZ = "Europe/Berlin";
+let sum = 0;
+for (let i = 0; i < 2000; i++) {
+ const t = new Date(1700000000000 + i * 3_599_999);
+ sum += t.getHours() + t.getDate() * 3 + t.getMonth() * 7 + t.getTimezoneOffset();
+}
+console.log("berlin sum", sum);
+
+const invalid = new Date(Number.NaN);
+console.log("invalid", fields(invalid), fields(invalid));
+
+// Instants around day and second boundaries, before the epoch and at the
+// ends of the Date range, in UTC and local fields; then 2000-2030 in a zone
+// with DST.
+function utcFields(d: Date): string {
+ return [
+ d.getUTCFullYear(),
+ d.getUTCMonth(),
+ d.getUTCDate(),
+ d.getUTCDay(),
+ d.getUTCHours(),
+ d.getUTCMinutes(),
+ d.getUTCSeconds(),
+ d.getUTCMilliseconds(),
+ ].join(",");
+}
+const edges = [
+ 0, -0, -0.5, 0.5, -1, 1, -999, -1000, -1001, 999.9, -86_400_000, -86_400_001, 86_399_999,
+ -62_198_755_200_000, -62_198_755_200_001, 8_640_000_000_000_000, -8_640_000_000_000_000,
+ 8_639_999_999_999_999, -8_639_999_999_999_999, 951_782_400_000, 4_107_542_399_999,
+];
+process.env.TZ = "UTC";
+for (const t of edges) {
+ const x = new Date(t);
+ console.log(t, utcFields(x), fields(x));
+}
+process.env.TZ = "America/Sao_Paulo";
+let walk = 0;
+for (let t = 946_684_800_000; t < 1_893_456_000_000; t += 77_777_777) {
+ const x = new Date(t);
+ walk = (walk * 31 + x.getUTCDate() + x.getUTCHours() * 7 + x.getDate() + x.getMinutes() + x.getDay()) % 1_000_003;
+}
+console.log("walk", walk);
diff --git a/tests/corpus/5919-record-unions-shared-arms.ts b/tests/corpus/5919-record-unions-shared-arms.ts
new file mode 100644
index 000000000..89f34dd83
--- /dev/null
+++ b/tests/corpus/5919-record-unions-shared-arms.ts
@@ -0,0 +1,60 @@
+// Record unions that share arms: Third = D | A, First = A | B and
+// Second = B | C. Only C is cyclic. Every arm of a union whose values the
+// cycle collector can visit carries a collector header, and the shared
+// arms pass that requirement from Second back through First to Third.
+type A = { kind: "a"; v: number };
+type B = { kind: "b"; w: number };
+type C = { kind: "c"; kids: C[] };
+type D = { kind: "d"; label: string };
+type Third = D | A;
+type First = A | B;
+type Second = B | C;
+
+function third(u: Third): string {
+ return u.kind === "d" ? u.label : `a${u.v}`;
+}
+
+function first(u: First): number {
+ return u.kind === "a" ? u.v : u.w;
+}
+
+function second(u: Second): number {
+ return u.kind === "b" ? u.w : u.kids.length + u.kids.reduce((n, k) => n + second(k), 0);
+}
+
+const zs: Third[] = [
+ { kind: "d", label: "dee" },
+ { kind: "a", v: 7 },
+];
+console.log(zs.map(third).join(","));
+
+const xs: First[] = [
+ { kind: "a", v: 1 },
+ { kind: "b", w: 2 },
+];
+console.log(xs.map(first).join(","));
+
+const leaf: C = { kind: "c", kids: [] };
+const ys: Second[] = [
+ { kind: "b", w: 3 },
+ { kind: "c", kids: [leaf, { kind: "c", kids: [leaf] }] },
+];
+console.log(ys.map(second).join(","));
+
+// One record held through two unions.
+const sharedA: A = { kind: "a", v: 5 };
+const sharedB: B = { kind: "b", w: 6 };
+const viaThird: Third = sharedA;
+const viaFirst: First[] = [sharedA, sharedB];
+const viaSecond: Second = sharedB;
+console.log(third(viaThird), viaFirst.map(first).join(","), second(viaSecond));
+
+// A cyclic C reachable only through Second, dropped and rebuilt in a loop.
+let total = 0;
+for (let i = 0; i < 200; i++) {
+ const node: C = { kind: "c", kids: [] };
+ node.kids.push(node);
+ const held: Second[] = [node, { kind: "b", w: i }];
+ total += held.length + node.kids.length;
+}
+console.log("total", total);
diff --git a/tests/corpus/5930-record-union-pointers.ts b/tests/corpus/5930-record-union-pointers.ts
new file mode 100644
index 000000000..01882d730
--- /dev/null
+++ b/tests/corpus/5930-record-union-pointers.ts
@@ -0,0 +1,79 @@
+// Unions whose arms are all object literals (discriminated unions) hold the
+// record itself: construction, narrowing, storage in arrays, maps, fields
+// and closures, identity, and printing behave exactly as in Node.
+type Shape =
+ | { kind: "circle"; r: number }
+ | { kind: "rect"; w: number; h: number }
+ | { kind: "tri"; b: number; h: number; label: string };
+
+function area(s: Shape): number {
+ switch (s.kind) {
+ case "circle":
+ return Math.round(3.14159 * s.r * s.r);
+ case "rect":
+ return s.w * s.h;
+ case "tri":
+ return 0.5 * s.b * s.h;
+ }
+}
+
+function make(i: number): Shape {
+ if (i % 3 === 0) return { kind: "circle", r: i };
+ if (i % 3 === 1) return { kind: "rect", w: i, h: i + 1 };
+ return { kind: "tri", b: i, h: 2, label: "t" + i };
+}
+
+const shapes: Shape[] = [];
+for (let i = 0; i < 9; i++) shapes.push(make(i));
+let total = 0;
+for (const s of shapes) total += area(s);
+console.log("total", total);
+console.log(shapes.map((s) => s.kind).join(","));
+console.log(shapes.filter((s) => s.kind === "rect").length);
+
+// Identity: the same record compares equal, a structurally equal one does not.
+const first = shapes[0]!;
+const again: Shape = shapes[0]!;
+const copy: Shape = { kind: "circle", r: 0 };
+console.log(first === again, first === copy, shapes.indexOf(again), shapes.includes(copy));
+
+// Mutation through one alias is visible through the union value.
+const rect = { kind: "rect" as const, w: 2, h: 3 };
+const asShape: Shape = rect;
+rect.w = 10;
+console.log(area(asShape), asShape === rect);
+
+// Fields, maps and closures holding union values.
+interface Holder {
+ current: Shape;
+ history: Shape[];
+}
+const holder: Holder = { current: make(1), history: [] };
+for (let i = 0; i < 4; i++) {
+ holder.history.push(holder.current);
+ holder.current = make(i + 5);
+}
+console.log(holder.current.kind, holder.history.map(area).join(" "));
+
+const byName = new Map();
+for (const s of shapes) byName.set(s.kind + area(s), s);
+console.log(byName.size, byName.get("rect2")?.kind, byName.get("missing") === undefined);
+
+const pick = (k: Shape["kind"]): (() => Shape | undefined) => {
+ const found = shapes.find((s) => s.kind === k);
+ return () => found;
+};
+const getTri = pick("tri");
+const tri = getTri();
+console.log(tri !== undefined && tri.kind === "tri" ? tri.label : "none");
+
+// Printing and serialization see the plain objects.
+console.log(shapes.slice(0, 3));
+console.log(JSON.stringify(shapes.slice(0, 3)));
+const second = shapes[2]!;
+console.log(`${String(shapes[1]!.kind)} ${"label" in second} ${"label" in shapes[1]!}`);
+
+// Spread copies and sorting.
+const grown = shapes.map((s): Shape => (s.kind === "circle" ? { ...s, r: s.r + 1 } : s));
+grown.sort((a, b) => area(a) - area(b));
+console.log(grown.map((s) => `${s.kind}:${area(s)}`).join(" "));
diff --git a/tests/corpus/5931-record-union-unit-arms.ts b/tests/corpus/5931-record-union-unit-arms.ts
new file mode 100644
index 000000000..1ade8a0a3
--- /dev/null
+++ b/tests/corpus/5931-record-union-unit-arms.ts
@@ -0,0 +1,67 @@
+// Record unions with undefined and null arms: NULL and the null sentinel
+// encode the unit arms, optional properties keep their absent state, and
+// narrowing, nullish operators and equality agree with Node.
+type Ok = { ok: true; value: number };
+type Err = { ok: false; error: string };
+type Res = Ok | Err;
+
+function parse(text: string): Res {
+ const n = Number(text);
+ return Number.isNaN(n) ? { ok: false, error: `bad ${text}` } : { ok: true, value: n };
+}
+
+function lookup(table: Res[], i: number): Res | undefined {
+ return i < table.length ? table[i] : undefined;
+}
+
+function maybe(i: number): Res | null | undefined {
+ if (i % 3 === 0) return null;
+ if (i % 3 === 1) return undefined;
+ return parse(String(i));
+}
+
+const table = ["1", "x", "3"].map(parse);
+for (let i = 0; i < 4; i++) {
+ const r = lookup(table, i);
+ if (r === undefined) console.log(i, "missing");
+ else if (r.ok) console.log(i, "value", r.value);
+ else console.log(i, "error", r.error);
+}
+
+for (let i = 0; i < 6; i++) {
+ const m = maybe(i);
+ const tag = m === null ? "null" : m === undefined ? "undefined" : m.ok ? "ok" : "err";
+ console.log(i, tag, m == null, m ?? "default", typeof m);
+}
+
+// Optional properties of a record-union type.
+interface Slot {
+ name: string;
+ result?: Res;
+ last?: Res | null;
+}
+const slots: Slot[] = [
+ { name: "a" },
+ { name: "b", result: parse("7") },
+ { name: "c", result: parse("q"), last: null },
+];
+for (const s of slots) {
+ console.log(s.name, "result" in s, s.result?.ok, s.last === null, s.last === undefined);
+}
+console.log(JSON.stringify(slots));
+slots[0]!.result = table[1];
+slots[1]!.result = undefined;
+console.log(slots.map((s) => (s.result === undefined ? "-" : s.result.ok ? "ok" : "err")).join(""));
+
+// Equality across arms and unit encodings.
+const a: Res | undefined = table[0];
+const b: Res | undefined = undefined;
+const c: Res | null = null;
+console.log(a === table[0], a === b, b === undefined, c === null, (c as Res | null) === table[2]);
+
+// Unions of records reaching unknown and back.
+const values: unknown[] = [table[0], table[1], undefined, null];
+for (const v of values) {
+ if (v === null || v === undefined) console.log("unit", v);
+ else console.log("object", JSON.stringify(v));
+}
diff --git a/tests/corpus/5932-record-union-trees.ts b/tests/corpus/5932-record-union-trees.ts
new file mode 100644
index 000000000..060cadc4b
--- /dev/null
+++ b/tests/corpus/5932-record-union-trees.ts
@@ -0,0 +1,77 @@
+// A recursive discriminated union (an expression tree) whose leaf arm holds
+// no references: every arm shares the cycle collector's header, so trees
+// can be built, rewritten, shared and dropped in cycles.
+type Expr =
+ | { kind: "num"; value: number }
+ | { kind: "neg"; arg: Expr }
+ | { kind: "add"; left: Expr; right: Expr }
+ | { kind: "ref"; target: { expr: Expr | null } };
+
+function evaluate(e: Expr): number {
+ switch (e.kind) {
+ case "num":
+ return e.value;
+ case "neg":
+ return -evaluate(e.arg);
+ case "add":
+ return evaluate(e.left) + evaluate(e.right);
+ case "ref":
+ return e.target.expr === null ? 0 : evaluate(e.target.expr);
+ }
+}
+
+function build(depth: number, seed: number): Expr {
+ if (depth === 0) return { kind: "num", value: seed % 10 };
+ if (seed % 4 === 0) return { kind: "neg", arg: build(depth - 1, seed + 1) };
+ return { kind: "add", left: build(depth - 1, seed * 3 + 1), right: build(depth - 1, seed + 7) };
+}
+
+function simplify(e: Expr): Expr {
+ switch (e.kind) {
+ case "neg": {
+ const inner = simplify(e.arg);
+ return inner.kind === "neg" ? inner.arg : inner.kind === "num" ? { kind: "num", value: -inner.value } : { kind: "neg", arg: inner };
+ }
+ case "add": {
+ const l = simplify(e.left);
+ const r = simplify(e.right);
+ if (l.kind === "num" && r.kind === "num") return { kind: "num", value: l.value + r.value };
+ return l === e.left && r === e.right ? e : { kind: "add", left: l, right: r };
+ }
+ default:
+ return e;
+ }
+}
+
+function show(e: Expr): string {
+ switch (e.kind) {
+ case "num":
+ return String(e.value);
+ case "neg":
+ return `-(${show(e.arg)})`;
+ case "add":
+ return `(${show(e.left)} + ${show(e.right)})`;
+ case "ref":
+ return "ref";
+ }
+}
+
+let checksum = 0;
+for (let round = 0; round < 50; round++) {
+ const tree = build(6, round);
+ const simple = simplify(tree);
+ checksum += evaluate(tree) * 3 + evaluate(simple);
+ // A cycle through a record arm: the tree refers back to itself.
+ const target: { expr: Expr | null } = { expr: null };
+ const loop: Expr = { kind: "add", left: tree, right: { kind: "ref", target } };
+ target.expr = { kind: "num", value: round };
+ checksum += evaluate(loop);
+ target.expr = loop.left;
+}
+console.log(checksum);
+console.log(show(simplify(build(3, 5))));
+console.log(show(build(2, 4)), evaluate(build(4, 2)));
+
+const shared: Expr = { kind: "num", value: 4 };
+const twice: Expr = { kind: "add", left: shared, right: shared };
+console.log(evaluate(twice), twice.kind === "add" && twice.left === twice.right);
diff --git a/tests/corpus/5933-record-union-conversions.ts b/tests/corpus/5933-record-union-conversions.ts
new file mode 100644
index 000000000..80b411a9b
--- /dev/null
+++ b/tests/corpus/5933-record-union-conversions.ts
@@ -0,0 +1,78 @@
+// Record unions meet other representations: widening into unions with
+// string and number arms, narrowing back, `in` tests on undiscriminated
+// arms, generic helpers, class fields and promises.
+type Point = { x: number; y: number };
+type Named = { name: string };
+type Item = Point | Named;
+type Wide = Item | string | number;
+
+function describe(i: Item): string {
+ return "x" in i ? `point(${i.x},${i.y})` : `named(${i.name})`;
+}
+
+function widen(i: Item): Wide {
+ return i;
+}
+
+function narrow(w: Wide): Item | undefined {
+ if (typeof w === "string" || typeof w === "number") return undefined;
+ return w;
+}
+
+const items: Item[] = [{ x: 1, y: 2 }, { name: "a" }, { x: 3, y: 4 }];
+const wide: Wide[] = items.map(widen);
+wide.push("text", 42);
+for (const w of wide) {
+ const back = narrow(w);
+ console.log(typeof w, back === undefined ? "-" : describe(back), back !== undefined && items.includes(back));
+}
+
+// Generic helpers instantiate over the union.
+function firstWhere(xs: T[], pred: (x: T) => boolean): T | undefined {
+ for (const x of xs) if (pred(x)) return x;
+ return undefined;
+}
+const named = firstWhere(items, (i) => "name" in i);
+console.log(named === items[1], named !== undefined ? describe(named) : "none");
+const pair: [Item, Item] = [items[2]!, items[0]!];
+const [p0, p1] = pair;
+console.log(describe(p0), describe(p1));
+
+// Class fields and methods.
+class Cursor {
+ private at: Item;
+ readonly seen: Item[] = [];
+ constructor(start: Item) {
+ this.at = start;
+ }
+ move(next: Item): Item {
+ const prev = this.at;
+ this.seen.push(prev);
+ this.at = next;
+ return prev;
+ }
+ get current(): Item {
+ return this.at;
+ }
+}
+const cursor = new Cursor(items[0]!);
+for (const i of items) cursor.move(i);
+console.log(describe(cursor.current), cursor.seen.map(describe).join(" "));
+
+// Promises carry union payloads.
+async function load(i: number): Promise- {
+ await null;
+ return items[i % items.length]!;
+}
+async function main(): Promise {
+ const loaded: Item[] = [];
+ for (const i of [0, 1, 5]) loaded.push(await load(i));
+ console.log(loaded.map(describe).join(" "), loaded[0] === items[0]);
+ const slow = new Promise
- ((resolve) => setTimeout(() => resolve({ x: 9, y: 9 }), 5));
+ const raced = await Promise.race([load(1), slow]);
+ console.log(describe(raced));
+ const pending: Promise
- [] = [load(2), load(0)];
+ const all = await Promise.all(pending);
+ console.log(all.map(describe).join(" "), all[1] === items[0]);
+}
+main().then(() => console.log("done"));
diff --git a/tests/corpus/5934-record-union-set-members.ts b/tests/corpus/5934-record-union-set-members.ts
new file mode 100644
index 000000000..46f838870
--- /dev/null
+++ b/tests/corpus/5934-record-union-set-members.ts
@@ -0,0 +1,25 @@
+// A record union used as Set elements and Map keys keeps the runtime's
+// tagged representation; other unions over the same records convert to and
+// from it without changing identity semantics.
+type Point = { x: number; y: number };
+type Named = { name: string };
+type Item = Point | Named;
+
+const items: Item[] = [{ x: 1, y: 2 }, { name: "a" }, { x: 3, y: 4 }];
+const marked = new Set
- ();
+marked.add(items[0]!);
+marked.add(items[0]!);
+marked.add({ x: 1, y: 2 });
+console.log(marked.size, marked.has(items[0]!), marked.has(items[1]!));
+
+const counts = new Map
- ();
+for (const i of [...items, items[0]!, items[2]!]) counts.set(i, (counts.get(i) ?? 0) + 1);
+console.log([...counts.values()].join(","));
+
+function maybeItem(i: number): Item | undefined {
+ return i < items.length ? items[i] : undefined;
+}
+for (let i = 0; i < 4; i++) {
+ const m = maybeItem(i);
+ console.log(i, m === undefined ? "none" : "x" in m ? m.x : m.name, m !== undefined && marked.has(m));
+}
diff --git a/tests/corpus/5976-builtin-class-aliases.ts b/tests/corpus/5976-builtin-class-aliases.ts
new file mode 100644
index 000000000..692b3928d
--- /dev/null
+++ b/tests/corpus/5976-builtin-class-aliases.ts
@@ -0,0 +1,109 @@
+// Builtin classes through const aliases: EventEmitter, the stream classes,
+// the error classes, Map and Set construct, extend and test instanceof
+// through an alias, and typeof, .name and === answer as in Node for the
+// aliases and the direct spellings.
+import { EventEmitter } from "node:events";
+import * as events from "node:events";
+import { PassThrough, Readable, Transform } from "node:stream";
+import * as stream from "node:stream";
+import { AssertionError } from "node:assert";
+import * as assert from "node:assert";
+
+function lateEmitter(): boolean {
+ return new LateEmitter() instanceof EventEmitter && typeof LateMap === "function";
+}
+
+function localHoist(): string {
+ function inner(): string {
+ return `${typeof LocalMap} ${LocalMap.name} ${new LocalMap().size}`;
+ }
+ const LocalMap = Map;
+ return inner();
+}
+
+const Emitter = EventEmitter;
+const NsEmitter = events.EventEmitter;
+const Again = Emitter;
+const LateEmitter = events.EventEmitter;
+const LateMap = Map;
+
+const e = new Emitter();
+e.on("ping", (n: number) => console.log("ping", n));
+e.emit("ping", 1);
+console.log("emitter", e instanceof Emitter, e instanceof NsEmitter, e instanceof Again);
+console.log("hoisted use", lateEmitter(), localHoist());
+
+class Bus extends Again {
+ send(text: string): void {
+ this.emit("text", text);
+ }
+}
+const bus = new Bus();
+bus.on("text", (text: string) => console.log("bus", text));
+bus.send("hello");
+console.log("subclass", bus instanceof Emitter, bus instanceof Bus);
+
+const Pass = PassThrough;
+const R = Readable;
+const T = stream.Transform;
+const pass = new Pass();
+console.log("stream", pass instanceof Pass, pass instanceof R, pass instanceof T);
+console.log("readable", new R({ read() {} }) instanceof Pass, e instanceof R);
+
+class Upper extends T {
+ override _transform(
+ chunk: Buffer,
+ _encoding: BufferEncoding,
+ callback: (error: Error | null, data?: string) => void,
+ ): void {
+ callback(null, chunk.toString().toUpperCase());
+ }
+}
+const upper = new Upper();
+upper.on("data", (chunk: Buffer) => console.log("upper", chunk.toString()));
+upper.end("quiet");
+
+const TE = TypeError;
+const RE = RangeError;
+class LimitError extends RE {
+ constructor(limit: number) {
+ super(`over ${limit}`);
+ this.name = "LimitError";
+ }
+}
+try {
+ throw new TE("bad input");
+} catch (err) {
+ console.log("error", err instanceof TE, err instanceof RE, err instanceof Error, (err as Error).message);
+}
+try {
+ throw new LimitError(3);
+} catch (err) {
+ console.log("subclass error", err instanceof RE, err instanceof LimitError, (err as Error).message);
+}
+const AE = AssertionError;
+try {
+ assert.strictEqual(1, 2);
+} catch (err) {
+ console.log("assertion", err instanceof AE, err instanceof TE);
+}
+const viaNamespace = new assert.AssertionError({ message: "custom" });
+console.log("namespace error", viaNamespace.message, viaNamespace instanceof AE);
+
+const M = Map;
+const S = Set;
+const m = new M([
+ ["a", 1],
+ ["b", 2],
+]);
+m.set("c", 3);
+const s = new S([1, 2, 2, 3]);
+console.log("collections", m.get("b"), m.size, s.size, m instanceof M, s instanceof S, s instanceof M);
+const groups = M.groupBy([1, 2, 3, 4, 5], (n: number) => (n % 2 === 0 ? "even" : "odd"));
+console.log("groupBy", groups.get("even"), groups.get("odd"));
+
+console.log("typeof", typeof Emitter, typeof R, typeof TE, typeof M, typeof EventEmitter, typeof Readable);
+console.log("name", Emitter.name, NsEmitter.name, Pass.name, T.name, TE.name, AE.name, M.name, S.name);
+console.log("direct name", EventEmitter.name, stream.Duplex.name, Error.name, RangeError.name, Map.name, Set.name);
+console.log("identity", Emitter === EventEmitter, Again === NsEmitter, T === Transform, R === stream.Readable);
+console.log("other class", (Pass as unknown) === (R as unknown), (M as unknown) !== (S as unknown), (TE as unknown) === (RE as unknown));
diff --git a/tests/corpus/5977-builtin-class-aliases.cjs b/tests/corpus/5977-builtin-class-aliases.cjs
new file mode 100644
index 000000000..c149884ba
--- /dev/null
+++ b/tests/corpus/5977-builtin-class-aliases.cjs
@@ -0,0 +1,45 @@
+// Builtin classes through const aliases in a CommonJS file. A copy of a
+// name (or of a globalThis property) keeps its value, so it can be stored
+// and compared; construction, instanceof, typeof and .name resolve to the
+// class. Builtin module members construct through the alias.
+const { EventEmitter } = require("node:events");
+const events = require("node:events");
+const zlib = require("node:zlib");
+const { PassThrough } = require("node:stream");
+
+const Emitter = EventEmitter;
+const NsEmitter = events.EventEmitter;
+const Gz = zlib.Gzip;
+const Gz2 = Gz;
+const M = Map;
+const GlobalSet = globalThis.Set;
+const Pass = PassThrough;
+const TE = TypeError;
+
+const e = new Emitter();
+e.on("ping", (n) => console.log("ping", n));
+e.emit("ping", 2);
+console.log("emitter", e instanceof Emitter, e instanceof NsEmitter, e instanceof EventEmitter);
+
+const m = new M();
+m.set("k", 1);
+const s = new GlobalSet(["a", "b", "a"]);
+console.log("collections", m.get("k"), s.size, m instanceof M, s instanceof GlobalSet, m instanceof GlobalSet);
+
+const gz = new Gz2();
+console.log("zlib", gz instanceof Gz, gz instanceof zlib.Gunzip, Gz2 === zlib.Gzip);
+
+const pass = new Pass();
+console.log("stream", pass instanceof Pass, pass instanceof EventEmitter);
+
+try {
+ throw new TE("typed");
+} catch (err) {
+ console.log("error", err instanceof TE, err.message);
+}
+
+const stored = [Emitter, M, GlobalSet, Pass];
+console.log("stored", stored.length, stored[0] === EventEmitter, stored[1] === Map, stored[2] === Set);
+console.log("typeof", typeof Emitter, typeof NsEmitter, typeof Gz2, typeof M, typeof GlobalSet, typeof TE);
+console.log("name", Emitter.name, NsEmitter.name, Gz2.name, M.name, GlobalSet.name, Pass.name, TE.name);
+console.log("identity", Emitter === NsEmitter, M === Map, GlobalSet === Set, M === GlobalSet);
diff --git a/tests/corpus/5978-builtin-class-alias-modules/classes.ts b/tests/corpus/5978-builtin-class-alias-modules/classes.ts
new file mode 100644
index 000000000..73590d1e8
--- /dev/null
+++ b/tests/corpus/5978-builtin-class-alias-modules/classes.ts
@@ -0,0 +1,8 @@
+import { EventEmitter } from "node:events";
+import { PassThrough } from "node:stream";
+
+export const Emitter = EventEmitter;
+export const Pass = PassThrough;
+export const Dict = Map;
+export const Bag = Set;
+export const Failure = TypeError;
diff --git a/tests/corpus/5978-builtin-class-alias-modules/main.ts b/tests/corpus/5978-builtin-class-alias-modules/main.ts
new file mode 100644
index 000000000..cf39cd712
--- /dev/null
+++ b/tests/corpus/5978-builtin-class-alias-modules/main.ts
@@ -0,0 +1,33 @@
+// Builtin class aliases exported from one module and used through named
+// and namespace imports: construction, instanceof, typeof, .name and ===
+// resolve to the builtin class in the importing module.
+import { EventEmitter } from "node:events";
+import { Emitter, Pass, Dict, Bag, Failure } from "./classes.ts";
+import * as classes from "./classes.ts";
+
+const e = new Emitter();
+e.on("tick", (n: number) => console.log("tick", n));
+e.emit("tick", 7);
+console.log("emitter", e instanceof Emitter, e instanceof classes.Emitter, e instanceof EventEmitter);
+
+const viaNs = new classes.Emitter();
+console.log("namespace emitter", viaNs instanceof Emitter, Emitter === classes.Emitter);
+
+const pass = new classes.Pass();
+console.log("stream", pass instanceof Pass, pass instanceof classes.Emitter);
+
+const dict = new Dict([["k", 1]]);
+const viaNsDict = new classes.Dict();
+viaNsDict.set("a", "b");
+const bag = new classes.Bag([1, 1, 2]);
+console.log("collections", dict.get("k"), viaNsDict.get("a"), bag.size, dict instanceof classes.Dict, bag instanceof Bag);
+
+try {
+ throw new classes.Failure("wrong type");
+} catch (err) {
+ console.log("error", err instanceof Failure, err instanceof classes.Failure, (err as Error).message);
+}
+
+console.log("typeof", typeof Emitter, typeof classes.Pass, typeof Dict, typeof classes.Bag, typeof Failure);
+console.log("name", Emitter.name, classes.Pass.name, Dict.name, classes.Bag.name, Failure.name);
+console.log("identity", Dict === Map, classes.Bag === Set, Failure === TypeError, (Dict as unknown) === (Bag as unknown));
diff --git a/tests/corpus/6100-record-get-or-create.ts b/tests/corpus/6100-record-get-or-create.ts
new file mode 100644
index 000000000..807cb303e
--- /dev/null
+++ b/tests/corpus/6100-record-get-or-create.ts
@@ -0,0 +1,119 @@
+// Get-or-create over nested index-signature records without
+// noUncheckedIndexedAccess: a missing key reads as undefined when the next
+// statement tests the binding for absence, in every spelling of that test,
+// at module scope and inside functions. A write with a computed key through
+// a binding that may hold a missing element throws Node's TypeError after
+// the key and the value evaluated.
+type ExportMap = Record>;
+
+interface Target {
+ subpath: string;
+ condition: string;
+ target: string;
+}
+
+function build(targets: Target[]): { exports: ExportMap; unmapped: string[] } {
+ const exports: ExportMap = {};
+ const unmapped: string[] = [];
+ for (const t of targets) {
+ if (t.target === "") {
+ unmapped.push(t.subpath);
+ continue;
+ }
+ let entry = exports[t.subpath];
+ if (entry === undefined) {
+ entry = {};
+ exports[t.subpath] = entry;
+ }
+ entry[t.condition] = `./${t.target}`;
+ entry["types"] = `./${t.target}`;
+ }
+ return { exports: exports, unmapped: unmapped };
+}
+
+const result = build([
+ { subpath: ".", condition: "import", target: "src/index.ts" },
+ { subpath: ".", condition: "require", target: "src/index.ts" },
+ { subpath: "./x", condition: "default", target: "" },
+ { subpath: "./y", condition: "node", target: "src/y.ts" },
+]);
+console.log(JSON.stringify(result.exports), result.unmapped.join(","));
+
+function group(words: string[]): Record {
+ const byInitial: Record = {};
+ for (const w of words) {
+ const k = w.slice(0, 1);
+ let list = byInitial[k];
+ if (list == null) {
+ list = [];
+ byInitial[k] = list;
+ }
+ list.push(w);
+ }
+ return byInitial;
+}
+console.log(JSON.stringify(group(["apple", "avocado", "banana", "blueberry", "cherry"])));
+
+function count(words: string[]): Record {
+ const counts: Record = {};
+ for (const w of words) {
+ const n = counts[w];
+ if (typeof n === "undefined") counts[w] = 1;
+ else counts[w] = n + 1;
+ }
+ return counts;
+}
+console.log(JSON.stringify(count(["a", "b", "a", "c", "a"])));
+
+function lookup(table: Record, key: string): string {
+ const hit = table[key];
+ if (hit !== undefined) return hit;
+ return `<${key} missing>`;
+}
+console.log(lookup({ a: "A" }, "a"), lookup({ a: "A" }, "b"));
+
+function rows(table: Record>, key: string, col: string): number {
+ const row = table[key];
+ if (row != undefined) return row[col] ?? -1;
+ return -2;
+}
+console.log(rows({ r: { c: 3 } }, "r", "c"), rows({ r: { c: 3 } }, "r", "d"), rows({}, "x", "c"));
+
+// Module scope.
+const nested: Record> = {};
+const key = process.argv.length > 99 ? "unused" : "k";
+let slot = nested[key];
+if (slot === undefined) {
+ slot = {};
+ nested[key] = slot;
+}
+slot[key] = 1;
+slot["fixed"] = 2;
+console.log(JSON.stringify(nested));
+
+const names: Record = { present: "yes" };
+const missing = names[key];
+if (!missing) console.log("falsy guard:", missing);
+const present = names["present"];
+if (present === void 0) console.log("unreachable");
+else console.log("present:", present);
+
+// A write with a computed key through a binding that may hold a missing
+// array element throws Node's TypeError naming the key, after evaluating
+// the key and the value.
+function probe(tables: Record[]): void {
+ const firstTable = tables[0];
+ const lastTable = tables[5];
+ const pick = (k: string): string => {
+ console.log("key", k);
+ return k;
+ };
+ firstTable[pick("b")] = pick("second");
+ console.log(JSON.stringify(firstTable));
+ try {
+ lastTable[pick("c")] = pick("value");
+ } catch (e) {
+ console.log((e as Error).name, (e as Error).message);
+ }
+}
+probe([{ a: "first" }]);
diff --git a/tests/corpus/6101-record-get-or-create-unchecked/main.ts b/tests/corpus/6101-record-get-or-create-unchecked/main.ts
new file mode 100644
index 000000000..a2b4ef705
--- /dev/null
+++ b/tests/corpus/6101-record-get-or-create-unchecked/main.ts
@@ -0,0 +1,70 @@
+// Get-or-create over nested index-signature records under
+// noUncheckedIndexedAccess: after the presence guard the binding is still
+// stored with its undefined arm, and reads, writes and absence probes with
+// computed keys go through it (an internal compiler error before).
+type ExportMap = Record>;
+
+function build(rows: { k: string; c: string; v: string }[]): ExportMap {
+ const map: ExportMap = {};
+ for (const r of rows) {
+ let entry = map[r.k];
+ if (entry === undefined) {
+ entry = {};
+ map[r.k] = entry;
+ }
+ entry[r.c] = r.v;
+ }
+ return map;
+}
+console.log(
+ JSON.stringify(
+ build([
+ { k: "a", c: "x", v: "1" },
+ { k: "a", c: "y", v: "2" },
+ { k: "b", c: "x", v: "3" },
+ ]),
+ ),
+);
+
+function touch(map: ExportMap, k: string): string {
+ let e = map[k];
+ if (e === undefined) {
+ e = { [k]: "init" };
+ map[k] = e;
+ }
+ const before = e[k] ?? "none";
+ e[k] = e[k] + "!";
+ return before + (k in e ? "+" : "-");
+}
+const m: ExportMap = {};
+console.log(touch(m, "a"), touch(m, "a"), touch(m, "b"), JSON.stringify(m));
+
+function tally(counts: Record>, group: string, item: string): void {
+ let row = counts[group];
+ if (!row) {
+ row = {};
+ counts[group] = row;
+ }
+ row[item] = (row[item] ?? 0) + 1;
+}
+const counts: Record> = {};
+for (const [g, i] of [
+ ["fruit", "apple"],
+ ["fruit", "pear"],
+ ["veg", "kale"],
+ ["fruit", "apple"],
+]) {
+ tally(counts, g!, i!);
+}
+console.log(JSON.stringify(counts));
+
+// Module scope.
+const top: ExportMap = {};
+const key = process.argv.length > 99 ? "unused" : "k";
+let slot = top[key];
+if (slot === undefined) {
+ slot = {};
+ top[key] = slot;
+}
+slot[key] = "v";
+console.log(JSON.stringify(top), slot[key], slot["other"] ?? "missing");
diff --git a/tests/corpus/6101-record-get-or-create-unchecked/tsconfig.json b/tests/corpus/6101-record-get-or-create-unchecked/tsconfig.json
new file mode 100644
index 000000000..f2e1c10cd
--- /dev/null
+++ b/tests/corpus/6101-record-get-or-create-unchecked/tsconfig.json
@@ -0,0 +1,6 @@
+{
+ "compilerOptions": {
+ "strict": true,
+ "noUncheckedIndexedAccess": true
+ }
+}
diff --git a/tests/corpus/6103-typeof-builtin-bindings.cjs b/tests/corpus/6103-typeof-builtin-bindings.cjs
new file mode 100644
index 000000000..ecef94dd3
--- /dev/null
+++ b/tests/corpus/6103-typeof-builtin-bindings.cjs
@@ -0,0 +1,15 @@
+// typeof on bindings destructured from Node builtin modules: Node answers by
+// the export's kind ("function" for functions and classes, "object" for
+// namespace objects), never by how the binding is represented.
+const { inspect, format, promisify, deprecate, isDeepStrictEqual } = require("util");
+const { join, resolve } = require("path");
+const { readFileSync, promises } = require("fs");
+const { EventEmitter } = require("events");
+const { createHash } = require("crypto");
+const util = require("node:util");
+const { inspect: aliased } = util;
+console.log(typeof inspect, typeof format, typeof promisify, typeof deprecate, typeof isDeepStrictEqual);
+console.log(typeof join, typeof resolve, typeof readFileSync, typeof promises);
+console.log(typeof EventEmitter, typeof createHash, typeof aliased, typeof util.inspect);
+console.log(typeof inspect === "function" ? "callable" : "not callable");
+console.log(join("a", "b"), format("%s=%d", "x", 1), inspect({ a: [1, 2] }));
diff --git a/tests/corpus/6104-typeof-builtin-bindings.mjs b/tests/corpus/6104-typeof-builtin-bindings.mjs
new file mode 100644
index 000000000..dd2976775
--- /dev/null
+++ b/tests/corpus/6104-typeof-builtin-bindings.mjs
@@ -0,0 +1,11 @@
+// typeof on named, default-destructured and namespace-destructured builtin
+// bindings in an ES module: Node answers by the export's kind.
+import util, { inspect as namedInspect, format } from "node:util";
+import * as path from "node:path";
+import { createHash } from "node:crypto";
+import { promises } from "node:fs";
+const { inspect, promisify } = util;
+const { join } = path;
+console.log(typeof inspect, typeof promisify, typeof namedInspect, typeof format);
+console.log(typeof join, typeof createHash, typeof util.inspect, typeof promises);
+console.log(namedInspect("s"), format("%d", 2), join("x", "y"));
diff --git a/tests/corpus/6105-cjs-top-level-this/lib.js b/tests/corpus/6105-cjs-top-level-this/lib.js
new file mode 100644
index 000000000..7addc1840
--- /dev/null
+++ b/tests/corpus/6105-cjs-top-level-this/lib.js
@@ -0,0 +1,11 @@
+// A required CommonJS module sees its own exports object as `this`, and the
+// UMD test for a CommonJS host takes the module.exports branch.
+const kind = typeof this;
+let host = "global";
+if (typeof module !== "undefined" && module.exports) host = "commonjs";
+(function (root) {
+ console.log("lib wrapper root:", typeof root, root ? "truthy" : "falsy");
+})(this);
+exports.describe = function () {
+ return kind + " " + host;
+};
diff --git a/tests/corpus/6105-cjs-top-level-this/main.js b/tests/corpus/6105-cjs-top-level-this/main.js
new file mode 100644
index 000000000..1da910171
--- /dev/null
+++ b/tests/corpus/6105-cjs-top-level-this/main.js
@@ -0,0 +1,12 @@
+// Top-level `this` in a CommonJS module is module.exports (an object), also
+// inside arrow functions at module scope.
+const lib = require("./lib.js");
+const self = this;
+const arrow = () => this;
+console.log(typeof this, self === this, arrow() === this, this ? "truthy" : "falsy");
+console.log(lib.describe());
+(function () {
+ console.log("call(this):", typeof this);
+}).call(this);
+if ((typeof module !== "undefined" && module !== null) && module.exports) console.log("commonjs host");
+else console.log("browser host");
diff --git a/tests/corpus/6105-cjs-top-level-this/package.json b/tests/corpus/6105-cjs-top-level-this/package.json
new file mode 100644
index 000000000..a3c15a7a6
--- /dev/null
+++ b/tests/corpus/6105-cjs-top-level-this/package.json
@@ -0,0 +1 @@
+{ "type": "commonjs" }
diff --git a/tests/corpus/6106-esm-top-level-this.mjs b/tests/corpus/6106-esm-top-level-this.mjs
new file mode 100644
index 000000000..6079ecc1e
--- /dev/null
+++ b/tests/corpus/6106-esm-top-level-this.mjs
@@ -0,0 +1,4 @@
+// Top-level `this` in an ES module stays undefined, also inside arrow
+// functions at module scope.
+const arrow = () => this;
+console.log(typeof this, this === undefined, typeof arrow());
diff --git a/tests/corpus/6110-cjs-speculative-wrapper-fences.cjs b/tests/corpus/6110-cjs-speculative-wrapper-fences.cjs
new file mode 100644
index 000000000..1b3546518
--- /dev/null
+++ b/tests/corpus/6110-cjs-speculative-wrapper-fences.cjs
@@ -0,0 +1,30 @@
+// @deferred-fences: 9
+// Function expressions lowered speculatively — the `.call(this)` and
+// `.apply` receivers of CommonJS wrappers, and a computed class base — keep
+// every statement of their bodies. A statement with no static lowering
+// (here `module.exports` read as a value) compiles to its runtime fence
+// instead of vanishing; these never run, so the binary matches Node.
+// Coverage counts the three fenced statements nine times: each lowering of
+// a wrapper body counts its statements, and the `.call` and `.apply`
+// wrapper bodies are each lowered four times.
+const verbose = process.argv.length > 99;
+(function () {
+ var root = this;
+ console.log("call wrapper");
+ if (verbose) console.log(root === module.exports);
+ console.log("call wrapper done");
+}).call(this);
+(function (label) {
+ console.log("apply wrapper", label);
+ if (verbose) console.log(typeof module.exports);
+}).apply(null, ["ok"]);
+class Base {
+ hello() {
+ return "hello";
+ }
+}
+class Derived extends (function () {
+ if (verbose) console.log(module.exports);
+ return Base;
+})() {}
+console.log(new Derived().hello());
diff --git a/tests/corpus/6120-type-only-import-graph/flags.ts b/tests/corpus/6120-type-only-import-graph/flags.ts
new file mode 100644
index 000000000..5af639c0a
--- /dev/null
+++ b/tests/corpus/6120-type-only-import-graph/flags.ts
@@ -0,0 +1,2 @@
+export type Flag = boolean;
+console.log("flags evaluated");
diff --git a/tests/corpus/6120-type-only-import-graph/main.ts b/tests/corpus/6120-type-only-import-graph/main.ts
new file mode 100644
index 000000000..251ce6809
--- /dev/null
+++ b/tests/corpus/6120-type-only-import-graph/main.ts
@@ -0,0 +1,21 @@
+// Node's type stripping erases `import type` and `export type … from`, so
+// the modules and packages they name are never loaded: a package with no
+// runtime entry, a module importing a builtin scriptc does not compile, and
+// modules with load-time output. An inline `type` specifier keeps an empty
+// import clause and still evaluates its module, as does a mixed clause.
+import type { Frozen, Shape } from "types-only-pkg";
+import type { Label } from "./uses-vm.ts";
+import type * as Models from "./models.ts";
+import type { Reexported } from "./reexports.ts";
+import { type Mode, describe } from "./mixed.ts";
+import { type Flag } from "./flags.ts";
+
+const shape: Shape = { width: 3, height: 4 };
+const frozen: Frozen = { width: 5, height: 6 };
+const label: Label = "area";
+const model: Models.Model = { id: 1 };
+const payload: Reexported = { ok: true };
+const mode: Mode = "fast";
+const flag: Flag = true;
+console.log(label, shape.width * shape.height, model.id, payload.ok);
+console.log(describe(mode), flag, frozen.width + frozen.height);
diff --git a/tests/corpus/6120-type-only-import-graph/mixed.ts b/tests/corpus/6120-type-only-import-graph/mixed.ts
new file mode 100644
index 000000000..ec5a08bbb
--- /dev/null
+++ b/tests/corpus/6120-type-only-import-graph/mixed.ts
@@ -0,0 +1,8 @@
+export type { Payload } from "./payload.ts";
+
+export type Mode = "fast" | "slow";
+
+export function describe(mode: Mode): string {
+ return `mode=${mode}`;
+}
+console.log("mixed evaluated");
diff --git a/tests/corpus/6120-type-only-import-graph/models.ts b/tests/corpus/6120-type-only-import-graph/models.ts
new file mode 100644
index 000000000..195cf9a17
--- /dev/null
+++ b/tests/corpus/6120-type-only-import-graph/models.ts
@@ -0,0 +1,4 @@
+export interface Model {
+ id: number;
+}
+console.log("models evaluated");
diff --git a/tests/corpus/6120-type-only-import-graph/node_modules/types-only-pkg/index.d.ts b/tests/corpus/6120-type-only-import-graph/node_modules/types-only-pkg/index.d.ts
new file mode 100644
index 000000000..e9c1b5497
--- /dev/null
+++ b/tests/corpus/6120-type-only-import-graph/node_modules/types-only-pkg/index.d.ts
@@ -0,0 +1,6 @@
+export interface Shape {
+ width: number;
+ height: number;
+}
+
+export type Frozen = { readonly [K in keyof T]: T[K] };
diff --git a/tests/corpus/6120-type-only-import-graph/node_modules/types-only-pkg/package.json b/tests/corpus/6120-type-only-import-graph/node_modules/types-only-pkg/package.json
new file mode 100644
index 000000000..c2be952b9
--- /dev/null
+++ b/tests/corpus/6120-type-only-import-graph/node_modules/types-only-pkg/package.json
@@ -0,0 +1,5 @@
+{
+ "name": "types-only-pkg",
+ "version": "1.0.0",
+ "types": "./index.d.ts"
+}
diff --git a/tests/corpus/6120-type-only-import-graph/payload.ts b/tests/corpus/6120-type-only-import-graph/payload.ts
new file mode 100644
index 000000000..beb97c5ac
--- /dev/null
+++ b/tests/corpus/6120-type-only-import-graph/payload.ts
@@ -0,0 +1,4 @@
+export interface Payload {
+ ok: boolean;
+}
+console.log("payload evaluated");
diff --git a/tests/corpus/6120-type-only-import-graph/reexports.ts b/tests/corpus/6120-type-only-import-graph/reexports.ts
new file mode 100644
index 000000000..52e86f294
--- /dev/null
+++ b/tests/corpus/6120-type-only-import-graph/reexports.ts
@@ -0,0 +1,2 @@
+export type { Payload as Reexported } from "./payload.ts";
+console.log("reexports evaluated");
diff --git a/tests/corpus/6120-type-only-import-graph/tsconfig.json b/tests/corpus/6120-type-only-import-graph/tsconfig.json
new file mode 100644
index 000000000..aee0ec940
--- /dev/null
+++ b/tests/corpus/6120-type-only-import-graph/tsconfig.json
@@ -0,0 +1,5 @@
+{
+ "compilerOptions": {
+ "strict": true
+ }
+}
diff --git a/tests/corpus/6120-type-only-import-graph/uses-vm.ts b/tests/corpus/6120-type-only-import-graph/uses-vm.ts
new file mode 100644
index 000000000..bed6646bf
--- /dev/null
+++ b/tests/corpus/6120-type-only-import-graph/uses-vm.ts
@@ -0,0 +1,4 @@
+import * as vm from "node:vm";
+
+export type Label = string;
+console.log("uses-vm evaluated", vm.runInThisContext("1 + 1"));
diff --git a/tests/corpus/6121-type-specifier-edges/a.ts b/tests/corpus/6121-type-specifier-edges/a.ts
new file mode 100644
index 000000000..59d6385da
--- /dev/null
+++ b/tests/corpus/6121-type-specifier-edges/a.ts
@@ -0,0 +1,10 @@
+import { b } from "./b.ts";
+
+export interface AInfo {
+ tag: string;
+}
+
+export function start(): string {
+ return `a+${b()}`;
+}
+console.log("a evaluated");
diff --git a/tests/corpus/6121-type-specifier-edges/b.ts b/tests/corpus/6121-type-specifier-edges/b.ts
new file mode 100644
index 000000000..a71f62def
--- /dev/null
+++ b/tests/corpus/6121-type-specifier-edges/b.ts
@@ -0,0 +1,7 @@
+import { type AInfo } from "./a.ts";
+
+export function b(): string {
+ const info: AInfo = { tag: "b" };
+ return info.tag;
+}
+console.log("b evaluated");
diff --git a/tests/corpus/6121-type-specifier-edges/config-barrel.ts b/tests/corpus/6121-type-specifier-edges/config-barrel.ts
new file mode 100644
index 000000000..dc461f654
--- /dev/null
+++ b/tests/corpus/6121-type-specifier-edges/config-barrel.ts
@@ -0,0 +1,2 @@
+export { type Config } from "./config.ts";
+console.log("barrel evaluated");
diff --git a/tests/corpus/6121-type-specifier-edges/config.ts b/tests/corpus/6121-type-specifier-edges/config.ts
new file mode 100644
index 000000000..1a42a9093
--- /dev/null
+++ b/tests/corpus/6121-type-specifier-edges/config.ts
@@ -0,0 +1,4 @@
+export interface Config {
+ name: string;
+}
+console.log("config evaluated");
diff --git a/tests/corpus/6121-type-specifier-edges/main.ts b/tests/corpus/6121-type-specifier-edges/main.ts
new file mode 100644
index 000000000..23855b470
--- /dev/null
+++ b/tests/corpus/6121-type-specifier-edges/main.ts
@@ -0,0 +1,10 @@
+// An inline `type` specifier keeps its module request under Node's type
+// stripping: `export { type X } from` and `import { type X } from` both
+// evaluate the named module, once, in source order. A cycle closed only by
+// such an import binds nothing at runtime and runs like a side-effect
+// import.
+import { type Config } from "./config-barrel.ts";
+import { start } from "./a.ts";
+
+const config: Config = { name: "demo" };
+console.log("main", config.name, start());
diff --git a/tests/corpus/6122-punycode-conversions.ts b/tests/corpus/6122-punycode-conversions.ts
new file mode 100644
index 000000000..061987a78
--- /dev/null
+++ b/tests/corpus/6122-punycode-conversions.ts
@@ -0,0 +1,45 @@
+// @exit: 1
+// @no-node-shims
+// node:punycode's conversions (punycode.js 2.1.0): encode/decode of bare
+// labels, toASCII/toUnicode over domains and email addresses (only the part
+// after '@' maps; every RFC 3490 separator splits labels), ucs2 code point
+// arrays with astral pairs and lone surrogates, and the RangeErrors of
+// malformed input. A static import links the module before anything runs,
+// so its DEP0040 report precedes the program and no 'warning' listener the
+// program registers sees it (the Node side runs without the preload shim,
+// which would delay that report past the program's first turn). The report
+// carries the process id, so the @exit directive keeps stderr out of the
+// comparison.
+import * as punycode from "node:punycode";
+
+process.on("warning", (w) => console.log("listener", w.name));
+
+const show = (label: string, f: () => unknown): void => {
+ try {
+ console.log(label, JSON.stringify(f()));
+ } catch (e) {
+ const err = e as Error;
+ console.log(label, "threw", err instanceof RangeError, err.name, err.message);
+ }
+};
+
+console.log(punycode.version);
+for (const s of ["mañana", "bücher", "ü", "abc", "", "-> $1.00 <-", "ليهمابتكلموشعربي؟", "他们为什么不说中文", "😀x", "MaJiでKoiする5秒前"]) {
+ show(`encode ${s}`, () => punycode.encode(s));
+ show(`roundtrip ${s}`, () => punycode.decode(punycode.encode(s)));
+}
+for (const s of ["maana-pta", "bcher-kva", "tda", "abc-", "-abc", "ABC-kva", "a-b-c-kva", "ü-kva", "kva-ü", "99999999999", "zzzzzzzzzzzzz", "a-", "-"]) {
+ show(`decode ${s}`, () => punycode.decode(s));
+}
+for (const d of ["mañana.com", "bücher.de", "example.com", "", "a..b", "ex@mple@bücher.de", "user@bücher.de", "日本。jp", "a.b。c", "ÄÖÜ.test", "a.b."]) {
+ show(`toASCII ${d}`, () => punycode.toASCII(d));
+}
+for (const d of ["xn--maana-pta.com", "xn--bcher-kva.de", "XN--bcher-kva.de", "xn--BCHER-KVA.de", "example.com", "user@xn--bcher-kva.de", "xn--.x", "xn--ü.x", "xn--zzzzzzzzzzzzz.x", "xn--a-Kva.x"]) {
+ show(`toUnicode ${d}`, () => punycode.toUnicode(d));
+}
+show("ucs2.decode", () => punycode.ucs2.decode("aé😀\ud800b\udc00"));
+show("ucs2.encode", () => punycode.ucs2.encode([97, 0xe9, 0x1f600, 0xd800, 0xdc00, 98]));
+show("ucs2.encode join", () => punycode.ucs2.encode([0xd83d, 0xde00]).length);
+show("ucs2.encode range", () => punycode.ucs2.encode([0x110000]));
+show("ucs2 roundtrip", () => punycode.ucs2.encode(punycode.ucs2.decode("x😀\udfffy")) === "x😀\udfffy");
+process.exitCode = 1;
diff --git a/tests/corpus/6124-punycode-require-warnings.cjs b/tests/corpus/6124-punycode-require-warnings.cjs
new file mode 100644
index 000000000..c3255e6b7
--- /dev/null
+++ b/tests/corpus/6124-punycode-require-warnings.cjs
@@ -0,0 +1,32 @@
+// DEP0040 for require("punycode"): reported once, on the next tick after
+// the requiring turn, to the stderr report and the 'warning' listeners;
+// require("node:punycode") is the same module and does not report again.
+// process.noDeprecation drops the report and the listener call. The program
+// re-runs itself per case and prints each child's output with process ids
+// normalized.
+"use strict";
+const { spawnSync } = require("child_process");
+
+const mode = process.argv[2];
+if (mode === "direct") {
+ process.on("warning", (w) => console.log("listener", w.name, w.code, w.message));
+ process.nextTick(() => console.log("earlier tick"));
+ console.log("before require");
+ const direct = require("punycode");
+ console.log("after require", direct.toASCII("bücher.de"));
+ const prefixed = require("node:punycode");
+ console.log("prefixed", prefixed.toUnicode("xn--bcher-kva.de"));
+ setTimeout(() => console.log("timer"), 0);
+} else if (mode === "silenced") {
+ process.on("warning", (w) => console.log("listener", w.code));
+ process.noDeprecation = true;
+ const silencedModule = require("punycode");
+ console.log("silenced", silencedModule.encode("ü"));
+} else {
+ for (const child of ["direct", "silenced"]) {
+ const run = spawnSync(process.execPath, [process.argv[1], child], { encoding: "utf8" });
+ console.log(`== ${child}: status ${run.status}`);
+ console.log(run.stdout);
+ console.log(run.stderr.replace(/\(node:\d+\)/g, "(node:)"));
+ }
+}
diff --git a/tests/corpus/6125-constants-module.cjs b/tests/corpus/6125-constants-module.cjs
new file mode 100644
index 000000000..d7a547ac5
--- /dev/null
+++ b/tests/corpus/6125-constants-module.cjs
@@ -0,0 +1,26 @@
+// node:constants (deprecated in documentation only, so no warning): one
+// frozen object merging os.constants' dlopen, errno, priority and signals
+// groups with fs.constants and crypto.constants, in that order. Members
+// read like the owning modules' constants, and a constant the platform
+// lacks is absent (graceful-fs probes O_SYMLINK with hasOwnProperty).
+const constants = require("constants");
+const nodeConstants = require("node:constants");
+const os = require("os");
+
+console.log(typeof constants, Object.isFrozen(constants), constants === nodeConstants);
+console.log(constants.O_RDONLY, constants.O_WRONLY, constants.O_RDWR);
+console.log(constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC);
+console.log(constants.S_IFMT, constants.S_IFREG, constants.S_IFDIR, constants.S_IRWXU);
+console.log(constants.F_OK, constants.R_OK, constants.W_OK, constants.X_OK);
+console.log(constants.COPYFILE_EXCL, constants.UV_FS_COPYFILE_FICLONE, constants.UV_DIRENT_DIR);
+console.log(constants.SIGTERM === os.constants.signals.SIGTERM, constants.SIGINT);
+console.log(constants.ENOENT === os.constants.errno.ENOENT, constants.EACCES);
+console.log(constants.PRIORITY_HIGH, constants.PRIORITY_LOW, constants.RTLD_LAZY);
+console.log(constants.TLS1_2_VERSION, constants.RSA_PKCS1_PADDING, typeof constants.defaultCoreCipherList);
+console.log(constants.hasOwnProperty("O_SYMLINK") === ("O_SYMLINK" in constants));
+console.log(constants.hasOwnProperty("O_RDWR"), "SIGKILL" in constants, "UV_UDP_REUSEADDR" in constants);
+console.log(constants.hasOwnProperty("NOT_A_CONSTANT"), "dlopen" in constants, "errno" in constants);
+const keys = Object.keys(constants);
+console.log(keys.indexOf("RTLD_LAZY") < keys.indexOf("E2BIG"), keys.indexOf("E2BIG") < keys.indexOf("PRIORITY_LOW"));
+console.log(keys.indexOf("PRIORITY_LOW") < keys.indexOf("SIGHUP"), keys.indexOf("SIGHUP") < keys.indexOf("O_RDONLY"));
+console.log(keys.indexOf("O_RDONLY") < keys.indexOf("OPENSSL_VERSION_NUMBER"), keys.includes("POINT_CONVERSION_HYBRID"));
diff --git a/tests/corpus/6126-punycode-dependency/main.cjs b/tests/corpus/6126-punycode-dependency/main.cjs
new file mode 100644
index 000000000..530503a71
--- /dev/null
+++ b/tests/corpus/6126-punycode-dependency/main.cjs
@@ -0,0 +1,12 @@
+// A dependency under node_modules that requires node:punycode: Node's
+// isInsideNodeModules() rule skips DEP0040 for it, so nothing reaches stderr
+// or the 'warning' listeners, and the program's own later require of the
+// cached module does not report either.
+"use strict";
+process.on("warning", (w) => console.log("listener", w.code));
+const toAscii = require("puny-dep");
+const punycode = require("punycode");
+
+console.log(toAscii("mañana.com"), toAscii("example.org"));
+console.log(punycode.toUnicode("xn--maana-pta.com"));
+setTimeout(() => console.log("timer"), 0);
diff --git a/tests/corpus/6126-punycode-dependency/node_modules/puny-dep/index.d.ts b/tests/corpus/6126-punycode-dependency/node_modules/puny-dep/index.d.ts
new file mode 100644
index 000000000..89b712210
--- /dev/null
+++ b/tests/corpus/6126-punycode-dependency/node_modules/puny-dep/index.d.ts
@@ -0,0 +1,2 @@
+declare function toAscii(domain: string): string;
+export = toAscii;
diff --git a/tests/corpus/6126-punycode-dependency/node_modules/puny-dep/index.js b/tests/corpus/6126-punycode-dependency/node_modules/puny-dep/index.js
new file mode 100644
index 000000000..417c44889
--- /dev/null
+++ b/tests/corpus/6126-punycode-dependency/node_modules/puny-dep/index.js
@@ -0,0 +1,8 @@
+// A dependency requiring node:punycode: Node's isInsideNodeModules() rule
+// skips DEP0040 for it.
+"use strict";
+const punycode = require("punycode");
+
+module.exports = function toAscii(domain) {
+ return punycode.toASCII(domain);
+};
diff --git a/tests/corpus/6126-punycode-dependency/node_modules/puny-dep/package.json b/tests/corpus/6126-punycode-dependency/node_modules/puny-dep/package.json
new file mode 100644
index 000000000..71edeb25c
--- /dev/null
+++ b/tests/corpus/6126-punycode-dependency/node_modules/puny-dep/package.json
@@ -0,0 +1,5 @@
+{
+ "name": "puny-dep",
+ "version": "1.0.0",
+ "main": "index.js"
+}
diff --git a/tests/coverage-fixtures/tier-demotions.ts b/tests/coverage-fixtures/tier-demotions.ts
new file mode 100644
index 000000000..52f2058f1
--- /dev/null
+++ b/tests/coverage-fixtures/tier-demotions.ts
@@ -0,0 +1,25 @@
+// The report-only tier assignment of `scriptc coverage --explain-dynamic`.
+class Point {
+ constructor(
+ public x: number,
+ public y: number,
+ ) {}
+}
+
+// Reflection on a class: the dynamic tier would give Point a prototype object.
+(Point.prototype as any).norm = function () {
+ return 0;
+};
+
+// Already dynamic: a property read on an `any` value.
+function readName(text: string): string {
+ const data: any = JSON.parse(text);
+ return String(data.name);
+}
+
+// An operation without a static lowering, on a parameter.
+function precise(n: number): string {
+ return n.toPrecision(3);
+}
+
+console.log(readName('{"name":"a"}'), precise(1.5), new Point(1, 2).x);
diff --git a/tests/diagnostics/builtin-class-alias-escape.ts b/tests/diagnostics/builtin-class-alias-escape.ts
new file mode 100644
index 000000000..b3f9be5d3
--- /dev/null
+++ b/tests/diagnostics/builtin-class-alias-escape.ts
@@ -0,0 +1,15 @@
+// A builtin class alias owns no runtime value in a TypeScript file:
+// construction, instanceof, typeof and .name compile, other uses report
+// at the use. A let binding of a class is not an alias.
+import { EventEmitter } from "node:events";
+
+const Emitter = EventEmitter;
+const kinds = [Emitter];
+console.log(kinds.length);
+
+const M = Map;
+const boxed: unknown = M;
+console.log(boxed !== undefined);
+
+let Mutable = EventEmitter;
+console.log(typeof Mutable);
diff --git a/tests/diagnostics/class-values.ts b/tests/diagnostics/class-values.ts
index 02c5360fa..3a53e5185 100644
--- a/tests/diagnostics/class-values.ts
+++ b/tests/diagnostics/class-values.ts
@@ -18,8 +18,10 @@ class Unrelated {
static tag = "unrelated";
}
-// Builtin classes as values: their construction is libCall-shaped.
+// Builtin classes as values: a const alias resolves at compile time, and
+// an array of builtin classes has no element type.
const E = Error;
+const errorClasses = [E];
// Classes extending builtin bases as values (the error-message completion
// rule has no thunk ABI).
diff --git a/tests/diagnostics/local-class-type-query-brand.ts b/tests/diagnostics/local-class-type-query-brand.ts
new file mode 100644
index 000000000..566304627
--- /dev/null
+++ b/tests/diagnostics/local-class-type-query-brand.ts
@@ -0,0 +1,14 @@
+// The same speculative element-type query, reaching a function-local class
+// with a private brand through the function's returned array.
+function make() {
+ class Box {
+ #v = 1;
+ read(): number {
+ return this.#v;
+ }
+ }
+ return [new Box()];
+}
+const xs = make();
+const x = xs[0];
+console.log(x === undefined ? -1 : x.read());
diff --git a/tests/diagnostics/local-class-type-query-js.js b/tests/diagnostics/local-class-type-query-js.js
new file mode 100644
index 000000000..2da619133
--- /dev/null
+++ b/tests/diagnostics/local-class-type-query-js.js
@@ -0,0 +1,26 @@
+// A whole-program analysis that queries types before reachability is known
+// (the computed-key write census here, through `holder.p`) names a
+// function-local class whose collection is refused. The refusal is reported,
+// once, at the class; it used to crash the compiler with an uncaught
+// PoisonError.
+function main() {
+ const Project = class _Project {
+ constructor() {
+ this.name = "p";
+ }
+ close() {
+ if (this.missingFilesMap) {
+ this.missingFilesMap.clear();
+ this.missingFilesMap = void 0;
+ }
+ }
+ add(k) {
+ (this.missingFilesMap || (this.missingFilesMap = new Map())).set(k, 1);
+ }
+ };
+ const holder = { p: new Project() };
+ const key = process.argv.length > 5 ? String(process.argv.length) : "name";
+ holder.p[key] = "q";
+ console.log(holder.p.name);
+}
+main();
diff --git a/tests/diagnostics/local-class-type-query.ts b/tests/diagnostics/local-class-type-query.ts
new file mode 100644
index 000000000..217653ff2
--- /dev/null
+++ b/tests/diagnostics/local-class-type-query.ts
@@ -0,0 +1,14 @@
+// A whole-program analysis queries the element type of `items` before any
+// body lowers. The function-local class it names is refused: the refusal is
+// reported once, at the class (it used to crash the compiler with an uncaught
+// PoisonError).
+function make() {
+ class Box {
+ static count = 0;
+ v = 1;
+ }
+ return new Box();
+}
+const items = [make()];
+const first = items[0];
+console.log(first ? first.v : 0);
diff --git a/tests/diagnostics/speculative-function-refusal.ts b/tests/diagnostics/speculative-function-refusal.ts
new file mode 100644
index 000000000..fc48d9935
--- /dev/null
+++ b/tests/diagnostics/speculative-function-refusal.ts
@@ -0,0 +1,14 @@
+// Function expressions lowered speculatively — an `.apply` receiver and a
+// computed class base — report a refused statement in their body like any
+// other function: the statement never disappears from the build.
+(function () {
+ console.log("wrapper");
+ console.log(eval("1 + 1"));
+}).apply(null, []);
+
+class Base {}
+class Derived extends (function () {
+ console.log(new Function("return 1")());
+ return Base;
+})() {}
+console.log(typeof Derived);
diff --git a/tests/fixtures/npm-static/node_modules/@types/twinned/index.d.ts b/tests/fixtures/npm-static/node_modules/@types/twinned/index.d.ts
new file mode 100644
index 000000000..ef7c0d6c6
--- /dev/null
+++ b/tests/fixtures/npm-static/node_modules/@types/twinned/index.d.ts
@@ -0,0 +1,5 @@
+export interface Greeting {
+ text: string;
+}
+
+export default function wait(text: string): string;
diff --git a/tests/fixtures/npm-static/node_modules/@types/twinned/package.json b/tests/fixtures/npm-static/node_modules/@types/twinned/package.json
new file mode 100644
index 000000000..2bdbbfa6e
--- /dev/null
+++ b/tests/fixtures/npm-static/node_modules/@types/twinned/package.json
@@ -0,0 +1,5 @@
+{
+ "name": "@types/twinned",
+ "version": "1.0.0",
+ "types": "index.d.ts"
+}
diff --git a/tests/fixtures/npm-static/node_modules/twinned/index.js b/tests/fixtures/npm-static/node_modules/twinned/index.js
new file mode 100644
index 000000000..0aa0a79f0
--- /dev/null
+++ b/tests/fixtures/npm-static/node_modules/twinned/index.js
@@ -0,0 +1,9 @@
+// A CommonJS package without its own declarations: its types live in the
+// separate @types/twinned package, and it depends on the untyped ms.
+'use strict';
+
+const ms = require('ms');
+
+module.exports = function wait(text) {
+ return 'twinned:' + text + ':' + ms(90000);
+};
diff --git a/tests/fixtures/npm-static/node_modules/twinned/package.json b/tests/fixtures/npm-static/node_modules/twinned/package.json
new file mode 100644
index 000000000..14cce5ab2
--- /dev/null
+++ b/tests/fixtures/npm-static/node_modules/twinned/package.json
@@ -0,0 +1,5 @@
+{
+ "name": "twinned",
+ "version": "1.0.0",
+ "main": "index.js"
+}
diff --git a/tests/fixtures/npm-static/twinned-cli.ts b/tests/fixtures/npm-static/twinned-cli.ts
new file mode 100644
index 000000000..e24383184
--- /dev/null
+++ b/tests/fixtures/npm-static/twinned-cli.ts
@@ -0,0 +1,5 @@
+// twinned's declarations come from @types/twinned; the package itself and
+// its untyped ms dependency ship JavaScript only.
+import wait from "twinned";
+
+console.log(wait("ready"));
diff --git a/tests/fixtures/npm-static/type-only-cli.ts b/tests/fixtures/npm-static/type-only-cli.ts
new file mode 100644
index 000000000..9fd332331
--- /dev/null
+++ b/tests/fixtures/npm-static/type-only-cli.ts
@@ -0,0 +1,7 @@
+// `import type` erases under Node's type stripping: twinned is never
+// loaded, so no package is selected or refused, and its declared
+// interface types a value the program builds itself.
+import type { Greeting } from "twinned";
+
+const greeting: Greeting = { text: "hello" };
+console.log(greeting.text);
diff --git a/tests/harness/__snapshots__/builtin-class-alias-escape.ts.txt b/tests/harness/__snapshots__/builtin-class-alias-escape.ts.txt
new file mode 100644
index 000000000..9d1d981e2
--- /dev/null
+++ b/tests/harness/__snapshots__/builtin-class-alias-escape.ts.txt
@@ -0,0 +1,22 @@
+builtin-class-alias-escape.ts:7:16 - error SC1090: a builtin class alias as an escaping value (use 'Emitter' with new, instanceof, typeof or .name) is not supported yet
+
+ 6 | const Emitter = EventEmitter;
+ 7 | const kinds = [Emitter];
+ | ^~~~~~~
+ 8 | console.log(kinds.length);
+
+builtin-class-alias-escape.ts:11:24 - error SC1090: a builtin class alias as an escaping value (use 'M' with new, instanceof, typeof or .name) is not supported yet
+
+ 10 | const M = Map;
+ 11 | const boxed: unknown = M;
+ | ^
+ 12 | console.log(boxed !== undefined);
+
+builtin-class-alias-escape.ts:14:15 - error SC2020: 'events.EventEmitter' is part of the standard library types but has no scriptc lowering yet
+
+ 13 |
+ 14 | let Mutable = EventEmitter;
+ | ^~~~~~~~~~~~
+ 15 | console.log(typeof Mutable);
+
+ 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__/class-values.ts.txt b/tests/harness/__snapshots__/class-values.ts.txt
index 9d5a43c1f..953d1c2d1 100644
--- a/tests/harness/__snapshots__/class-values.ts.txt
+++ b/tests/harness/__snapshots__/class-values.ts.txt
@@ -1,52 +1,52 @@
-class-values.ts:22:7 - error SC2020: 'ErrorConstructor' is part of the standard library types but has no scriptc lowering yet
+class-values.ts:24:7 - error SC2009: values of type 'ErrorConstructor[]' cannot be compiled: the array shape is supported, but its element type 'ErrorConstructor' does not compile
- 21 | // Builtin classes as values: their construction is libCall-shaped.
- 22 | const E = Error;
- | ^
- 23 |
+ 23 | const E = Error;
+ 24 | const errorClasses = [E];
+ | ^~~~~~~~~~~~
+ 25 |
- hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations)
+ 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')
-class-values.ts:22:11 - error SC2020: 'Error' is part of the standard library types but has no scriptc lowering yet
+class-values.ts:24:22 - error SC2009: values of type 'ErrorConstructor[]' cannot be compiled: the array shape is supported, but its element type 'ErrorConstructor' does not compile
- 21 | // Builtin classes as values: their construction is libCall-shaped.
- 22 | const E = Error;
- | ^~~~~
- 23 |
+ 23 | const E = Error;
+ 24 | const errorClasses = [E];
+ | ^~~
+ 25 |
- hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations)
+ 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')
-class-values.ts:44:1 - error SC1090: assigning the static 'tag' through a class value (JS creates an OWN property on the runtime class — assign through the declaring class's name, which subclasses here) is not supported yet
+class-values.ts:46:1 - error SC1090: assigning the static 'tag' through a class value (JS creates an OWN property on the runtime class — assign through the declaring class's name, which subclasses here) is not supported yet
- 43 | let someClass: typeof Base = Base;
- 44 | someClass.tag = "write";
+ 45 | let someClass: typeof Base = Base;
+ 46 | someClass.tag = "write";
| ^~~~~~~~~~~~~
- 45 |
+ 47 |
-class-values.ts:48:13 - error SC1090: reading the static member 'tag' through a class value (a subclass of 'Base' redeclares it, so the runtime class decides which declaration answers) is not supported yet
+class-values.ts:50:13 - error SC1090: reading the static member 'tag' through a class value (a subclass of 'Base' redeclares it, so the runtime class decides which declaration answers) is not supported yet
- 47 | // runtime class decides which declaration answers.
- 48 | console.log(someClass.tag);
+ 49 | // runtime class decides which declaration answers.
+ 50 | console.log(someClass.tag);
| ^~~~~~~~~~~~~
- 49 |
+ 51 |
-class-values.ts:52:10 - error SC1090: function-local classes with statics, decorators, or generic members are not supported yet
+class-values.ts:54:10 - error SC1090: function-local classes with statics, decorators, or generic members are not supported yet
- 51 | function make(): unknown {
- 52 | return class { static value = 1; };
+ 53 | function make(): unknown {
+ 54 | return class { static value = 1; };
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~
- 53 | }
+ 55 | }
-class-values.ts:59:18 - error SC1090: class expressions with static initializers or static blocks outside a whole-initializer position (their declaration-time code must run exactly where the expression evaluates — bind the class in its own top-level `const C = class …` statement) is not supported yet
+class-values.ts:61:18 - error SC1090: class expressions with static initializers or static blocks outside a whole-initializer position (their declaration-time code must run exactly where the expression evaluates — bind the class in its own top-level `const C = class …` statement) is not supported yet
- 58 | // evaluates.
- 59 | const pair = [1, class WithStatic { static x = 1; }] as const;
+ 60 | // evaluates.
+ 61 | const pair = [1, class WithStatic { static x = 1; }] as const;
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- 60 |
+ 62 |
-class-values.ts:79:56 - error SC1090: receiver-dependent static methods through a polymorphic class value is not supported yet
+class-values.ts:81:56 - error SC1090: receiver-dependent static methods through a polymorphic class value is not supported yet
- 78 | class OtherThis extends UsesThis {}
- 79 | function self(value: typeof UsesThis): string { return value.self(); }
+ 80 | class OtherThis extends UsesThis {}
+ 81 | function self(value: typeof UsesThis): string { return value.self(); }
| ^~~~~~~~~~
- 80 | console.log(self(OtherThis));
\ No newline at end of file
+ 82 | console.log(self(OtherThis));
\ No newline at end of file
diff --git a/tests/harness/__snapshots__/coverage-divergences.txt b/tests/harness/__snapshots__/coverage-divergences.txt
index 67c9b57e3..2f811f892 100644
--- a/tests/harness/__snapshots__/coverage-divergences.txt
+++ b/tests/harness/__snapshots__/coverage-divergences.txt
@@ -2,6 +2,7 @@ scriptc coverage tests/coverage-fixtures/divergences.ts
statements analyzed 18
compile statically 18 (100%)
+ in dynamic tier 1 (operate on values typed only at runtime; --explain-dynamic lists them)
fully static — all analyzed statements compile to native code.
diff --git a/tests/harness/__snapshots__/coverage-dynamic-mix.txt b/tests/harness/__snapshots__/coverage-dynamic-mix.txt
index a3e714bde..2ad84f0af 100644
--- a/tests/harness/__snapshots__/coverage-dynamic-mix.txt
+++ b/tests/harness/__snapshots__/coverage-dynamic-mix.txt
@@ -2,6 +2,7 @@ scriptc coverage tests/coverage-fixtures/dynamic-mix.ts
statements analyzed 8
compile statically 7 (87%)
+ in dynamic tier 2 (operate on values typed only at runtime; --explain-dynamic lists them)
blockers:
×1 SC2012 '.toPrecision()' on numbers cannot compile statically
diff --git a/tests/harness/__snapshots__/coverage-explain-dynamic.txt b/tests/harness/__snapshots__/coverage-explain-dynamic.txt
new file mode 100644
index 000000000..1c86a808c
--- /dev/null
+++ b/tests/harness/__snapshots__/coverage-explain-dynamic.txt
@@ -0,0 +1,16 @@
+ dynamic tier (tier assignment, report-only; --tier=auto)
+ 4 functions already operate on dynamic values (1 statement)
+ readName tests/coverage-fixtures/tier-demotions.ts:15:1: %dyn.error.stringConstructor at tests/coverage-fixtures/tier-demotions.ts:17:10
+ %dyn.error.stringConstructor.0 tests/coverage-fixtures/tier-demotions.ts:17:10: dynCall at tests/coverage-fixtures/tier-demotions.ts:17:10
+ %dyn.class.property.0 tests/coverage-fixtures/tier-demotions.ts:17:17: %dyn.class.readPropertiesOrEmpty at tests/coverage-fixtures/tier-demotions.ts:17:17
+ %method.value:%%Error.toString tests/coverage-fixtures/tier-demotions.ts:17:10: dyn.this at tests/coverage-fixtures/tier-demotions.ts:17:10
+
+ 2 demotion requests (refusals and fences the dynamic tier would replace)
+ class Point tests/coverage-fixtures/tier-demotions.ts:2:1 (1 request)
+ <- SC1090 reflection at tests/coverage-fixtures/tier-demotions.ts:10:1: assignment to non-variables are not supported yet
+ -> function turns dynamic
+ function tests/coverage-fixtures/tier-demotions.ts:1:1 (1 site inside)
+ function precise tests/coverage-fixtures/tier-demotions.ts:21:1 (1 site inside)
+ binding n in precise tests/coverage-fixtures/tier-demotions.ts:21:18 (1 request)
+ <- SC2012 operation at tests/coverage-fixtures/tier-demotions.ts:22:10: '.toPrecision()' on numbers cannot compile statically
+ -> function precise turns dynamic
\ No newline at end of file
diff --git a/tests/harness/__snapshots__/coverage-js-gap.txt b/tests/harness/__snapshots__/coverage-js-gap.txt
index 3150216b6..3b6fa678c 100644
--- a/tests/harness/__snapshots__/coverage-js-gap.txt
+++ b/tests/harness/__snapshots__/coverage-js-gap.txt
@@ -2,5 +2,6 @@ scriptc coverage tests/coverage-fixtures/js-gap/gap.js
statements analyzed 5
compile statically 5 (100%)
+ in dynamic tier 3 (operate on values typed only at runtime; --explain-dynamic lists them)
fully static — all analyzed statements compile to native code.
\ No newline at end of file
diff --git a/tests/harness/__snapshots__/coverage-mixed.txt b/tests/harness/__snapshots__/coverage-mixed.txt
index 88b61c39b..c3b348f9c 100644
--- a/tests/harness/__snapshots__/coverage-mixed.txt
+++ b/tests/harness/__snapshots__/coverage-mixed.txt
@@ -2,6 +2,7 @@ scriptc coverage tests/coverage-fixtures/mixed.ts
statements analyzed 13
compile statically 12 (92%)
+ in dynamic tier 1 (operate on values typed only at runtime; --explain-dynamic lists them)
blockers:
×1 SC1031 object destructuring of non-record values (the source is array-typed)
diff --git a/tests/harness/__snapshots__/coverage-npm-packages.txt b/tests/harness/__snapshots__/coverage-npm-packages.txt
index 0ba05286a..475b10e45 100644
--- a/tests/harness/__snapshots__/coverage-npm-packages.txt
+++ b/tests/harness/__snapshots__/coverage-npm-packages.txt
@@ -2,6 +2,7 @@ scriptc coverage tests/corpus/native-npm-imports/main.ts
statements analyzed 14
compile statically 14 (100%)
+ in dynamic tier 3 (operate on values typed only at runtime; --explain-dynamic lists them)
npm package compilation:
mathkit static
diff --git a/tests/harness/__snapshots__/coverage-tier-demotions.txt b/tests/harness/__snapshots__/coverage-tier-demotions.txt
new file mode 100644
index 000000000..7b52f9d63
--- /dev/null
+++ b/tests/harness/__snapshots__/coverage-tier-demotions.txt
@@ -0,0 +1,13 @@
+scriptc coverage tests/coverage-fixtures/tier-demotions.ts
+
+ statements analyzed 5
+ compile statically 3 (60%)
+ in dynamic tier 1 (operate on values typed only at runtime; --explain-dynamic lists them)
+
+ blockers:
+ ×1 SC1090 assignment to non-variables
+ at tests/coverage-fixtures/tier-demotions.ts:10:1
+ hint: this form is valid TypeScript but does not compile yet — rewrite it with a supported equivalent of the construct the message names (https://scriptc.dev/limitations lists the boundary)
+ ×1 SC2012 '.toPrecision()' on numbers cannot compile statically
+ at tests/coverage-fixtures/tier-demotions.ts:22:10
+ hint: use a supported API or rewrite this operation for native compilation
\ No newline at end of file
diff --git a/tests/harness/__snapshots__/coverage-width-copies.txt b/tests/harness/__snapshots__/coverage-width-copies.txt
index dd99d4bb5..46e8e949d 100644
--- a/tests/harness/__snapshots__/coverage-width-copies.txt
+++ b/tests/harness/__snapshots__/coverage-width-copies.txt
@@ -2,6 +2,7 @@ scriptc coverage tests/coverage-fixtures/width-copies.ts
statements analyzed 14
compile statically 14 (100%)
+ in dynamic tier 1 (operate on values typed only at runtime; --explain-dynamic lists them)
fully static — all analyzed statements compile to native code.
diff --git a/tests/harness/__snapshots__/local-class-type-query-brand.ts.txt b/tests/harness/__snapshots__/local-class-type-query-brand.ts.txt
new file mode 100644
index 000000000..6db3418a8
--- /dev/null
+++ b/tests/harness/__snapshots__/local-class-type-query-brand.ts.txt
@@ -0,0 +1,6 @@
+local-class-type-query-brand.ts:4:3 - error SC1090: function-local classes with private brands are not supported yet
+
+ 3 | function make() {
+ 4 | class Box {
+ | ^~~~~~~~~~~
+ 5 | #v = 1;
\ No newline at end of file
diff --git a/tests/harness/__snapshots__/local-class-type-query-js.js.txt b/tests/harness/__snapshots__/local-class-type-query-js.js.txt
new file mode 100644
index 000000000..60422a5dc
--- /dev/null
+++ b/tests/harness/__snapshots__/local-class-type-query-js.js.txt
@@ -0,0 +1,6 @@
+local-class-type-query-js.js:14:9 - error SC1090: fields assigned outside the constructor's top level ('this.missingFilesMap' would be undefined until the first assignment runs — assign it unconditionally at the top of the constructor) is not supported yet
+
+ 13 | this.missingFilesMap.clear();
+ 14 | this.missingFilesMap = void 0;
+ | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ 15 | }
\ No newline at end of file
diff --git a/tests/harness/__snapshots__/local-class-type-query.ts.txt b/tests/harness/__snapshots__/local-class-type-query.ts.txt
new file mode 100644
index 000000000..87489fcd5
--- /dev/null
+++ b/tests/harness/__snapshots__/local-class-type-query.ts.txt
@@ -0,0 +1,6 @@
+local-class-type-query.ts:6:3 - error SC1090: function-local classes with statics, decorators, or generic members are not supported yet
+
+ 5 | function make() {
+ 6 | class Box {
+ | ^~~~~~~~~~~
+ 7 | static count = 0;
\ No newline at end of file
diff --git a/tests/harness/__snapshots__/speculative-function-refusal.ts.txt b/tests/harness/__snapshots__/speculative-function-refusal.ts.txt
new file mode 100644
index 000000000..c8b0cd28c
--- /dev/null
+++ b/tests/harness/__snapshots__/speculative-function-refusal.ts.txt
@@ -0,0 +1,17 @@
+speculative-function-refusal.ts:6:15 - error SC2020: 'eval' is part of the standard library types but has no scriptc lowering yet
+
+ 5 | console.log("wrapper");
+ 6 | console.log(eval("1 + 1"));
+ | ^~~~
+ 7 | }).apply(null, []);
+
+ hint: runtime code evaluation cannot be compiled ahead of time
+
+speculative-function-refusal.ts:11:15 - error SC2020: 'new Function' is part of the standard library types but has no scriptc lowering yet
+
+ 10 | class Derived extends (function () {
+ 11 | console.log(new Function("return 1")());
+ | ^~~~~~~~~~~~~~~~~~~~~~~~
+ 12 | return Base;
+
+ hint: runtime code generation cannot be compiled ahead of time (the eval stance) — write the function
\ No newline at end of file
diff --git a/tests/harness/__snapshots__/zlib-class-alias-escape.ts.txt b/tests/harness/__snapshots__/zlib-class-alias-escape.ts.txt
index ea9d76089..703038025 100644
--- a/tests/harness/__snapshots__/zlib-class-alias-escape.ts.txt
+++ b/tests/harness/__snapshots__/zlib-class-alias-escape.ts.txt
@@ -1,4 +1,4 @@
-zlib-class-alias-escape.ts:7:18 - error SC1090: a zlib class alias as an escaping value (use 'Gz' with new, instanceof, typeof or .name) is not supported yet
+zlib-class-alias-escape.ts:7:18 - error SC1090: a builtin class alias as an escaping value (use 'Gz' with new, instanceof, typeof or .name) is not supported yet
6 | const Gz = zlib.Gzip;
7 | const classes = [Gz];
diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json
index 0f5a88f81..876ac4369 100644
--- a/tests/harness/corpus-costs.json
+++ b/tests/harness/corpus-costs.json
@@ -1976,6 +1976,7 @@
"5630-promisify-zlib.ts": 2503,
"5631-promisify-crypto.ts": 2200,
"5632-promisify-dns.ts": 1700,
+ "5633-zlib-typed-options.ts": 1800,
"5644-net-socket-addresses.ts": 2220,
"5645-net-socket-callbacks.ts": 2442,
"5646-net-server-connections.ts": 2389,
@@ -1997,6 +1998,7 @@
"5680-array-growth-after-copy.ts": 3791,
"5681-array-pop-and-length.ts": 3754,
"5682-publish-wide-objects.ts": 3326,
+ "5683-date-local-fields-tz.ts": 3848,
"5700-cycle-release-retained.ts": 2156,
"5701-cycle-release-old-edges.ts": 2601,
"5702-publish-candidate-buffers.ts": 3469,
@@ -2139,6 +2141,7 @@
"5913-prototype-type-error-messages.ts": 2484,
"5914-dyn-in-class-prototype-members.ts": 3169,
"5915-dyn-error-string-conversions.ts": 3200,
+ "5919-record-unions-shared-arms.ts": 3182,
"5920-promise-try-args.ts": 3028,
"5921-promise-try-args-js.js": 3179,
"5922-matchall-groups-callbacks.ts": 3650,
@@ -2151,10 +2154,15 @@
"5929-groupby-js.js": 2997,
"5930-contract-int32-arrays.ts": 3715,
"5930-method-census-asserted-number-keys.ts": 3330,
+ "5930-record-union-pointers.ts": 3182,
"5931-asserted-reads-keep-number-parameters.ts": 2990,
+ "5931-record-union-unit-arms.ts": 2745,
"5932-generic-class-observed-methods.ts": 4272,
+ "5932-record-union-trees.ts": 2863,
"5933-class-fields-unknown-object.ts": 4189,
+ "5933-record-union-conversions.ts": 4140,
"5934-js-class-fields-property-order.js": 4613,
+ "5934-record-union-set-members.ts": 2265,
"5935-js-subclass-fields-and-element-calls.js": 2484,
"5936-checked-js-class-types.js": 3412,
"5937-override-narrowed-union-returns.ts": 3036,
@@ -2186,6 +2194,9 @@
"5973-large-closure-environment.ts": 4499,
"5974-typeof-module-exports.cjs": 2000,
"5975-zlib-class-values.ts": 2800,
+ "5976-builtin-class-aliases.ts": 3100,
+ "5977-builtin-class-aliases.cjs": 2950,
+ "5978-builtin-class-alias-modules/main.ts": 2750,
"5990-worker-uncloneable.mjs": 3283,
"5991-worker-async-dispose.mjs": 5699,
"5992-worker-thread-name-lifetime.mjs": 2687,
@@ -2204,6 +2215,13 @@
"6042-regex-match-object-destructuring.ts": 3393,
"6043-string-index-narrowed-union.ts": 2983,
"605-closures-forward-capture-tdz.ts": 3277,
+ "6100-record-get-or-create.ts": 3771,
+ "6101-record-get-or-create-unchecked/main.ts": 2107,
+ "6103-typeof-builtin-bindings.cjs": 2202,
+ "6104-typeof-builtin-bindings.mjs": 2031,
+ "6105-cjs-top-level-this/main.js": 1371,
+ "6106-esm-top-level-this.mjs": 1915,
+ "6110-cjs-speculative-wrapper-fences.cjs": 1980,
"6140-cjs-require-tdz-throws/main.js": 2731,
"6141-cjs-require-tdz-checked/main.js": 2856,
"6142-cjs-require-published-early/main.js": 3400,
@@ -5002,6 +5020,7 @@
"5630-promisify-zlib.ts": 3392,
"5631-promisify-crypto.ts": 2600,
"5632-promisify-dns.ts": 2000,
+ "5633-zlib-typed-options.ts": 2200,
"5644-net-socket-addresses.ts": 2393,
"5645-net-socket-callbacks.ts": 2060,
"5646-net-server-connections.ts": 2755,
@@ -5023,6 +5042,7 @@
"5680-array-growth-after-copy.ts": 4581,
"5681-array-pop-and-length.ts": 3216,
"5682-publish-wide-objects.ts": 3376,
+ "5683-date-local-fields-tz.ts": 3911,
"5700-cycle-release-retained.ts": 3414,
"5701-cycle-release-old-edges.ts": 4210,
"5702-publish-candidate-buffers.ts": 3848,
@@ -5165,6 +5185,7 @@
"5913-prototype-type-error-messages.ts": 4068,
"5914-dyn-in-class-prototype-members.ts": 4327,
"5915-dyn-error-string-conversions.ts": 4247,
+ "5919-record-unions-shared-arms.ts": 4941,
"5920-promise-try-args.ts": 3195,
"5921-promise-try-args-js.js": 3082,
"5922-matchall-groups-callbacks.ts": 5960,
@@ -5177,10 +5198,15 @@
"5929-groupby-js.js": 3482,
"5930-contract-int32-arrays.ts": 4745,
"5930-method-census-asserted-number-keys.ts": 4815,
+ "5930-record-union-pointers.ts": 4941,
"5931-asserted-reads-keep-number-parameters.ts": 3015,
+ "5931-record-union-unit-arms.ts": 4558,
"5932-generic-class-observed-methods.ts": 5678,
+ "5932-record-union-trees.ts": 4329,
"5933-class-fields-unknown-object.ts": 4834,
+ "5933-record-union-conversions.ts": 4357,
"5934-js-class-fields-property-order.js": 4883,
+ "5934-record-union-set-members.ts": 4003,
"5935-js-subclass-fields-and-element-calls.js": 4470,
"5936-checked-js-class-types.js": 4183,
"5937-override-narrowed-union-returns.ts": 3827,
@@ -5212,6 +5238,9 @@
"5973-large-closure-environment.ts": 5145,
"5974-typeof-module-exports.cjs": 2500,
"5975-zlib-class-values.ts": 4200,
+ "5976-builtin-class-aliases.ts": 5450,
+ "5977-builtin-class-aliases.cjs": 4300,
+ "5978-builtin-class-alias-modules/main.ts": 4450,
"5990-worker-uncloneable.mjs": 22432,
"5991-worker-async-dispose.mjs": 25768,
"5992-worker-thread-name-lifetime.mjs": 28932,
@@ -5230,6 +5259,13 @@
"6042-regex-match-object-destructuring.ts": 3091,
"6043-string-index-narrowed-union.ts": 3627,
"605-closures-forward-capture-tdz.ts": 4638,
+ "6100-record-get-or-create.ts": 3173,
+ "6101-record-get-or-create-unchecked/main.ts": 3228,
+ "6103-typeof-builtin-bindings.cjs": 1796,
+ "6104-typeof-builtin-bindings.mjs": 1715,
+ "6105-cjs-top-level-this/main.js": 3213,
+ "6106-esm-top-level-this.mjs": 1672,
+ "6110-cjs-speculative-wrapper-fences.cjs": 2500,
"6140-cjs-require-tdz-throws/main.js": 2989,
"6141-cjs-require-tdz-checked/main.js": 4062,
"6142-cjs-require-published-early/main.js": 5156,
diff --git a/tests/harness/coverage.test.ts b/tests/harness/coverage.test.ts
index 06f0e75a8..420651418 100644
--- a/tests/harness/coverage.test.ts
+++ b/tests/harness/coverage.test.ts
@@ -3,7 +3,13 @@ import { globSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { expect, test } from "vitest";
-import { analyze, coverageEnvelope, coveragePasses, renderCoverage } from "@scriptc/compiler";
+import {
+ analyze,
+ coverageEnvelope,
+ coveragePasses,
+ renderCoverage,
+ renderExplainDynamic,
+} from "@scriptc/compiler";
import { shardSelect, shardSuffix } from "./shard.js";
const repoRoot = join(import.meta.dirname, "../..");
@@ -230,6 +236,105 @@ test("CLI: build --print=diagnostics reports failures and divergence warnings as
}
});
+test("coverage counts the statements of dynamic-tier functions", async () => {
+ expect(analyze(join(repoRoot, "tests/corpus/400-fib.ts")).coverage.stats.statementsDynamic).toBe(
+ 0,
+ );
+ const { coverage, sourceTexts } = analyze(fixture("tier-demotions.ts"));
+ expect(coverage.stats.statementsDynamic).toBe(1);
+ expect(coverage.tiers).toBeUndefined();
+ expect(coverageEnvelope(coverage, { compilerVersion: "test", sourceTexts }).stats).toMatchObject({
+ statementsTotal: 5,
+ statementsStatic: 3,
+ statementsDynamic: 1,
+ statementsFailed: 2,
+ });
+ await expect(report(fixture("tier-demotions.ts"))).toMatchFileSnapshot(
+ "__snapshots__/coverage-tier-demotions.txt",
+ );
+});
+
+test("--explain-dynamic attributes each refusal to the unit the dynamic tier would take", async () => {
+ const { coverage, sourceTexts } = analyze(fixture("tier-demotions.ts"), { explainDynamic: true });
+ const tiers = coverage.tiers!;
+ expect(tiers.mode).toBe("auto");
+ expect(tiers.statementsDynamic).toBe(1);
+ expect(tiers.dynamicFunctions.map((fn) => fn.name)).toContain("readName");
+ const requests = [...tiers.requests].sort((a, b) => a.loc.start - b.loc.start);
+ expect(
+ requests.map((r) => [r.code, r.seed, r.scope, r.unit.kind, r.unit.name, r.enclosing.name]),
+ ).toEqual([
+ ["SC1090", "reflection", "reached", "class", "Point", ""],
+ ["SC2012", "operation", "reached", "binding", "n", "precise"],
+ ]);
+ expect(tiers.units.map((u) => [u.kind, u.name, u.requests, u.sites])).toEqual([
+ ["class", "Point", 1, 0],
+ ["function", "", 0, 1],
+ ["function", "precise", 0, 1],
+ ["binding", "n", 1, 0],
+ ]);
+ const envelope = coverageEnvelope(coverage, { compilerVersion: "test", sourceTexts });
+ expect(envelope.tiers?.requests[0]).toMatchObject({ unit: { kind: expect.any(String) } });
+ const relative = JSON.parse(JSON.stringify(tiers).replaceAll(repoRoot + "/", "")) as typeof tiers;
+ await expect(renderExplainDynamic(relative)).toMatchFileSnapshot(
+ "__snapshots__/coverage-explain-dynamic.txt",
+ );
+ expect(renderExplainDynamic(relative, { query: "Point" })).toContain("class Point");
+ expect(renderExplainDynamic(relative, { query: "Point" })).not.toContain("binding n");
+ expect(renderExplainDynamic(relative, { query: "nothing" })).toContain(
+ "no demoted unit matches 'nothing'",
+ );
+});
+
+test("CLI: --tier and --explain-dynamic", () => {
+ const scriptcCli = join(repoRoot, "packages/cli/src/main.ts");
+ const outDir = mkdtempSync(join(tmpdir(), "scriptc-tier-"));
+ const run = (...args: string[]) =>
+ spawnSync(process.execPath, ["--import", "tsx", scriptcCli, ...args], {
+ cwd: repoRoot,
+ encoding: "utf8",
+ env: { ...process.env, SCRIPTC_NO_CACHE: "1" },
+ });
+ try {
+ const explained = run(
+ "coverage",
+ fixture("tier-demotions.ts"),
+ "--explain-dynamic",
+ "--tier=static-only",
+ );
+ expect(explained.status).toBe(0);
+ expect(explained.stdout).toContain("in dynamic tier 1");
+ expect(explained.stdout).toContain("--tier=static-only");
+ expect(explained.stdout).toContain("binding n in precise");
+ const one = run("coverage", fixture("tier-demotions.ts"), "--explain-dynamic=Point");
+ expect(one.stdout).toContain("class Point");
+ expect(one.stdout).not.toContain("binding n in precise");
+ const bad = run("coverage", fixture("tier-demotions.ts"), "--tier=dynamic-only");
+ expect(bad.status).toBe(1);
+ expect(bad.stderr).toContain('unknown --tier value "dynamic-only"');
+ const misplaced = run("build", fixture("tier-demotions.ts"), "--explain-dynamic");
+ expect(misplaced.status).toBe(1);
+ expect(misplaced.stderr).toContain("--explain-dynamic is a coverage option");
+ // Until demotion lands, static-only compiles exactly like the default.
+ const ir = (name: string, ...args: string[]) => {
+ const out = join(outDir, name);
+ const built = run(
+ "build",
+ join(repoRoot, "tests/corpus/400-fib.ts"),
+ "--emit=ir",
+ "-o",
+ out,
+ ...args,
+ );
+ expect(built.status).toBe(0);
+ return readFileSync(out, "utf8");
+ };
+ expect(ir("static-only.ir.json", "--tier=static-only")).toBe(ir("auto.ir.json"));
+ } finally {
+ rmSync(outDir, { recursive: true, force: true });
+ }
+});
+
test("fully static program reports 100%", () => {
const out = report(join(repoRoot, "tests/corpus/400-fib.ts"));
expect(out).toContain("(100%)");
diff --git a/tests/harness/errors.test.ts b/tests/harness/errors.test.ts
index 28daa88d3..51f04d310 100644
--- a/tests/harness/errors.test.ts
+++ b/tests/harness/errors.test.ts
@@ -960,3 +960,56 @@ for (const input of [ciphertext, Buffer.from("forged data"), Buffer.alloc(0)]) {
expect(r.stderr).toBe("");
});
});
+
+// Function expressions the frontend lowers speculatively (the receiver of
+// `.call`/`.apply`, a computed class base) keep their refused statements as
+// runtime fences. They used to vanish: the capture buffer held the refusal,
+// the candidate was kept, and the statement was simply missing from it.
+describe("refused statements in speculatively lowered JavaScript functions", () => {
+ test.each([
+ [
+ "speculative-call-this",
+ `(function () {
+ var root = this;
+ console.log("before");
+ console.log(root === module.exports);
+ console.log("after");
+}).call(this);
+`,
+ "before\n",
+ 4,
+ ],
+ [
+ "speculative-apply",
+ `(function () {
+ console.log("before");
+ console.log(typeof module.exports);
+}).apply(null, []);
+`,
+ "before\n",
+ 3,
+ ],
+ [
+ "speculative-class-base",
+ `class Base {}
+class Derived extends (function () {
+ console.log("before");
+ console.log(typeof module.exports);
+ return Base;
+})() {}
+console.log(typeof Derived);
+`,
+ "before\n",
+ 4,
+ ],
+ ] as const)("%s", async (name, source, stdout, line) => {
+ const r = await compileAndRun(name, source, "cjs");
+ expect(r.exitCode).toBe(1);
+ expect(r.stdout).toBe(stdout);
+ expect(r.stderr).toMatch(
+ new RegExp(
+ `^Uncaught Error: 'module\\.exports' is part of the standard library types but has no scriptc lowering yet \\[SC2020 at .*${name}\\.cjs:${line}\\]\\n$`,
+ ),
+ );
+ });
+});
diff --git a/tests/harness/npm-static.test.ts b/tests/harness/npm-static.test.ts
index c5021fd06..ba716076c 100644
--- a/tests/harness/npm-static.test.ts
+++ b/tests/harness/npm-static.test.ts
@@ -821,10 +821,71 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => {
package: "ms",
status: "refused",
detail: "it ships no own .d.ts declaration surface",
+ hint: "to compile it anyway, name it with --npm-static=ms (or --npm-static=ms... to include its dependencies)",
},
]);
}, 120_000);
+ // Types-first resolution answers @types/twinned, but the refusal names the
+ // package that has the code and the flag that compiles it.
+ test("an @types-typed package is refused under its runtime name with the opt-in remedy", () => {
+ const { coverage } = analyze(join(pilotRoot, "twinned-cli.ts"));
+ expect(coverage.npmStatic).toEqual([
+ {
+ package: "twinned",
+ status: "refused",
+ detail:
+ "its declared types come from the separate @types/twinned package, not the package itself",
+ hint: "to compile it anyway, name it with --npm-static=twinned (or --npm-static=twinned... to include its dependencies)",
+ },
+ ]);
+ const diagnostic = coverage.diagnostics.find((d) => d.code === "SC2013");
+ expect(diagnostic?.message).toContain("'twinned'");
+ expect(diagnostic?.hint).toContain("--npm-static=twinned");
+ }, 120_000);
+
+ test("a named package's untyped dependency points at the transitive opt-in", () => {
+ const { coverage } = analyze(join(pilotRoot, "twinned-cli.ts"), { npmStatic: ["twinned"] });
+ expect(coverage.npmStatic).toContainEqual({
+ package: "ms",
+ status: "refused",
+ detail: "it ships no own .d.ts declaration surface",
+ hint: "to compile it anyway, name it with --npm-static=ms, or use --npm-static=twinned... to include all of twinned's dependencies",
+ });
+ }, 120_000);
+
+ test("--npm-static=pkg... compiles the package's dependency closure", async () => {
+ const entry = join(pilotRoot, "twinned-cli.ts");
+ const { coverage } = analyze(entry, { npmStatic: ["twinned..."] });
+ expect(coverage.npmStatic).toEqual([
+ { package: "twinned", status: "static" },
+ { package: "ms", status: "static" },
+ ]);
+ expect(coverage.preflightFailed).toBe(false);
+ const binary = await buildStatic(entry, ["twinned..."]);
+ const [nodeRes, nativeRes] = await Promise.all([
+ runBinary("node", [entry]),
+ runBinary(binary, []),
+ ]);
+ expect(nativeRes.stdout.toString("utf8")).toBe(nodeRes.stdout.toString("utf8"));
+ expect(nativeRes.exitCode).toBe(nodeRes.exitCode);
+ }, 180_000);
+
+ test("type-only imports select no package", async () => {
+ const entry = join(pilotRoot, "type-only-cli.ts");
+ const { coverage } = analyze(entry);
+ expect(coverage.npmStatic ?? []).toEqual([]);
+ expect(coverage.preflightFailed).toBe(false);
+ expect(coverage.diagnostics).toEqual([]);
+ const binary = await buildStatic(entry, []);
+ const [nodeRes, nativeRes] = await Promise.all([
+ runBinary("node", [entry]),
+ runBinary(binary, []),
+ ]);
+ expect(nativeRes.stdout.toString("utf8")).toBe(nodeRes.stdout.toString("utf8"));
+ expect(nativeRes.exitCode).toBe(nodeRes.exitCode);
+ }, 180_000);
+
test("ms supports invalid duration strings without runtime fences", () => {
const { coverage } = analyze(join(pilotRoot, "ms-invalid-cli.ts"), { npmStatic: ["ms"] });
expect(coverage.npmStatic).toEqual([{ package: "ms", status: "static" }]);
@@ -974,6 +1035,7 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => {
package: "typegapped",
status: "refused",
detail: "it ships no own .d.ts declaration surface",
+ hint: "to compile it anyway, name it with --npm-static=typegapped (or --npm-static=typegapped... to include its dependencies)",
},
{ package: "escape-string-regexp", status: "static" },
]);