mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-07 08:33:50 +08:00
Merge origin/main into opt/pi-small-fixes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> # Conflicts: # packages/runtime/src/scr_async.h
This commit is contained in:
@@ -9,6 +9,7 @@ node_modules/
|
||||
!tests/corpus/*/node_modules/
|
||||
!tests/fixtures/npm-static/node_modules/
|
||||
!tests/fixtures/npm-static/nested/inner/node_modules/
|
||||
!tests/fixtures/npm-static/node_modules/*/node_modules/
|
||||
!tests/fixtures/npm/store/*/node_modules/
|
||||
!tests/fixtures/npm/cases/*/node_modules/
|
||||
!tests/library-mode/*/node_modules/
|
||||
@@ -32,6 +33,7 @@ output/
|
||||
!tests/fixtures/npm/cases/workspace-copied/members/*/dist/
|
||||
!tests/fixtures/npm/cases/workspace-copied/node_modules/*/dist/
|
||||
!tests/fixtures/provenance/node_modules/*/dist/
|
||||
!tests/fixtures/npm-static/node_modules/*/dist/
|
||||
.scriptc/
|
||||
*.log
|
||||
.DS_Store
|
||||
|
||||
@@ -119,7 +119,7 @@ Verifies and loads the installed precompiled runtime objects and vendored librar
|
||||
<dd>On Linux, when the program calls <code>process.exit()</code>, hand the release of its memory to a helper process so the parent sees the exit sooner. The kernel otherwise frees the whole address space before the exit is reported, which takes tens of milliseconds per gigabyte. Opt in only where the trade-offs are acceptable: for up to about 100 ms after the exit, the helper keeps the executable mapped (overwriting the binary in place fails with "text file busy"), its memory stays charged to the cgroup, and the teardown CPU time no longer appears in the parent's resource usage. Valid only for executable builds; it has no effect on other platforms.</dd>
|
||||
|
||||
<dt><code>--npm-static <pkg[,pkg…]|auto></code></dt>
|
||||
<dd>Name npm packages to compile even if they don't ship their own type declarations (repeatable). <code>auto</code> uses the default automatic selection. Packages that can't compile produce diagnostics. See <a href="/docs/dependencies#explicit-package-selection">npm Dependencies</a>.</dd>
|
||||
<dd>Name npm packages to compile even if they don't ship their own type declarations (repeatable). A trailing <code>...</code>, as in <code>fs-extra...</code>, also names every package that package depends on. <code>auto</code> uses the default automatic selection. Packages that can't compile produce diagnostics. See <a href="/docs/dependencies#explicit-package-selection">npm Dependencies</a>.</dd>
|
||||
|
||||
<dt><code>--provenance-sources</code></dt>
|
||||
<dd>Experimental. Compile npm dependencies from the source at the commit their provenance attestation names. Packages without a usable attestation compile from their installed files.</dd>
|
||||
|
||||
@@ -46,6 +46,8 @@ Your callback and the commander implementation both compile to native code. No e
|
||||
|
||||
scriptc finds packages in `node_modules` with Node.js's resolution rules, including the export conditions in `package.json`, and compiles their dependencies too. TypeScript workspace packages compile as part of your program, including `.js` import specifiers that resolve to `.ts` sources.
|
||||
|
||||
Type-only imports follow Node.js's type stripping. `import type` and `export type … from` statements are erased, so a package or module named only by them supplies types and isn't compiled or loaded; it can even be a package with no JavaScript, such as `type-fest`. A clause whose specifiers are all marked `type`, such as `import { type Options } from "./config.ts"`, still loads its module, as it does under Node.js.
|
||||
|
||||
## Which packages compile
|
||||
|
||||
scriptc compiles a package automatically when it:
|
||||
@@ -53,7 +55,7 @@ scriptc compiles a package automatically when it:
|
||||
- ships its own type declarations, and
|
||||
- ships readable, unminified JavaScript.
|
||||
|
||||
A package that doesn't meet these requirements, or that uses something scriptc can't compile, gets a package-specific diagnostic that explains why. The usual fixes are to switch to a version or alternative package that compiles, or to replace the call with code you write yourself.
|
||||
A package that doesn't meet these requirements, or that uses something scriptc can't compile, gets a package-specific diagnostic that explains why. The usual fixes are to switch to a version or alternative package that compiles, or to replace the call with code you write yourself. A package whose declarations come from a separate `@types/` package doesn't ship its own; its diagnostic names the package and the `--npm-static` option that compiles it anyway.
|
||||
|
||||
<span id="value-conversion" />
|
||||
<span id="static-package-compilation-experimental" />
|
||||
@@ -67,7 +69,7 @@ Some packages rely on separately installed declarations, such as an `@types/` pa
|
||||
$ scriptc build app.ts --npm-static=three -o app >/dev/null
|
||||
```
|
||||
|
||||
Naming a package skips the declaration requirement above, but the package still has to compile. `--npm-static=auto` uses the default automatic selection. The option accepts a comma-separated list and can be repeated.
|
||||
Naming a package skips the declaration requirement above, but the package still has to compile. The option selects only the packages it names: a dependency of a named package that doesn't ship its own declarations is still refused. Add a trailing `...` to select a package together with every package it depends on, for example `--npm-static=fs-extra...`. `--npm-static=auto` uses the default automatic selection. The option accepts a comma-separated list and can be repeated.
|
||||
|
||||
## CommonJS packages
|
||||
|
||||
|
||||
@@ -76,6 +76,8 @@ title: "Limitations"
|
||||
|
||||
<include>../limitations/builtin-apis/legacy-url-api.mdx</include>
|
||||
|
||||
<include>../limitations/builtin-apis/deprecated-modules.mdx</include>
|
||||
|
||||
<include>../limitations/builtin-apis/filesystem-flags.mdx</include>
|
||||
|
||||
<include>../limitations/builtin-apis/copy.mdx</include>
|
||||
|
||||
@@ -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.
|
||||
@@ -2,6 +2,6 @@
|
||||
"schemaVersion": 3,
|
||||
"nodeVersion": "24.22.0",
|
||||
"nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
|
||||
"artifactVersion": "a610bdd1ae56e1c4cd38",
|
||||
"artifactVersion": "4421a7b808191ac7f410",
|
||||
"rowCount": 3880
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { type AllocHost, emitObjectAlloc, emitObjectFree, inlineAllocSupported } from "./alloc.js";
|
||||
import {
|
||||
type AllocHost,
|
||||
disposeHookLoadLines,
|
||||
emitObjectAlloc,
|
||||
emitObjectFree,
|
||||
inlineAllocSupported,
|
||||
} from "./alloc.js";
|
||||
|
||||
function host(inlineAlloc: boolean): AllocHost & { decls: Set<string>; oom: boolean } {
|
||||
const h = {
|
||||
@@ -63,7 +69,12 @@ describe("emitted object allocation", () => {
|
||||
]);
|
||||
expect(emitObjectFree(h, false)).toEqual([
|
||||
" call void @scr_obj_free_note()",
|
||||
" %sf.obs = load i1, ptr @sc_weak_keys",
|
||||
" br i1 %sf.obs, label %sf.observe, label %sf.block",
|
||||
"sf.observe:",
|
||||
" call void @scr_weak_dispose(ptr %o)",
|
||||
" br label %sf.block",
|
||||
"sf.block:",
|
||||
" call void @scr_rt_free(ptr %o)",
|
||||
]);
|
||||
expect([...h.decls].some((d) => d.includes("@scr_sa"))).toBe(false);
|
||||
@@ -167,4 +178,30 @@ describe("emitted object allocation", () => {
|
||||
expect(lines.at(-1)).toBe("sf.done:");
|
||||
}
|
||||
});
|
||||
|
||||
test("frees test the dispose hook only where the program can observe them", () => {
|
||||
const h = host(true);
|
||||
const weak = emitObjectFree(h, false);
|
||||
expectSingleDefinitions(weak);
|
||||
expect(weak.slice(0, 2)).toEqual([
|
||||
" %sf.obs = load i1, ptr @sc_weak_keys",
|
||||
" br i1 %sf.obs, label %sf.hooked, label %sf.block",
|
||||
]);
|
||||
const records = emitObjectFree(h, true, "weak-or-source");
|
||||
expectSingleDefinitions(records);
|
||||
expect(records.slice(0, 4)).toEqual([
|
||||
" %sf.obsw = load i1, ptr @sc_weak_keys",
|
||||
" %sf.obss = load i1, ptr @sc_record_sources",
|
||||
" %sf.obs = or i1 %sf.obsw, %sf.obss",
|
||||
" br i1 %sf.obs, label %sf.hooked, label %sf.block",
|
||||
]);
|
||||
expect(records).toContain(" call void %sf.hook(ptr %o)");
|
||||
});
|
||||
|
||||
test("loads the dispose hook from emitted code only with the inline allocator", () => {
|
||||
expect(disposeHookLoadLines(host(false), "%t1")).toBeNull();
|
||||
expect(disposeHookLoadLines(host(true), "%t1")).toEqual([
|
||||
" %t1 = load ptr, ptr @scr_weak_dispose_hook",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -79,6 +79,17 @@ function threadLocalState(host: AllocHost, prefix: string, lines: string[]): str
|
||||
return [...preamble, ...text.split("\n")];
|
||||
}
|
||||
|
||||
/** Lines that load the runtime's dispose hook into `dest`, or null on
|
||||
* targets without the inline allocator, where the hook's storage is not
|
||||
* addressed from emitted code. The hook is NULL until a weak collection
|
||||
* or a kept record source installs it, and then never NULL again. */
|
||||
export function disposeHookLoadLines(host: AllocHost, dest: string): string[] | null {
|
||||
if (host.inlineAlloc !== true) return null;
|
||||
declareGlobal(host, `@scr_weak_dispose_hook = external global ptr`);
|
||||
const prefix = dest.replace(/^%/, "").replace(/[^-$._A-Za-z0-9]/g, "_");
|
||||
return threadLocalState(host, prefix, [` ${dest} = load ptr, ptr @scr_weak_dispose_hook`]);
|
||||
}
|
||||
|
||||
/** Worker executables: the calling script thread's ScrThreadHot block into
|
||||
* `dest`. Its address also identifies the thread (the owner word of a
|
||||
* lifetime-region chunk; llvm/regions.ts). */
|
||||
@@ -325,20 +336,56 @@ function allocLines(
|
||||
return lines;
|
||||
}
|
||||
|
||||
/* ── dispose observers ────────────────────────────────────────────────
|
||||
* The runtime's dispose hook (scr_weak_dispose_hook) has two clients: weak
|
||||
* collections, whose keys can be any object, and the kept sources of
|
||||
* checked records (scr_record_source_note and the JSON schema parser),
|
||||
* keyed by closed records. A program without either can never install the
|
||||
* hook, so its frees need not test it. Whether the program has them is
|
||||
* known only once every helper is emitted (dynCheck builders and JSON
|
||||
* schemas are requested lazily), so the emitted frees and record writers
|
||||
* test these two module constants, which the emitter defines last
|
||||
* (LlvmEmitter.emitParts) and LLVM folds away. */
|
||||
|
||||
/** True when the program constructs a WeakMap or WeakSet. */
|
||||
export const WEAK_KEYS_GATE = "@sc_weak_keys";
|
||||
/** True when a checked conversion can keep a record's undeclared source. */
|
||||
export const RECORD_SOURCES_GATE = "@sc_record_sources";
|
||||
|
||||
/** Which dispose observers an object type can have: weak-collection keys
|
||||
* only, or (closed record layouts) kept record sources too. */
|
||||
export type FreeObservers = "weak" | "weak-or-source";
|
||||
|
||||
/**
|
||||
* Lines that free the object `%o` after its members were released: the
|
||||
* RC-audit free note, the weak-collection dispose hook, and the block —
|
||||
* `scr_cyc_free` for collector-headered objects, `scr_rt_free` otherwise.
|
||||
* Labels use the `sf.` prefix; the lines end in an open block.
|
||||
* RC-audit free note, the dispose hook (when the program can observe this
|
||||
* type's disposals), and the block — `scr_cyc_free` for collector-headered
|
||||
* objects, `scr_rt_free` otherwise. Labels use the `sf.` prefix; the lines
|
||||
* end in an open block.
|
||||
*/
|
||||
export function emitObjectFree(host: AllocHost, cyc: boolean): string[] {
|
||||
const lines = threadLocalState(host, "sf", freeLines(host, cyc));
|
||||
export function emitObjectFree(
|
||||
host: AllocHost,
|
||||
cyc: boolean,
|
||||
observers: FreeObservers = "weak",
|
||||
): string[] {
|
||||
const lines = threadLocalState(host, "sf", freeLines(host, cyc, observers));
|
||||
if (host.objectAudit === false) return lines.filter((line) => line !== FREE_NOTE);
|
||||
host.declare(`declare void @scr_obj_free_note()`);
|
||||
return lines;
|
||||
}
|
||||
|
||||
function freeLines(host: AllocHost, cyc: boolean): string[] {
|
||||
/** Lines that leave in `%<prefix>.obs` whether the program can observe a
|
||||
* disposal of this type (folds to a constant). */
|
||||
function observedLines(prefix: string, observers: FreeObservers): string[] {
|
||||
if (observers === "weak") return [` %${prefix}.obs = load i1, ptr ${WEAK_KEYS_GATE}`];
|
||||
return [
|
||||
` %${prefix}.obsw = load i1, ptr ${WEAK_KEYS_GATE}`,
|
||||
` %${prefix}.obss = load i1, ptr ${RECORD_SOURCES_GATE}`,
|
||||
` %${prefix}.obs = or i1 %${prefix}.obsw, %${prefix}.obss`,
|
||||
];
|
||||
}
|
||||
|
||||
function freeLines(host: AllocHost, cyc: boolean, observers: FreeObservers): string[] {
|
||||
if (host.inlineAlloc !== true) {
|
||||
if (cyc) {
|
||||
host.declare(`declare void @scr_cyc_free(ptr)`);
|
||||
@@ -348,7 +395,12 @@ function freeLines(host: AllocHost, cyc: boolean): string[] {
|
||||
host.declare(`declare void @scr_weak_dispose(ptr)`);
|
||||
return [
|
||||
` call void @scr_obj_free_note()`,
|
||||
...observedLines("sf", observers),
|
||||
` br i1 %sf.obs, label %sf.observe, label %sf.block`,
|
||||
`sf.observe:`,
|
||||
` call void @scr_weak_dispose(ptr %o)`,
|
||||
` br label %sf.block`,
|
||||
`sf.block:`,
|
||||
` call void @scr_rt_free(ptr %o)`,
|
||||
];
|
||||
}
|
||||
@@ -356,11 +408,14 @@ function freeLines(host: AllocHost, cyc: boolean): string[] {
|
||||
host.declare(`declare void @scr_rt_free(ptr)`);
|
||||
declareGlobal(host, `@scr_weak_dispose_hook = external global ptr`);
|
||||
const lines = [
|
||||
...observedLines("sf", observers),
|
||||
` br i1 %sf.obs, label %sf.hooked, label %sf.block`,
|
||||
`sf.hooked:`,
|
||||
` %sf.hook = load ptr, ptr @scr_weak_dispose_hook`,
|
||||
` %sf.has = icmp ne ptr %sf.hook, null`,
|
||||
` %sf.hasx = call i1 @llvm.expect.i1(i1 %sf.has, i1 false)`,
|
||||
` br i1 %sf.hasx, label %sf.observe, label %sf.block`,
|
||||
`sf.observe: ; weak collections observe the disposal`,
|
||||
`sf.observe: ; a weak collection or a kept record source observes the disposal`,
|
||||
` call void %sf.hook(ptr %o)`,
|
||||
` br label %sf.block`,
|
||||
`sf.block:`,
|
||||
|
||||
@@ -1106,6 +1106,10 @@ export class LlDyn {
|
||||
private readonly jsonSchemas = new Map<string, string>();
|
||||
private readonly jsonParsers = new Map<string, string>();
|
||||
private readonly recordNameTables = new Map<string, { names: string; lens: string }>();
|
||||
/** Some emitted helper can keep a record's undeclared source: a checked
|
||||
* record builder that notes it, or a JSON schema of a closed record
|
||||
* (alloc.ts RECORD_SOURCES_GATE). */
|
||||
keepsRecordSources = false;
|
||||
|
||||
/** Targets the one-pass parser (scr_json_parse_schema) can build
|
||||
* directly: number, boolean and string leaves inside arrays and records
|
||||
@@ -1221,6 +1225,7 @@ export class LlDyn {
|
||||
const index = new Map(shape.fields.map((f, i) => [f.name, i + base]));
|
||||
// The runtime names kept-source keys by this index (record-sources.ts).
|
||||
const order = recordSourceFieldOrder(shape);
|
||||
if (recordMayKeepSource(shape)) this.keepsRecordSources = true;
|
||||
const fieldTy = `{ ptr, ${S}, ${S}, ptr }`;
|
||||
const entries = order.map((fieldName) => {
|
||||
const field = shape.fields[index.get(fieldName)! - base]!;
|
||||
@@ -1981,6 +1986,7 @@ export class LlDyn {
|
||||
// that has them stays beside the record so its reflective
|
||||
// surfaces still observe them (scr_record_source_note).
|
||||
if (recordMayKeepSource(shape)) {
|
||||
this.keepsRecordSources = true;
|
||||
const names = this.recordNameTable(t.shapeId, shape);
|
||||
host.declare(
|
||||
`declare void @scr_record_source_note(ptr, ptr, ptr, ptr, ${host.sizeType})`,
|
||||
|
||||
@@ -2,7 +2,14 @@ import { type ArraySearchKind, emitArraySearchHelpers } from "./array-search.js"
|
||||
import { emitStringInputs } from "./string-lifetimes.js";
|
||||
import { emitLlvmLayouts } from "./layouts.js";
|
||||
import { compileRegexMatcher, type RegexMatcherCode } from "./regex-matchers.js";
|
||||
import { CYC_BUF, CYC_HDR, FIRST_EMITTED_CYC_KIND, inlineAllocSupported } from "./alloc.js";
|
||||
import {
|
||||
CYC_BUF,
|
||||
CYC_HDR,
|
||||
FIRST_EMITTED_CYC_KIND,
|
||||
inlineAllocSupported,
|
||||
RECORD_SOURCES_GATE,
|
||||
WEAK_KEYS_GATE,
|
||||
} from "./alloc.js";
|
||||
import { hasLoneSurrogate, llvmBytes as llBytes, wtf8Bytes } from "../literals.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { createHash } from "node:crypto";
|
||||
@@ -1964,7 +1971,10 @@ export class LlEmitter {
|
||||
}
|
||||
|
||||
emitParts(): string[] {
|
||||
const parts = limitInlineDynReleases(this.emitModuleParts());
|
||||
const parts = [
|
||||
...limitInlineDynReleases(this.emitModuleParts()),
|
||||
...this.disposeObserverGates(),
|
||||
];
|
||||
if (!this.executableTls) return parts;
|
||||
// A worker executable's thread-locals all live in the executable's own
|
||||
// TLS block: the program's internal ones, and the runtime's external
|
||||
@@ -1987,6 +1997,21 @@ export class LlEmitter {
|
||||
);
|
||||
}
|
||||
|
||||
/** The dispose-observer gates the emitted frees and record writers test
|
||||
* (alloc.ts): defined after every helper is emitted, because dynCheck
|
||||
* builders, JSON schemas and weak collection constructors are requested
|
||||
* lazily. */
|
||||
private disposeObserverGates(): string[] {
|
||||
const weak = [...this.decls].some(
|
||||
(d) => d.includes("@scr_weak_map_new(") || d.includes("@scr_weak_set_new("),
|
||||
);
|
||||
return [
|
||||
`${WEAK_KEYS_GATE} = internal constant i1 ${weak}`,
|
||||
`${RECORD_SOURCES_GATE} = internal constant i1 ${this.dyn.keepsRecordSources}`,
|
||||
``,
|
||||
];
|
||||
}
|
||||
|
||||
private emitModuleParts(): string[] {
|
||||
// Function bodies first (the literal/unit/fn-value tables fill as they
|
||||
// emit), then the file assembles around them — the runtime ABI’s order.
|
||||
|
||||
@@ -9,6 +9,7 @@ import { emitTestLibCall } from "./lib-test.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
|
||||
import { MAY_THROW_LIB_FNS } from "../../ir/builtin-effects.js";
|
||||
import type { IrExpr, IrType } from "../../ir/ir.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
import type { LlvmEmitterContext, LibCallExpr, LibCallPrefix, LlValue } from "./expr-context.js";
|
||||
import {
|
||||
@@ -21,6 +22,7 @@ import { isDynUndefined } from "../../ir/runtime-features.js";
|
||||
import { DYN_KIND } from "./dyn.js";
|
||||
import { rawBytes } from "./lib-abi.js";
|
||||
import { emitInlineAtomicsOp } from "./expr-bytes.js";
|
||||
import { regexLiteralCaptures } from "./regex-matchers.js";
|
||||
|
||||
export function emitAssertInspectLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
const B = host.B;
|
||||
@@ -642,8 +644,66 @@ function emitAssertFailing(
|
||||
* `a.name.localeCompare(b.name)` reads both fields without retaining). */
|
||||
const READ_ONLY_STRING_LIB_FNS: ReadonlySet<string> = new Set(["str.localeCompare"]);
|
||||
|
||||
/** The parameter count of a replace callback the runtime can call through
|
||||
* the closure ABI (scr_regex_replace_fn), or null when it needs the dyn
|
||||
* box. Each declared parameter must receive a value of its declared kind
|
||||
* in every match: the match string at 0; with more than one parameter the
|
||||
* pattern must be a literal whose captures are known, so position i is a
|
||||
* capture that every match sets (a string), the offset (a number) right
|
||||
* after the captures, or the subject (a string) after that. */
|
||||
function typedReplacerArity(callback: IrType, pattern: IrExpr): number | null {
|
||||
if (callback.kind !== "func" || callback.rest || callback.argumentsAll) return null;
|
||||
if (callback.ret.kind !== "string") return null;
|
||||
const params = callback.params;
|
||||
if (params.length > 4) return null;
|
||||
if (params.length === 0) return 0;
|
||||
if (params[0]!.kind !== "string") return null;
|
||||
if (params.length === 1) return 1;
|
||||
if (pattern.kind !== "regexLit") return null;
|
||||
const captures = regexLiteralCaptures(pattern.pattern, pattern.flags);
|
||||
if (!captures) return null;
|
||||
for (let i = 1; i < params.length; i++) {
|
||||
const kind = params[i]!.kind;
|
||||
const ok =
|
||||
i < captures.count
|
||||
? kind === "string" && captures.always[i] === true
|
||||
: i === captures.count
|
||||
? kind === "f64"
|
||||
: i === captures.count + 1 && kind === "string";
|
||||
if (!ok) return null;
|
||||
}
|
||||
return params.length;
|
||||
}
|
||||
|
||||
/** `s.replace(re, fn)` / `replaceAll` with a typed callback the runtime can
|
||||
* call directly: the dyn box the IR wraps it in is never observable (the
|
||||
* runtime only calls it, with `this` undefined), so the closure goes to
|
||||
* scr_regex_replace_fn as it is. Null keeps the generic path. */
|
||||
function emitTypedRegexReplace(host: LlvmEmitterContext, e: LibCallExpr): LlValue | null {
|
||||
const [subject, pattern, callback, all] = e.args;
|
||||
if (!subject || !pattern || callback?.kind !== "dynFrom" || all?.kind !== "boolLit") return null;
|
||||
const arity = typedReplacerArity(callback.value.type, pattern);
|
||||
if (arity === null) return null;
|
||||
const B = host.B;
|
||||
const s = host.emitExpr(subject);
|
||||
const re = host.emitExpr(pattern);
|
||||
const fn = host.emitExpr(callback.value);
|
||||
host.declare(`declare ptr @scr_regex_replace_fn(ptr, ptr, ptr, i32, i1 zeroext)`);
|
||||
const result = B.tmp();
|
||||
B.line(
|
||||
`${result} = call ptr @scr_regex_replace_fn(ptr ${s.name}, ptr ${re.name}, ptr ${fn.name}, i32 ${arity}, i1 zeroext ${all.value})`,
|
||||
);
|
||||
const output = host.own({ name: result, type: e.type });
|
||||
host.emitPendingCheck();
|
||||
return output;
|
||||
}
|
||||
|
||||
export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
if (USES_TIMERS_LIB_FNS.has(e.fn)) host.usesTimers = true;
|
||||
if (e.fn === "regex.replaceCallback") {
|
||||
const typed = emitTypedRegexReplace(host, e);
|
||||
if (typed) return typed;
|
||||
}
|
||||
if (e.fn === "process.nextTick") return host.emitAsyncContextLibCall(e);
|
||||
if (
|
||||
e.fn === "sp.pipeline" ||
|
||||
@@ -774,6 +834,8 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
case "blob":
|
||||
case "record":
|
||||
case "disposable":
|
||||
case "punycode":
|
||||
case "constants":
|
||||
return host.emitGenericLibCall(e);
|
||||
case "test":
|
||||
return emitTestLibCall(host, e);
|
||||
|
||||
@@ -144,6 +144,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"num.toFixed0": "scr_num_to_fixed0",
|
||||
"num.toFixed": "scr_num_to_fixed",
|
||||
"num.toStringRadix": "scr_num_to_string_radix",
|
||||
"num.toStringRadixF64": "scr_num_to_string_radix_f64",
|
||||
"num.sameValue": "scr_num_same_value",
|
||||
"intl.numFormatEnUs": "scr_intl_num_format_en_us",
|
||||
"number.isFinite": "scr_num_is_finite",
|
||||
@@ -194,6 +195,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"os.cpusModel": "scr_os_cpus_model",
|
||||
"os.cpusField": "scr_os_cpus_field",
|
||||
"os.constants": "scr_os_constants",
|
||||
"constants.object": "scr_node_constants",
|
||||
"os.release": "scr_os_release",
|
||||
"os.userName": "scr_os_user_name",
|
||||
"os.userShell": "scr_os_user_shell",
|
||||
@@ -638,6 +640,13 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"url.legacyFormat": "scr_url_legacy_format",
|
||||
"url.legacyResolve": "scr_url_legacy_resolve",
|
||||
"url.domainToASCII": "scr_url_domain_to_ascii",
|
||||
"punycode.decode": "scr_punycode_decode",
|
||||
"punycode.encode": "scr_punycode_encode",
|
||||
"punycode.toASCII": "scr_punycode_to_ascii",
|
||||
"punycode.toUnicode": "scr_punycode_to_unicode",
|
||||
"punycode.ucs2Decode": "scr_punycode_ucs2_decode",
|
||||
"punycode.ucs2Encode": "scr_str_from_code_point",
|
||||
"punycode.warn": "scr_punycode_warn",
|
||||
"url.domainToUnicode": "scr_url_domain_to_unicode",
|
||||
"url.pathToFileURL": "scr_url_from_path",
|
||||
"url.pathToFileURLPlatform": "scr_url_from_path_platform",
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, test } from "vitest";
|
||||
import {
|
||||
compileRegexMatcher,
|
||||
regexLiteralBailReason,
|
||||
regexLiteralCaptures,
|
||||
regexLiteralCompiles,
|
||||
} from "./regex-matchers.js";
|
||||
|
||||
@@ -11,6 +12,30 @@ function body(pattern: string, flags = "", vector = true): string {
|
||||
return code.lines.join("\n");
|
||||
}
|
||||
|
||||
describe("regex literal captures", () => {
|
||||
test("counts captures with the whole match and marks the ones every match sets", () => {
|
||||
expect(regexLiteralCaptures("[&<>]", "g")).toEqual({ count: 1, always: [true] });
|
||||
expect(regexLiteralCaptures("(\\w+)=(\\d)+", "")).toEqual({
|
||||
count: 3,
|
||||
always: [true, true, true],
|
||||
});
|
||||
expect(regexLiteralCaptures("(?<k>a)(b)?|(c)", "")).toEqual({
|
||||
count: 4,
|
||||
always: [true, false, false, false],
|
||||
});
|
||||
expect(regexLiteralCaptures("x(?:(a)|(b))y(c)*(d){0,2}(e){1,3}", "")).toEqual({
|
||||
count: 6,
|
||||
always: [true, false, false, false, false, true],
|
||||
});
|
||||
});
|
||||
|
||||
test("answers null outside the parsed subset", () => {
|
||||
expect(regexLiteralCaptures("(?=a)(b)", "")).toBeNull();
|
||||
expect(regexLiteralCaptures("(a)\\1", "")).toBeNull();
|
||||
expect(regexLiteralCaptures("a", "v")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("regex literal matcher subset", () => {
|
||||
test.each([
|
||||
['^(\\d+\\.\\d+\\.\\d+\\.\\d+) - - \\[([^\\]]+)\\] "(\\w+) ([^ ?"]+)(?:\\?([^ "]*))?$', ""],
|
||||
|
||||
@@ -1639,6 +1639,40 @@ function parseSubset(pattern: string, flags: string): { root: RxNode; ncap: numb
|
||||
}
|
||||
}
|
||||
|
||||
/** The capture count of a regex literal (the whole match included) and,
|
||||
* per capture, whether every match sets it: no alternation of several
|
||||
* branches and no repetition that can run zero times encloses it. Null
|
||||
* when the pattern is outside the parsed subset. */
|
||||
export function regexLiteralCaptures(
|
||||
pattern: string,
|
||||
flags: string,
|
||||
): { count: number; always: boolean[] } | null {
|
||||
const parsed = parseSubset(pattern, flags);
|
||||
if (!parsed) return null;
|
||||
const always = Array.from({ length: parsed.ncap }, (_, i) => i === 0);
|
||||
const walk = (node: RxNode, sure: boolean): void => {
|
||||
switch (node.kind) {
|
||||
case "cap":
|
||||
always[node.index] = sure;
|
||||
walk(node.body, sure);
|
||||
return;
|
||||
case "seq":
|
||||
for (const item of node.items) walk(item, sure);
|
||||
return;
|
||||
case "alt":
|
||||
for (const alt of node.alts) walk(alt, sure && node.alts.length === 1);
|
||||
return;
|
||||
case "rep":
|
||||
walk(node.body, sure && node.min > 0);
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
};
|
||||
walk(parsed.root, true);
|
||||
return { count: parsed.ncap, always };
|
||||
}
|
||||
|
||||
/** Whether a literal compiles to a native matcher (tests and size caps). */
|
||||
export function regexLiteralCompiles(pattern: string, flags: string): boolean {
|
||||
return parseSubset(pattern, flags) !== null;
|
||||
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
stackUnionBox,
|
||||
type InlineUnionFields,
|
||||
} from "./inline-unions.js";
|
||||
import { recordMayKeepSource } from "./record-sources.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
|
||||
/** What the tables need from the emitter: the extern-declaration ledger
|
||||
@@ -1524,7 +1525,9 @@ export function emitRecordShapes(
|
||||
}
|
||||
t++;
|
||||
}
|
||||
freeBody.push(...emitObjectFree(host, traced));
|
||||
// Closed layouts can carry a kept record source (record-sources.ts).
|
||||
const observers = recordMayKeepSource(shape) ? "weak-or-source" : "weak";
|
||||
freeBody.push(...emitObjectFree(host, traced, observers));
|
||||
defs.push(
|
||||
...releaseBody(
|
||||
host,
|
||||
@@ -1639,7 +1642,7 @@ export function emitRecordShapes(
|
||||
` call void ${releaseSym(host, m.type)}(ptr %v${i}) ; ${llvmCommentText(m.name)} (acyclic)`,
|
||||
);
|
||||
});
|
||||
gf.push(...emitObjectFree(host, true), ` ret void`, `}`, ``);
|
||||
gf.push(...emitObjectFree(host, true, observers), ` ret void`, `}`, ``);
|
||||
defs.push(...gf);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
traceAdapter,
|
||||
type ShapeHost,
|
||||
} from "./shapes.js";
|
||||
import { disposeHookLoadLines, RECORD_SOURCES_GATE } from "./alloc.js";
|
||||
import { recordMayKeepSource, recordSourceFieldOrder } from "./record-sources.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
import { emitUnionPeek, emitUnionTag } from "./union-repr.js";
|
||||
@@ -397,6 +398,24 @@ export class LlWalkers {
|
||||
const writer = this.recordFieldWriter(shapeId);
|
||||
const lSource = B.newLabel("jw.source");
|
||||
const lNative = B.newLabel("jw.native");
|
||||
// Only a program that can keep record sources looks one up (the gate
|
||||
// folds to a constant), and only once one was kept: keeping a source
|
||||
// installs the dispose hook, so a NULL hook means the table is empty.
|
||||
const lHooked = B.newLabel("jw.hooked");
|
||||
const kept = B.tmp();
|
||||
B.line(`${kept} = load i1, ptr ${RECORD_SOURCES_GATE}`);
|
||||
B.condBr(kept, lHooked, lNative);
|
||||
B.startBlock(lHooked);
|
||||
const hook = B.tmp();
|
||||
const hookLines = disposeHookLoadLines(host, hook);
|
||||
if (hookLines !== null) {
|
||||
const lProbe = B.newLabel("jw.probe");
|
||||
for (const line of hookLines) B.line(line.trimStart());
|
||||
const installed = B.tmp();
|
||||
B.line(`${installed} = icmp ne ptr ${hook}, null`);
|
||||
B.condBr(installed, lProbe, lNative);
|
||||
B.startBlock(lProbe);
|
||||
}
|
||||
if (traceAdapter(host, { kind: "record", shapeId }) === null) {
|
||||
// One lookup: the runtime writes the record when it kept a source.
|
||||
host.declare(`declare zeroext i1 @scr_jb_try_record_source(ptr, ptr, ptr)`);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -67,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";
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
@@ -42,6 +42,8 @@ export const SUPPORTED_BUILTIN_MODULES = [
|
||||
"module",
|
||||
"console",
|
||||
"process",
|
||||
"punycode",
|
||||
"constants",
|
||||
] as const;
|
||||
|
||||
/** Builtins Node itself serves ONLY under the node: prefix —
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { type Lowerer } from "../lowerer.js";
|
||||
import { type ClassInfo } from "../lower-classes.js";
|
||||
import { isJsSourceFile } from "../../program.js";
|
||||
import { builtinMemberRequireDecl } from "./module-bindings.js";
|
||||
import { stripTypeCasts } from "./arguments.js";
|
||||
import { streamClassAliasDecl } from "../lower-stream.js";
|
||||
import { zlibClassName, zlibStreamModeOf } from "./compression.js";
|
||||
import { stdlibGlobalNameOf } from "../surfaces.js";
|
||||
|
||||
/** A builtin class an expression names at compile time: the error
|
||||
* classes, node:events EventEmitter, the node:stream classes (each a
|
||||
* runtime ClassInfo), the node:zlib stream classes (by node_zlib_mode),
|
||||
* and the Map and Set constructors. `name` is the class's `name`
|
||||
* property. */
|
||||
export type BuiltinClassRef =
|
||||
| { kind: "error" | "emitter" | "stream"; info: ClassInfo; name: string }
|
||||
| { kind: "zlib"; mode: number; name: string }
|
||||
| { kind: "collection"; which: "Map" | "Set"; name: string };
|
||||
|
||||
/** The builtin class `expr` names: a direct spelling (`EventEmitter`,
|
||||
* `stream.Readable`, `zlib.Gzip`, `Map`, `globalThis.TypeError`) or a
|
||||
* `const` binding of one (`const E = EventEmitter`, an alias of an alias,
|
||||
* an imported alias), through parentheses and type assertions. Null for
|
||||
* anything else. */
|
||||
export function builtinClassRefOf(lowerer: Lowerer, expr: ts.Expression): BuiltinClassRef | null {
|
||||
const e = stripTypeCasts(expr);
|
||||
// Checker lookups only: answering must not run the fences and deferred
|
||||
// diagnostics of a lowered reference (resolveValueSymbol).
|
||||
let symbol: ts.Symbol | undefined;
|
||||
if (ts.isIdentifier(e)) {
|
||||
symbol = lowerer.checker.getSymbolAtLocation(e);
|
||||
} else if (ts.isPropertyAccessExpression(e) && !e.questionDotToken) {
|
||||
symbol = lowerer.checker.getSymbolAtLocation(e.name);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
if (!symbol) return null;
|
||||
const alias = builtinClassAliasOf(lowerer, symbol);
|
||||
if (alias !== null) return alias;
|
||||
if (symbol.flags & ts.SymbolFlags.Alias) symbol = lowerer.checker.getAliasedSymbol(symbol);
|
||||
const which = stdlibGlobalNameOf(lowerer, e);
|
||||
if (which === "Map" || which === "Set") return { kind: "collection", which, name: which };
|
||||
const error = lowerer.builtinErrorInfoOf(symbol);
|
||||
if (error) return { kind: "error", info: error, name: runtimeClassName(error) };
|
||||
const emitter = lowerer.builtinEmitterInfoOf(symbol);
|
||||
if (emitter) return { kind: "emitter", info: emitter, name: runtimeClassName(emitter) };
|
||||
const stream = lowerer.builtinStreamInfoOf(symbol);
|
||||
if (stream) return { kind: "stream", info: stream, name: runtimeClassName(stream) };
|
||||
const mode = zlibStreamModeOf(lowerer, symbol);
|
||||
if (mode !== null) return { kind: "zlib", mode, name: zlibClassName(mode) };
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The builtin class a `const` binding names (`const E = EventEmitter`,
|
||||
* `const M = Map`, `const C = zlib.Gzip`, or an alias of such a binding),
|
||||
* or null. `let` and `var` bindings could be reassigned, and annotated
|
||||
* bindings give the value another type, so they are not aliases. Resolved from the declaration, so uses lowered before the
|
||||
* declaration statement (hoisted functions) see the same answer. */
|
||||
export function builtinClassAliasOf(lowerer: Lowerer, symbol: ts.Symbol): BuiltinClassRef | null {
|
||||
const cached = lowerer.builtinClassAliases.get(symbol);
|
||||
if (cached !== undefined) return cached;
|
||||
// An import of an exported alias names the exporting binding.
|
||||
if (symbol.flags & ts.SymbolFlags.Alias) {
|
||||
lowerer.builtinClassAliases.set(symbol, null);
|
||||
const target = lowerer.checker.getAliasedSymbol(symbol);
|
||||
const ref = target === symbol ? null : builtinClassAliasOf(lowerer, target);
|
||||
lowerer.builtinClassAliases.set(symbol, ref);
|
||||
return ref;
|
||||
}
|
||||
const init = constInitializerOf(lowerer, symbol);
|
||||
if (init === null) {
|
||||
lowerer.builtinClassAliases.set(symbol, null);
|
||||
return null;
|
||||
}
|
||||
// Marked first: a cycle through other bindings answers null.
|
||||
lowerer.builtinClassAliases.set(symbol, null);
|
||||
const ref = builtinClassRefOf(lowerer, init);
|
||||
lowerer.builtinClassAliases.set(symbol, ref);
|
||||
return ref;
|
||||
}
|
||||
|
||||
/** True when `symbol` is a builtin class alias whose declaration owns no
|
||||
* runtime slot: new, instanceof, typeof, .name and === lower at each use,
|
||||
* and other uses report SC1090. In JavaScript files, a binding whose
|
||||
* initializer has a value (`const E = EventEmitter`, `const M =
|
||||
* globalThis.Map`, both identity tokens) keeps its slot; only builtin
|
||||
* module members (`zlib.Gzip`) and copies of slotless aliases own none.
|
||||
* The `require("events").EventEmitter` and `stream.Writable` spellings
|
||||
* keep their own declaration handling. */
|
||||
export function builtinClassAliasOwnsNoSlot(lowerer: Lowerer, symbol: ts.Symbol): boolean {
|
||||
if (builtinClassAliasOf(lowerer, symbol) === null) return false;
|
||||
const binding =
|
||||
symbol.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(symbol) : symbol;
|
||||
const decl = lowerer.checker.valueDeclarationOf(binding);
|
||||
if (decl === undefined || !ts.isVariableDeclaration(decl)) return false;
|
||||
if (builtinMemberRequireDecl(decl.name, decl.initializer)) return false;
|
||||
if (streamClassAliasDecl(lowerer, decl.name, decl.initializer)) return false;
|
||||
if (!isJsSourceFile(decl.getSourceFile())) return true;
|
||||
let init = decl.initializer;
|
||||
while (init !== undefined && ts.isParenthesizedExpression(init)) init = init.expression;
|
||||
if (init === undefined) return true;
|
||||
let source: ts.Symbol | undefined;
|
||||
if (ts.isPropertyAccessExpression(init)) {
|
||||
if (lowerer.builtinNamespaceModuleOf(init.expression) !== null) return true;
|
||||
source = lowerer.checker.getSymbolAtLocation(init.name);
|
||||
} else {
|
||||
source = lowerer.checker.getSymbolAtLocation(init);
|
||||
}
|
||||
return source !== undefined && builtinClassAliasOwnsNoSlot(lowerer, source);
|
||||
}
|
||||
|
||||
/** True for a `const` declaration that is a builtin class alias owning no
|
||||
* runtime slot (see builtinClassAliasOwnsNoSlot): both declaration walks
|
||||
* skip it. */
|
||||
export function registerBuiltinClassAlias(
|
||||
lowerer: Lowerer,
|
||||
nameNode: ts.Node,
|
||||
init: ts.Expression | undefined,
|
||||
): boolean {
|
||||
if (!ts.isIdentifier(nameNode) || init === undefined) return false;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(nameNode);
|
||||
return symbol !== undefined && builtinClassAliasOwnsNoSlot(lowerer, symbol);
|
||||
}
|
||||
|
||||
/** True when two references name the same builtin class. */
|
||||
export function sameBuiltinClass(a: BuiltinClassRef, b: BuiltinClassRef): boolean {
|
||||
if (a.kind === "zlib") return b.kind === "zlib" && a.mode === b.mode;
|
||||
if (a.kind === "collection") return b.kind === "collection" && a.which === b.which;
|
||||
return b.kind === a.kind && b.info === a.info;
|
||||
}
|
||||
|
||||
/** The initializer of an unannotated `const` binding whose initializer
|
||||
* can name a class (an identifier or a property access), or null. */
|
||||
function constInitializerOf(lowerer: Lowerer, symbol: ts.Symbol): ts.Expression | null {
|
||||
if ((symbol.flags & ts.SymbolFlags.BlockScopedVariable) === 0) return null;
|
||||
const decl = lowerer.checker.valueDeclarationOf(symbol);
|
||||
if (
|
||||
decl === undefined ||
|
||||
!ts.isVariableDeclaration(decl) ||
|
||||
!ts.isIdentifier(decl.name) ||
|
||||
decl.type !== undefined
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const list = decl.parent;
|
||||
if (!ts.isVariableDeclarationList(list) || (list.flags & ts.NodeFlags.Const) === 0) return null;
|
||||
let init = decl.initializer;
|
||||
while (init !== undefined && ts.isParenthesizedExpression(init)) init = init.expression;
|
||||
if (init === undefined) return null;
|
||||
if (ts.isIdentifier(init)) return init;
|
||||
if (ts.isPropertyAccessExpression(init) && !init.questionDotToken) return init;
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Node's constructor name of a runtime class (`%TypeError` → TypeError). */
|
||||
function runtimeClassName(info: ClassInfo): string {
|
||||
return info.def.name.startsWith("%") ? info.def.name.slice(1) : info.def.name;
|
||||
}
|
||||
@@ -132,8 +132,8 @@ export function zlibStreamModeOf(
|
||||
symbol: ts.Symbol | null | undefined,
|
||||
): number | null {
|
||||
if (!symbol) return null;
|
||||
const aliasMode = zlibClassAliasModeOf(lowerer, symbol, 0);
|
||||
if (aliasMode !== undefined) return aliasMode;
|
||||
const alias = lowerer.builtinClassAliasOf(symbol);
|
||||
if (alias !== null) return alias.kind === "zlib" ? alias.mode : null;
|
||||
const target =
|
||||
symbol.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(symbol) : symbol;
|
||||
const mode = ZLIB_STREAM_MODES.get(target.name);
|
||||
@@ -499,80 +499,12 @@ export function lowerZlibModuleCall(
|
||||
throw new InternalCompilerError(`unhandled lowered zlib member ${bi.member}`);
|
||||
}
|
||||
|
||||
/** The zlib stream class an expression names (`zlib.Gzip`, an imported
|
||||
* `Gzip`, a `const C = zlib.Gzip` alias), as its node_zlib_mode, or null. */
|
||||
export function zlibClassModeOfExpr(lowerer: Lowerer, expr: ts.Expression): number | null {
|
||||
let e = expr;
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
if (ts.isIdentifier(e)) return zlibStreamModeOf(lowerer, lowerer.checker.getSymbolAtLocation(e));
|
||||
if (ts.isPropertyAccessExpression(e) && !e.questionDotToken) {
|
||||
return zlibStreamModeOf(lowerer, lowerer.checker.getSymbolAtLocation(e.name));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The class name of a zlib stream mode (Node's constructor name). */
|
||||
export function zlibClassName(mode: number): string {
|
||||
for (const [name, m] of ZLIB_STREAM_MODES) if (m === mode) return name;
|
||||
return "Zlib";
|
||||
}
|
||||
|
||||
/** The mode of a `const C = zlib.Gzip` binding (or an alias of such a
|
||||
* binding), null when the symbol is a const binding of something else,
|
||||
* undefined when it is not a const binding. Resolved from the declaration,
|
||||
* so uses lowered before the declaration statement see the same answer. */
|
||||
function zlibClassAliasModeOf(
|
||||
lowerer: Lowerer,
|
||||
symbol: ts.Symbol,
|
||||
depth: number,
|
||||
): number | null | undefined {
|
||||
const cached = lowerer.zlibClassAliases.get(symbol);
|
||||
if (cached !== undefined) return cached;
|
||||
if ((symbol.flags & ts.SymbolFlags.BlockScopedVariable) === 0) return undefined;
|
||||
const decl = lowerer.checker.valueDeclarationOf(symbol);
|
||||
if (decl === undefined || !ts.isVariableDeclaration(decl) || !ts.isIdentifier(decl.name)) {
|
||||
return undefined;
|
||||
}
|
||||
// const only: a var or let binding could be reassigned
|
||||
const list = decl.parent;
|
||||
if (!ts.isVariableDeclarationList(list) || (list.flags & ts.NodeFlags.Const) === 0) {
|
||||
return undefined;
|
||||
}
|
||||
let init = decl.initializer;
|
||||
while (init !== undefined && ts.isParenthesizedExpression(init)) init = init.expression;
|
||||
if (init === undefined || depth > 16) return null;
|
||||
let mode: number | null = null;
|
||||
if (ts.isIdentifier(init)) {
|
||||
const next = lowerer.checker.getSymbolAtLocation(init);
|
||||
if (next !== undefined) {
|
||||
const nested = zlibClassAliasModeOf(lowerer, next, depth + 1);
|
||||
mode = nested === undefined ? zlibStreamModeOf(lowerer, next) : nested;
|
||||
}
|
||||
} else if (ts.isPropertyAccessExpression(init) && !init.questionDotToken) {
|
||||
mode = zlibStreamModeOf(lowerer, lowerer.checker.getSymbolAtLocation(init.name));
|
||||
}
|
||||
if (mode !== null) lowerer.zlibClassAliases.set(symbol, mode);
|
||||
return mode;
|
||||
}
|
||||
|
||||
/** True for `const C = zlib.Gzip` (or an alias of an alias): a
|
||||
* compile-time zlib class alias that owns no runtime slot, so the
|
||||
* statement lowering skips it. */
|
||||
export function registerZlibClassAlias(
|
||||
lowerer: Lowerer,
|
||||
nameNode: ts.Node,
|
||||
init: ts.Expression | undefined,
|
||||
): boolean {
|
||||
if (!ts.isIdentifier(nameNode) || init === undefined) return false;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(nameNode);
|
||||
return symbol !== undefined && zlibClassAliasOf(lowerer, symbol) !== null;
|
||||
}
|
||||
|
||||
/** The mode of a `const C = zlib.Gzip` alias symbol, or null. */
|
||||
export function zlibClassAliasOf(lowerer: Lowerer, symbol: ts.Symbol): number | null {
|
||||
return zlibClassAliasModeOf(lowerer, symbol, 0) ?? null;
|
||||
}
|
||||
|
||||
/** `value instanceof zlib.Gzip` (any spelling of the class, aliases
|
||||
* included). zlib streams are runtime Transforms: a Transform operand asks
|
||||
* the runtime for the stream's mode, and an operand typed as one of
|
||||
|
||||
@@ -503,6 +503,24 @@ export function requireExactArityValue(
|
||||
? lowerer.mapTypeOf(lowerer.typeOf(contextual))
|
||||
: null;
|
||||
if (mapped && typeEquals(mapped, funcType)) return;
|
||||
// An object-literal METHOD has no contextual type of its own: the
|
||||
// literal's contextual type names the member slot it fills. An
|
||||
// overloaded member's slot holds its erased function, which spells the
|
||||
// completed parameters of an implementation like `m(a?: string)`.
|
||||
if (
|
||||
contextual === null &&
|
||||
ts.isMethodDeclaration(blame) &&
|
||||
blame.parent !== undefined &&
|
||||
ts.isObjectLiteralExpression(blame.parent) &&
|
||||
(ts.isIdentifier(blame.name) || ts.isStringLiteral(blame.name))
|
||||
) {
|
||||
const literalType = lowerer.checker.getContextualType(blame.parent);
|
||||
const member = literalType
|
||||
? lowerer.checker.getPropertyOfType(literalType, blame.name.text)
|
||||
: undefined;
|
||||
const slot = member ? lowerer.memberSlotTypeOf(lowerer.checker.getTypeOfSymbol(member)) : null;
|
||||
if (slot && typeEquals(slot, funcType)) return;
|
||||
}
|
||||
// A union-typed slot (`runner || defaultRunner` under a
|
||||
// `CommandRunner | undefined` context): the value can only inhabit
|
||||
// the union's one func arm — judge by it.
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
builtinModulesArrayLit,
|
||||
stdlibGlobalNameOf,
|
||||
} from "./surfaces.js";
|
||||
import { CRYPTO_CONSTANTS } from "./crypto-tables.js";
|
||||
|
||||
const PROTOTYPE_METHODS: Record<string, readonly string[]> = {
|
||||
Date: ["getTime", "valueOf", "toISOString", "toJSON"],
|
||||
@@ -680,3 +681,34 @@ export function lowerBuiltinLoaderValue(lowerer: Lowerer, loc: SrcLoc): IrExpr {
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** The deprecated node:constants module object: the runtime merges
|
||||
* os.constants' groups and fs.constants with crypto.constants, which a
|
||||
* lifted function answers from the baked table the first time the object
|
||||
* is built. */
|
||||
export function builtinConstantsObject(lowerer: Lowerer, loc: SrcLoc): IrExpr {
|
||||
const name = "%builtin.constants.crypto";
|
||||
if (!lowerer.builtinCallableValueFns.has(name)) {
|
||||
lowerer.builtinCallableValueFns.set(name, name);
|
||||
const fields = Object.entries(CRYPTO_CONSTANTS).map(([key, value]) => ({
|
||||
key: strLit(key, loc),
|
||||
value: box(builtinConstLit(value, loc)),
|
||||
}));
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params: [],
|
||||
returnType: DYN,
|
||||
locals: [],
|
||||
body: [{ kind: "return", value: { kind: "dynObjLit", fields, type: DYN, loc }, loc }],
|
||||
loc,
|
||||
});
|
||||
}
|
||||
const table: IrExpr = {
|
||||
kind: "closure",
|
||||
fnName: name,
|
||||
captures: [],
|
||||
type: { kind: "func", params: [], ret: DYN },
|
||||
loc,
|
||||
};
|
||||
return { kind: "libCall", fn: "constants.object", args: [box(table)], type: DYN, loc };
|
||||
}
|
||||
|
||||
@@ -61,7 +61,11 @@ import { lowerProtocolMethodCall } from "./class-protocol-dispatch.js";
|
||||
import { isJsSourceFile, locOf, requireSpecOf } from "../program.js";
|
||||
import { lowerJsonRequireCall } from "./json-require.js";
|
||||
import { lowerAssertInstanceCall } from "./assert-instances.js";
|
||||
import { isGenericCallableMemberType, typeKey } from "../type-mapper.js";
|
||||
import {
|
||||
isGenericCallableMemberType,
|
||||
isOverloadedCallableMemberType,
|
||||
typeKey,
|
||||
} from "../type-mapper.js";
|
||||
import { dynFallbackType, jsFuncNameOf, newFnCtx, staticImportNamespaceType } from "./lowerer.js";
|
||||
import {
|
||||
NARROW_FIRST,
|
||||
@@ -4188,10 +4192,32 @@ function lowerNumberToStringCall(
|
||||
}
|
||||
if (call.arguments.length === 1) {
|
||||
if (operand.type.kind !== "f64") return null;
|
||||
const arg = call.arguments[0]!;
|
||||
// A number radix needs no box: ToIntegerOrInfinity of a number is a
|
||||
// truncation, which the typed entry point applies itself.
|
||||
if (!ts.isSpreadElement(arg) && lowerer.mapTypeOf(lowerer.typeOf(arg))?.kind === "f64") {
|
||||
const radix = lowerer.lowerExpr(arg);
|
||||
if (radix.type.kind === "f64") {
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "num.toStringRadixF64",
|
||||
args: [operand, radix],
|
||||
type: STRING,
|
||||
loc: locOf(call),
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "num.toStringRadix",
|
||||
args: [operand, lowerer.coerceToExpected(radix, DYN)],
|
||||
type: STRING,
|
||||
loc: locOf(call),
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "num.toStringRadix",
|
||||
args: [operand, lowerer.lowerExprExpecting(call.arguments[0]!, DYN)],
|
||||
args: [operand, lowerer.lowerExprExpecting(arg, DYN)],
|
||||
type: STRING,
|
||||
loc: locOf(call),
|
||||
};
|
||||
@@ -8203,6 +8229,30 @@ function lowerObjectAssignHybrid(lowerer: Lowerer, call: ts.CallExpression): IrE
|
||||
return { kind: "recordLit", fields, type: mapped, loc };
|
||||
}
|
||||
|
||||
/** The accessed member is an overload set (isOverloadedCallableMemberType),
|
||||
* also under an optional chain whose access still spells `| undefined`. */
|
||||
function isOverloadedMemberAccess(lowerer: Lowerer, access: ts.PropertyAccessExpression): boolean {
|
||||
return isOverloadedCallableMemberType(
|
||||
lowerer.checker.getNonNullableType(lowerer.typeOf(access)),
|
||||
lowerer.checker,
|
||||
);
|
||||
}
|
||||
|
||||
/** The result type of the overload a call through an overloaded member
|
||||
* resolved to: the erased slot's result converts to it. A non-generic
|
||||
* overload answers through its declaration, because the resolved signature
|
||||
* of an optional-chain call (`api?.m(x)`) also carries the chain's
|
||||
* undefined; a generic one answers through its instantiation. */
|
||||
function resolvedOverloadResultType(lowerer: Lowerer, call: ts.CallExpression): IrType {
|
||||
const { checker } = lowerer;
|
||||
const signature = checker.getResolvedSignature(call);
|
||||
const decl = signature ? checker.signatureDeclaration(signature) : undefined;
|
||||
const declared = decl ? checker.getSignatureFromDeclaration(decl) : undefined;
|
||||
const source = declared && declared.getTypeParameters().length === 0 ? declared : signature;
|
||||
const result = source ? lowerer.mapTypeOf(checker.getReturnTypeOfSignature(source)) : null;
|
||||
return result ?? lowerer.irTypeOf(call);
|
||||
}
|
||||
|
||||
export function lowerRecordFieldCall(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
@@ -8234,7 +8284,18 @@ export function lowerRecordFieldCall(
|
||||
const callee = lowerer.lowerUnionProperty(access);
|
||||
if (callee?.type.kind !== "func") return null;
|
||||
const args = completeFuncValueArgs(lowerer, call, callee.type, locOf(call));
|
||||
return { kind: "callValue", callee, args, type: callee.type.ret, loc: locOf(call) };
|
||||
const value: IrExpr = {
|
||||
kind: "callValue",
|
||||
callee,
|
||||
args,
|
||||
type: callee.type.ret,
|
||||
loc: locOf(call),
|
||||
};
|
||||
// An overloaded member's erased result converts to the resolved
|
||||
// overload's type, as on a single record receiver below.
|
||||
return isOverloadedMemberAccess(lowerer, access)
|
||||
? lowerer.coerceInto(call, value, resolvedOverloadResultType(lowerer, call))
|
||||
: value;
|
||||
}
|
||||
const target = lowerer.fieldTarget(access);
|
||||
const loc = locOf(call);
|
||||
@@ -8309,10 +8370,14 @@ export function lowerRecordFieldCall(
|
||||
type: callee.type.ret,
|
||||
loc,
|
||||
};
|
||||
// A generic member's field holds its erased instance: the result
|
||||
// converts to this call's instantiated return type at the call.
|
||||
if (isGenericCallableMemberType(lowerer.typeOf(access), lowerer.checker))
|
||||
// A generic member's field holds its erased instance, and an overloaded
|
||||
// member's field its erased function (the overloads joined): the result
|
||||
// converts to this call's instantiated or resolved return type.
|
||||
const memberType = lowerer.typeOf(access);
|
||||
if (isGenericCallableMemberType(memberType, lowerer.checker))
|
||||
result = lowerer.coerceInto(call, result, lowerer.irTypeOf(call));
|
||||
else if (isOverloadedMemberAccess(lowerer, access))
|
||||
result = lowerer.coerceInto(call, result, resolvedOverloadResultType(lowerer, call));
|
||||
return init ? { kind: "seqExpr", stmts: [init], result, type: result.type, loc } : result;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
@@ -924,12 +929,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
|
||||
@@ -1284,12 +1292,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
|
||||
@@ -1330,12 +1342,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;
|
||||
}
|
||||
@@ -4359,6 +4376,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) {
|
||||
@@ -4366,8 +4384,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 [];
|
||||
@@ -4504,6 +4525,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 =
|
||||
@@ -4539,10 +4561,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);
|
||||
}
|
||||
}
|
||||
@@ -7616,6 +7639,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({
|
||||
@@ -7630,10 +7654,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);
|
||||
}
|
||||
}
|
||||
@@ -8435,6 +8460,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);
|
||||
@@ -8729,7 +8760,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).
|
||||
@@ -9094,7 +9128,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) &&
|
||||
@@ -9226,7 +9261,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
|
||||
}
|
||||
// `new Set<T>()`: construct + bulk add from supported values,
|
||||
// including live collection cursors. Duplicates collapse in insertion order.
|
||||
if (symbol?.name === "Set" && lowerer.isStdlibSymbol(symbol)) {
|
||||
if (ts.isIdentifier(identifier) && lowerer.isStdlibGlobal(identifier, "Set")) {
|
||||
const tsType = lowerer.typeOf(expr);
|
||||
let mapped = lowerer.mapTypeOf(tsType);
|
||||
const fieldType = assignedThisFieldType(lowerer, expr);
|
||||
|
||||
@@ -35,10 +35,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";
|
||||
@@ -228,6 +230,7 @@ import {
|
||||
} from "./class-prototypes.js";
|
||||
import { isClassCallback } from "./class-callbacks.js";
|
||||
import {
|
||||
builtinConstantsObject,
|
||||
builtinPrototypeMethod,
|
||||
lowerArrayIsArrayValue,
|
||||
lowerCheckedPredicateValue,
|
||||
@@ -616,8 +619,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)) {
|
||||
@@ -902,13 +906,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)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1253,6 +1257,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.
|
||||
@@ -1295,6 +1310,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 };
|
||||
@@ -1584,6 +1600,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 };
|
||||
@@ -1849,10 +1866,11 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
const table = lowerInternTableProperty(lowerer, expr);
|
||||
if (table) return table;
|
||||
}
|
||||
// `zlib.Gzip.name`, `C.name` over a zlib class alias.
|
||||
// `EventEmitter.name`, `zlib.Gzip.name`, `C.name` over a builtin
|
||||
// class alias.
|
||||
if (expr.name.text === "name" && !expr.questionDotToken) {
|
||||
const mode = zlibClassModeOfExpr(lowerer, expr.expression);
|
||||
if (mode !== null) return { kind: "strLit", value: zlibClassName(mode), type: STRING, loc };
|
||||
const ref = builtinClassRefOf(lowerer, expr.expression);
|
||||
if (ref !== null) return { kind: "strLit", value: ref.name, type: STRING, loc };
|
||||
}
|
||||
// Expando statics may still be absent even when JS inference sees
|
||||
// the eventual assignment's scalar type. Preserve the checked value.
|
||||
@@ -10266,17 +10284,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 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12352,11 +12371,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);
|
||||
|
||||
@@ -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,7 @@ 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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -142,6 +142,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";
|
||||
@@ -230,9 +231,11 @@ import {
|
||||
containsUnion,
|
||||
describeComponentBlocker,
|
||||
describeRecordMemberBlocker,
|
||||
erasedOverloadMemberType,
|
||||
formatIrType,
|
||||
FETCH_AMBIENT_TYPES,
|
||||
isOsConstantsSymbol,
|
||||
isOverloadedCallableMemberType,
|
||||
isUnitOnlyTsType,
|
||||
isWorkerHandleType,
|
||||
jsOpenObjectType,
|
||||
@@ -261,6 +264,7 @@ import {
|
||||
stdlibGlobalMember,
|
||||
nodeTypesOnlySymbol,
|
||||
} from "./surfaces.js";
|
||||
import { type BuiltinClassRef, builtinClassAliasOf } from "./builtins/class-values.js";
|
||||
import {
|
||||
type FileParts,
|
||||
splitFiles,
|
||||
@@ -1858,11 +1862,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<ts.Symbol, ts.Type> | 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<ts.Symbol, number>();
|
||||
/** 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<ts.Symbol, BuiltinClassRef | null>();
|
||||
/** 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). */
|
||||
@@ -3097,6 +3101,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}`;
|
||||
@@ -3920,6 +3928,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
|
||||
@@ -3929,6 +3996,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
|
||||
@@ -7218,23 +7286,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 ?? [];
|
||||
@@ -7262,6 +7315,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. */
|
||||
@@ -7703,6 +7804,15 @@ export class Lowerer {
|
||||
: mapped;
|
||||
}
|
||||
|
||||
/** The runtime type of a record member slot typed `t`: an overloaded
|
||||
* member's erased function (erasedOverloadMemberType — the type a record
|
||||
* shape stores for it), otherwise mapTypeOf. */
|
||||
memberSlotTypeOf(t: ts.Type): IrType | null {
|
||||
if (isOverloadedCallableMemberType(t, this.checker))
|
||||
return erasedOverloadMemberType(t, this.typeCtx) ?? null;
|
||||
return this.mapTypeOf(t);
|
||||
}
|
||||
|
||||
/** The one position where a contextual UNION must not be adopted over the
|
||||
* expression's own: the LEFT operand of `&&`, `||`, or `??`. Adopting a
|
||||
* contextual union is normally safe because tsc proved the value
|
||||
@@ -9896,27 +10006,21 @@ export class Lowerer {
|
||||
* 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
|
||||
* class is unmappable for the query and its diagnostics are dropped;
|
||||
* lowering the class's own declaration collects it again and reports
|
||||
* them if the program reaches it. */
|
||||
* 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;
|
||||
}
|
||||
const sink: ScrDiagnostic[] = [];
|
||||
const previousSink = this.diagSink;
|
||||
this.diagSink = sink;
|
||||
try {
|
||||
this.lowerClassExpressionInfo(decl);
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (e instanceof PoisonError) return false;
|
||||
throw e;
|
||||
} finally {
|
||||
this.diagSink = previousSink;
|
||||
}
|
||||
for (const d of sink) this.pushDiag(d);
|
||||
return true;
|
||||
}
|
||||
|
||||
lowerClassExpression(expr: ts.ClassLikeDeclaration): IrExpr {
|
||||
@@ -11735,7 +11839,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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -10,6 +10,7 @@ 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);
|
||||
@@ -257,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 {
|
||||
@@ -269,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)
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { npmStaticIneligibleReason } from "./npm-static.js";
|
||||
import { npmStaticIneligibleReason, npmStaticTransformPkgJson } from "./npm-static.js";
|
||||
|
||||
test("an unreadable runtime entry is an eligibility refusal", async () => {
|
||||
const dir = await mkdtemp("/tmp/scriptc-npm-static-unreadable-");
|
||||
@@ -14,3 +14,25 @@ test("an unreadable runtime entry is an eligibility refusal", async () => {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("a hoisted node condition leaves no types claim behind", () => {
|
||||
// uuid 14: the node object carries its own "types" key. Left in place,
|
||||
// tsgo maps the hidden declaration to its same-stem browser build.
|
||||
const pkg: Record<string, unknown> = {
|
||||
types: "./dist/index.d.ts",
|
||||
exports: {
|
||||
".": {
|
||||
node: { types: "./dist/index.d.ts", default: "./dist-node/index.js" },
|
||||
default: "./dist/index.js",
|
||||
},
|
||||
"./package.json": "./package.json",
|
||||
},
|
||||
};
|
||||
npmStaticTransformPkgJson(pkg);
|
||||
expect(pkg).toEqual({
|
||||
exports: {
|
||||
".": { default: "./dist-node/index.js" },
|
||||
"./package.json": "./package.json",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -157,8 +157,6 @@ export function npmStaticTransformPkgJson(pkg: Record<string, unknown>): void {
|
||||
return;
|
||||
}
|
||||
const obj = v as Record<string, unknown>;
|
||||
delete obj["types"];
|
||||
delete obj["typings"];
|
||||
// 0. The "node" condition HOISTS: Node's runtime resolution matches it
|
||||
// (yaml's `{ types, node: "./dist/index.js", default: "./browser/
|
||||
// index.js" }` runs the node dist; the browser dist is a DIFFERENT
|
||||
@@ -187,6 +185,14 @@ export function npmStaticTransformPkgJson(pkg: Record<string, unknown>): void {
|
||||
}
|
||||
}
|
||||
}
|
||||
// 1. The types claims go AFTER the hoist: a node object's own
|
||||
// "types" key (uuid's `node: { types: "./dist/index.d.ts", default:
|
||||
// "./dist-node/index.js" }`) splices in with its siblings, and left
|
||||
// in place tsgo follows it, maps the hidden declaration to its
|
||||
// same-stem JS (dist/index.js, the browser build) and disagrees with
|
||||
// this resolver's runtime answer.
|
||||
delete obj["types"];
|
||||
delete obj["typings"];
|
||||
for (const value of Object.values(obj)) transform(value);
|
||||
};
|
||||
if (pkg["exports"] !== undefined) transform(pkg["exports"]);
|
||||
@@ -208,6 +214,13 @@ function npmStaticTransformPkgJsonText(text: string): string {
|
||||
}
|
||||
if (doc === null || typeof doc !== "object" || Array.isArray(doc)) return text;
|
||||
npmStaticTransformPkgJson(doc as Record<string, unknown>);
|
||||
// tsgo deduplicates installs by package id (name + version): a second
|
||||
// copy of the same name@version (express/node_modules/fresh beside
|
||||
// send/node_modules/fresh) becomes a redirect to the first copy loaded,
|
||||
// so its own path never joins the program. Node loads every installed
|
||||
// copy as its own module, and resolve.ts answers each importer's own
|
||||
// copy; without a version the checker keeps the copies apart too.
|
||||
delete (doc as Record<string, unknown>)["version"];
|
||||
return JSON.stringify(doc);
|
||||
}
|
||||
|
||||
@@ -275,6 +288,20 @@ function packageIsUntyped(path: string): boolean {
|
||||
return untyped;
|
||||
}
|
||||
|
||||
/** An installed TypeScript source with no runtime JavaScript at its stem
|
||||
* (tldts ships `index.ts` beside `main: "dist/cjs/index.js"`). Node never
|
||||
* executes TypeScript under node_modules and resolve.ts answers only
|
||||
* runtime JS for opted-in packages, yet tsgo's TypeScript-first lookup
|
||||
* would take the package-root `index.ts` over `main`. A same-stem JS twin
|
||||
* keeps the source visible: that pair is the shape npmStaticProgramDep
|
||||
* accepts in the JavaScript's place. */
|
||||
function isUnloadableInstalledTsSource(path: string): boolean {
|
||||
if (!/\.(ts|tsx|mts|cts)$/.test(path) || isDeclarationFileName(path)) return false;
|
||||
if (!isNodeModulesPathNorm(path)) return false;
|
||||
const stem = path.replace(/\.(ts|tsx|mts|cts)$/, "");
|
||||
return ![".js", ".jsx", ".mjs", ".cjs"].some((ext) => trackedExists(stem + ext));
|
||||
}
|
||||
|
||||
export interface NpmStaticFsShadow {
|
||||
/** Shadowed CONTENT for a real path (the types-stripped package.json),
|
||||
* or undefined (no shadow — fall through). */
|
||||
@@ -383,7 +410,7 @@ export function npmStaticFsShadow(services: FrontendServices): NpmStaticFsShadow
|
||||
const target = shadowTargetOf(path);
|
||||
if (!target) return false;
|
||||
if (target.viaTypes) return true; // the @types twin hides whole
|
||||
return isDeclarationFileName(path);
|
||||
return isDeclarationFileName(path) || isUnloadableInstalledTsSource(path);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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<string>,
|
||||
sites?: Map<string, SrcLoc>,
|
||||
transitive: Set<string> = 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<string, { typesFile: string; fromFile: string; spec: string }>();
|
||||
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<string>();
|
||||
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<Record<string, string>>,
|
||||
libraryNpmStatic: readonly string[] = [],
|
||||
transitive: Set<string> = 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();
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -635,9 +640,14 @@ export function entryPackageFilePredicate(entryPath: string): (file: string) =>
|
||||
}
|
||||
|
||||
/** maxNodeModuleJsDepth can omit relative imports within an admitted
|
||||
* package. Discover its runtime sources to a fixpoint so the checker and
|
||||
* module resolver see the same graph regardless of import traversal order. */
|
||||
function entryPackageProgramRoots7(program: ts.Program, entryPath: string): string[] {
|
||||
* package, and opted-in packages deep in the dependency graph. Discover
|
||||
* their runtime sources to a fixpoint so the checker and module resolver
|
||||
* see the same graph regardless of import traversal order or depth. */
|
||||
function entryPackageProgramRoots7(
|
||||
program: ts.Program,
|
||||
entryPath: string,
|
||||
rootNpmStaticEdges: boolean,
|
||||
): string[] {
|
||||
const belongsToEntry = entryPackageFilePredicate(entryPath);
|
||||
const roots = new Set<string>();
|
||||
for (const sf of program.getImplementationSourceFiles()) {
|
||||
@@ -656,15 +666,63 @@ function entryPackageProgramRoots7(program: ts.Program, entryPath: string): stri
|
||||
program.getSourceFile(target) === undefined
|
||||
)
|
||||
roots.add(target);
|
||||
// An opted-in package's bare edge counts against the same depth
|
||||
// budget: tsgo charges every node_modules JS hop, so express's
|
||||
// body-parser → qs → side-channel → get-intrinsic chain runs out of
|
||||
// depth before its leaves load. Root the runtime file this edge
|
||||
// lowers to (preflight's own resolution and miss rule) so the leaf
|
||||
// joins like a shallow one; complete programs add nothing. Only
|
||||
// the shadowed lowering world does this (the authored world checks
|
||||
// user code against declarations), and only from package files: a
|
||||
// program file's own edges are never deeper than one hop.
|
||||
// (resolveBareModule directly: resolveNpmImport7's workspace
|
||||
// registration stays with preflight's own walk.)
|
||||
if (
|
||||
target === null &&
|
||||
rootNpmStaticEdges &&
|
||||
npmStaticPackageOfPath(sf.fileName) !== null &&
|
||||
!isRelativeSpecifier(specifier.text)
|
||||
) {
|
||||
const spec = specifier.text;
|
||||
if (spec.startsWith("#") || canonicalBuiltinModule(spec) !== null) continue;
|
||||
const npm = resolveBareModule(
|
||||
sf.fileName,
|
||||
spec,
|
||||
undefined,
|
||||
moduleSpecifierKind7(specifier),
|
||||
);
|
||||
if (
|
||||
npm !== null &&
|
||||
isNpmStaticPackage(npm.packageName) &&
|
||||
(isNodeModulesPath(npm.typesFile) || npm.workspaceDir !== undefined) &&
|
||||
isJsSourceFileName(npm.typesFile) &&
|
||||
npmStaticProgramFile(program, npm.typesFile) === null
|
||||
)
|
||||
roots.add(tsgoPath(resolve(npm.typesFile)));
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...roots].sort();
|
||||
}
|
||||
|
||||
/** "require" for a CommonJS edge (`require("x")`, `import x = require("x")`),
|
||||
* "import" for every ESM form — the resolution kind preflight uses. */
|
||||
function moduleSpecifierKind7(specifier: ts.StringLiteralLike): "import" | "require" {
|
||||
const parent = specifier.parent;
|
||||
if (ts.isExternalModuleReference(parent)) return "require";
|
||||
return ts.isCallExpression(parent) &&
|
||||
ts.isIdentifier(parent.expression) &&
|
||||
parent.expression.text === "require"
|
||||
? "require"
|
||||
: "import";
|
||||
}
|
||||
|
||||
function loadProgram7(
|
||||
host: ts.Ts7Host,
|
||||
entryPath: string,
|
||||
externalTypes: ReadonlyMap<string, string> = new Map(),
|
||||
/** The --npm-static lowering world: root opted-in packages' deep edges. */
|
||||
rootNpmStaticEdges = false,
|
||||
): LoadResult & { disposeAll: () => void } {
|
||||
const config = adoptProjectConfig7(host, entryPath);
|
||||
const configuredPaths = config.options["paths"];
|
||||
@@ -712,7 +770,7 @@ function loadProgram7(
|
||||
try {
|
||||
for (let pass = 0; pass < 32; pass++) {
|
||||
const candidates = [
|
||||
...entryPackageProgramRoots7(program, entryPath),
|
||||
...entryPackageProgramRoots7(program, entryPath, rootNpmStaticEdges),
|
||||
...createRequireProgramRoots7(program),
|
||||
...forkTargetPaths(program, program.getImplementationSourceFiles()),
|
||||
...workerTargetPaths(program, program.getImplementationSourceFiles()),
|
||||
@@ -926,7 +984,7 @@ export function loadProgram(
|
||||
};
|
||||
const host = services.createProgramHost({ cwd: dirname(entryPath), fsShadow });
|
||||
try {
|
||||
const load = loadProgram7(host, entryPath, externalTypes);
|
||||
const load = loadProgram7(host, entryPath, externalTypes, npmStaticPackages.length > 0);
|
||||
let authoredHost: ts.Ts7Host | null = null;
|
||||
let authoredLoad: ReturnType<typeof loadProgram7> | null = null;
|
||||
return {
|
||||
@@ -2146,7 +2204,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 +2887,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) &&
|
||||
@@ -4006,6 +4065,24 @@ function locOf7(node: ts.Node): { file: string; start: number; end: number } {
|
||||
return { file: sf.fileName, start: node.getStart(sf), end: node.getEnd() };
|
||||
}
|
||||
|
||||
/** The program file holding an opted-in package's resolved runtime JS:
|
||||
* the file itself, or its same-stem TypeScript twin. */
|
||||
function npmStaticProgramFile(program: ts.Program, resolvedFile: string): ts.SourceFile | null {
|
||||
let dep = program.getSourceFile(resolvedFile) ?? null;
|
||||
if (dep === null) {
|
||||
// A workspace-linked package shipping its .ts SOURCE beside the
|
||||
// compiled .js: the checker's resolution preferred the source (it
|
||||
// outranks the sibling .js under bundler resolution), so the program
|
||||
// holds the .ts twin at the same stem — the same module, in its
|
||||
// richer spelling.
|
||||
for (const ext of [".ts", ".tsx", ".mts", ".cts"]) {
|
||||
dep = program.getSourceFile(resolvedFile.replace(/\.(js|mjs|cjs)$/, ext)) ?? null;
|
||||
if (dep !== null) break;
|
||||
}
|
||||
}
|
||||
return dep;
|
||||
}
|
||||
|
||||
/** The PROGRAM source file an opted-in --npm-static package's resolved
|
||||
* entry maps to, or null with the package marked an offender (resolution
|
||||
* still answering declarations, or the file missing from the type-checked
|
||||
@@ -4022,18 +4099,7 @@ function npmStaticProgramDep(
|
||||
);
|
||||
return null;
|
||||
}
|
||||
let dep = program.getSourceFile(resolvedFile) ?? null;
|
||||
if (dep === null) {
|
||||
// A workspace-linked package shipping its .ts SOURCE beside the
|
||||
// compiled .js: the checker's resolution preferred the source (it
|
||||
// outranks the sibling .js under bundler resolution), so the program
|
||||
// holds the .ts twin at the same stem — the same module, in its
|
||||
// richer spelling.
|
||||
for (const ext of [".ts", ".tsx", ".mts", ".cts"]) {
|
||||
dep = program.getSourceFile(resolvedFile.replace(/\.(js|mjs|cjs)$/, ext)) ?? null;
|
||||
if (dep !== null) break;
|
||||
}
|
||||
}
|
||||
const dep = npmStaticProgramFile(program, resolvedFile);
|
||||
if (dep === null) {
|
||||
reportNpmStaticOffender(
|
||||
packageName,
|
||||
@@ -4305,21 +4371,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
|
||||
@@ -4444,8 +4530,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)) {
|
||||
@@ -5714,42 +5799,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,
|
||||
@@ -5757,11 +5806,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;
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
BOOL,
|
||||
bytesOf,
|
||||
canConvertToDyn,
|
||||
canDynCheckTo,
|
||||
CHILD_T,
|
||||
CRYPTOHASH_T,
|
||||
CRYPTOHMAC_T,
|
||||
@@ -1727,16 +1728,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;
|
||||
}
|
||||
@@ -3918,158 +3941,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
if (hybrid) return hybrid;
|
||||
}
|
||||
const callSigs = checker.getCallSignatures(widened);
|
||||
if (callSigs.length === 1) {
|
||||
const sig = callSigs[0]!;
|
||||
if (sig.getTypeParameters().length && ctx.erasingSignature !== sig) return null;
|
||||
// A SYNTHESIZED rest param (tsc's JS inference for a function body
|
||||
// reading `arguments` — `(...args: any[]) => any` whose args symbol
|
||||
// has no declaration): the dotDotDot check below can't see it, and a
|
||||
// fixed-arity func mapping would LIE about the value's calling
|
||||
// convention. Detected by the param-count mismatch against the
|
||||
// signature's own declaration, excluding an erased `this` parameter.
|
||||
{
|
||||
const sigDecl = checker.signatureDeclaration(sig);
|
||||
const declParams =
|
||||
sigDecl !== undefined && ts.isFunctionLike(sigDecl) ? sigDecl.parameters : undefined;
|
||||
if (
|
||||
declParams !== undefined &&
|
||||
runtimeParameterCount(declParams) !== sig.getParameters().length
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
// tsgo never SYNTHESIZES that rest param into the inferred signature
|
||||
// (5.9.3 did — the count mismatch above was the whole detector
|
||||
// there), so the declaration's own body answers directly.
|
||||
if (sigDecl !== undefined && ts.isFunctionLike(sigDecl) && bodyReadsArguments(sigDecl)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
const params: IrType[] = [];
|
||||
let typedRest = false;
|
||||
for (const p of sig.getParameters()) {
|
||||
const decl = checker.valueDeclarationOf(p);
|
||||
const rest = decl !== undefined && ts.isParameter(decl) && decl.dotDotDotToken !== undefined;
|
||||
|
||||
// arity ABI selected by lambdaSignature. Treating an inferred any[]
|
||||
// declaration as a typed packed-array closure would bypass its boxed
|
||||
// spread/error semantics and change coverage classification.
|
||||
if (rest && decl !== undefined && isJsSourceFile(decl.getSourceFile())) return null;
|
||||
const optional =
|
||||
decl !== undefined &&
|
||||
ts.isParameter(decl) &&
|
||||
(decl.questionToken !== undefined || decl.initializer !== undefined);
|
||||
let pt = mapType(checker.getTypeOfSymbol(p), ctx);
|
||||
if (
|
||||
decl &&
|
||||
ts.isParameter(decl) &&
|
||||
ts.isSetAccessorDeclaration(decl.parent) &&
|
||||
isJsSourceFile(decl.getSourceFile())
|
||||
)
|
||||
pt = DYN;
|
||||
// An inferred JavaScript binding pattern consumes an iterable; it
|
||||
// does not require the caller's array to have exactly this length.
|
||||
const executor = decl && ts.isParameter(decl) ? decl.parent : null;
|
||||
const construction = executor?.parent;
|
||||
const constructorSymbol =
|
||||
construction && ts.isNewExpression(construction)
|
||||
? checker.getSymbolAtLocation(construction.expression)
|
||||
: undefined;
|
||||
const nativeExecutor =
|
||||
executor &&
|
||||
(ts.isArrowFunction(executor) || ts.isFunctionExpression(executor)) &&
|
||||
construction &&
|
||||
ts.isNewExpression(construction) &&
|
||||
construction.arguments?.[0] === executor &&
|
||||
ts.isIdentifier(construction.expression) &&
|
||||
construction.expression.text === "Promise" &&
|
||||
constructorSymbol &&
|
||||
checker
|
||||
.declarationsOf(constructorSymbol)
|
||||
.some((site) => ctx.isStdlibFile(site.getSourceFile()));
|
||||
if (
|
||||
!nativeExecutor &&
|
||||
decl &&
|
||||
ts.isParameter(decl) &&
|
||||
!decl.dotDotDotToken &&
|
||||
!ts.isSetAccessorDeclaration(decl.parent) &&
|
||||
isJsSourceFile(decl.getSourceFile()) &&
|
||||
!decl.type &&
|
||||
!/@(?:param|type)\b/.test(
|
||||
decl.getSourceFile().text.slice(decl.parent?.pos ?? decl.pos, decl.getStart()),
|
||||
)
|
||||
)
|
||||
pt = DYN;
|
||||
if (pt) pt = jsOpenObjectType(decl, pt, ctx.shapes, ctx.unions);
|
||||
// Belt and braces for non-strict type worlds: an optional param's
|
||||
// ABI slot is always the undefined-armed union (strictNullChecks
|
||||
// already spells it that way; arm it here if the world didn't).
|
||||
if (optional && pt && pt.kind !== "void" && pt.kind !== "dyn") {
|
||||
const armed = withUndefinedArm(pt, ctx.unions);
|
||||
if (!armed) return null;
|
||||
pt = armed;
|
||||
}
|
||||
if (!pt) return null;
|
||||
if (rest) {
|
||||
if (pt.kind !== "array") return null;
|
||||
typedRest = true;
|
||||
}
|
||||
// `(value: void) => void` (Promise<void>'s resolve) is callable with
|
||||
// no arguments — a void param is dropped, not a mapping failure.
|
||||
if (pt.kind === "void") continue;
|
||||
params.push(pt);
|
||||
}
|
||||
const retT = checker.getReturnTypeOfSignature(sig);
|
||||
// `() => never` (a throw-only lambda's inferred type) is assignable to
|
||||
// `() => void` and its calls never produce a value — map the return
|
||||
// like declaredReturnType does for declarations. JS null/undefined
|
||||
// returns can read mutable fields, so callable values use the same
|
||||
// checked-native return ABI as their implementations.
|
||||
const sigDecl = checker.signatureDeclaration(sig);
|
||||
const jsUnitReturn =
|
||||
sigDecl !== undefined &&
|
||||
isJsSourceFile(sigDecl.getSourceFile()) &&
|
||||
(retT.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) !== 0;
|
||||
let ret =
|
||||
jsUnitReturn || (retT.flags & ts.TypeFlags.Any) !== 0
|
||||
? DYN
|
||||
: retT.flags & ts.TypeFlags.Never
|
||||
? VOID
|
||||
: mapType(retT, ctx);
|
||||
if (sigDecl && ts.isGetAccessorDeclaration(sigDecl) && isJsSourceFile(sigDecl.getSourceFile()))
|
||||
ret = DYN;
|
||||
if (sigDecl && isJsSourceFile(sigDecl.getSourceFile()) && ret?.kind === "array") ret = DYN;
|
||||
if (
|
||||
sigDecl &&
|
||||
ts.isFunctionLike(sigDecl) &&
|
||||
isJsSourceFile(sigDecl.getSourceFile()) &&
|
||||
!sigDecl.type &&
|
||||
!/@returns?\b/.test(sigDecl.getSourceFile().text.slice(sigDecl.pos, sigDecl.getStart())) &&
|
||||
ret?.kind === "record"
|
||||
)
|
||||
ret = DYN;
|
||||
if (
|
||||
sigDecl &&
|
||||
ts.isFunctionLike(sigDecl) &&
|
||||
isJsSourceFile(sigDecl.getSourceFile()) &&
|
||||
!sigDecl.type &&
|
||||
!/@returns?\b/.test(sigDecl.getSourceFile().text.slice(sigDecl.pos, sigDecl.getStart())) &&
|
||||
(ret?.kind === "date" ||
|
||||
(ret?.kind === "union" &&
|
||||
ctx.unions.get(ret.unionId)?.arms.some((arm) => arm.kind === "func")))
|
||||
)
|
||||
ret = DYN;
|
||||
if (
|
||||
ret &&
|
||||
sigDecl &&
|
||||
isJsSourceFile(sigDecl.getSourceFile()) &&
|
||||
functionCanReturnUndefined(sigDecl)
|
||||
)
|
||||
ret = withUndefinedArm(ret, ctx.unions) ?? ret;
|
||||
if (!ret) return null;
|
||||
return typedRest
|
||||
? { kind: "func", params, ret, rest: true, restAbi: "typed" }
|
||||
: funcOf(params, ret);
|
||||
}
|
||||
if (callSigs.length === 1) return mapCallSignature(callSigs[0]!, ctx);
|
||||
// Records: object types whose members are all data properties (shorthand
|
||||
// methods in type position count — they're func-typed fields) with
|
||||
// mappable types; no call/construct signatures, no index signatures, not
|
||||
@@ -4343,6 +4215,162 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** One non-overloaded call signature's func type (mapTypeInner's
|
||||
* single-signature rule; erasedOverloadMemberType maps each overload of a
|
||||
* member through it). */
|
||||
function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null {
|
||||
const { checker } = ctx;
|
||||
if (sig.getTypeParameters().length && ctx.erasingSignature !== sig) return null;
|
||||
// A SYNTHESIZED rest param (tsc's JS inference for a function body
|
||||
// reading `arguments` — `(...args: any[]) => any` whose args symbol
|
||||
// has no declaration): the dotDotDot check below can't see it, and a
|
||||
// fixed-arity func mapping would LIE about the value's calling
|
||||
// convention. Detected by the param-count mismatch against the
|
||||
// signature's own declaration, excluding an erased `this` parameter.
|
||||
{
|
||||
const sigDecl = checker.signatureDeclaration(sig);
|
||||
const declParams =
|
||||
sigDecl !== undefined && ts.isFunctionLike(sigDecl) ? sigDecl.parameters : undefined;
|
||||
if (
|
||||
declParams !== undefined &&
|
||||
runtimeParameterCount(declParams) !== sig.getParameters().length
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
// tsgo never SYNTHESIZES that rest param into the inferred signature
|
||||
// (5.9.3 did — the count mismatch above was the whole detector
|
||||
// there), so the declaration's own body answers directly.
|
||||
if (sigDecl !== undefined && ts.isFunctionLike(sigDecl) && bodyReadsArguments(sigDecl)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
const params: IrType[] = [];
|
||||
let typedRest = false;
|
||||
for (const p of sig.getParameters()) {
|
||||
const decl = checker.valueDeclarationOf(p);
|
||||
const rest = decl !== undefined && ts.isParameter(decl) && decl.dotDotDotToken !== undefined;
|
||||
|
||||
// arity ABI selected by lambdaSignature. Treating an inferred any[]
|
||||
// declaration as a typed packed-array closure would bypass its boxed
|
||||
// spread/error semantics and change coverage classification.
|
||||
if (rest && decl !== undefined && isJsSourceFile(decl.getSourceFile())) return null;
|
||||
const optional =
|
||||
decl !== undefined &&
|
||||
ts.isParameter(decl) &&
|
||||
(decl.questionToken !== undefined || decl.initializer !== undefined);
|
||||
let pt = mapType(checker.getTypeOfSymbol(p), ctx);
|
||||
if (
|
||||
decl &&
|
||||
ts.isParameter(decl) &&
|
||||
ts.isSetAccessorDeclaration(decl.parent) &&
|
||||
isJsSourceFile(decl.getSourceFile())
|
||||
)
|
||||
pt = DYN;
|
||||
// An inferred JavaScript binding pattern consumes an iterable; it
|
||||
// does not require the caller's array to have exactly this length.
|
||||
const executor = decl && ts.isParameter(decl) ? decl.parent : null;
|
||||
const construction = executor?.parent;
|
||||
const constructorSymbol =
|
||||
construction && ts.isNewExpression(construction)
|
||||
? checker.getSymbolAtLocation(construction.expression)
|
||||
: undefined;
|
||||
const nativeExecutor =
|
||||
executor &&
|
||||
(ts.isArrowFunction(executor) || ts.isFunctionExpression(executor)) &&
|
||||
construction &&
|
||||
ts.isNewExpression(construction) &&
|
||||
construction.arguments?.[0] === executor &&
|
||||
ts.isIdentifier(construction.expression) &&
|
||||
construction.expression.text === "Promise" &&
|
||||
constructorSymbol &&
|
||||
checker
|
||||
.declarationsOf(constructorSymbol)
|
||||
.some((site) => ctx.isStdlibFile(site.getSourceFile()));
|
||||
if (
|
||||
!nativeExecutor &&
|
||||
decl &&
|
||||
ts.isParameter(decl) &&
|
||||
!decl.dotDotDotToken &&
|
||||
!ts.isSetAccessorDeclaration(decl.parent) &&
|
||||
isJsSourceFile(decl.getSourceFile()) &&
|
||||
!decl.type &&
|
||||
!/@(?:param|type)\b/.test(
|
||||
decl.getSourceFile().text.slice(decl.parent?.pos ?? decl.pos, decl.getStart()),
|
||||
)
|
||||
)
|
||||
pt = DYN;
|
||||
if (pt) pt = jsOpenObjectType(decl, pt, ctx.shapes, ctx.unions);
|
||||
// Belt and braces for non-strict type worlds: an optional param's
|
||||
// ABI slot is always the undefined-armed union (strictNullChecks
|
||||
// already spells it that way; arm it here if the world didn't).
|
||||
if (optional && pt && pt.kind !== "void" && pt.kind !== "dyn") {
|
||||
const armed = withUndefinedArm(pt, ctx.unions);
|
||||
if (!armed) return null;
|
||||
pt = armed;
|
||||
}
|
||||
if (!pt) return null;
|
||||
if (rest) {
|
||||
if (pt.kind !== "array") return null;
|
||||
typedRest = true;
|
||||
}
|
||||
// `(value: void) => void` (Promise<void>'s resolve) is callable with
|
||||
// no arguments — a void param is dropped, not a mapping failure.
|
||||
if (pt.kind === "void") continue;
|
||||
params.push(pt);
|
||||
}
|
||||
const retT = checker.getReturnTypeOfSignature(sig);
|
||||
// `() => never` (a throw-only lambda's inferred type) is assignable to
|
||||
// `() => void` and its calls never produce a value — map the return
|
||||
// like declaredReturnType does for declarations. JS null/undefined
|
||||
// returns can read mutable fields, so callable values use the same
|
||||
// checked-native return ABI as their implementations.
|
||||
const sigDecl = checker.signatureDeclaration(sig);
|
||||
const jsUnitReturn =
|
||||
sigDecl !== undefined &&
|
||||
isJsSourceFile(sigDecl.getSourceFile()) &&
|
||||
(retT.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) !== 0;
|
||||
let ret =
|
||||
jsUnitReturn || (retT.flags & ts.TypeFlags.Any) !== 0
|
||||
? DYN
|
||||
: retT.flags & ts.TypeFlags.Never
|
||||
? VOID
|
||||
: mapType(retT, ctx);
|
||||
if (sigDecl && ts.isGetAccessorDeclaration(sigDecl) && isJsSourceFile(sigDecl.getSourceFile()))
|
||||
ret = DYN;
|
||||
if (sigDecl && isJsSourceFile(sigDecl.getSourceFile()) && ret?.kind === "array") ret = DYN;
|
||||
if (
|
||||
sigDecl &&
|
||||
ts.isFunctionLike(sigDecl) &&
|
||||
isJsSourceFile(sigDecl.getSourceFile()) &&
|
||||
!sigDecl.type &&
|
||||
!/@returns?\b/.test(sigDecl.getSourceFile().text.slice(sigDecl.pos, sigDecl.getStart())) &&
|
||||
ret?.kind === "record"
|
||||
)
|
||||
ret = DYN;
|
||||
if (
|
||||
sigDecl &&
|
||||
ts.isFunctionLike(sigDecl) &&
|
||||
isJsSourceFile(sigDecl.getSourceFile()) &&
|
||||
!sigDecl.type &&
|
||||
!/@returns?\b/.test(sigDecl.getSourceFile().text.slice(sigDecl.pos, sigDecl.getStart())) &&
|
||||
(ret?.kind === "date" ||
|
||||
(ret?.kind === "union" &&
|
||||
ctx.unions.get(ret.unionId)?.arms.some((arm) => arm.kind === "func")))
|
||||
)
|
||||
ret = DYN;
|
||||
if (
|
||||
ret &&
|
||||
sigDecl &&
|
||||
isJsSourceFile(sigDecl.getSourceFile()) &&
|
||||
functionCanReturnUndefined(sigDecl)
|
||||
)
|
||||
ret = withUndefinedArm(ret, ctx.unions) ?? ret;
|
||||
if (!ret) return null;
|
||||
return typedRest
|
||||
? { kind: "func", params, ret, rest: true, restAbi: "typed" }
|
||||
: funcOf(params, ret);
|
||||
}
|
||||
|
||||
/** Retain a literal discriminator before widening erases its values. A
|
||||
* structural match alone is insufficient: `{ kind: "loop", body: ... }`
|
||||
* also fits a smaller `{ kind: "empty" }` record. Choosing that layout
|
||||
@@ -5447,6 +5475,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<TypeMapperCtx, "isStdlibFile">): 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
|
||||
@@ -5485,13 +5524,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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5577,6 +5617,164 @@ export function erasedGenericMemberType(t: ts.Type, ctx: TypeMapperCtx): IrType
|
||||
return erased?.kind === "func" ? erased : undefined;
|
||||
}
|
||||
|
||||
/** An OVERLOADED callable member type (`on(event: "a", h: (x: number) =>
|
||||
* void): () => void; on(event: "b", h: (x: string) => void): () => void`
|
||||
* in a program interface or type literal): two or more call signatures and
|
||||
* nothing else (no data properties, construct signatures, or index
|
||||
* signatures), every one declared in TypeScript program source. Such a
|
||||
* member keeps a record slot holding its erased runtime function
|
||||
* (erasedOverloadMemberType); calls convert per their resolved overload
|
||||
* (lowerRecordFieldCall). */
|
||||
export function isOverloadedCallableMemberType(t: ts.Type, checker: ts.TypeChecker): boolean {
|
||||
const sigs = checker.getCallSignatures(t);
|
||||
if (sigs.length < 2) return false;
|
||||
if (
|
||||
checker.getConstructSignatures(t).length > 0 ||
|
||||
checker.getPropertiesOfType(t).length > 0 ||
|
||||
checker.getIndexInfosOfType(t).length > 0
|
||||
)
|
||||
return false;
|
||||
return sigs.every((sig) => {
|
||||
const decl = checker.signatureDeclaration(sig);
|
||||
if (decl === undefined) return false;
|
||||
if (!ts.isMethodSignatureDeclaration(decl) && !ts.isCallSignatureDeclaration(decl))
|
||||
return false;
|
||||
const file = decl.getSourceFile();
|
||||
return !file.isDeclarationFile && !isJsSourceFile(file);
|
||||
});
|
||||
}
|
||||
|
||||
/** Scalar arms an overload join may combine into one union. */
|
||||
const OVERLOAD_JOIN_SCALARS = new Set(["f64", "string", "bool", "undefinedT", "nullT"]);
|
||||
|
||||
/** The join of one position's types across an overload set: equal types
|
||||
* stay; scalar mixes form their union; anything else is `unknown` when
|
||||
* every joined type crosses the checked-dynamic boundary in both directions
|
||||
* (the call site converts into the slot, the implementation converts out
|
||||
* of it). Differing callbacks (`(x: number) => void` beside `(x: string) =>
|
||||
* void`) therefore travel as checked function values, which any
|
||||
* implementation's own callback type adapts from. Null when no join
|
||||
* exists. */
|
||||
function joinOverloadTypes(types: readonly IrType[], ctx: TypeMapperCtx): IrType | null {
|
||||
const first = types[0]!;
|
||||
if (types.every((t) => typeEquals(t, first))) return first;
|
||||
const getRecord = (id: string) => ctx.shapes.get(id);
|
||||
const getUnion = (id: string) => ctx.unions.get(id);
|
||||
const arms = new Map<string, IrType>();
|
||||
let scalar = true;
|
||||
for (const t of types) {
|
||||
const parts = t.kind === "union" ? (ctx.unions.get(t.unionId)?.arms ?? []) : [t];
|
||||
if (parts.length === 0) scalar = false;
|
||||
for (const part of parts) {
|
||||
if (!OVERLOAD_JOIN_SCALARS.has(part.kind)) scalar = false;
|
||||
arms.set(typeKey(part), part);
|
||||
}
|
||||
}
|
||||
if (scalar && [...arms.values()].some((arm) => !isUnitType(arm))) {
|
||||
const sorted = [...arms.values()].sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1));
|
||||
return { kind: "union", unionId: ctx.unions.intern(sorted) };
|
||||
}
|
||||
if (
|
||||
types.every(
|
||||
(t) =>
|
||||
t.kind === "dyn" ||
|
||||
(t.kind !== "void" &&
|
||||
canConvertToDyn(t, getRecord, getUnion) &&
|
||||
canDynCheckTo(t, getRecord, getUnion)),
|
||||
)
|
||||
)
|
||||
return DYN;
|
||||
return null;
|
||||
}
|
||||
|
||||
/** An overloaded member's runtime type (isOverloadedCallableMemberType).
|
||||
* JavaScript stores ONE function under the member; overload signatures only
|
||||
* constrain call sites. The slot's type joins the overloads per parameter
|
||||
* position and result (joinOverloadTypes), and a position some overloads
|
||||
* omit is optional. Each overload's own type parameters erase to `unknown`
|
||||
* first. Calls pick their overload statically, convert arguments into the
|
||||
* slot's parameter types and the result back to the overload's; an
|
||||
* implementation converts into the slot like any function value. Undefined
|
||||
* when the set does not join (rest parameters, a void result beside a
|
||||
* value result, a component outside the checked-dynamic boundary): the
|
||||
* member keeps its fence. */
|
||||
export function erasedOverloadMemberType(t: ts.Type, ctx: TypeMapperCtx): IrType | undefined {
|
||||
const funcs: (IrType & { kind: "func" })[] = [];
|
||||
for (const sig of ctx.checker.getCallSignatures(t)) {
|
||||
const own = new Set(sig.getTypeParameters());
|
||||
const outer = ctx.resolveTypeParam;
|
||||
const mapped = mapCallSignature(
|
||||
sig,
|
||||
own.size === 0
|
||||
? ctx
|
||||
: {
|
||||
...ctx,
|
||||
erasingSignature: sig,
|
||||
resolveTypeParam: (param) => (own.has(param) ? DYN : (outer?.(param) ?? null)),
|
||||
},
|
||||
);
|
||||
if (mapped?.kind !== "func" || mapped.rest === true || mapped.argumentsAll === true)
|
||||
return undefined;
|
||||
funcs.push(mapped);
|
||||
}
|
||||
const arity = Math.max(...funcs.map((f) => f.params.length));
|
||||
const params: IrType[] = [];
|
||||
for (let i = 0; i < arity; i++) {
|
||||
const present = funcs.filter((f) => i < f.params.length).map((f) => f.params[i]!);
|
||||
let param = joinOverloadTypes(present, ctx);
|
||||
if (param !== null && present.length < funcs.length && param.kind !== "dyn")
|
||||
param = withUndefinedArm(param, ctx.unions);
|
||||
if (param === null) return undefined;
|
||||
params.push(param);
|
||||
}
|
||||
const voids = funcs.filter((f) => f.ret.kind === "void").length;
|
||||
if (voids !== 0 && voids !== funcs.length) return undefined;
|
||||
const ret =
|
||||
voids === funcs.length
|
||||
? VOID
|
||||
: joinOverloadTypes(
|
||||
funcs.map((f) => f.ret),
|
||||
ctx,
|
||||
);
|
||||
if (ret === null) return undefined;
|
||||
return funcOf(params, ret);
|
||||
}
|
||||
|
||||
/** Why an overloaded member's set has no erased type
|
||||
* (erasedOverloadMemberType): the first overload, and within it the first
|
||||
* parameter or result, that does not map; otherwise the join itself. A
|
||||
* message tail after "its member 'name'". */
|
||||
function describeOverloadSetBlocker(t: ts.Type, ctx: TypeMapperCtx): string {
|
||||
const { checker } = ctx;
|
||||
for (const sig of checker.getCallSignatures(t)) {
|
||||
const own = new Set(sig.getTypeParameters());
|
||||
const outer = ctx.resolveTypeParam;
|
||||
const sigCtx: TypeMapperCtx =
|
||||
own.size === 0
|
||||
? ctx
|
||||
: {
|
||||
...ctx,
|
||||
erasingSignature: sig,
|
||||
resolveTypeParam: (param) => (own.has(param) ? DYN : (outer?.(param) ?? null)),
|
||||
};
|
||||
const mapped = mapCallSignature(sig, sigCtx);
|
||||
if (mapped?.kind === "func" && mapped.rest !== true && mapped.argumentsAll !== true) continue;
|
||||
const decl = checker.signatureDeclaration(sig);
|
||||
const spelled = decl ? decl.getText().replace(/\s+/g, " ").replace(/;$/, "") : "";
|
||||
const which = spelled ? `overload '${spelled}'` : "one overload";
|
||||
if (mapped?.kind === "func")
|
||||
return `is an overload set, and its ${which} has a rest parameter (an overload set stores one fixed-arity function)`;
|
||||
for (const param of sig.getParameters()) {
|
||||
const paramTs = checker.getTypeOfSymbol(param);
|
||||
if (!mapType(paramTs, sigCtx))
|
||||
return `is an overload set, and its ${which} has parameter '${param.name}' of type '${checker.typeToString(paramTs)}', which does not compile`;
|
||||
}
|
||||
const retTs = checker.getReturnTypeOfSignature(sig);
|
||||
return `is an overload set, and its ${which} returns '${checker.typeToString(retTs)}', which does not compile`;
|
||||
}
|
||||
return "is an overload set whose signatures do not join into one stored function (a void result beside a value result, or positions whose differing types do not cross the checked-dynamic boundary)";
|
||||
}
|
||||
|
||||
/** The computed pieces of a record shape mapRecordTypeInner hands back for
|
||||
* the OUTER frame to intern — or, when a back-reference minted a recursive
|
||||
* placeholder for the type mid-construction, to finalize INTO that
|
||||
@@ -5948,7 +6146,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;
|
||||
@@ -5958,9 +6156,14 @@ function mapRecordTypeInner(
|
||||
// can hold them — see isGenericCallableMemberType): the shape keeps
|
||||
// its data fields, and calls of the member monomorphize per call
|
||||
// site against the defining object literal's declaration.
|
||||
// OVERLOADED members hold their erased runtime function (see
|
||||
// erasedOverloadMemberType); a set that does not join fails the
|
||||
// shape as before.
|
||||
let pt = isGenericCallableMemberType(fieldTs, checker)
|
||||
? erasedGenericMemberType(fieldTs, ctx)
|
||||
: mapType(fieldTs, ctx);
|
||||
: isOverloadedCallableMemberType(fieldTs, checker)
|
||||
? (erasedOverloadMemberType(fieldTs, ctx) ?? null)
|
||||
: mapType(fieldTs, ctx);
|
||||
if (pt === undefined) continue;
|
||||
// tsgo PANICS computing `readonly []` through the symbol-type query
|
||||
// (the TupleType conversion — the facade's panic fence answers
|
||||
@@ -6275,12 +6478,16 @@ 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;
|
||||
}
|
||||
const fieldTs = checker.getTypeOfSymbol(p);
|
||||
if (isOverloadedCallableMemberType(fieldTs, checker)) {
|
||||
if (erasedOverloadMemberType(fieldTs, ctx) !== undefined) continue;
|
||||
return `the record shape is supported, but its member '${p.name}' ${describeOverloadSetBlocker(fieldTs, ctx)}`;
|
||||
}
|
||||
let pt = isGenericCallableMemberType(fieldTs, checker)
|
||||
? erasedGenericMemberType(fieldTs, ctx)
|
||||
: mapType(fieldTs, ctx);
|
||||
|
||||
@@ -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<ts.SourceFile> {
|
||||
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<ts.SourceFile, ts.SourceFile[]>();
|
||||
const referenced = new Set<ts.SourceFile>();
|
||||
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<ts.SourceFile>();
|
||||
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)));
|
||||
}
|
||||
@@ -137,6 +137,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"fetch.streamFrom",
|
||||
"num.toFixed",
|
||||
"num.toStringRadix",
|
||||
"num.toStringRadixF64",
|
||||
"insp.jsonDyn",
|
||||
// diagnostics_channel: publish runs subscribers synchronously (a throw
|
||||
// propagates — the documented divergence from triggerUncaughtException);
|
||||
@@ -489,6 +490,13 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = 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].
|
||||
|
||||
@@ -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. */
|
||||
@@ -751,6 +755,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
"num.toFixed0": { argTypes: [F64], result: STRING },
|
||||
"num.toFixed": { argTypes: [F64, F64], result: STRING },
|
||||
"num.toStringRadix": { argTypes: [F64, DYN], result: STRING },
|
||||
/** toString(radix) with a number radix (scr_lib.c): truncates it as
|
||||
* ToIntegerOrInfinity does, THROWS RangeError outside 2..36. */
|
||||
"num.toStringRadixF64": { argTypes: [F64, F64], result: STRING },
|
||||
/** Object.is over two numbers — the spec's SameValue on doubles: NaN
|
||||
* equals NaN, +0 differs from -0, everything else is `===`. Plain bool
|
||||
* result; never throws. (Union-armed operands take unionEq's sameValue
|
||||
@@ -861,6 +868,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 },
|
||||
|
||||
@@ -46,6 +46,8 @@ export interface RuntimeFeatures {
|
||||
}
|
||||
|
||||
const DYN_ASYNC_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
// DEP0040 rides the dyn-async unit's warning machinery.
|
||||
"punycode.warn",
|
||||
"fs.callbackValue",
|
||||
"fs.callbackCall",
|
||||
"fs.callbackGlob",
|
||||
|
||||
@@ -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",
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5976-builtin-class-aliases.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5977-builtin-class-aliases.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5978-builtin-class-alias-modules/classes.ts",
|
||||
"<repo>/tests/corpus/5978-builtin-class-alias-modules/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5997-frame-records-in-record-unions.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6102-local-class-type-query.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6110-cjs-speculative-wrapper-fences.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6120-type-only-import-graph/mixed.ts",
|
||||
"<repo>/tests/corpus/6120-type-only-import-graph/flags.ts",
|
||||
"<repo>/tests/corpus/6120-type-only-import-graph/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6121-type-specifier-edges/config.ts",
|
||||
"<repo>/tests/corpus/6121-type-specifier-edges/config-barrel.ts",
|
||||
"<repo>/tests/corpus/6121-type-specifier-edges/b.ts",
|
||||
"<repo>/tests/corpus/6121-type-specifier-edges/a.ts",
|
||||
"<repo>/tests/corpus/6121-type-specifier-edges/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6122-punycode-conversions.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6124-punycode-require-warnings.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6125-constants-module.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6126-punycode-dependency/main.cjs"
|
||||
],
|
||||
"diags": [
|
||||
{
|
||||
"code": "SC1010",
|
||||
"message": "the 'puny-dep' module is not supported yet",
|
||||
"loc": {
|
||||
"file": "<repo>/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"
|
||||
}
|
||||
]
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6160-number-radix-typed.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6161-dispose-observer-gates.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6162-dispose-observer-weak-keys.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6163-dispose-observer-workers.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6164-regex-replace-typed-callback.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6165-regex-replace-callback-throws.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6180-interface-overload-events.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6181-interface-overload-arity.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6182-interface-overload-types.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6183-interface-overload-flow.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/builtin-class-alias-escape.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/local-class-type-query-js.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/overloaded-members.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/speculative-function-refusal.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -1179,7 +1179,7 @@ static void scr_arr_destroy(void *object) {
|
||||
#ifdef SCR_RC_AUDIT
|
||||
scr_live_arrays--;
|
||||
#endif
|
||||
scr_weak_dispose(a);
|
||||
scr_weak_dispose_inline(a);
|
||||
scr_mem_free(a);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,6 +218,11 @@ void scr_emit_warning(const char *name, const char *code, ScrStr *message);
|
||||
* process.emitWarning) reach it as (message, type[, code]) instead of the
|
||||
* report and the 'warning' listeners. */
|
||||
void scr_process_set_emit_warning(ScrDyn *fn);
|
||||
/* 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. */
|
||||
|
||||
@@ -957,6 +957,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. */
|
||||
|
||||
@@ -44,7 +44,7 @@ static void *scr_blob_retain_v(void *h) {
|
||||
static void scr_blob_release_v(void *h) {
|
||||
ScrBlob *b = h;
|
||||
if (--b->rc) return;
|
||||
scr_weak_dispose(b);
|
||||
scr_weak_dispose_inline(b);
|
||||
scr_blob_store_release(b->store);
|
||||
free(b->parts);
|
||||
scr_str_release(b->type);
|
||||
|
||||
@@ -296,7 +296,7 @@ ScrBytes *scr_bytes_as_buffer(ScrBytes *bytes) {
|
||||
void scr_bytes_release(ScrBytes *b) {
|
||||
if (!b || b->rc == SIZE_MAX) return; /* NULL: an uninitialized `let` local */
|
||||
if (--b->rc == 0) {
|
||||
scr_weak_dispose(b);
|
||||
scr_weak_dispose_inline(b);
|
||||
if (b->backing) {
|
||||
scr_bytes_release(b->backing); /* a view: data points into the owner */
|
||||
} else if (b->shared) {
|
||||
|
||||
@@ -1250,7 +1250,7 @@ static void scr_stdio_cleanup(void) {
|
||||
scr_stdio_write_hook = NULL;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
ScrStdio *stream = &scr_stdio_streams[i];
|
||||
scr_weak_dispose(stream);
|
||||
scr_weak_dispose_inline(stream);
|
||||
scr_dyn_release(stream->write);
|
||||
stream->write = NULL;
|
||||
scr_dyn_release(stream->write_error);
|
||||
|
||||
@@ -2522,7 +2522,7 @@ static SfResponse *sf_response_retain(SfResponse *r) {
|
||||
|
||||
static void sf_response_release(SfResponse *r) {
|
||||
if (!r || --r->rc > 0) return;
|
||||
scr_weak_dispose(r);
|
||||
scr_weak_dispose_inline(r);
|
||||
sf_stream_release(r->body);
|
||||
sf_headers_release(r->headers);
|
||||
scr_str_release(r->url);
|
||||
@@ -4760,7 +4760,7 @@ static void *sf_request_retain_v(void *ptr) {
|
||||
static void sf_request_release_v(void *ptr) {
|
||||
SfRequest *r = ptr;
|
||||
if (--r->rc) return;
|
||||
scr_weak_dispose(r);
|
||||
scr_weak_dispose_inline(r);
|
||||
sf_response_release(r->payload);
|
||||
scr_str_release(r->url); scr_str_release(r->method); scr_str_release(r->redirect);
|
||||
sf_signal_release(r->signal); free(r);
|
||||
|
||||
@@ -212,6 +212,8 @@ static void scr_jb_dispose(ScrJsonBuf *b) {
|
||||
scr_jb_init(b);
|
||||
}
|
||||
|
||||
void scr_jb_abandon(ScrJsonBuf *b) { scr_jb_dispose(b); }
|
||||
|
||||
void scr_jb_putc(ScrJsonBuf *b, char c) {
|
||||
scr_jb_grow(b, 1);
|
||||
b->data[b->len++] = c;
|
||||
@@ -13045,63 +13047,13 @@ double scr_buffer_byte_length_dyn(ScrDyn *value, ScrStr *encoding) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Radix digit generation follows V8's DoubleToRadixStringView algorithm.
|
||||
* Copyright 2012 the V8 project authors. BSD license in
|
||||
* ../vendor/v8-number-radix/LICENSE. The decimal path retains Ryū. */
|
||||
/* toString(radix) with any radix value: undefined means 10, anything else
|
||||
* converts with ToNumber (which can run program code) first. */
|
||||
ScrStr *scr_num_to_string_radix(double value, const ScrDyn *argument) {
|
||||
double requested = 10;
|
||||
if (argument && argument->kind != SCR_DYN_UNDEF &&
|
||||
!scr_dyn_number_coerce_js(argument, &requested)) return NULL;
|
||||
requested = isnan(requested) ? 0 : trunc(requested);
|
||||
if (requested < 2 || requested > 36 || !isfinite(requested)) {
|
||||
static const char message[] = "toString() radix argument must be between 2 and 36";
|
||||
scr_throw_error_msg(SCR_ERR_RANGE, message, sizeof message - 1);
|
||||
return NULL;
|
||||
}
|
||||
int radix = (int)requested;
|
||||
if (radix == 10 || !isfinite(value) || value == 0) return scr_f64_to_scrstr(value);
|
||||
static const char digits[] = "0123456789abcdefghijklmnopqrstuvwxyz";
|
||||
char buffer[2200];
|
||||
const size_t middle = sizeof buffer / 2;
|
||||
size_t begin = middle, end = middle;
|
||||
bool negative = value < 0;
|
||||
value = fabs(value);
|
||||
double integer = floor(value), fraction = value - integer;
|
||||
double tolerance = 0.5 * (nextafter(value, INFINITY) - value);
|
||||
if (tolerance <= 0) tolerance = nextafter(0, INFINITY);
|
||||
if (fraction >= tolerance) {
|
||||
buffer[end++] = '.';
|
||||
do {
|
||||
fraction *= radix;
|
||||
tolerance *= radix;
|
||||
int digit = (int)fraction;
|
||||
buffer[end++] = digits[digit];
|
||||
fraction -= digit;
|
||||
if ((fraction > 0.5 || (fraction == 0.5 && (digit & 1))) && fraction + tolerance > 1) {
|
||||
for (;;) {
|
||||
end--;
|
||||
if (end == middle) { integer += 1; break; }
|
||||
char last = buffer[end];
|
||||
digit = last > '9' ? last - 'a' + 10 : last - '0';
|
||||
if (digit + 1 < radix) { buffer[end++] = digits[digit + 1]; break; }
|
||||
}
|
||||
break;
|
||||
}
|
||||
} while (fraction >= tolerance);
|
||||
}
|
||||
/* V8's Double::Exponent is the unbiased exponent minus 52. Beyond
|
||||
* that precision boundary, trailing digits are unrepresented zeros. */
|
||||
while (integer / radix >= 0x1p53) {
|
||||
integer /= radix;
|
||||
buffer[--begin] = '0';
|
||||
}
|
||||
do {
|
||||
double remainder = fmod(integer, radix);
|
||||
buffer[--begin] = digits[(int)remainder];
|
||||
integer = (integer - remainder) / radix;
|
||||
} while (integer > 0);
|
||||
if (negative) buffer[--begin] = '-';
|
||||
return scr_str_new(buffer + begin, end - begin);
|
||||
return scr_num_to_string_radix_f64(value, requested);
|
||||
}
|
||||
|
||||
ScrDyn *scr_caught_to_dyn(const ScrCaught *c) {
|
||||
|
||||
@@ -119,6 +119,8 @@ _Static_assert(offsetof(ScrJsonBuf, data) == 0 && offsetof(ScrJsonBuf, len) == s
|
||||
"LLVM backend expects ScrJsonBuf to start with { data, len, cap }");
|
||||
|
||||
void scr_jb_init(ScrJsonBuf *b);
|
||||
/* Release a buffer's text without finishing it (error paths). */
|
||||
void scr_jb_abandon(ScrJsonBuf *b);
|
||||
/* Append a borrowed byte span verbatim, including embedded NUL bytes. */
|
||||
void scr_jb_write(ScrJsonBuf *b, const char *s, size_t n);
|
||||
/* scr_jb_write for text that holds no lone surrogate (fixed text, JSON
|
||||
|
||||
@@ -10716,6 +10716,64 @@ ScrStr *scr_num_to_fixed0(double x) {
|
||||
* the receiver's non-finite arm, as ECMA-262 requires. Invalid precision
|
||||
* raises V8's catchable RangeError text; otherwise the result is +1. The
|
||||
* exact-binary-value digits come from scr_f64_to_fixed (scr_number.c). */
|
||||
/* Number.prototype.toString(radix) with a number radix, which
|
||||
* ToIntegerOrInfinity truncates (NaN to 0). Radix digit generation follows
|
||||
* V8's DoubleToRadixStringView algorithm. Copyright 2012 the V8 project
|
||||
* authors. BSD license in ../vendor/v8-number-radix/LICENSE. The decimal
|
||||
* path retains Ryū. */
|
||||
ScrStr *scr_num_to_string_radix_f64(double value, double requested) {
|
||||
requested = isnan(requested) ? 0 : trunc(requested);
|
||||
if (requested < 2 || requested > 36 || !isfinite(requested)) {
|
||||
static const char message[] = "toString() radix argument must be between 2 and 36";
|
||||
scr_throw_error_msg(SCR_ERR_RANGE, message, sizeof message - 1);
|
||||
return NULL;
|
||||
}
|
||||
int radix = (int)requested;
|
||||
if (radix == 10 || !isfinite(value) || value == 0) return scr_f64_to_scrstr(value);
|
||||
static const char digits[] = "0123456789abcdefghijklmnopqrstuvwxyz";
|
||||
char buffer[2200];
|
||||
const size_t middle = sizeof buffer / 2;
|
||||
size_t begin = middle, end = middle;
|
||||
bool negative = value < 0;
|
||||
value = fabs(value);
|
||||
double integer = floor(value), fraction = value - integer;
|
||||
double tolerance = 0.5 * (nextafter(value, INFINITY) - value);
|
||||
if (tolerance <= 0) tolerance = nextafter(0, INFINITY);
|
||||
if (fraction >= tolerance) {
|
||||
buffer[end++] = '.';
|
||||
do {
|
||||
fraction *= radix;
|
||||
tolerance *= radix;
|
||||
int digit = (int)fraction;
|
||||
buffer[end++] = digits[digit];
|
||||
fraction -= digit;
|
||||
if ((fraction > 0.5 || (fraction == 0.5 && (digit & 1))) && fraction + tolerance > 1) {
|
||||
for (;;) {
|
||||
end--;
|
||||
if (end == middle) { integer += 1; break; }
|
||||
char last = buffer[end];
|
||||
digit = last > '9' ? last - 'a' + 10 : last - '0';
|
||||
if (digit + 1 < radix) { buffer[end++] = digits[digit + 1]; break; }
|
||||
}
|
||||
break;
|
||||
}
|
||||
} while (fraction >= tolerance);
|
||||
}
|
||||
/* V8's Double::Exponent is the unbiased exponent minus 52. Beyond
|
||||
* that precision boundary, trailing digits are unrepresented zeros. */
|
||||
while (integer / radix >= 0x1p53) {
|
||||
integer /= radix;
|
||||
buffer[--begin] = '0';
|
||||
}
|
||||
do {
|
||||
double remainder = fmod(integer, radix);
|
||||
buffer[--begin] = digits[(int)remainder];
|
||||
integer = (integer - remainder) / radix;
|
||||
} while (integer > 0);
|
||||
if (negative) buffer[--begin] = '-';
|
||||
return scr_str_new(buffer + begin, end - begin);
|
||||
}
|
||||
|
||||
ScrStr *scr_num_to_fixed(double x, double fraction_digits) {
|
||||
double fd = isnan(fraction_digits) ? 0 : trunc(fraction_digits);
|
||||
if (!(fd >= 0 && fd <= 100)) {
|
||||
|
||||
@@ -697,7 +697,7 @@ static void scr_map_trace(void *o, ScrTraceVisit visit, void *ctx) {
|
||||
static void scr_map_gcfree(void *o) {
|
||||
ScrMap *m = (ScrMap *)o;
|
||||
bool keys = m->traced & SCR_MAP_TRACED_KEY, vals = m->traced & SCR_MAP_TRACED_VAL;
|
||||
scr_weak_dispose(m);
|
||||
scr_weak_dispose_inline(m);
|
||||
for (size_t e = 0; e < m->nentries; e++) {
|
||||
if (!m->entries[e].hash) continue;
|
||||
if (!keys) scr_map_release_key(m, m->entries[e].key);
|
||||
@@ -752,7 +752,7 @@ ScrMap *scr_map_retain(ScrMap *m) {
|
||||
|
||||
static void scr_map_destroy(void *object) {
|
||||
ScrMap *m = object;
|
||||
scr_weak_dispose(m);
|
||||
scr_weak_dispose_inline(m);
|
||||
for (size_t e = 0; e < m->nentries; e++) {
|
||||
if (!m->entries[e].hash) continue;
|
||||
scr_map_release_key(m, m->entries[e].key);
|
||||
|
||||
@@ -73,6 +73,7 @@ bool scr_num_is_safe_integer(double x);
|
||||
ScrStr *scr_num_to_exponential(double x);
|
||||
ScrStr *scr_num_to_fixed0(double x);
|
||||
ScrStr *scr_num_to_fixed(double x, double fraction_digits);
|
||||
ScrStr *scr_num_to_string_radix_f64(double value, double radix);
|
||||
|
||||
/* Object.is over two numbers — the spec's SameValue on doubles: NaN
|
||||
* equals NaN, +0 differs from -0, everything else is ==. Never throws. */
|
||||
|
||||
@@ -27,6 +27,8 @@
|
||||
#include <sys/utsname.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
#include <fcntl.h>
|
||||
#include <sys/stat.h>
|
||||
#ifdef __linux__
|
||||
#include <sys/sysinfo.h>
|
||||
#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);
|
||||
}
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -423,8 +423,15 @@ static inline void *scr_cyc_alloc_inline(size_t size, ScrCycType *type) {
|
||||
scr_cyc_live++;
|
||||
return h + 1;
|
||||
}
|
||||
static inline void scr_cyc_free_inline(void *obj) {
|
||||
/* scr_weak_dispose's NULL test inline, for the runtime's hot frees: the
|
||||
* hook stays NULL unless the program creates a weak collection or keeps a
|
||||
* checked record's source, so the free costs one predictable branch, not a
|
||||
* call. */
|
||||
static inline void scr_weak_dispose_inline(void *obj) {
|
||||
if (scr_weak_dispose_hook) scr_weak_dispose_hook(obj);
|
||||
}
|
||||
static inline void scr_cyc_free_inline(void *obj) {
|
||||
scr_weak_dispose_inline(obj);
|
||||
scr_cyc_live--;
|
||||
if (scr_cyc_gen(scr_cyc_hdr(obj)) == SCR_CYC_OLD) scr_cyc_old_freed++;
|
||||
scr_mem_free(scr_cyc_hdr(obj));
|
||||
|
||||
@@ -1358,7 +1358,129 @@ ScrStr *scr_regex_replace_callback(ScrStr *s, ScrRegex *re, ScrDyn *callback, bo
|
||||
for (size_t i = 0; i < count; i++) scr_dyn_release(matches[i].arguments);
|
||||
free(matches);
|
||||
if (scr_exc_pending()) {
|
||||
free(output.data);
|
||||
scr_jb_abandon(&output);
|
||||
scr_regex_input_dispose(&input);
|
||||
return NULL;
|
||||
}
|
||||
scr_regex_put_span(&output, &input, next, len);
|
||||
scr_regex_input_dispose(&input);
|
||||
return scr_jb_finish(&output);
|
||||
}
|
||||
|
||||
/* One call of a typed replacer (scr_regex_replace_fn's contract): string
|
||||
* arguments are passed +1 (the closure ABI's owned parameters), the offset
|
||||
* as a number, and the result is the closure's +1 string. */
|
||||
static ScrStr *scr_regex_call_replacer(ScrClosure *f, uint32_t arity, int ncap,
|
||||
ScrStr *const *caps, double offset, ScrStr *subject) {
|
||||
void *p[4] = { NULL, NULL, NULL, NULL };
|
||||
for (uint32_t i = 0; i < arity && i < 4; i++) {
|
||||
if ((int)i < ncap) {
|
||||
if (!caps[i]) scr_trap("scriptc: internal error: typed replacer capture is unset\n");
|
||||
p[i] = scr_str_retain(caps[i]);
|
||||
} else if ((int)i == ncap + 1) {
|
||||
p[i] = scr_str_retain(subject);
|
||||
}
|
||||
}
|
||||
void *fn = f->fn;
|
||||
typedef ScrStr *R;
|
||||
switch (arity) {
|
||||
case 0: return ((R (*)(ScrClosure *))fn)(f);
|
||||
case 1: return ((R (*)(ScrClosure *, void *))fn)(f, p[0]);
|
||||
case 2:
|
||||
if (ncap == 1) return ((R (*)(ScrClosure *, void *, double))fn)(f, p[0], offset);
|
||||
return ((R (*)(ScrClosure *, void *, void *))fn)(f, p[0], p[1]);
|
||||
case 3:
|
||||
if (ncap == 1) return ((R (*)(ScrClosure *, void *, double, void *))fn)(f, p[0], offset, p[2]);
|
||||
if (ncap == 2) return ((R (*)(ScrClosure *, void *, void *, double))fn)(f, p[0], p[1], offset);
|
||||
return ((R (*)(ScrClosure *, void *, void *, void *))fn)(f, p[0], p[1], p[2]);
|
||||
case 4:
|
||||
if (ncap == 2)
|
||||
return ((R (*)(ScrClosure *, void *, void *, double, void *))fn)(f, p[0], p[1], offset, p[3]);
|
||||
if (ncap == 3)
|
||||
return ((R (*)(ScrClosure *, void *, void *, void *, double))fn)(f, p[0], p[1], p[2], offset);
|
||||
if (ncap >= 4)
|
||||
return ((R (*)(ScrClosure *, void *, void *, void *, void *))fn)(f, p[0], p[1], p[2], p[3]);
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
scr_trap("scriptc: internal error: typed replacer signature\n");
|
||||
}
|
||||
|
||||
/* scr_regex_replace_callback's algorithm for a typed replacer: the matches
|
||||
* are collected first, keeping only the captures the replacer reads, then
|
||||
* the replacer runs once per match in order. */
|
||||
ScrStr *scr_regex_replace_fn(ScrStr *s, ScrRegex *re, ScrClosure *f, uint32_t arity, bool all) {
|
||||
uint8_t *bc = scr_regex_bc(re);
|
||||
int flags = lre_get_flags(bc);
|
||||
bool global = (flags & LRE_FLAG_GLOBAL) != 0;
|
||||
if (all && !global) {
|
||||
static const char message[] = "String.prototype.replaceAll called with a non-global RegExp argument";
|
||||
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
|
||||
return NULL;
|
||||
}
|
||||
bool sticky = (flags & LRE_FLAG_STICKY) != 0;
|
||||
bool unicode = (flags & (LRE_FLAG_UNICODE | LRE_FLAG_UNICODE_SETS)) != 0;
|
||||
int capture_count = lre_get_capture_count(bc);
|
||||
size_t keep = arity < (uint32_t)capture_count ? arity : (size_t)capture_count;
|
||||
ScrRegexInput input;
|
||||
scr_regex_input_init(&input, re, bc, s);
|
||||
int len = input.len;
|
||||
struct { int start, end; } *matches = NULL;
|
||||
ScrStr **caps = NULL;
|
||||
size_t count = 0, capacity = 0;
|
||||
if (global) re->last_index = 0;
|
||||
int position = sticky ? scr_regex_start(re, len) : 0;
|
||||
if (position < 0) position = len + 1;
|
||||
if (sticky && !global) re->last_index = 0;
|
||||
while (position <= len && scr_exec(&input, bc, position) == 1) {
|
||||
uint8_t **capture = input.capture;
|
||||
int start = scr_capture_index(&input, capture[0]);
|
||||
int end = scr_capture_index(&input, capture[1]);
|
||||
if (sticky && !global) re->last_index = end;
|
||||
if (count == capacity) {
|
||||
size_t next = capacity ? capacity * 2 : 8;
|
||||
if (next < capacity || next > SIZE_MAX / sizeof(*matches) / (keep + 1)) scr_regex_oom();
|
||||
void *grown = realloc(matches, next * sizeof(*matches));
|
||||
if (!grown) scr_regex_oom();
|
||||
matches = grown;
|
||||
if (keep) {
|
||||
grown = realloc(caps, next * keep * sizeof(*caps));
|
||||
if (!grown) scr_regex_oom();
|
||||
caps = grown;
|
||||
}
|
||||
capacity = next;
|
||||
}
|
||||
for (size_t group = 0; group < keep; group++) {
|
||||
const uint8_t *cs = capture[group * 2], *ce = capture[group * 2 + 1];
|
||||
caps[count * keep + group] =
|
||||
cs && ce ? scr_regex_span(&input, scr_capture_index(&input, cs), scr_capture_index(&input, ce))
|
||||
: NULL;
|
||||
}
|
||||
matches[count].start = start;
|
||||
matches[count].end = end;
|
||||
count++;
|
||||
if (!global) break;
|
||||
position = start == end ? scr_advance(&input, end, unicode) : end;
|
||||
}
|
||||
ScrJsonBuf output;
|
||||
scr_jb_init(&output);
|
||||
int next = 0;
|
||||
scr_dyn_this_push_dyn(NULL);
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
ScrStr *replacement = scr_regex_call_replacer(f, arity, capture_count, caps + i * keep,
|
||||
(double)matches[i].start, s);
|
||||
if (scr_exc_pending()) break;
|
||||
scr_regex_put_span(&output, &input, next, matches[i].start);
|
||||
scr_jb_put_str(&output, replacement);
|
||||
scr_str_release(replacement);
|
||||
next = matches[i].end;
|
||||
}
|
||||
scr_dyn_this_pop();
|
||||
for (size_t i = 0; i < count * keep; i++) scr_str_release(caps[i]);
|
||||
free(caps);
|
||||
free(matches);
|
||||
if (scr_exc_pending()) {
|
||||
scr_jb_abandon(&output);
|
||||
scr_regex_input_dispose(&input);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -80,6 +80,16 @@ ScrStr *scr_regex_flags(ScrRegex *re); /* +1 */
|
||||
ScrStr *scr_regex_to_string(ScrRegex *re); /* +1 */
|
||||
/* replace: first match without /g, every match with it. */
|
||||
ScrStr *scr_regex_replace(ScrStr *s, ScrRegex *re, ScrStr *rep);
|
||||
/* replace/replaceAll with a typed replacer closure of `arity` (at most 4)
|
||||
* parameters returning a string. Parameter i receives what Node passes at
|
||||
* position i: the match and captures (strings) below the capture count n,
|
||||
* the match offset (a number) at n, the subject (a string) at n + 1. The
|
||||
* compiler admits a closure only when every position it declares is one of
|
||||
* those with the declared kind, and every capture it reads is set in every
|
||||
* match (scr_regex_replace_callback handles the rest). Borrows all. */
|
||||
struct ScrClosure;
|
||||
ScrStr *scr_regex_replace_fn(ScrStr *s, ScrRegex *re, struct ScrClosure *f, uint32_t arity,
|
||||
bool all);
|
||||
/* replaceAll: throws Node's TypeError when /g is missing (may-throw). */
|
||||
ScrStr *scr_regex_replace_all(ScrStr *s, ScrRegex *re, ScrStr *rep);
|
||||
/* split: captured substrings are interleaved with the split pieces;
|
||||
|
||||
@@ -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); }
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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<string, number>().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<string, number>([
|
||||
["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));
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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<string, number>([["k", 1]]);
|
||||
const viaNsDict = new classes.Dict<string, string>();
|
||||
viaNsDict.set("a", "b");
|
||||
const bag = new classes.Bag<number>([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));
|
||||
@@ -0,0 +1,88 @@
|
||||
// Records that are arms of a union of records and also live in frame
|
||||
// storage. An arm of such a union carries a hidden shape word before its
|
||||
// fields, so every field address has to skip it: the reference fields a
|
||||
// frame record owns are read, overwritten and released through the same
|
||||
// slots as heap records of the shape. The same shapes also flow through
|
||||
// the union, are compared by identity and are serialized.
|
||||
interface Ident {
|
||||
kind: "ident";
|
||||
name: string;
|
||||
tags: string[];
|
||||
}
|
||||
interface Call {
|
||||
kind: "call";
|
||||
callee: string;
|
||||
args: Ident[];
|
||||
}
|
||||
type Node = Ident | Call;
|
||||
|
||||
const words = ["alpha", "beta", "gamma", "delta", "epsilon"];
|
||||
function word(i: number): string {
|
||||
return words[i % words.length]! + i;
|
||||
}
|
||||
|
||||
function describe(node: Node): string {
|
||||
switch (node.kind) {
|
||||
case "ident":
|
||||
return `${node.name}[${node.tags.join(",")}]`;
|
||||
case "call":
|
||||
return `${node.callee}(${node.args.map((a) => a.name).join(", ")})`;
|
||||
}
|
||||
}
|
||||
function weight(node: Node): number {
|
||||
return node.kind === "ident" ? node.name.length + node.tags.length : node.args.length * 10;
|
||||
}
|
||||
|
||||
// Heap values of the union: the arms are tagged shapes.
|
||||
const nodes: Node[] = [];
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const id: Ident = { kind: "ident", name: word(i), tags: [word(i + 1)] };
|
||||
nodes.push(i % 2 === 0 ? id : { kind: "call", callee: word(i + 2), args: [id, id] });
|
||||
}
|
||||
console.log(nodes.map(describe).join(" | "));
|
||||
|
||||
// Frame records of the arms that only flow into field reads.
|
||||
let total = 0;
|
||||
let log = "";
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const w = word(i);
|
||||
const id: Ident = { kind: "ident", name: w + "!", tags: [w, word(i + 3)] };
|
||||
total += id.name.length + id.tags.length + id.tags[1]!.length;
|
||||
const call: Call = { kind: "call", callee: w.toUpperCase(), args: [] };
|
||||
total += call.callee.length + call.args.length;
|
||||
log += id.kind[0]! + call.kind[0]! + id.name[0]!;
|
||||
}
|
||||
console.log(total, log);
|
||||
|
||||
// A reassigned frame binding: each new value is built apart and copied over
|
||||
// the slot, and the replaced reference fields are released.
|
||||
let cur: Ident = { kind: "ident", name: word(0), tags: [] };
|
||||
for (let i = 1; i < 8; i++) {
|
||||
cur = { kind: "ident", name: cur.name.slice(0, 3) + word(i), tags: [...cur.tags, cur.name] };
|
||||
if (i % 3 === 0) cur = { kind: "ident", name: cur.tags[0] ?? "none", tags: cur.tags.slice(1) };
|
||||
}
|
||||
console.log(cur.name, cur.tags.length, cur.tags.join("+"));
|
||||
|
||||
// Field writes into a frame record's reference slots.
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const id: Ident = { kind: "ident", name: word(i), tags: [word(i)] };
|
||||
id.name = id.name + "/" + i;
|
||||
id.tags = [id.name, ...id.tags];
|
||||
id.tags.push(word(i + 7));
|
||||
console.log(id.name, id.tags.join(" "));
|
||||
}
|
||||
|
||||
// Through the union: describe and weigh by union, keep identity, and
|
||||
// serialize the arms and the union values.
|
||||
const a: Ident = { kind: "ident", name: "x", tags: ["t1", "t2"] };
|
||||
const b: Call = { kind: "call", callee: "f", args: [a] };
|
||||
const asNode: Node = a;
|
||||
const picked: Node = weight(b) > weight(a) ? b : a;
|
||||
console.log(describe(asNode), describe(b), weight(asNode), weight(picked));
|
||||
console.log(asNode === a, picked === b, b.args[0] === a, (picked as Call).args[0] === asNode);
|
||||
a.tags.push("t3");
|
||||
console.log(describe(asNode), describe(picked));
|
||||
console.log(JSON.stringify(a), JSON.stringify(picked));
|
||||
console.log(JSON.stringify(nodes.slice(0, 2)));
|
||||
const mixed: Node[] = [a, b, nodes[1]!];
|
||||
console.log(mixed.filter((n) => n.kind === "ident").length, mixed.indexOf(asNode), mixed.indexOf(b));
|
||||
@@ -1,42 +0,0 @@
|
||||
// A whole-program analysis that queries types before reachability is known
|
||||
// (the computed-key write census here) can name a function-local class whose
|
||||
// collection is refused. That query has no owner for the refusal: the class
|
||||
// stays unmapped for it, and the refusal belongs to the class's own
|
||||
// declaration, which this program never executes (it used to crash the
|
||||
// compiler with an uncaught PoisonError).
|
||||
function main(run) {
|
||||
if (!run) return "skipped";
|
||||
const Project = class {
|
||||
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 = String(run);
|
||||
holder.p[key] = "q";
|
||||
return holder.p.name;
|
||||
}
|
||||
|
||||
class Counter {
|
||||
constructor() {
|
||||
this.counts = {};
|
||||
}
|
||||
bump(key) {
|
||||
this.counts[key] = (this.counts[key] || 0) + 1;
|
||||
return this.counts[key];
|
||||
}
|
||||
}
|
||||
|
||||
const counter = new Counter();
|
||||
const word = process.argv.length > 99 ? "unused" : "word";
|
||||
counter.bump(word);
|
||||
console.log(main(0), counter.bump(word));
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user