mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-26 06:34:20 +08:00
Make compile blockers and Node differences actionable (#786)
This commit is contained in:
@@ -84,8 +84,11 @@ Verifies and reads the installed precompiled runtime objects and vendor archives
|
||||
<dt><code>--emit <ir|llvm|asm|obj|exe></code></dt>
|
||||
<dd>Select the invocation's one primary artifact. <code>ir</code> and <code>llvm</code> need only Node. <code>asm</code> and <code>obj</code> use a matching bundled LLVM helper on supported macOS, Linux, and Windows hosts and for WASI; they need no installed C compiler. <code>exe</code> is the default.</dd>
|
||||
|
||||
<dt><code>--print <native-link-info></code></dt>
|
||||
<dd>Build an object (equivalent to <code>--emit=obj</code>) and print its machine-readable external link recipe as JSON instead of printing the artifact path. The document names the exact installed precompiled runtime pack and all link inputs, but does not invoke a linker.</dd>
|
||||
<dt><code>--print <native-link-info|diagnostics></code></dt>
|
||||
<dd><code>native-link-info</code>: build an object (equivalent to <code>--emit=obj</code>) and print its machine-readable external link recipe as JSON instead of printing the artifact path. The document names the exact installed precompiled runtime pack and all link inputs, but does not invoke a linker. <code>diagnostics</code>: for <code>build</code> and <code>coverage</code>, print the versioned JSON diagnostics document described in <a href="/docs/coverage#machine-readable-diagnostics">Coverage Reports</a> instead of the human output.</dd>
|
||||
|
||||
<dt><code>--fail-on <blockers|divergences></code></dt>
|
||||
<dd>Coverage only. Exit with status 1 when the entry has blockers that would fail a build, or with <code>divergences</code>, also when it has Node divergence warnings. See <a href="/docs/coverage#gating-builds-and-ci">Gating builds and CI</a>.</dd>
|
||||
|
||||
<dt><code>--dynamic</code></dt>
|
||||
<dd>Embed the JavaScript engine to run npm dependencies and supported <code>any</code>-typed code. Static compilation is the default; without this flag, dynamic-tier sites produce compilation errors. See <a href="/dependencies">npm Dependencies</a>.</dd>
|
||||
|
||||
@@ -40,11 +40,17 @@ $ scriptc coverage cli.ts
|
||||
statements analyzed 1
|
||||
compile statically 0 (0%)
|
||||
|
||||
runs with --dynamic 2 sites
|
||||
×1 importing 'picocolors' requires the embedded dynamic engine, which this build does not include — the package's implementation runs there SC2013
|
||||
×1 values from the 'picocolors' package run in the embedded dynamic engine, which this build does not include SC2013
|
||||
runs with --dynamic 2 sites (embeds a JS engine, ~620KB — static stays the default)
|
||||
×1 SC2013 importing 'picocolors' requires the embedded dynamic engine, which this build does not include — the package's implementation runs there
|
||||
at cli.ts:1:1
|
||||
hint: build with --dynamic to run npm package code in the embedded engine (adds ~620KB to the binary), or try --npm-static auto to compile eligible packages' JavaScript statically; static builds never include the engine
|
||||
×1 SC2013 values from the 'picocolors' package run in the embedded dynamic engine, which this build does not include
|
||||
at cli.ts:3:13
|
||||
hint: build with --dynamic to run npm package code in the embedded engine (adds ~620KB to the binary), or try --npm-static auto to compile eligible packages' JavaScript statically; static builds never include the engine
|
||||
```
|
||||
|
||||
Each group collects the sites that share one root cause: the same code and message, with type arguments of generic instantiations collapsed, so one uncompilable declaration reported through several instantiations appears once. The `at` line lists the first sites in source order, and every group carries a hint.
|
||||
|
||||
The package must be installed before analysis; the quickstart shows [how to install and use it](/docs/quickstart#use-an-npm-dependency).
|
||||
|
||||
### Report fields
|
||||
@@ -71,11 +77,21 @@ The package must be installed before analysis; the quickstart shows [how to inst
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>blockers</code></td>
|
||||
<td>Unsupported operations, listed with a count, reason, and diagnostic code. Enabling <code>--dynamic</code> does not resolve these blockers.</td>
|
||||
<td>Unsupported operations, listed with a count, diagnostic code, reason, locations, and hint. Enabling <code>--dynamic</code> does not resolve these blockers.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>in unreached code</code></td>
|
||||
<td>Blockers in code the entry never reaches. A build never compiles that code, so these cannot fail it.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>differs from Node</code></td>
|
||||
<td>Sites that compile but can behave differently from Node.js. See <a href="#node-divergences">Node divergences</a>.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
The statement totals include the bodies of functions whose signatures cannot compile. Their statements count as not static, so the percentage reflects how much of the program compiles.
|
||||
|
||||
## Analyze with --dynamic
|
||||
|
||||
Add `--dynamic` to include dynamic execution in the analysis:
|
||||
@@ -147,10 +163,131 @@ Coverage uses the same diagnostic codes as `scriptc build`. Common examples incl
|
||||
- `SC2013`: a package import or package value requires the embedded engine. Enable `--dynamic`, remove the dependency, or evaluate the experimental [static package options](/docs/dependencies#static-package-compilation-experimental).
|
||||
- `SC1100`: a value crosses an unsupported `unknown`/`any` boundary. Narrow or cast the value as directed by the diagnostic.
|
||||
|
||||
## Node divergences
|
||||
|
||||
Some programs compile but behave differently natively than under Node.js. Coverage reports the cases it can detect at compile time as `SC6xxx` warnings; `scriptc build` prints the same warnings after a successful build. They never fail a build.
|
||||
|
||||
- `SC6001`: an object passed to, or stored as, a narrower record type is copied, and the program then writes through the copy or compares it by identity.
|
||||
- `SC6002`: an object literal's key order differs from its type's declaration order, and the program observes the order through `JSON.stringify`, `Object.keys`, `Object.values`, `Object.entries`, `for...in`, or console output.
|
||||
- `SC6003`: `localeCompare` compares a string literal outside the built-in Latin-script collation.
|
||||
- `SC6004`: a call passes a class instance to a function that casts it to an unrelated class.
|
||||
- `SC6005`: a `Date` string literal uses a form the native parser does not accept.
|
||||
|
||||
## Gating builds and CI
|
||||
|
||||
By default `scriptc coverage` exits with status 0 whenever the program typechecks, so the report can be used to measure progress. Add `--fail-on=blockers` to exit with status 1 when the entry has blockers that would fail `scriptc build`; blockers in unreached code and JavaScript statements deferred to runtime do not count. `--fail-on=divergences` additionally fails on Node divergences. A program that does not typecheck always exits with status 1.
|
||||
|
||||
An unchanged program reuses the previous verdict: coverage records the files and directories the analysis read and replays the verdict while all of them are unchanged. Edits that only change TypeScript comments also reuse it, with reported locations moved to the edited text. Set `SCRIPTC_NO_CACHE=1` to disable the cache.
|
||||
|
||||
## Machine-readable diagnostics
|
||||
|
||||
`scriptc coverage --print=diagnostics` and `scriptc build --print=diagnostics` write one JSON document to stdout instead of the human report. The exit status is unchanged. The document has the following shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema": "scriptc-diagnostics",
|
||||
"schemaVersion": 1,
|
||||
"compilerVersion": "0.2.7",
|
||||
"command": "coverage",
|
||||
"entry": "/work/app/src/main.ts",
|
||||
"success": false,
|
||||
"phase": "compile",
|
||||
"stats": {
|
||||
"statementsTotal": 13,
|
||||
"statementsStatic": 12,
|
||||
"statementsDynamic": 0,
|
||||
"statementsFailed": 1,
|
||||
"functionsSkipped": 0,
|
||||
"percentStatic": 92
|
||||
},
|
||||
"groups": [
|
||||
{
|
||||
"id": "SC1031-c2cc8425",
|
||||
"code": "SC1031",
|
||||
"category": "unsupported",
|
||||
"severity": "error",
|
||||
"scope": "reached",
|
||||
"message": "object destructuring of non-record values (the source is array-typed)",
|
||||
"hint": "...",
|
||||
"count": 1,
|
||||
"primary": { "file": "/work/app/src/main.ts", "line": 13, "column": 7 },
|
||||
"sites": [{ "file": "/work/app/src/main.ts", "line": 13, "column": 7 }]
|
||||
}
|
||||
],
|
||||
"diagnostics": [
|
||||
{
|
||||
"code": "SC1031",
|
||||
"category": "unsupported",
|
||||
"severity": "error",
|
||||
"scope": "reached",
|
||||
"file": "/work/app/src/main.ts",
|
||||
"line": 13,
|
||||
"column": 7,
|
||||
"endLine": 13,
|
||||
"endColumn": 28,
|
||||
"start": 352,
|
||||
"end": 373,
|
||||
"message": "object destructuring of non-record values (the source is array-typed) are not supported yet",
|
||||
"hint": "...",
|
||||
"group": "SC1031-c2cc8425"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Meaning</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>schemaVersion</code></td>
|
||||
<td>Incremented when a field is renamed or removed or its meaning changes. New optional fields, categories, and codes keep the version.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>success</code></td>
|
||||
<td>Coverage: no blockers on the entry path (and, with <code>--fail-on=divergences</code>, no divergences). Build: an artifact was produced, named by <code>artifact</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>phase</code></td>
|
||||
<td><code>preflight</code> when type checking, configuration, or the module graph stopped the command; <code>compile</code> when blockers remain; <code>native</code> when native code generation or linking failed; otherwise <code>complete</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>stats</code></td>
|
||||
<td>Coverage only, absent after a preflight failure: the report's statement counts.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>category</code></td>
|
||||
<td><code>invalid-source</code> (the program has a TypeScript or JSON error), <code>unsupported</code> (valid code scriptc cannot compile yet), <code>dynamic-only</code> (compiles with <code>--dynamic</code>), <code>divergence</code> (compiles, but can behave differently from Node.js), <code>environment</code> (project configuration, scriptc's type world, toolchain, or target), or <code>internal</code> (a compiler bug).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>severity</code></td>
|
||||
<td><code>error</code>, or <code>warning</code> for divergences.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>scope</code></td>
|
||||
<td><code>preflight</code>, <code>reached</code> (fails a build), <code>unreached</code> (code the entry never reaches), or <code>deferred</code> (a JavaScript statement that compiles to a throw of its diagnostic, which happens only if it executes).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>line</code>, <code>column</code></td>
|
||||
<td>1-based line and UTF-16 column; <code>start</code> and <code>end</code> are 0-based UTF-16 offsets.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>group</code></td>
|
||||
<td>The <code>id</code> of the root-cause group the diagnostic belongs to. A group's <code>primary</code> site is its first in source order.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## Type errors
|
||||
|
||||
Coverage requires a program that typechecks. If type checking fails, the report identifies the errors instead of producing statement counts. Fix those errors before interpreting compilation coverage.
|
||||
|
||||
scriptc type-checks with its own settings: strict null checks, the ES2025 library, and Node.js types (its bundled declarations when the project has no `@types/node`). An error those settings cause, rather than the project's own `tsc` configuration, carries a hint naming the cause, such as a library member newer than ES2025 or a global of another runtime, and machine-readable output reports it in the `environment` category.
|
||||
|
||||
## External host type declarations
|
||||
|
||||
An embedder may provide modules that are absent from `node_modules`. Use `--external-types` to map an exact bare module specifier to the embedder's declaration file during analysis. For example: `scriptc coverage src/core.ts --external-types @native-sdk/core=types/native-sdk-core.d.ts`.
|
||||
|
||||
@@ -98,7 +98,7 @@ Native error-first filesystem callbacks can be stored and passed to platform ada
|
||||
|
||||
`node:worker_threads` supports native threads in static executables. Worker entry points must resolve at build time: use a source-relative `new URL("./worker.ts", import.meta.url)`, the current module URL, an absolute path, or CommonJS `__filename`. Each worker initializes its own module state, event loop, environment snapshot, and reference-counted heap. Nested workers inherit their parent's environment snapshot. Supported constructor options are `workerData` and `argv`; runtime paths, `eval`, loaders, custom stdio, shared environments, resource limits, and transfer lists are unsupported. Worker argument strings containing null bytes are also unsupported.
|
||||
|
||||
Workers support `online`, `message`, `error`, and `exit` events, `postMessage`, `terminate`, `ref`, and `unref`. The parent port supports message listeners, `postMessage`, `close`, `start`, `ref`, `unref`, and `hasRef`. Messages clone primitives, plain object graphs, arrays, errors, ArrayBuffers, typed arrays, and DataViews, preserving cycles and aliases. Functions, proxies, other native handles, and nonempty transfer lists are refused. SharedArrayBuffers share their backing storage while retaining separate wrappers in each thread.
|
||||
Workers support `online`, `message`, `error`, and `exit` events, `postMessage`, `terminate`, `ref`, and `unref`. The parent port supports message listeners, `postMessage`, `close`, `start`, `ref`, `unref`, and `hasRef`. Messages clone primitives, plain object graphs, arrays, errors, ArrayBuffers, typed arrays, and DataViews, preserving cycles and aliases. Program class instances arrive as plain objects with their own data properties, without their prototype, methods, accessors, or private fields, as in Node. Functions, proxies, other native handles, and nonempty transfer lists are refused. SharedArrayBuffers share their backing storage while retaining separate wrappers in each thread.
|
||||
|
||||
SharedArrayBuffer storage is fixed-length. Atomics supports integer `load`, `store`, `exchange`, `compareExchange`, `add`, `sub`, `and`, `or`, and `xor` over 8-, 16-, and 32-bit integer arrays. `wait` and `notify` use Int32Array views; BigInt atomics, `waitAsync`, and growable buffers are unsupported. `terminate` unwinds at runtime checkpoints, including loops and parked atomic waits, without running JavaScript catch or finally blocks. Blocking native calls finish before the next checkpoint.
|
||||
|
||||
@@ -144,25 +144,29 @@ Compiled code has runtime behavior that can differ from Node.js. Account for the
|
||||
|
||||
### Array bounds and runtime errors
|
||||
|
||||
Statically typed numeric reads from typed arrays abort the process for invalid indexes. Node.js returns `undefined` for these reads. Invalid numeric indexed writes are ignored, while the assignment expression retains its original value.
|
||||
Statically typed numeric reads from typed arrays throw a catchable `RangeError` for invalid indexes, because a typed numeric read cannot produce `undefined`. Node.js returns `undefined` for these reads. Invalid numeric indexed writes are ignored, while the assignment expression retains its original value.
|
||||
|
||||
Ordinary arrays track holes separately from present `undefined` values. Missing reads and empty `pop()`/`shift()` return `undefined`, indexed writes can grow the array, and length growth creates holes.
|
||||
|
||||
Runtime traps cannot be caught. User-thrown values and supported exceptions, including JSON parse, checked-cast, filesystem, and regex errors, can be caught; hard traps such as invalid typed-array reads terminate the process.
|
||||
Runtime traps cannot be caught. User-thrown values and supported exceptions, including JSON parse, checked-cast, filesystem, regex, invalid typed-array read, and `String.prototype.repeat` count errors, can be caught; hard traps such as out-of-range reads of statically typed ordinary arrays terminate the process.
|
||||
|
||||
Deep recursion that exhausts the native stack terminates the process with a signal instead of throwing Node's catchable `RangeError: Maximum call stack size exceeded`.
|
||||
|
||||
### Runtime type checks
|
||||
|
||||
Casts from dynamic data validate the value. For example, `JSON.parse(s) as Config` throws a catchable error if the data does not match `Config`, identifying the offending path, such as `expected number at $.port, got string`. A TypeScript assertion alone does not perform this validation when run under Node.js.
|
||||
|
||||
Casts between program classes are checked too: casting an instance to a class it does not extend throws a `TypeError`, where Node.js reads the members structurally. `SC6004` reports calls that pass such an instance to a function that casts it.
|
||||
|
||||
A Promise converted from `unknown` to a typed result validates its fulfilled value. A mismatch rejects with a `TypeError`. Rejection reasons and reference equality are preserved across conversions.
|
||||
|
||||
### Record identity and property order
|
||||
|
||||
A record converted to a strict field-subset shape is copied. Mutations through the narrower reference do not affect the original. Empty record layouts are also snapshots because they have no property storage.
|
||||
A record converted to a strict field-subset shape is copied. Mutations through the narrower reference do not affect the original, and the copy is not `===` to it. Empty record layouts are also snapshots because they have no property storage. `scriptc coverage` and `scriptc build` report `SC6001` where a copied value is then written or compared.
|
||||
|
||||
Typed records with declared fields, indexed records, and arrays retain identity and mutations through native checked storage. Class instances retain identity at their exact type or a base class. Dense array literals created directly in checked storage preserve identity, and supported native handles pass by reference.
|
||||
|
||||
`Object.keys`, `Object.values`, `Object.entries`, and `JSON.stringify` use a record's declaration order rather than per-object insertion order. Their order matches Node.js when objects are built in declaration order.
|
||||
`Object.keys`, `Object.values`, `Object.entries`, and `JSON.stringify` use a record's declaration order rather than per-object insertion order. Their order matches Node.js when objects are built in declaration order. `SC6002` reports object literals whose order differs where the program observes it.
|
||||
|
||||
### Buffer construction
|
||||
|
||||
@@ -178,13 +182,15 @@ Callback values use checked storage. Typed records and arrays retain identity an
|
||||
|
||||
### Strings and sorting
|
||||
|
||||
Strings are stored as UTF-8. Length and string methods use UTF-16 semantics, but relational comparisons such as `<` and `>` use code-point order. Operations that split a surrogate pair produce U+FFFD. `localeCompare` compares code units without ICU collation.
|
||||
Strings are stored as UTF-8. Length and string methods use UTF-16 semantics, but relational comparisons such as `<` and `>` use code-point order. Operations that split a surrogate pair produce U+FFFD.
|
||||
|
||||
One-argument `localeCompare` follows Node's default root-locale collation for Latin-script text (ASCII, Latin-1 Supplement, Latin Extended-A, and combining diacritical marks): letters order before case and accents, so `"a".localeCompare("B")` is `-1`. Strings containing other characters compare by code point, which can differ from Node's ICU collation; `SC6003` reports such string literals. The locales and options arguments are unsupported.
|
||||
|
||||
`sort` and `toSorted` use stable natural merge sort. Default ordering supports string, number, and boolean arrays, comparing their string forms in UTF-16 order; for example, `[2, 10, 1].sort()` produces `[1, 10, 2]`. `sort` preserves holes at the end, while `toSorted` produces a fresh array with holes filled by `undefined`. With an explicit comparator, consistent comparators produce the same sorted results as Node, but callback order and count can differ.
|
||||
|
||||
### Dates and timezones
|
||||
|
||||
String constructors and `Date.parse` accept ECMAScript date-only forms, date-times with an explicit `Z` or numeric offset, and GMT certificate-validity strings from the supported X509 APIs. Other V8-specific forms and offset-less local date-times produce an Invalid Date or `NaN`.
|
||||
String constructors and `Date.parse` accept ECMAScript date-only forms, date-times with an explicit `Z` or numeric offset, and GMT certificate-validity strings from the supported X509 APIs. Other V8-specific forms and offset-less local date-times produce an Invalid Date or `NaN`; `SC6005` reports string literals in those forms.
|
||||
|
||||
Local calendar getters use the operating system's timezone database. Historical results can differ from Node.js when timezone data differs. For valid extreme years outside a platform calendar API's range, scriptc uses a calendar-equivalent year in the 400-year Gregorian cycle to obtain the timezone rule.
|
||||
|
||||
@@ -208,9 +214,11 @@ JavaScript can store `process.stdin`, `stdout`, and `stderr`, replace output `wr
|
||||
|
||||
### Process arguments and errors
|
||||
|
||||
`process.argv[0]` is `"scriptc"`, `process.argv[1]` is the binary's path, and program arguments begin at index 2.
|
||||
`process.argv[0]` is `"scriptc"`, `process.argv[1]` is the binary's path, and program arguments begin at index 2. `process.version` and `process.versions.node` report the Node.js release the runtime is verified against.
|
||||
|
||||
Uncaught exceptions print `Uncaught <value>` to stderr instead of Node.js's stack-trace block. Exit codes and output written before the exception are preserved. Runtime errors expose `message` and Node's `code`, but not `errno`, `syscall`, or `path`.
|
||||
Uncaught exceptions print `Uncaught <value>` to stderr instead of Node.js's source excerpt and stack-trace block; thrown strings and numbers print their value. Exit codes and output written before the exception are preserved. Runtime errors expose `message` and Node's `code`, but not `errno`, `syscall`, or `path`.
|
||||
|
||||
Errors carry no stack frames. `console.log` and `util.inspect` print an Error in Node's stackless form, `[TypeError: message]`, where Node prints `TypeError: message` followed by stack frames; the two agree when `Error.stackTraceLimit` is `0`.
|
||||
|
||||
## Embedded JavaScript engine
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@ async function tryFastPath(): Promise<number | null> {
|
||||
return values.help ? 0 : 1;
|
||||
}
|
||||
const [command, inputArg] = positionals;
|
||||
if (command === "coverage") return coverageFastPath(values, inputArg, positionals.length);
|
||||
if (
|
||||
(command !== "build" && command !== "run") ||
|
||||
inputArg === undefined ||
|
||||
@@ -163,6 +164,64 @@ async function tryFastPath(): Promise<number | null> {
|
||||
});
|
||||
}
|
||||
|
||||
/** An unchanged program's coverage verdict replays from the verdict cache
|
||||
* without loading the compiler graph. Anything else (a miss, a comment-only
|
||||
* edit, options the replay does not cover) defers to the full CLI. */
|
||||
async function coverageFastPath(
|
||||
values: ReturnType<
|
||||
typeof parseArgs<{ options: typeof CLI_OPTIONS; allowPositionals: true; allowNegative: true }>
|
||||
>["values"],
|
||||
inputArg: string | undefined,
|
||||
positionalCount: number,
|
||||
): Promise<number | null> {
|
||||
const print = values.print;
|
||||
const failOn = values["fail-on"];
|
||||
if (
|
||||
inputArg === undefined ||
|
||||
positionalCount !== 2 ||
|
||||
(print !== undefined && print !== "diagnostics") ||
|
||||
(failOn !== undefined && failOn !== "blockers" && failOn !== "divergences") ||
|
||||
values.emit !== undefined ||
|
||||
values.strip ||
|
||||
values.lib ||
|
||||
values.profile !== undefined ||
|
||||
values.ffi !== undefined ||
|
||||
values.backend !== undefined ||
|
||||
values.optimization !== undefined ||
|
||||
values["windows-subsystem"] !== undefined ||
|
||||
values["provenance-sources"] ||
|
||||
values["emit-ir"] ||
|
||||
(values["npm-static"] ?? []).length > 0 ||
|
||||
(values["external-types"] ?? []).length > 0
|
||||
)
|
||||
return null;
|
||||
let startup: typeof import("@scriptc/compiler/startup-cache");
|
||||
try {
|
||||
startup = await import("@scriptc/compiler/startup-cache");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
const hit = await startup
|
||||
.readCoverageVerdict(resolve(inputArg), { dynamic: values.dynamic })
|
||||
.catch(() => null);
|
||||
if (hit === null) return null;
|
||||
const { coverage, sourceTexts } = hit;
|
||||
if (print === "diagnostics") {
|
||||
const envelope = startup.coverageEnvelope(coverage, {
|
||||
compilerVersion: packageVersion(),
|
||||
sourceTexts,
|
||||
...(failOn === undefined ? {} : { failOn }),
|
||||
});
|
||||
process.stdout.write(`${JSON.stringify(envelope, null, 2)}\n`);
|
||||
} else {
|
||||
const color = process.stdout.isTTY ?? false;
|
||||
process.stdout.write(
|
||||
startup.renderCoverage(coverage, { color, sourceTexts, root: process.cwd() }) + "\n",
|
||||
);
|
||||
}
|
||||
return startup.coveragePasses(coverage, failOn) ? 0 : 1;
|
||||
}
|
||||
|
||||
const fastExit = await tryFastPath();
|
||||
if (fastExit === null) {
|
||||
await import("./main.js");
|
||||
|
||||
@@ -3,7 +3,7 @@ import { existsSync, readFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import {
|
||||
analyze,
|
||||
analyzeCached,
|
||||
compile,
|
||||
compileLibrary,
|
||||
resolveProvenanceSources,
|
||||
@@ -27,7 +27,7 @@ function version(): string {
|
||||
|
||||
process.exitCode = await runCli(process.argv.slice(2), {
|
||||
version,
|
||||
analyze: async (entry, options) => analyze(entry, options),
|
||||
analyze: analyzeCached,
|
||||
compile,
|
||||
compileLibrary,
|
||||
resolveProvenanceSources,
|
||||
|
||||
@@ -214,3 +214,39 @@ test.skipIf(!runtimePackHost)(
|
||||
},
|
||||
120_000,
|
||||
);
|
||||
|
||||
test("bootstrap replays an unchanged coverage verdict without loading the compiler graph", async () => {
|
||||
const work = await mkdtemp(join(tmpdir(), "scriptc-bootstrap-coverage-"));
|
||||
const preload = join(work, "preload.mjs");
|
||||
const entry = join(work, "main.ts");
|
||||
const env = { ...process.env, SCRIPTC_CACHE_DIR: join(work, "cache") };
|
||||
try {
|
||||
await writeFile(
|
||||
preload,
|
||||
[
|
||||
"import { registerHooks } from 'node:module';",
|
||||
"registerHooks({ load(url, context, nextLoad) {",
|
||||
" if (url.includes('/packages/compiler/dist/index.js')) throw new Error('compiler graph loaded');",
|
||||
" return nextLoad(url, context);",
|
||||
"}});",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
await writeFile(entry, "const [[a]] = [[1]] as number[][];\nconsole.log(a);\n");
|
||||
const first = await execFileAsync(process.execPath, [bootstrap, "coverage", entry], { env });
|
||||
expect(first.stdout).toContain("statements analyzed");
|
||||
const replayed = await execFileAsync(
|
||||
process.execPath,
|
||||
["--import", preload, bootstrap, "coverage", entry],
|
||||
{ env },
|
||||
);
|
||||
expect(replayed.stdout).toBe(first.stdout);
|
||||
// A changed source must load the compiler again.
|
||||
await writeFile(entry, "console.log(1);\n");
|
||||
await expect(
|
||||
execFileAsync(process.execPath, ["--import", preload, bootstrap, "coverage", entry], { env }),
|
||||
).rejects.toThrow("compiler graph loaded");
|
||||
} finally {
|
||||
await rm(work, { recursive: true, force: true }).catch(() => undefined);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -181,6 +181,8 @@ declare var process: {
|
||||
/* Stable native dictionary. node and openssl name compatibility targets;
|
||||
* components absent from the runtime have no entries. */
|
||||
readonly versions: NodeJS.ProcessVersions;
|
||||
/* "v" plus versions.node: the compatibility target's release. */
|
||||
readonly version: string;
|
||||
/* The build-configuration snapshot Node exposes from its gyp config.
|
||||
* A scriptc binary has no V8 and no gyp, so `variables` answers the
|
||||
* honest capability record (v8_enable_i18n_support: 0 — no ICU; asan
|
||||
|
||||
@@ -1498,6 +1498,7 @@ export class LlEmitter {
|
||||
if (entryMayThrow || runsLoop) this.declare(`declare zeroext i1 @scr_exc_pending()`);
|
||||
if (runsLoop) {
|
||||
this.declare(`declare zeroext i1 @scr_loop_run(ptr)`);
|
||||
if (this.mod.commonJsEntry === true) this.declare(`declare void @scr_loop_commonjs_entry()`);
|
||||
this.declare(`declare zeroext i1 @scr_report_unhandled_rejections()`);
|
||||
this.declare(`declare void @scr_discard_unhandled_rejections()`);
|
||||
}
|
||||
@@ -2000,6 +2001,7 @@ export class LlEmitter {
|
||||
// both exit 1, like Node — the C main's loop block exactly.
|
||||
...(runsLoop
|
||||
? [
|
||||
...(this.mod.commonJsEntry === true ? [` call void @scr_loop_commonjs_entry()`] : []),
|
||||
` %loop_rejection = call zeroext i1 @scr_loop_run(ptr ${asyncEntry ? "%top" : "null"})`,
|
||||
` %lexc = call zeroext i1 @scr_exc_pending()`,
|
||||
` br i1 %lexc, label %luncaught, label %lok`,
|
||||
|
||||
@@ -108,9 +108,7 @@ export function emitBytesIndex(
|
||||
B.startBlock(invalid);
|
||||
if (skipInvalid) B.br(skipInvalid);
|
||||
else {
|
||||
host.declare(`declare double @scr_bytes_get(ptr, double)`);
|
||||
B.line(`call double @scr_bytes_get(ptr ${receiver}, double ${index})`);
|
||||
B.terminate("unreachable");
|
||||
emitInvalidBytesRead(host, receiver, index);
|
||||
}
|
||||
B.startBlock(valid);
|
||||
if (host.sizeType === "i64") return wideIndex;
|
||||
@@ -144,15 +142,22 @@ export function emitBytesIndex(
|
||||
B.startBlock(invalid);
|
||||
if (skipInvalid) B.br(skipInvalid);
|
||||
else {
|
||||
host.declare(`declare double @scr_bytes_get(ptr, double)`);
|
||||
B.line(`call double @scr_bytes_get(ptr ${receiver}, double ${index})`);
|
||||
B.terminate("unreachable");
|
||||
emitInvalidBytesRead(host, receiver, index);
|
||||
}
|
||||
|
||||
B.startBlock(valid);
|
||||
return idx;
|
||||
}
|
||||
|
||||
/** An invalid typed-array read: the runtime throws Node's catchable
|
||||
* RangeError, and this path unwinds to the handler (it never continues). */
|
||||
function emitInvalidBytesRead(host: LlvmEmitterContext, receiver: string, index: string): void {
|
||||
host.declare(`declare double @scr_bytes_get(ptr, double)`);
|
||||
host.B.line(`call double @scr_bytes_get(ptr ${receiver}, double ${index})`);
|
||||
host.emitPendingCheck();
|
||||
host.B.terminate("unreachable");
|
||||
}
|
||||
|
||||
export function emitBytesData(host: LlvmEmitterContext, receiver: string): string {
|
||||
const p = host.B.tmp();
|
||||
const data = host.B.tmp();
|
||||
@@ -193,6 +198,7 @@ export function emitBytesGet(
|
||||
host.declare(`declare double @scr_bytes_get(ptr, double)`);
|
||||
const result = B.tmp();
|
||||
B.line(`${result} = call double @scr_bytes_get(ptr ${receiver}, double ${index.name})`);
|
||||
host.emitPendingCheck();
|
||||
return { name: result, type: F64 };
|
||||
}
|
||||
const idx = host.emitBytesIndex(receiver, index, expr, inBounds);
|
||||
|
||||
@@ -273,14 +273,18 @@ export function emitStrIntrinsic(
|
||||
"ptr",
|
||||
true,
|
||||
);
|
||||
case "repeat":
|
||||
return call(
|
||||
case "repeat": {
|
||||
// A negative or infinite count throws Node's catchable RangeError.
|
||||
const result = call(
|
||||
"scr_str_repeat",
|
||||
"ptr (ptr, double)",
|
||||
`ptr ${r.name}, double ${args[0]!.name}`,
|
||||
"ptr",
|
||||
true,
|
||||
);
|
||||
host.emitPendingCheck();
|
||||
return result;
|
||||
}
|
||||
case "trim":
|
||||
return call("scr_str_trim", "ptr (ptr)", `ptr ${r.name}`, "ptr", true);
|
||||
case "trimStart":
|
||||
|
||||
@@ -76,6 +76,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"math.random": "scr_math_random",
|
||||
"num.parseInt": "scr_parse_int",
|
||||
"num.parseFloat": "scr_parse_float",
|
||||
"str.localeCompare": "scr_str_locale_compare",
|
||||
"num.fromString": "scr_string_to_number",
|
||||
"math.round": "scr_math_round",
|
||||
"math.sin": "sin",
|
||||
|
||||
@@ -324,7 +324,9 @@ test.each(methodCases)(
|
||||
const ir = body(mod(f), "sc_bf_method", bits);
|
||||
expect(ir).toContain(`@scr_str_${target}(`);
|
||||
expect(ir).not.toContain("@scr_str_retain_v");
|
||||
if (method !== "normalize") expect(ir).not.toContain("@scr_str_release");
|
||||
// Throwing methods release their owned result on the unwind path.
|
||||
if (method !== "normalize" && method !== "repeat")
|
||||
expect(ir).not.toContain("@scr_str_release");
|
||||
expect(ir).toContain(
|
||||
result.kind === "f64" ? "ret double" : result.kind === "bool" ? "ret i1" : "ret ptr",
|
||||
);
|
||||
|
||||
@@ -231,7 +231,8 @@ export function computeMayThrow(mod: IrModule): {
|
||||
// setFrom and the numeric read/write families throw catchable
|
||||
// RangeErrors (Node's bounds discipline); the rest trap or
|
||||
// cannot fail.
|
||||
if (MAY_THROW_BYTES_METHODS.has(rec.method)) {
|
||||
// An out-of-range `get` throws the catchable RangeError too.
|
||||
if (MAY_THROW_BYTES_METHODS.has(rec.method) || rec.method === "get") {
|
||||
f.throws = true;
|
||||
}
|
||||
break;
|
||||
@@ -244,7 +245,7 @@ export function computeMayThrow(mod: IrModule): {
|
||||
f.throws = true;
|
||||
break;
|
||||
case "strIntrinsic":
|
||||
if (rec.method === "normalize") f.throws = true;
|
||||
if (rec.method === "normalize" || rec.method === "repeat") f.throws = true;
|
||||
break;
|
||||
case "regexIntrinsic":
|
||||
// Keep the conservative exception check for these operations;
|
||||
|
||||
@@ -3,7 +3,14 @@ import { dirname, join, resolve } from "node:path";
|
||||
import { parseArgs } from "node:util";
|
||||
import { isExactExternalTypeSpecifier } from "../frontend/program.js";
|
||||
import { renderDiagnostics } from "../diagnostics/render.js";
|
||||
import { renderCoverage } from "../coverage/report.js";
|
||||
import {
|
||||
coverageEnvelope,
|
||||
coveragePasses,
|
||||
renderCoverage,
|
||||
type CoverageFailOn,
|
||||
} from "../coverage/report.js";
|
||||
import { buildEnvelope } from "../diagnostics/envelope.js";
|
||||
import { renderWarnings } from "../diagnostics/render.js";
|
||||
import { isNativeOptimization } from "../backend/optimization.js";
|
||||
import { setProvenanceSources } from "../frontend/provenance-registry.js";
|
||||
import type { CliHost, NativeCacheWarmProfile } from "./host.js";
|
||||
@@ -74,6 +81,7 @@ async function main(args: string[], host: CliHost): Promise<number> {
|
||||
values.backend !== undefined ||
|
||||
values.emit !== undefined ||
|
||||
values.print !== undefined ||
|
||||
values["fail-on"] !== undefined ||
|
||||
values.ffi !== undefined ||
|
||||
values.profile !== undefined ||
|
||||
values.strip ||
|
||||
@@ -138,6 +146,7 @@ async function main(args: string[], host: CliHost): Promise<number> {
|
||||
values.backend !== undefined ||
|
||||
values.emit !== undefined ||
|
||||
values.print !== undefined ||
|
||||
values["fail-on"] !== undefined ||
|
||||
values.optimization !== undefined ||
|
||||
values.strip ||
|
||||
values.ffi !== undefined ||
|
||||
@@ -189,10 +198,28 @@ async function main(args: string[], host: CliHost): Promise<number> {
|
||||
if (command === "coverage" && values.strip) {
|
||||
fail(`--strip is a build/run option\n\n${USAGE}`);
|
||||
}
|
||||
if (values.print !== undefined && values.print !== "native-link-info") {
|
||||
fail(`unknown print kind "${values.print}" (supported: native-link-info)\n\n${USAGE}`);
|
||||
if (
|
||||
values.print !== undefined &&
|
||||
values.print !== "native-link-info" &&
|
||||
values.print !== "diagnostics"
|
||||
) {
|
||||
fail(
|
||||
`unknown print kind "${values.print}" (supported: native-link-info, diagnostics)\n\n${USAGE}`,
|
||||
);
|
||||
}
|
||||
const printNativeLinkInfo = values.print === "native-link-info";
|
||||
const printDiagnostics = values.print === "diagnostics";
|
||||
if (printDiagnostics && command === "run") {
|
||||
fail(`--print=diagnostics is a build/coverage option\n\n${USAGE}`);
|
||||
}
|
||||
const failOnRaw = values["fail-on"];
|
||||
if (failOnRaw !== undefined && failOnRaw !== "blockers" && failOnRaw !== "divergences") {
|
||||
fail(`unknown --fail-on value "${failOnRaw}" (supported: blockers, divergences)\n\n${USAGE}`);
|
||||
}
|
||||
if (failOnRaw !== undefined && command !== "coverage") {
|
||||
fail(`--fail-on is a coverage option (a build already fails on blockers)\n\n${USAGE}`);
|
||||
}
|
||||
const failOn = failOnRaw as CoverageFailOn | undefined;
|
||||
if (printNativeLinkInfo && command !== "build") {
|
||||
fail(`--print=native-link-info is a build option\n\n${USAGE}`);
|
||||
}
|
||||
@@ -311,9 +338,21 @@ async function main(args: string[], host: CliHost): Promise<number> {
|
||||
...(ffiProfilePath !== undefined ? { ffiProfilePath } : {}),
|
||||
...(Object.keys(externalTypes).length > 0 ? { externalTypes } : {}),
|
||||
});
|
||||
const color = process.stdout.isTTY ?? false;
|
||||
process.stdout.write(renderCoverage(coverage, { color, sourceTexts }) + "\n");
|
||||
return coverage.preflightFailed ? 1 : 0;
|
||||
if (printDiagnostics) {
|
||||
// Keep stdout pure JSON for tooling.
|
||||
const envelope = coverageEnvelope(coverage, {
|
||||
compilerVersion: host.version(),
|
||||
sourceTexts,
|
||||
...(failOn === undefined ? {} : { failOn }),
|
||||
});
|
||||
process.stdout.write(`${JSON.stringify(envelope, null, 2)}\n`);
|
||||
} else {
|
||||
const color = process.stdout.isTTY ?? false;
|
||||
process.stdout.write(
|
||||
renderCoverage(coverage, { color, sourceTexts, root: process.cwd() }) + "\n",
|
||||
);
|
||||
}
|
||||
return coveragePasses(coverage, failOn) ? 0 : 1;
|
||||
}
|
||||
|
||||
if (output === null || !output.ok) throw new Error("internal output-option state");
|
||||
@@ -328,6 +367,7 @@ async function main(args: string[], host: CliHost): Promise<number> {
|
||||
);
|
||||
|
||||
let nativeLinkInfo: object | undefined;
|
||||
let diagnosticsEnvelope: object | undefined;
|
||||
const build = async (): Promise<string> => {
|
||||
const result = await host.compile(input, {
|
||||
outPath,
|
||||
@@ -345,6 +385,17 @@ async function main(args: string[], host: CliHost): Promise<number> {
|
||||
...(printNativeLinkInfo ? { nativeLinkInfo: true } : {}),
|
||||
});
|
||||
if (!result.ok) {
|
||||
if (printDiagnostics) {
|
||||
const envelope = buildEnvelope({
|
||||
compilerVersion: host.version(),
|
||||
entry: input,
|
||||
ok: false,
|
||||
diagnostics: result.diagnostics,
|
||||
sourceTexts: result.sourceTexts,
|
||||
});
|
||||
process.stdout.write(`${JSON.stringify(envelope, null, 2)}\n`);
|
||||
throw new CliExit(1);
|
||||
}
|
||||
const color = process.stderr.isTTY ?? false;
|
||||
process.stderr.write(
|
||||
renderDiagnostics(result.diagnostics, result.sourceTexts, { color }) + "\n",
|
||||
@@ -353,6 +404,26 @@ async function main(args: string[], host: CliHost): Promise<number> {
|
||||
process.stderr.write(`\n${n} error${n === 1 ? "" : "s"}.\n`);
|
||||
throw new CliExit(1);
|
||||
}
|
||||
const warnings = result.warnings ?? [];
|
||||
if (printDiagnostics) {
|
||||
diagnosticsEnvelope = buildEnvelope({
|
||||
compilerVersion: host.version(),
|
||||
entry: input,
|
||||
ok: true,
|
||||
artifact: result.artifact.path,
|
||||
diagnostics: [],
|
||||
warnings,
|
||||
...(result.sourceTexts === undefined ? {} : { sourceTexts: result.sourceTexts }),
|
||||
});
|
||||
} else if (warnings.length > 0 && command === "build") {
|
||||
// Divergence warnings never fail a build; they explain where the
|
||||
// native program can behave differently from Node. `run` keeps
|
||||
// stderr for the program's own output.
|
||||
const color = process.stderr.isTTY ?? false;
|
||||
process.stderr.write(
|
||||
renderWarnings(warnings, result.sourceTexts ?? new Map(), { color }) + "\n",
|
||||
);
|
||||
}
|
||||
if (result.artifact.kind === "exe") {
|
||||
if (!values["keep-llvm"]) rmSync(result.artifact.translationUnitPath, { force: true });
|
||||
} else if (result.artifact.kind === "obj") {
|
||||
@@ -367,7 +438,9 @@ async function main(args: string[], host: CliHost): Promise<number> {
|
||||
return host.run(binary);
|
||||
}
|
||||
|
||||
if (printNativeLinkInfo) {
|
||||
if (printDiagnostics) {
|
||||
process.stdout.write(`${JSON.stringify(diagnosticsEnvelope, null, 2)}\n`);
|
||||
} else if (printNativeLinkInfo) {
|
||||
if (nativeLinkInfo === undefined) throw new Error("internal native-link-info state");
|
||||
// Keep stdout pure JSON for tooling; the ordinary artifact path is in
|
||||
// program.object inside the document.
|
||||
|
||||
@@ -22,7 +22,13 @@ Options:
|
||||
--emit <kind> primary output: ir, llvm, asm, obj, or exe
|
||||
(default: exe). asm/obj use the matching platform helper
|
||||
--print <kind> print machine-readable metadata instead of the output path
|
||||
(native-link-info implies --emit=obj and never links)
|
||||
(native-link-info implies --emit=obj and never links;
|
||||
diagnostics prints the versioned JSON diagnostics
|
||||
envelope for build and coverage)
|
||||
--fail-on <blockers|divergences>
|
||||
coverage only: exit 1 when the entry path has blockers
|
||||
(a build would fail), or also when it has Node
|
||||
divergences
|
||||
--backend <b> code generator (llvm)
|
||||
--optimization <release|dev|speed>
|
||||
native optimization posture (default: release/-O2). dev
|
||||
@@ -70,6 +76,7 @@ export const CLI_OPTIONS = {
|
||||
out: { type: "string", short: "o" },
|
||||
emit: { type: "string" },
|
||||
print: { type: "string" },
|
||||
"fail-on": { type: "string" },
|
||||
backend: { type: "string" },
|
||||
optimization: { type: "string" },
|
||||
strip: { type: "boolean", default: false },
|
||||
|
||||
@@ -93,8 +93,21 @@ export type CompileFailure = {
|
||||
sourceTexts: Map<string, string>;
|
||||
};
|
||||
|
||||
/** Non-failing findings of a successful build: divergence warnings
|
||||
* (SC6xxx — sites that compile but can behave differently from Node), with
|
||||
* the sources they point into. Present only when the build lowered the
|
||||
* program and found at least one; a cache hit that skips lowering carries
|
||||
* none (`scriptc coverage` always reports them). */
|
||||
export interface CompileWarnings {
|
||||
warnings?: ScrDiagnostic[];
|
||||
sourceTexts?: Map<string, string>;
|
||||
}
|
||||
|
||||
export type CompileSourceResult =
|
||||
| { ok: true; artifact: Extract<CompileArtifact, { kind: "ir" | "llvm" | "asm" | "obj" }> }
|
||||
| ({
|
||||
ok: true;
|
||||
artifact: Extract<CompileArtifact, { kind: "ir" | "llvm" | "asm" | "obj" }>;
|
||||
} & CompileWarnings)
|
||||
| CompileFailure;
|
||||
|
||||
/** Historical executable result shape retained for source compatibility. */
|
||||
@@ -112,7 +125,7 @@ export type CompileExecutableResult =
|
||||
/** The generated LLVM source is retained beside the executable. */
|
||||
| (Extract<CompileResult, { ok: true }> & {
|
||||
artifact: Extract<CompileArtifact, { kind: "exe" }>;
|
||||
})
|
||||
} & CompileWarnings)
|
||||
| CompileFailure;
|
||||
|
||||
/** Result union for callers that choose outputKind dynamically. */
|
||||
|
||||
@@ -7,6 +7,18 @@
|
||||
* collects — the diagnostics registry is the coverage taxonomy.
|
||||
*/
|
||||
import type { Milestone, ScrDiagnostic } from "../diagnostics/diagnostic.js";
|
||||
import {
|
||||
DIAGNOSTICS_SCHEMA,
|
||||
DIAGNOSTICS_SCHEMA_VERSION,
|
||||
DYNAMIC_CAPABLE_CODES,
|
||||
SourceLocator,
|
||||
groupDiagnostics,
|
||||
type DiagnosticGroup,
|
||||
type DiagnosticScope,
|
||||
type DiagnosticsEnvelope,
|
||||
type LocatedDiagnostic,
|
||||
type SourcePosition,
|
||||
} from "../diagnostics/envelope.js";
|
||||
import { renderDiagnostics } from "../diagnostics/render.js";
|
||||
import type { LowerStats } from "../frontend/lowering/lowerer.js";
|
||||
import type { NpmBuiltinUse, NpmLazyTrap } from "../frontend/npm.js";
|
||||
@@ -27,7 +39,11 @@ export interface CoverageInput {
|
||||
/** The unreached remainder: bodies nothing on the entry path reaches,
|
||||
* lowered in a throwaway analysis pass. Its blockers cannot fail a
|
||||
* build — the report shows them as a secondary, dimmed group. */
|
||||
unreached?: { stats: LowerStats; diagnostics: ScrDiagnostic[] };
|
||||
unreached?: { stats: LowerStats; diagnostics: ScrDiagnostic[]; runtimeFences?: ScrDiagnostic[] };
|
||||
/** Compile-time-detectable behavior differences from Node (SC6xxx):
|
||||
* the program compiles, but these sites can behave differently natively.
|
||||
* Never blockers; `--fail-on=divergences` opts into failing on them. */
|
||||
divergences?: ScrDiagnostic[];
|
||||
/** --dynamic only: every Node builtin the embedded npm graph imports —
|
||||
* what the island must provide for this program (unshimmed ones also
|
||||
* appear as SC2030 blockers, EXCEPT lazily-reached ones, which embed
|
||||
@@ -75,23 +91,53 @@ const MILESTONE_ORDER: Milestone[] = ["M1", "M2", "M3", "M4", "M5", "later"];
|
||||
|
||||
/** Codes whose construct RUNS under --dynamic (the embedded engine) — the
|
||||
* report separates "one flag away" from "cannot compile at all". */
|
||||
const DYNAMIC_CAPABLE = new Set(["SC2010", "SC2011", "SC2012", "SC2013"]);
|
||||
const DYNAMIC_CAPABLE = DYNAMIC_CAPABLE_CODES;
|
||||
|
||||
/** Human rows cap their message: long embedded type text would otherwise
|
||||
* push lines past several hundred columns. The full message is in the
|
||||
* --print=diagnostics envelope (and in `scriptc build` output). */
|
||||
const MESSAGE_CAP = 200;
|
||||
/** Sites listed per group before "+N more". */
|
||||
const SITES_SHOWN = 3;
|
||||
|
||||
interface Blocker {
|
||||
code: string;
|
||||
/** Message with the " (planned: ...)" suffix stripped — the milestone gets
|
||||
* its own column. */
|
||||
/** Root-cause message (per-instantiation type arguments collapsed). */
|
||||
what: string;
|
||||
milestone: Milestone | undefined;
|
||||
count: number;
|
||||
group: DiagnosticGroup;
|
||||
}
|
||||
|
||||
export function renderCoverage(
|
||||
input: CoverageInput,
|
||||
opts: { color?: boolean; sourceTexts?: Map<string, string> } = {},
|
||||
): string {
|
||||
export interface CoverageRenderOptions {
|
||||
color?: boolean;
|
||||
sourceTexts?: Map<string, string>;
|
||||
/** Paths under this directory render relative to it. */
|
||||
root?: string;
|
||||
}
|
||||
|
||||
export function renderCoverage(input: CoverageInput, opts: CoverageRenderOptions = {}): string {
|
||||
const c = (code: string, s: string) => (opts.color ? code + s + RESET : s);
|
||||
const out: string[] = [];
|
||||
const locator = new SourceLocator(opts.sourceTexts);
|
||||
const groupBlockers = (diags: ScrDiagnostic[], scope: DiagnosticScope = "reached") =>
|
||||
groupBlockersAt(diags, scope, locator);
|
||||
const where = (p: SourcePosition) => `${displayPath(p.file, opts.root)}:${p.line}:${p.column}`;
|
||||
// One root-cause group: count, code and message, then where it occurs
|
||||
// (first sites in source order) and what to do about it.
|
||||
const renderRows = (group: Blocker[], tone: string, dim = false) => {
|
||||
for (const b of group) {
|
||||
const what = capMessage(b.what);
|
||||
out.push(
|
||||
` ${c(dim ? DIM : tone, `×${b.count}`.padStart(4))} ${c(DIM, b.code)} ${dim ? c(DIM, what) : what}`,
|
||||
);
|
||||
const sites = b.group.sites;
|
||||
const shown = sites.slice(0, SITES_SHOWN).map(where).join(", ");
|
||||
const more = sites.length > SITES_SHOWN ? ` (+${sites.length - SITES_SHOWN} more)` : "";
|
||||
out.push(` ${c(DIM, `at ${shown}${more}`)}`);
|
||||
out.push(` ${c(DIM, `hint: ${b.group.hint}`)}`);
|
||||
}
|
||||
};
|
||||
out.push(`${c(BOLD, "scriptc coverage")} ${DIMPath(input.file, opts.color ?? false)}`);
|
||||
out.push("");
|
||||
|
||||
@@ -109,10 +155,17 @@ export function renderCoverage(
|
||||
}
|
||||
const tsc = input.diagnostics.filter((d) => d.code === "SC0001");
|
||||
if (tsc.length > 0) {
|
||||
const world = tsc.filter((d) => d.typeWorld === true).length;
|
||||
out.push(
|
||||
` ${c(RED, "not analyzable")}: ${tsc.length} TypeScript error${tsc.length === 1 ? "" : "s"} — ` +
|
||||
`fix type errors first (scriptc only analyzes programs that typecheck)`,
|
||||
);
|
||||
if (world > 0) {
|
||||
out.push(
|
||||
` ${c(YELLOW, "note")}: ${world === tsc.length ? (world === 1 ? "this error comes" : "these errors come") : `${world} of them come`} from scriptc's type world, ` +
|
||||
`not necessarily from the project's own tsc configuration — see each hint`,
|
||||
);
|
||||
}
|
||||
// The same rendered diagnostics a build prints: coverage is the
|
||||
// command people reach for first, so the verdict line alone would
|
||||
// swallow the code frames that say what to fix.
|
||||
@@ -127,13 +180,7 @@ export function renderCoverage(
|
||||
` ${c(RED, "analysis stopped at preflight")}: ${n} unsupported import site${n === 1 ? "" : "s"} — ` +
|
||||
`the module graph must load before statements can be counted`,
|
||||
);
|
||||
const blockers = groupBlockers(input.diagnostics);
|
||||
const widest = Math.max(...blockers.map((b) => b.what.length));
|
||||
for (const b of blockers) {
|
||||
out.push(
|
||||
` ${c(RED, `×${b.count}`.padStart(4))} ${b.what.padEnd(widest)} ${c(DIM, b.code)}`,
|
||||
);
|
||||
}
|
||||
renderRows(groupBlockers(input.diagnostics, "preflight"), RED);
|
||||
if (opts.sourceTexts) {
|
||||
out.push("");
|
||||
out.push(
|
||||
@@ -167,7 +214,7 @@ export function renderCoverage(
|
||||
}
|
||||
if (skipped > 0) {
|
||||
out.push(
|
||||
` ${c(DIM, `(+${skipped} function${skipped === 1 ? "" : "s"} not analyzed — signature blocked)`)}`,
|
||||
` ${c(DIM, `(${skipped} function${skipped === 1 ? "" : "s"} not analyzed — signature blocked; their statements count as not static)`)}`,
|
||||
);
|
||||
}
|
||||
out.push("");
|
||||
@@ -290,26 +337,35 @@ export function renderCoverage(
|
||||
// JS program never hides its dynamic gaps.
|
||||
const fences = input.runtimeFences ?? [];
|
||||
if (fences.length > 0) {
|
||||
const grouped = groupBlockers(fences);
|
||||
const widestF = Math.max(...grouped.map((b) => b.what.length));
|
||||
out.push(
|
||||
` ${c(YELLOW, "deferred to runtime")} ${fences.length} site${fences.length === 1 ? "" : "s"} ${c(DIM, "(JS statements that throw their fence if executed)")}`,
|
||||
);
|
||||
for (const b of grouped) {
|
||||
out.push(
|
||||
` ${c(YELLOW, `×${b.count}`.padStart(4))} ${b.what.padEnd(widestF)} ${c(DIM, b.code)}`,
|
||||
);
|
||||
}
|
||||
renderRows(groupBlockers(fences, "deferred"), YELLOW);
|
||||
out.push("");
|
||||
}
|
||||
|
||||
const unreachedDiags = un?.diagnostics ?? [];
|
||||
// Divergences: the program compiles, but these sites can behave
|
||||
// differently from Node. Never blockers (--fail-on=divergences opts in).
|
||||
const divergences = input.divergences ?? [];
|
||||
const renderDivergences = () => {
|
||||
if (divergences.length === 0) return;
|
||||
out.push(
|
||||
` ${c(YELLOW, "differs from Node")} ${divergences.length} site${divergences.length === 1 ? "" : "s"} ${c(DIM, "(compiles; the native program can behave differently)")}`,
|
||||
);
|
||||
renderRows(groupBlockers(divergences), YELLOW);
|
||||
};
|
||||
|
||||
const unreachedDiags = [...(un?.diagnostics ?? []), ...(un?.runtimeFences ?? [])];
|
||||
if (failed === 0 && input.diagnostics.length === 0 && unreachedDiags.length === 0) {
|
||||
out.push(
|
||||
island > 0
|
||||
? ` ${c(GREEN, "builds with --dynamic")} — no remaining blockers (the island sites above run in the embedded engine).`
|
||||
: ` ${c(GREEN, "fully static")} — this program has no dynamic remainder.`,
|
||||
);
|
||||
if (divergences.length > 0) {
|
||||
out.push("");
|
||||
renderDivergences();
|
||||
}
|
||||
return out.join("\n");
|
||||
}
|
||||
|
||||
@@ -319,18 +375,22 @@ export function renderCoverage(
|
||||
// Blockers split by what a flag can fix: sites that RUN under --dynamic
|
||||
// (the embedded engine) versus constructs with no lowering at all.
|
||||
const blockers = groupBlockers(input.diagnostics);
|
||||
const unreachedBlockers = groupBlockers(unreachedDiags);
|
||||
const unreachedBlockers = groupBlockers(unreachedDiags, "unreached");
|
||||
const dynamic = blockers.filter((b) => DYNAMIC_CAPABLE.has(b.code));
|
||||
const rejected = blockers.filter((b) => !DYNAMIC_CAPABLE.has(b.code));
|
||||
const widest = Math.max(...[...blockers, ...unreachedBlockers].map((b) => b.what.length));
|
||||
const renderGroup = (group: Blocker[], dim = false) => {
|
||||
for (const b of group) {
|
||||
const what = b.what.padEnd(widest);
|
||||
out.push(
|
||||
` ${c(dim ? DIM : RED, `×${b.count}`.padStart(4))} ${dim ? c(DIM, what) : what} ${c(DIM, b.code)}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
const renderGroup = (group: Blocker[], dim = false) => renderRows(group, RED, dim);
|
||||
if (
|
||||
failed > 0 &&
|
||||
blockers.length === 0 &&
|
||||
unreachedBlockers.length === 0 &&
|
||||
fences.length === 0
|
||||
) {
|
||||
// Every failed statement must be explained; reaching this is a
|
||||
// compiler bug, reported instead of silently omitted.
|
||||
out.push(
|
||||
` ${c(RED, "blockers:")} ${failed} statement${failed === 1 ? "" : "s"} failed without a diagnostic (a compiler bug — please report it)`,
|
||||
);
|
||||
}
|
||||
if (dynamic.length > 0) {
|
||||
const sites = dynamic.reduce((n, b) => n + b.count, 0);
|
||||
out.push(
|
||||
@@ -352,6 +412,10 @@ export function renderCoverage(
|
||||
);
|
||||
renderGroup(unreachedBlockers, true);
|
||||
}
|
||||
if (divergences.length > 0) {
|
||||
out.push("");
|
||||
renderDivergences();
|
||||
}
|
||||
return out.join("\n");
|
||||
}
|
||||
|
||||
@@ -359,19 +423,121 @@ function DIMPath(file: string, color: boolean): string {
|
||||
return color ? DIM + file + RESET : file;
|
||||
}
|
||||
|
||||
function groupBlockers(diags: ScrDiagnostic[]): Blocker[] {
|
||||
const byCode = new Map<string, Blocker>();
|
||||
for (const d of diags) {
|
||||
const what = d.message.replace(/ (?:is|are) not supported yet$/, "");
|
||||
const key = `${d.code}:${what}`;
|
||||
const existing = byCode.get(key);
|
||||
if (existing) existing.count++;
|
||||
else byCode.set(key, { code: d.code, what, milestone: d.milestone, count: 1 });
|
||||
}
|
||||
function groupBlockersAt(
|
||||
diags: ScrDiagnostic[],
|
||||
scope: DiagnosticScope,
|
||||
locator: SourceLocator,
|
||||
): Blocker[] {
|
||||
const { located, groups } = groupDiagnostics(diags, scope, locator);
|
||||
// A group's schedule is its first diagnostic's (one root cause, one
|
||||
// milestone).
|
||||
const milestone = new Map<string, Milestone | undefined>();
|
||||
diags.forEach((d, i) => {
|
||||
const id = located[i]!.group;
|
||||
if (!milestone.has(id)) milestone.set(id, d.milestone);
|
||||
});
|
||||
const rank = (m: Milestone | undefined) =>
|
||||
m === undefined ? MILESTONE_ORDER.length : MILESTONE_ORDER.indexOf(m);
|
||||
return [...byCode.values()].sort(
|
||||
(a, b) =>
|
||||
rank(a.milestone) - rank(b.milestone) || b.count - a.count || a.code.localeCompare(b.code),
|
||||
);
|
||||
return groups
|
||||
.map((g) => ({
|
||||
code: g.code,
|
||||
what: g.message,
|
||||
milestone: milestone.get(g.id),
|
||||
count: g.count,
|
||||
group: g,
|
||||
}))
|
||||
.sort(
|
||||
(a, b) =>
|
||||
rank(a.milestone) - rank(b.milestone) || b.count - a.count || a.code.localeCompare(b.code),
|
||||
);
|
||||
}
|
||||
|
||||
function capMessage(text: string): string {
|
||||
return text.length <= MESSAGE_CAP ? text : `${text.slice(0, MESSAGE_CAP - 1)}…`;
|
||||
}
|
||||
|
||||
function displayPath(file: string, root: string | undefined): string {
|
||||
if (root === undefined || root === "") return file;
|
||||
const prefix = root.endsWith("/") ? root : `${root}/`;
|
||||
return file.startsWith(prefix) ? file.slice(prefix.length) : file;
|
||||
}
|
||||
|
||||
/** What `coverage --fail-on` checks: reached blockers (they fail a build)
|
||||
* and, optionally, divergences. Unreached blockers never fail. */
|
||||
export type CoverageFailOn = "blockers" | "divergences";
|
||||
|
||||
/** Whether the analysis passes the selected gate. Preflight failures
|
||||
* always fail; `failOn` undefined means only preflight fails (the report's
|
||||
* historical contract). Divergence gating implies blocker gating. */
|
||||
export function coveragePasses(input: CoverageInput, failOn?: CoverageFailOn): boolean {
|
||||
if (input.preflightFailed) return false;
|
||||
if (failOn === undefined) return true;
|
||||
// Reached blockers are exactly the diagnostics a build would fail on;
|
||||
// runtime fences build (the statement throws only if it executes).
|
||||
if (input.diagnostics.length > 0) return false;
|
||||
if (failOn === "divergences" && (input.divergences?.length ?? 0) > 0) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/** The versioned machine-readable form of a coverage analysis
|
||||
* (`scriptc coverage --print=diagnostics`). */
|
||||
export function coverageEnvelope(
|
||||
input: CoverageInput,
|
||||
opts: {
|
||||
compilerVersion: string;
|
||||
sourceTexts?: Map<string, string>;
|
||||
failOn?: CoverageFailOn;
|
||||
},
|
||||
): DiagnosticsEnvelope {
|
||||
const locator = new SourceLocator(opts.sourceTexts);
|
||||
const located: LocatedDiagnostic[] = [];
|
||||
const groups: DiagnosticGroup[] = [];
|
||||
const add = (diags: readonly ScrDiagnostic[] | undefined, scope: DiagnosticScope) => {
|
||||
if (diags === undefined || diags.length === 0) return;
|
||||
const r = groupDiagnostics(diags, scope, locator);
|
||||
located.push(...r.located);
|
||||
groups.push(...r.groups);
|
||||
};
|
||||
if (input.preflightFailed) {
|
||||
add(input.diagnostics, "preflight");
|
||||
} else {
|
||||
add(input.diagnostics, "reached");
|
||||
add(input.runtimeFences, "deferred");
|
||||
add(input.unreached?.diagnostics, "unreached");
|
||||
add(input.unreached?.runtimeFences, "deferred");
|
||||
add(input.divergences, "reached");
|
||||
}
|
||||
const un = input.unreached;
|
||||
const total = input.stats.statementsTotal + (un?.stats.statementsTotal ?? 0);
|
||||
const failed = input.stats.statementsFailed + (un?.stats.statementsFailed ?? 0);
|
||||
const island = input.stats.statementsIsland + (un?.stats.statementsIsland ?? 0);
|
||||
const skipped = input.stats.functionsSkipped + (un?.stats.functionsSkipped ?? 0);
|
||||
const ok = total - failed - island;
|
||||
return {
|
||||
schema: DIAGNOSTICS_SCHEMA,
|
||||
schemaVersion: DIAGNOSTICS_SCHEMA_VERSION,
|
||||
compilerVersion: opts.compilerVersion,
|
||||
command: "coverage",
|
||||
entry: input.file,
|
||||
success: coveragePasses(input, opts.failOn ?? "blockers"),
|
||||
phase: input.preflightFailed
|
||||
? "preflight"
|
||||
: input.diagnostics.length > 0
|
||||
? "compile"
|
||||
: "complete",
|
||||
...(input.preflightFailed
|
||||
? {}
|
||||
: {
|
||||
stats: {
|
||||
statementsTotal: total,
|
||||
statementsStatic: ok,
|
||||
statementsDynamic: island,
|
||||
statementsFailed: failed,
|
||||
functionsSkipped: skipped,
|
||||
percentStatic: total === 0 ? 100 : Math.floor((ok / total) * 100),
|
||||
},
|
||||
}),
|
||||
groups,
|
||||
diagnostics: located,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterAll, expect, test } from "vitest";
|
||||
import { analyze } from "../index.js";
|
||||
import { analyzeWithVerdictCache, type VerdictStore } from "./verdict-cache.js";
|
||||
import { commentOnlyReplay } from "./verdict-semantic.js";
|
||||
|
||||
const dir = mkdtempSync(join(tmpdir(), "scriptc-verdict-"));
|
||||
afterAll(() => rmSync(dir, { recursive: true, force: true }));
|
||||
|
||||
test("unchanged and comment-only reruns replay the verdict; code edits relower", () => {
|
||||
const entry = join(dir, "main.ts");
|
||||
const program = `const pairs: [number, number][] = [[1, 2]];\nconst [[a, b]] = pairs;\nconsole.log(a + b);\nconst { length } = [1, 2] as number[];\nconsole.log(length);\n`;
|
||||
writeFileSync(entry, program);
|
||||
const entries = new Map<string, string>();
|
||||
const store: VerdictStore = {
|
||||
read: (k) => entries.get(k) ?? null,
|
||||
write: (k, t) => void entries.set(k, t),
|
||||
};
|
||||
let lowered = 0;
|
||||
const run = () => {
|
||||
const { result, outcome } = analyzeWithVerdictCache(
|
||||
entry,
|
||||
{},
|
||||
{
|
||||
store,
|
||||
identity: "test",
|
||||
platform: process.platform,
|
||||
semanticReplay: commentOnlyReplay,
|
||||
},
|
||||
() => {
|
||||
lowered++;
|
||||
return analyze(entry);
|
||||
},
|
||||
);
|
||||
return { ...result, cache: outcome };
|
||||
};
|
||||
const first = run();
|
||||
expect(first.cache).toBe("miss");
|
||||
const blockerStart = first.coverage.diagnostics[0]?.loc.start;
|
||||
expect(blockerStart).toBeDefined();
|
||||
|
||||
const second = run();
|
||||
expect(second.cache).toBe("hit");
|
||||
expect(lowered).toBe(1);
|
||||
expect(second.coverage).toEqual(first.coverage);
|
||||
|
||||
// A comment edit shifts every location by the inserted text.
|
||||
const prefix = "// a note about the pairs\n";
|
||||
writeFileSync(entry, prefix + program);
|
||||
const third = run();
|
||||
expect(third.cache).toBe("semantic-hit");
|
||||
expect(lowered).toBe(1);
|
||||
expect(third.coverage.diagnostics[0]!.loc.start).toBe(blockerStart! + prefix.length);
|
||||
expect(third.sourceTexts.get(entry)).toBe(readFileSync(entry, "utf8"));
|
||||
|
||||
writeFileSync(entry, prefix + program + "console.log(1);\n");
|
||||
expect(run().cache).toBe("miss");
|
||||
expect(lowered).toBe(2);
|
||||
});
|
||||
@@ -0,0 +1,272 @@
|
||||
/* The coverage verdict cache: `scriptc coverage` on an unchanged program
|
||||
* replays the previous analysis instead of lowering the whole program
|
||||
* again. The entry records every filesystem observation the analysis made
|
||||
* (the frontend input tracker's probes — the same evidence the executable
|
||||
* cache trusts) plus the compiler identity and the analysis options; a
|
||||
* rerun replays the probes and reuses the verdict only when all of them
|
||||
* still hold. With a semantic matcher (the Node frontend's), edits that
|
||||
* only touch TypeScript comments also reuse it, with every reported
|
||||
* location moved to the edited text. */
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import type { AnalyzeOptions, AnalyzeResult } from "../compile-types.js";
|
||||
import type { ScrDiagnostic } from "../diagnostics/diagnostic.js";
|
||||
import type { LowerStats } from "../frontend/lowering/lowerer.js";
|
||||
import type { NpmStaticStatus } from "./report.js";
|
||||
import {
|
||||
FrontendInputTracker,
|
||||
frontendInputsStillMatch,
|
||||
type FrontendInputSnapshot,
|
||||
} from "../frontend/input-tracker.js";
|
||||
|
||||
const SCHEMA = "scriptc.coverage-verdict.v1";
|
||||
|
||||
/** Key-value storage for verdict entries (best effort: failures miss). */
|
||||
export interface VerdictStore {
|
||||
read(key: string): string | null;
|
||||
write(key: string, text: string): void;
|
||||
}
|
||||
|
||||
/** The cacheable part of a coverage analysis: JSON-shaped fields only. An
|
||||
* analysis that carries anything else (npm builtin tables, provenance)
|
||||
* is not cached. */
|
||||
export interface CachedCoverage {
|
||||
file: string;
|
||||
dynamic: boolean;
|
||||
stats: LowerStats;
|
||||
diagnostics: ScrDiagnostic[];
|
||||
runtimeFences?: ScrDiagnostic[];
|
||||
unreached?: { stats: LowerStats; diagnostics: ScrDiagnostic[]; runtimeFences?: ScrDiagnostic[] };
|
||||
divergences?: ScrDiagnostic[];
|
||||
npmStatic?: NpmStaticStatus[];
|
||||
preflightFailed: boolean;
|
||||
}
|
||||
|
||||
export interface CachedSource {
|
||||
path: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface SemanticReplayResult {
|
||||
probes: FrontendInputSnapshot;
|
||||
sources: CachedSource[];
|
||||
coverage: CachedCoverage;
|
||||
}
|
||||
|
||||
/** A semantic replay: the entry's probes still hold once the listed
|
||||
* sources are allowed to differ by comments; the coverage comes back with
|
||||
* its locations moved to the current text. Null when it does not apply. */
|
||||
export type SemanticReplay = (
|
||||
probes: FrontendInputSnapshot,
|
||||
sources: CachedSource[],
|
||||
coverage: CachedCoverage,
|
||||
) => SemanticReplayResult | null;
|
||||
|
||||
export interface VerdictCacheOptions {
|
||||
store: VerdictStore;
|
||||
/** The compiler implementation's identity (package digest or binary). */
|
||||
identity: string;
|
||||
/** The build's target platform: part of the analysis' meaning. */
|
||||
platform: string;
|
||||
semanticReplay?: SemanticReplay;
|
||||
}
|
||||
|
||||
export type VerdictCacheOutcome = "hit" | "semantic-hit" | "miss" | "uncacheable";
|
||||
|
||||
interface VerdictEntry {
|
||||
schema: string;
|
||||
key: string;
|
||||
probes: FrontendInputSnapshot;
|
||||
coverage: CachedCoverage;
|
||||
sources: CachedSource[];
|
||||
}
|
||||
|
||||
function digest(text: string): string {
|
||||
return createHash("sha256").update(text).digest("hex");
|
||||
}
|
||||
|
||||
export function verdictKey(
|
||||
entry: string,
|
||||
opts: AnalyzeOptions,
|
||||
cache: VerdictCacheOptions,
|
||||
): string | null {
|
||||
let ffi = "";
|
||||
if (opts.ffiProfilePath !== undefined) {
|
||||
try {
|
||||
ffi = digest(readFileSync(opts.ffiProfilePath, "utf8"));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
const npmStatic =
|
||||
opts.npmStatic === undefined
|
||||
? ""
|
||||
: typeof opts.npmStatic === "string"
|
||||
? opts.npmStatic
|
||||
: opts.npmStatic.join(",");
|
||||
const external: string[] = [];
|
||||
const types = opts.externalTypes;
|
||||
if (types !== undefined) {
|
||||
for (const specifier of Object.keys(types).sort())
|
||||
external.push(`${specifier}=${types[specifier]}`);
|
||||
}
|
||||
return digest(
|
||||
[
|
||||
SCHEMA,
|
||||
cache.identity,
|
||||
cache.platform,
|
||||
process.env["SCRIPTC_TARGET"] ?? "",
|
||||
entry,
|
||||
opts.dynamic === true ? "dynamic" : "static",
|
||||
npmStatic,
|
||||
opts.ffiProfilePath ?? "",
|
||||
ffi,
|
||||
external.join("\n"),
|
||||
].join("\0"),
|
||||
);
|
||||
}
|
||||
|
||||
function cacheable(coverage: AnalyzeResult["coverage"]): CachedCoverage | null {
|
||||
if (
|
||||
coverage.provenance !== undefined ||
|
||||
coverage.statsByFile !== undefined ||
|
||||
coverage.provenanceElided !== undefined ||
|
||||
coverage.npmBuiltins !== undefined ||
|
||||
coverage.npmLazyTraps !== undefined
|
||||
)
|
||||
return null;
|
||||
const out: CachedCoverage = {
|
||||
file: coverage.file,
|
||||
dynamic: coverage.dynamic ?? false,
|
||||
stats: coverage.stats,
|
||||
diagnostics: coverage.diagnostics,
|
||||
preflightFailed: coverage.preflightFailed,
|
||||
};
|
||||
if (coverage.runtimeFences !== undefined) out.runtimeFences = coverage.runtimeFences;
|
||||
if (coverage.unreached !== undefined) out.unreached = coverage.unreached;
|
||||
if (coverage.divergences !== undefined) out.divergences = coverage.divergences;
|
||||
if (coverage.npmStatic !== undefined) out.npmStatic = coverage.npmStatic;
|
||||
return out;
|
||||
}
|
||||
|
||||
function restore(cached: CachedCoverage, sources: CachedSource[]): AnalyzeResult {
|
||||
const sourceTexts = new Map<string, string>();
|
||||
for (const source of sources) sourceTexts.set(source.path, source.text);
|
||||
const coverage: AnalyzeResult["coverage"] = {
|
||||
file: cached.file,
|
||||
dynamic: cached.dynamic,
|
||||
stats: cached.stats,
|
||||
diagnostics: cached.diagnostics,
|
||||
preflightFailed: cached.preflightFailed,
|
||||
};
|
||||
if (cached.runtimeFences !== undefined) coverage.runtimeFences = cached.runtimeFences;
|
||||
if (cached.unreached !== undefined) coverage.unreached = cached.unreached;
|
||||
if (cached.divergences !== undefined) coverage.divergences = cached.divergences;
|
||||
if (cached.npmStatic !== undefined) coverage.npmStatic = cached.npmStatic;
|
||||
return { coverage, sourceTexts };
|
||||
}
|
||||
|
||||
/** The cached analysis when an entry for the same inputs still holds
|
||||
* exactly; null otherwise. */
|
||||
export function replayVerdict(
|
||||
entry: string,
|
||||
opts: AnalyzeOptions,
|
||||
cache: VerdictCacheOptions,
|
||||
): AnalyzeResult | null {
|
||||
const key = verdictKey(entry, opts, cache);
|
||||
if (key === null) return null;
|
||||
const cached = readEntry(cache.store, key);
|
||||
if (cached === null || !frontendInputsStillMatch(cached.probes)) return null;
|
||||
return restore(cached.coverage, cached.sources);
|
||||
}
|
||||
|
||||
/** Runs `analyze` unless an entry for the same inputs proves its verdict
|
||||
* still holds. */
|
||||
export function analyzeWithVerdictCache(
|
||||
entry: string,
|
||||
opts: AnalyzeOptions,
|
||||
cache: VerdictCacheOptions,
|
||||
analyze: () => AnalyzeResult,
|
||||
): { result: AnalyzeResult; outcome: VerdictCacheOutcome } {
|
||||
const key = verdictKey(entry, opts, cache);
|
||||
if (key === null) return { result: analyze(), outcome: "uncacheable" };
|
||||
const cached = readEntry(cache.store, key);
|
||||
if (cached !== null) {
|
||||
if (frontendInputsStillMatch(cached.probes)) {
|
||||
return { result: restore(cached.coverage, cached.sources), outcome: "hit" };
|
||||
}
|
||||
const replay = cache.semanticReplay;
|
||||
if (replay !== undefined) {
|
||||
const replayed = replay(cached.probes, cached.sources, cached.coverage);
|
||||
if (replayed !== null) {
|
||||
writeEntry(cache.store, key, replayed.probes, replayed.coverage, replayed.sources);
|
||||
return { result: restore(replayed.coverage, replayed.sources), outcome: "semantic-hit" };
|
||||
}
|
||||
}
|
||||
}
|
||||
const tracker = new FrontendInputTracker();
|
||||
const result = tracker.runSynchronous(analyze);
|
||||
const probes = tracker.snapshot();
|
||||
const stored = cacheable(result.coverage);
|
||||
if (stored !== null && probes.stable && frontendInputsStillMatch(probes)) {
|
||||
const sources: CachedSource[] = [];
|
||||
for (const [path, text] of result.sourceTexts) sources.push({ path, text });
|
||||
writeEntry(cache.store, key, probes, stored, sources);
|
||||
}
|
||||
return { result, outcome: "miss" };
|
||||
}
|
||||
|
||||
function readEntry(store: VerdictStore, key: string): VerdictEntry | null {
|
||||
try {
|
||||
const text = store.read(key);
|
||||
if (text === null) return null;
|
||||
const entry = JSON.parse(text) as VerdictEntry;
|
||||
if (entry.schema !== SCHEMA || entry.key !== key) return null;
|
||||
return entry;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function writeEntry(
|
||||
store: VerdictStore,
|
||||
key: string,
|
||||
probes: FrontendInputSnapshot,
|
||||
coverage: CachedCoverage,
|
||||
sources: CachedSource[],
|
||||
): void {
|
||||
try {
|
||||
const entry: VerdictEntry = { schema: SCHEMA, key, probes, coverage, sources };
|
||||
store.write(key, JSON.stringify(entry));
|
||||
} catch {
|
||||
// A cache write failure never changes the analysis.
|
||||
}
|
||||
}
|
||||
|
||||
/** A directory of `<key>.json` entries, published by rename so readers
|
||||
* never see a partial write. */
|
||||
export function fileVerdictStore(directory: string): VerdictStore {
|
||||
return {
|
||||
read: (key) => {
|
||||
try {
|
||||
return readFileSync(join(directory, `${key}.json`), "utf8");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
write: (key, text) => {
|
||||
let stage: string | null = null;
|
||||
try {
|
||||
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
||||
stage = mkdtempSync(join(directory, ".write-"));
|
||||
writeFileSync(join(stage, "entry"), text);
|
||||
renameSync(join(stage, "entry"), join(directory, `${key}.json`));
|
||||
} catch {
|
||||
// Best effort: a failed write is a later miss.
|
||||
} finally {
|
||||
if (stage !== null) rmSync(stage, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/* The Node frontend's semantic replay for the coverage verdict cache: an
|
||||
* entry whose only changed inputs are TypeScript sources with the same
|
||||
* tokens (comment-only edits) is reused, with its reported locations moved
|
||||
* to the edited text. */
|
||||
import { frontendInputsSemanticallyMatch } from "../frontend/input-tracker.js";
|
||||
import { rebaseSourceLocations, semanticallyEqualSource } from "../library/semantic-source.js";
|
||||
import type { SemanticReplay } from "./verdict-cache.js";
|
||||
|
||||
export const commentOnlyReplay: SemanticReplay = (probes, sources, coverage) => {
|
||||
const previous = new Map<string, string>();
|
||||
for (const source of sources) previous.set(source.path, source.text);
|
||||
const match = frontendInputsSemanticallyMatch(probes, previous, semanticallyEqualSource);
|
||||
if (match === null || match.changed.length === 0) return null;
|
||||
const current = new Map(previous);
|
||||
for (const [path, text] of match.currentSources) current.set(path, text);
|
||||
const rebased = rebaseSourceLocations(structuredClone(coverage), previous, current);
|
||||
return {
|
||||
probes: match.snapshot,
|
||||
sources: [...current].map(([path, text]) => ({ path, text })),
|
||||
coverage: rebased,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,84 @@
|
||||
/* The startup-sized half of the coverage verdict cache: the compiler
|
||||
* identity proof and an exact-hit replay, importable by the CLI bootstrap
|
||||
* without loading the lowering graph. */
|
||||
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
||||
import { join, resolve } from "node:path";
|
||||
import type { AnalyzeOptions, AnalyzeResult } from "../compile-types.js";
|
||||
import { prepareBuildCacheRoot, resolveBuildCacheRoot } from "../backend/build-cache.js";
|
||||
import { configuredTargetPlatform } from "../backend/native-toolchain.js";
|
||||
import {
|
||||
compilerImplementationDependenciesStillMatch,
|
||||
compilerImplementationIdentity,
|
||||
compilerImplementationRoot,
|
||||
type CompilerImplementationDependency,
|
||||
} from "../library/compiler-self-identity.js";
|
||||
import { replayVerdict, fileVerdictStore } from "./verdict-cache.js";
|
||||
|
||||
/** The coverage cache directory under the build cache root, or null when
|
||||
* caching is disabled or unavailable. */
|
||||
export async function coverageCacheDirectory(): Promise<string | null> {
|
||||
const root = await prepareBuildCacheRoot(resolveBuildCacheRoot());
|
||||
return root === null ? null : join(root, "coverage-v1");
|
||||
}
|
||||
|
||||
/** The compiler package's content digest for coverage verdict keys. The
|
||||
* digest hashes every byte of the package; a stat-only proof of the files
|
||||
* it covered (inode, size, times) lets an unchanged installation reuse it
|
||||
* without rereading the package. `hashIfStale` false answers null instead
|
||||
* of hashing (the bootstrap then loads the full compiler). */
|
||||
export async function coverageCompilerIdentity(
|
||||
directory: string,
|
||||
hashIfStale = true,
|
||||
): Promise<{ digest: string } | null> {
|
||||
const proofPath = join(directory, "compiler-identity.json");
|
||||
try {
|
||||
const proof = JSON.parse(await readFile(proofPath, "utf8")) as {
|
||||
root: string;
|
||||
digest: string;
|
||||
dependencies: CompilerImplementationDependency[];
|
||||
};
|
||||
if (
|
||||
proof.root === compilerImplementationRoot() &&
|
||||
/^[0-9a-f]{64}$/.test(proof.digest) &&
|
||||
(await compilerImplementationDependenciesStillMatch(proof.dependencies))
|
||||
)
|
||||
return { digest: proof.digest };
|
||||
} catch {
|
||||
// No proof yet, or an unreadable one.
|
||||
}
|
||||
if (!hashIfStale) return null;
|
||||
const identity = await compilerImplementationIdentity(true);
|
||||
try {
|
||||
await mkdir(directory, { recursive: true, mode: 0o700 });
|
||||
const stage = `${proofPath}.${process.pid}.tmp`;
|
||||
await writeFile(
|
||||
stage,
|
||||
JSON.stringify({
|
||||
root: compilerImplementationRoot(),
|
||||
digest: identity.digest,
|
||||
dependencies: identity.dependencies,
|
||||
}),
|
||||
);
|
||||
await rename(stage, proofPath);
|
||||
} catch {
|
||||
// The proof is an optimization; the digest stands without it.
|
||||
}
|
||||
return { digest: identity.digest };
|
||||
}
|
||||
|
||||
/** An exact verdict-cache hit for `scriptc coverage`, or null (the caller
|
||||
* then runs the full analysis, which also handles comment-only edits). */
|
||||
export async function readCoverageVerdict(
|
||||
entry: string,
|
||||
opts: AnalyzeOptions,
|
||||
): Promise<AnalyzeResult | null> {
|
||||
const directory = await coverageCacheDirectory();
|
||||
if (directory === null) return null;
|
||||
const identity = await coverageCompilerIdentity(directory, false);
|
||||
if (identity === null) return null;
|
||||
return replayVerdict(resolve(entry), opts, {
|
||||
store: fileVerdictStore(directory),
|
||||
identity: identity.digest,
|
||||
platform: configuredTargetPlatform(),
|
||||
});
|
||||
}
|
||||
@@ -52,6 +52,15 @@
|
||||
* (SC5002), and a TypeScript signature that does not match its
|
||||
* declared native ABI classes (SC5003), and native toolchain
|
||||
* failures while applying a valid profile (SC5004)
|
||||
* SC6xxx divergences: the program compiles, but the native binary can
|
||||
* behave differently from Node at this site — WARNINGS, never
|
||||
* build failures (coverage --fail-on=divergences opts in):
|
||||
* width copies written or compared through (SC6001), object key
|
||||
* order that differs from insertion order where the program
|
||||
* observes it (SC6002), localeCompare over text the built-in
|
||||
* collation does not cover (SC6003), a call feeding an instance
|
||||
* to a cast to an unrelated class (SC6004), and Date strings the native parser does not
|
||||
* accept (SC6005)
|
||||
* SC9xxx internal compiler errors (still source-anchored)
|
||||
*/
|
||||
import type { SrcLoc } from "../ir/ir.js";
|
||||
@@ -73,6 +82,12 @@ export interface ScrDiagnostic {
|
||||
* recognizably the profile's. Only library-mode refusals carry one —
|
||||
* the text always arrives prefixed `from the '<profile name>' profile:`. */
|
||||
note?: string;
|
||||
/** SC0001 only: the TypeScript error comes from scriptc's own type world
|
||||
* (its fixed library set, the bundled Node fallback types, or globals it
|
||||
* does not declare) rather than from the project's own configuration —
|
||||
* the error may not reproduce under the project's tsc. Machine-readable
|
||||
* output reports these under the "environment" category. */
|
||||
typeWorld?: boolean;
|
||||
}
|
||||
|
||||
/** SC3002–SC3004 — native-helper target, installation, and execution
|
||||
@@ -93,6 +108,102 @@ export function nativeCodegenDiag(
|
||||
};
|
||||
}
|
||||
|
||||
/* ── SC6xxx: divergences from Node (warnings) ─────────────────────────── */
|
||||
|
||||
/** SC6001 — a value passed to (or declared as) a narrower record type is
|
||||
* copied field by field, and the program then writes through or compares
|
||||
* the copy: under Node it is the same object, so the write would be
|
||||
* visible to the original and `===` would be true. */
|
||||
export function widthCopyDivergenceDiag(
|
||||
sourceType: string,
|
||||
targetType: string,
|
||||
use: "write" | "compare",
|
||||
useText: string,
|
||||
site: "call" | "declaration",
|
||||
loc: SrcLoc,
|
||||
): ScrDiagnostic {
|
||||
const how =
|
||||
site === "call"
|
||||
? `passing a '${sourceType}' where '${targetType}' is expected`
|
||||
: `storing a '${sourceType}' in a '${targetType}' binding`;
|
||||
const effect =
|
||||
use === "write"
|
||||
? `the write to '${useText}' updates only the copy, while under Node it updates the original object`
|
||||
: `'${useText}' compares the copy, which is never the original object, while under Node it is the same object`;
|
||||
return {
|
||||
code: "SC6001",
|
||||
message: `${how} copies the object (a narrower record shape compiles as a field copy): ${effect}`,
|
||||
loc,
|
||||
hint: `declare the parameter or binding with the value's full type ('${sourceType}'), or make the shape a class — class instances keep their identity when passed as a base class`,
|
||||
};
|
||||
}
|
||||
|
||||
/** SC6002 — an object literal whose written key order differs from the
|
||||
* order its declared type compiles to, for a type the program observes
|
||||
* (JSON.stringify, Object.keys/values/entries, for-in, console output). */
|
||||
export function keyOrderDivergenceDiag(
|
||||
typeText: string,
|
||||
nodeOrder: string,
|
||||
nativeOrder: string,
|
||||
detail: "reordered" | "added-later",
|
||||
loc: SrcLoc,
|
||||
): ScrDiagnostic {
|
||||
return {
|
||||
code: "SC6002",
|
||||
message:
|
||||
detail === "reordered"
|
||||
? `this object's keys enumerate as '${nativeOrder}' natively (the declaration order of '${typeText}') but as '${nodeOrder}' under Node (insertion order), and the program observes the order (JSON.stringify, Object.keys, for-in or console output)`
|
||||
: `a property this object leaves out is assigned later: under Node its keys then enumerate as '${nodeOrder}' (insertion order), natively as '${nativeOrder}' (the declaration order of '${typeText}'), and the program observes the order`,
|
||||
loc,
|
||||
hint:
|
||||
detail === "reordered"
|
||||
? `write the properties in the declaration order of '${typeText}' (or reorder the declaration to match how objects are built)`
|
||||
: `initialize the property in the literal (for example with undefined), or declare it after the properties the literal sets`,
|
||||
};
|
||||
}
|
||||
|
||||
/** SC6003 — one-argument localeCompare over text outside the root-locale
|
||||
* collation table the runtime carries: those characters compare by code
|
||||
* point natively, where Node applies full ICU collation. */
|
||||
export function localeCompareDivergenceDiag(text: string, loc: SrcLoc): ScrDiagnostic {
|
||||
const shown = text.length > 24 ? `${text.slice(0, 21)}...` : text;
|
||||
return {
|
||||
code: "SC6003",
|
||||
message: `localeCompare over '${shown}' compares characters outside the built-in collation (Latin letters, digits, punctuation and symbols) by code point natively, while Node applies ICU's full collation`,
|
||||
loc,
|
||||
hint: "results agree with Node for Latin-script text; for other scripts compare explicitly (for example by code point with < and >) so both runtimes agree",
|
||||
};
|
||||
}
|
||||
|
||||
/** SC6004 — a call passes an instance of one class where the callee casts
|
||||
* that parameter to an unrelated class: Node reads the members
|
||||
* structurally, while scriptc checks class casts at runtime and throws a
|
||||
* TypeError when the cast executes. */
|
||||
export function unrelatedClassCastDivergenceDiag(
|
||||
source: string,
|
||||
target: string,
|
||||
loc: SrcLoc,
|
||||
): ScrDiagnostic {
|
||||
return {
|
||||
code: "SC6004",
|
||||
message: `this '${source}' reaches a cast to the unrelated class '${target}' in the callee: natively the checked cast throws a TypeError when it executes, while Node reads the members structurally`,
|
||||
loc,
|
||||
hint: `narrow with 'instanceof ${target}' before casting, or cast to a base class or interface both classes share`,
|
||||
};
|
||||
}
|
||||
|
||||
/** SC6005 — a Date string literal outside the grammar the native parser
|
||||
* accepts (ISO 8601 with an explicit offset, date-only forms, and the
|
||||
* certificate-time shape): Node parses many more forms. */
|
||||
export function dateLiteralDivergenceDiag(text: string, loc: SrcLoc): ScrDiagnostic {
|
||||
return {
|
||||
code: "SC6005",
|
||||
message: `the date string '${text}' parses to an invalid date (NaN) natively, while Node accepts it (the native parser takes ISO 8601 date-times with an explicit offset, such as '2024-01-02T03:04:05Z', and date-only forms)`,
|
||||
loc,
|
||||
hint: "write the date in ISO 8601 with an explicit offset ('Z' or '+HH:MM'), or build it from numbers with Date.UTC",
|
||||
};
|
||||
}
|
||||
|
||||
/* ── SC5xxx: native FFI ───────────────────────────────────────────────── */
|
||||
|
||||
/** SC5001 — the outbound native-FFI manifest is malformed or unreadable. */
|
||||
@@ -778,13 +889,15 @@ export function requiresDynamicDiag(feature: string, loc: SrcLoc): ScrDiagnostic
|
||||
* name the construct: the call RUNS under --dynamic (the engine executes
|
||||
* it with JS-exact semantics); without the flag each use site reports the
|
||||
* choice instead of ICEing or failing at link time. */
|
||||
export function requiresDynamicApiDiag(feature: string, loc: SrcLoc): ScrDiagnostic {
|
||||
export function requiresDynamicApiDiag(feature: string, loc: SrcLoc, hint?: string): ScrDiagnostic {
|
||||
return {
|
||||
code: "SC2012",
|
||||
message: `${feature} runs in the embedded dynamic engine, which this build does not include`,
|
||||
loc,
|
||||
milestone: "M4",
|
||||
hint: "build with --dynamic to run this call in the embedded engine (adds ~620KB to the binary); static builds never include it",
|
||||
hint:
|
||||
hint ??
|
||||
"build with --dynamic to run this call in the embedded engine (adds ~620KB to the binary); static builds never include it",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -798,7 +911,7 @@ export function requiresDynamicImportDiag(pkg: string, loc: SrcLoc): ScrDiagnost
|
||||
message: `importing '${pkg}' requires the embedded dynamic engine, which this build does not include — the package's implementation runs there`,
|
||||
loc,
|
||||
milestone: "M4",
|
||||
hint: "build with --dynamic to run npm package code in the embedded engine (adds ~620KB to the binary); static builds never include it",
|
||||
hint: "build with --dynamic to run npm package code in the embedded engine (adds ~620KB to the binary), or try --npm-static auto to compile eligible packages' JavaScript statically; static builds never include the engine",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -811,7 +924,7 @@ export function requiresDynamicPackageDiag(pkg: string, loc: SrcLoc): ScrDiagnos
|
||||
message: `values from the '${pkg}' package run in the embedded dynamic engine, which this build does not include`,
|
||||
loc,
|
||||
milestone: "M4",
|
||||
hint: "build with --dynamic to run npm package code in the embedded engine (adds ~620KB to the binary); static builds never include it",
|
||||
hint: "build with --dynamic to run npm package code in the embedded engine (adds ~620KB to the binary), or try --npm-static auto to compile eligible packages' JavaScript statically; static builds never include the engine",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import { expect, test } from "vitest";
|
||||
import type { ScrDiagnostic } from "./diagnostic.js";
|
||||
import { SourceLocator, buildEnvelope, categoryOf, groupDiagnostics, hintOf } from "./envelope.js";
|
||||
|
||||
const diag = (
|
||||
code: `SC${number}`,
|
||||
message: string,
|
||||
start: number,
|
||||
extra: Partial<ScrDiagnostic> = {},
|
||||
): ScrDiagnostic => ({
|
||||
code,
|
||||
message,
|
||||
loc: { file: "/src/main.ts", start, end: start + 3 },
|
||||
...extra,
|
||||
});
|
||||
|
||||
test("instantiations of one component fence share a root-cause group", () => {
|
||||
const source = "a\nbb\nccc\n";
|
||||
const locator = new SourceLocator(new Map([["/src/main.ts", source]]));
|
||||
const { groups, located } = groupDiagnostics(
|
||||
[
|
||||
diag(
|
||||
"SC2009",
|
||||
"values of type 'Box<number>' cannot be compiled: member 'load' of 'Loader<number>' does not compile",
|
||||
5,
|
||||
),
|
||||
diag(
|
||||
"SC2009",
|
||||
"values of type 'Map<string, Box<string>>' cannot be compiled: member 'load' of 'Loader<string>' does not compile",
|
||||
2,
|
||||
),
|
||||
diag("SC1090", "spreading unions is not supported yet", 0),
|
||||
],
|
||||
"reached",
|
||||
locator,
|
||||
);
|
||||
expect(groups).toHaveLength(2);
|
||||
expect(groups[0]!.message).toBe("member 'load' of 'Loader<…>' does not compile");
|
||||
expect(groups[0]!.count).toBe(2);
|
||||
// Sites in source order; the first is the primary location.
|
||||
expect(groups[0]!.sites.map((s) => [s.line, s.column])).toEqual([
|
||||
[2, 1],
|
||||
[3, 1],
|
||||
]);
|
||||
expect(groups[0]!.primary).toEqual({ file: "/src/main.ts", line: 2, column: 1 });
|
||||
expect(located[2]).toMatchObject({ line: 1, column: 1, endLine: 2, endColumn: 2 });
|
||||
expect(located[2]!.group).toBe(groups[1]!.id);
|
||||
});
|
||||
|
||||
test("every diagnostic carries a category and a hint", () => {
|
||||
expect(categoryOf(diag("SC6001", "copy", 0))).toBe("divergence");
|
||||
expect(categoryOf(diag("SC2012", "engine", 0))).toBe("dynamic-only");
|
||||
expect(categoryOf(diag("SC0001", "TS error", 0))).toBe("invalid-source");
|
||||
expect(categoryOf(diag("SC0001", "TS error", 0, { typeWorld: true }))).toBe("environment");
|
||||
expect(categoryOf(diag("SC9001", "ice", 0))).toBe("internal");
|
||||
expect(categoryOf(diag("SC1090", "form", 0))).toBe("unsupported");
|
||||
for (const code of [
|
||||
"SC1090",
|
||||
"SC1031",
|
||||
"SC2001",
|
||||
"SC2003",
|
||||
"SC0001",
|
||||
"SC9001",
|
||||
"SC3002",
|
||||
] as const)
|
||||
expect(hintOf(diag(code, "m", 0)).length).toBeGreaterThan(0);
|
||||
expect(hintOf(diag("SC1090", "m", 0, { hint: "specific" }))).toBe("specific");
|
||||
});
|
||||
|
||||
test("the build envelope reports the phase that stopped the build", () => {
|
||||
const sources = new Map([["/src/main.ts", "let x = 1;\n"]]);
|
||||
const failed = buildEnvelope({
|
||||
compilerVersion: "0.0.0",
|
||||
entry: "/src/main.ts",
|
||||
ok: false,
|
||||
diagnostics: [diag("SC1090", "form is not supported yet", 4)],
|
||||
sourceTexts: sources,
|
||||
});
|
||||
expect(failed).toMatchObject({
|
||||
schema: "scriptc-diagnostics",
|
||||
schemaVersion: 1,
|
||||
command: "build",
|
||||
success: false,
|
||||
phase: "compile",
|
||||
});
|
||||
expect(failed.diagnostics[0]).toMatchObject({
|
||||
severity: "error",
|
||||
scope: "reached",
|
||||
line: 1,
|
||||
column: 5,
|
||||
});
|
||||
const preflight = buildEnvelope({
|
||||
compilerVersion: "0.0.0",
|
||||
entry: "/src/main.ts",
|
||||
ok: false,
|
||||
diagnostics: [diag("SC0001", "Cannot find name 'y'.", 0)],
|
||||
sourceTexts: sources,
|
||||
});
|
||||
expect(preflight.phase).toBe("preflight");
|
||||
expect(preflight.diagnostics[0]!.scope).toBe("preflight");
|
||||
const ok = buildEnvelope({
|
||||
compilerVersion: "0.0.0",
|
||||
entry: "/src/main.ts",
|
||||
ok: true,
|
||||
artifact: "/out/main",
|
||||
diagnostics: [],
|
||||
warnings: [diag("SC6002", "order", 0)],
|
||||
sourceTexts: sources,
|
||||
});
|
||||
expect(ok).toMatchObject({ success: true, phase: "complete", artifact: "/out/main" });
|
||||
expect(ok.diagnostics[0]).toMatchObject({ category: "divergence", severity: "warning" });
|
||||
});
|
||||
@@ -0,0 +1,359 @@
|
||||
/* Machine-readable diagnostics: the versioned JSON envelope that
|
||||
* `scriptc coverage --print=diagnostics` and `scriptc build
|
||||
* --print=diagnostics` write to stdout, plus the location, category,
|
||||
* root-cause grouping and fallback-hint helpers the human coverage report
|
||||
* shares with it. One module owns all of them so the two views of the same
|
||||
* analysis cannot drift apart.
|
||||
*
|
||||
* Schema (version 1) — documented in docs/content/docs/coverage.mdx:
|
||||
*
|
||||
* { schema: "scriptc-diagnostics", schemaVersion: 1, compilerVersion,
|
||||
* command: "coverage" | "build", entry, success, phase, stats?,
|
||||
* artifact?, groups: DiagnosticGroup[], diagnostics: LocatedDiagnostic[] }
|
||||
*
|
||||
* Additive changes (new optional fields, new categories or codes) keep the
|
||||
* version; renaming or removing a field, or changing a field's meaning,
|
||||
* bumps it. */
|
||||
import { readFileSync } from "node:fs";
|
||||
import type { ScrDiagnostic } from "./diagnostic.js";
|
||||
|
||||
export const DIAGNOSTICS_SCHEMA = "scriptc-diagnostics";
|
||||
export const DIAGNOSTICS_SCHEMA_VERSION = 1;
|
||||
|
||||
/** What kind of problem a diagnostic reports, independent of its code band:
|
||||
* - invalid-source: the program itself is wrong (TypeScript errors, bad JSON modules)
|
||||
* - unsupported: valid TypeScript/JavaScript scriptc cannot compile yet
|
||||
* - dynamic-only: compiles when the build embeds the dynamic engine (--dynamic)
|
||||
* - divergence: compiles, but the native program behaves differently from Node
|
||||
* - environment: project configuration, scriptc's type world, toolchain or target
|
||||
* - internal: a compiler bug */
|
||||
export type DiagnosticCategory =
|
||||
| "invalid-source"
|
||||
| "unsupported"
|
||||
| "dynamic-only"
|
||||
| "divergence"
|
||||
| "environment"
|
||||
| "internal";
|
||||
|
||||
/** Where the diagnostic was found:
|
||||
* - preflight: before lowering (type checking, configuration, module graph)
|
||||
* - reached: code the entry path reaches (fails a build)
|
||||
* - unreached: code nothing on the entry path reaches (cannot fail a build)
|
||||
* - deferred: a JavaScript statement that compiles to a throw of this
|
||||
* diagnostic when it executes */
|
||||
export type DiagnosticScope = "preflight" | "reached" | "unreached" | "deferred";
|
||||
|
||||
export interface SourcePosition {
|
||||
file: string;
|
||||
/** 1-based line and UTF-16 column. */
|
||||
line: number;
|
||||
column: number;
|
||||
}
|
||||
|
||||
export interface LocatedDiagnostic extends SourcePosition {
|
||||
code: string;
|
||||
category: DiagnosticCategory;
|
||||
severity: "error" | "warning";
|
||||
scope: DiagnosticScope;
|
||||
endLine: number;
|
||||
endColumn: number;
|
||||
/** 0-based UTF-16 offsets into the file. */
|
||||
start: number;
|
||||
end: number;
|
||||
message: string;
|
||||
/** Always present: the diagnostic's own hint, or the code family's. */
|
||||
hint: string;
|
||||
/** The id of the root-cause group this site belongs to. */
|
||||
group: string;
|
||||
}
|
||||
|
||||
export interface DiagnosticGroup {
|
||||
id: string;
|
||||
code: string;
|
||||
category: DiagnosticCategory;
|
||||
severity: "error" | "warning";
|
||||
scope: DiagnosticScope;
|
||||
/** The message with per-instantiation type arguments collapsed. */
|
||||
message: string;
|
||||
hint: string;
|
||||
count: number;
|
||||
/** The first site in source order: the place to start fixing. */
|
||||
primary: SourcePosition;
|
||||
/** Every site in source order (primary included). */
|
||||
sites: SourcePosition[];
|
||||
}
|
||||
|
||||
export interface DiagnosticsEnvelope {
|
||||
schema: typeof DIAGNOSTICS_SCHEMA;
|
||||
schemaVersion: typeof DIAGNOSTICS_SCHEMA_VERSION;
|
||||
compilerVersion: string;
|
||||
command: "coverage" | "build";
|
||||
entry: string;
|
||||
/** coverage: no reached blockers (and, with --fail-on=divergences, no
|
||||
* divergences); build: an artifact was produced. */
|
||||
success: boolean;
|
||||
/** How far the command got: "preflight" when it stopped there,
|
||||
* "compile" when blockers remain, "native" when native code generation
|
||||
* or linking failed, "complete" otherwise. */
|
||||
phase: "preflight" | "compile" | "native" | "complete";
|
||||
stats?: {
|
||||
statementsTotal: number;
|
||||
statementsStatic: number;
|
||||
statementsDynamic: number;
|
||||
statementsFailed: number;
|
||||
functionsSkipped: number;
|
||||
percentStatic: number;
|
||||
};
|
||||
/** build only: the produced artifact path. */
|
||||
artifact?: string;
|
||||
groups: DiagnosticGroup[];
|
||||
diagnostics: LocatedDiagnostic[];
|
||||
}
|
||||
|
||||
/** Codes whose construct RUNS under --dynamic (the embedded engine). */
|
||||
export const DYNAMIC_CAPABLE_CODES: ReadonlySet<string> = new Set([
|
||||
"SC2010",
|
||||
"SC2011",
|
||||
"SC2012",
|
||||
"SC2013",
|
||||
]);
|
||||
|
||||
/** The divergence band: compiles, but the program can behave differently
|
||||
* from Node. Warnings, never build failures. */
|
||||
export function isDivergenceCode(code: string): boolean {
|
||||
return /^SC6\d{3}$/.test(code);
|
||||
}
|
||||
|
||||
export function categoryOf(diag: ScrDiagnostic): DiagnosticCategory {
|
||||
const code = diag.code;
|
||||
if (isDivergenceCode(code)) return "divergence";
|
||||
if (DYNAMIC_CAPABLE_CODES.has(code)) return "dynamic-only";
|
||||
if (code === "SC0001") return diag.typeWorld === true ? "environment" : "invalid-source";
|
||||
if (code === "SC0003") return "invalid-source";
|
||||
if (code === "SC0002" || code === "SC0004") return "environment";
|
||||
if (/^SC[35]\d{3}$/.test(code)) return "environment";
|
||||
if (/^SC9\d{3}$/.test(code)) return "internal";
|
||||
return "unsupported";
|
||||
}
|
||||
|
||||
/** Code-family guidance for diagnostics whose construct site has no
|
||||
* specific hint: every group the report prints and every envelope entry
|
||||
* carries one. */
|
||||
export function hintOf(diag: ScrDiagnostic): string {
|
||||
if (diag.hint !== undefined && diag.hint !== "") return diag.hint;
|
||||
const code = diag.code;
|
||||
if (code === "SC0001")
|
||||
return "fix the TypeScript error; scriptc only compiles programs that typecheck under its type world (strict null checks, the ES2025 library, Node types)";
|
||||
if (code === "SC1090")
|
||||
return "this form is valid TypeScript but does not compile yet — rewrite it with a supported equivalent of the construct the message names (https://scriptc.dev/limitations lists the boundary)";
|
||||
if (/^SC1\d{3}$/.test(code))
|
||||
return "this construct is valid TypeScript outside the compiled subset — rewrite it with a supported equivalent (https://scriptc.dev/limitations)";
|
||||
if (code === "SC2001")
|
||||
return "restate the value's type inside the compilable set (number, string, boolean, arrays, Maps, Sets, RegExp, functions, classes, records, unions of those, and 'unknown')";
|
||||
if (/^SC2\d{3}$/.test(code))
|
||||
return "restate the type inside the compilable set (https://scriptc.dev/limitations describes the boundary)";
|
||||
if (/^SC9\d{3}$/.test(code))
|
||||
return "this is a compiler bug — please report it with the program that triggers it";
|
||||
return "see https://scriptc.dev/limitations";
|
||||
}
|
||||
|
||||
/** The root-cause text of a message: type arguments of instantiated generic
|
||||
* types collapse to `<…>`, so every instantiation of one uncompilable
|
||||
* declaration lands in one group. Component fences (SC2009) group on the
|
||||
* named component alone — the outer container type repeats per use. */
|
||||
export function rootCauseMessage(diag: ScrDiagnostic): string {
|
||||
let text = diag.message.replace(/ (?:is|are) not supported yet$/, "");
|
||||
if (diag.code === "SC2009") {
|
||||
const at = text.indexOf("cannot be compiled: ");
|
||||
if (at >= 0) text = text.slice(at + "cannot be compiled: ".length);
|
||||
}
|
||||
return collapseTypeArguments(text);
|
||||
}
|
||||
|
||||
function collapseTypeArguments(text: string): string {
|
||||
// Only inside quoted spans: `<` elsewhere is prose (or an operator name).
|
||||
return text.replace(/'[^']*'/g, (quoted) => {
|
||||
let inner = quoted;
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const next = inner.replace(/(\w)<[^<>]*>/g, "$1<…>");
|
||||
if (next === inner) break;
|
||||
inner = next;
|
||||
}
|
||||
return inner;
|
||||
});
|
||||
}
|
||||
|
||||
export function groupId(diag: ScrDiagnostic, scope: DiagnosticScope): string {
|
||||
// A short stable id: FNV-1a over code, scope and root-cause text.
|
||||
const key = `${diag.code}\0${scope}\0${rootCauseMessage(diag)}`;
|
||||
let h = 0x811c9dc5;
|
||||
for (let i = 0; i < key.length; i++) {
|
||||
h ^= key.charCodeAt(i);
|
||||
h = Math.imul(h, 0x01000193) >>> 0;
|
||||
}
|
||||
return `${diag.code}-${h.toString(16).padStart(8, "0")}`;
|
||||
}
|
||||
|
||||
/** Offset → line/column over a source text, with the line table cached
|
||||
* per file. Files missing from the analysis' sources are read from disk;
|
||||
* a file that cannot be read reports line 1 with the raw offset as column. */
|
||||
export class SourceLocator {
|
||||
private readonly lineStarts = new Map<string, number[] | null>();
|
||||
|
||||
constructor(private readonly sources: ReadonlyMap<string, string> = new Map()) {}
|
||||
|
||||
private starts(file: string): number[] | null {
|
||||
let starts = this.lineStarts.get(file);
|
||||
if (starts !== undefined) return starts;
|
||||
let text = this.sources.get(file);
|
||||
if (text === undefined) {
|
||||
try {
|
||||
text = readFileSync(file, "utf8");
|
||||
} catch {
|
||||
text = undefined;
|
||||
}
|
||||
}
|
||||
if (text === undefined) {
|
||||
starts = null;
|
||||
} else {
|
||||
starts = [0];
|
||||
for (let i = 0; i < text.length; i++) if (text.charCodeAt(i) === 10) starts.push(i + 1);
|
||||
}
|
||||
this.lineStarts.set(file, starts);
|
||||
return starts;
|
||||
}
|
||||
|
||||
position(file: string, offset: number): { line: number; column: number } {
|
||||
const starts = this.starts(file);
|
||||
if (starts === null) return { line: 1, column: offset + 1 };
|
||||
let lo = 0;
|
||||
let hi = starts.length - 1;
|
||||
while (lo < hi) {
|
||||
const mid = (lo + hi + 1) >> 1;
|
||||
if (starts[mid]! <= offset) lo = mid;
|
||||
else hi = mid - 1;
|
||||
}
|
||||
return { line: lo + 1, column: offset - starts[lo]! + 1 };
|
||||
}
|
||||
}
|
||||
|
||||
export function locateDiagnostic(
|
||||
diag: ScrDiagnostic,
|
||||
scope: DiagnosticScope,
|
||||
locator: SourceLocator,
|
||||
): LocatedDiagnostic {
|
||||
const start = locator.position(diag.loc.file, diag.loc.start);
|
||||
const end = locator.position(diag.loc.file, diag.loc.end);
|
||||
return {
|
||||
code: diag.code,
|
||||
category: categoryOf(diag),
|
||||
severity: isDivergenceCode(diag.code) ? "warning" : "error",
|
||||
scope,
|
||||
file: diag.loc.file,
|
||||
line: start.line,
|
||||
column: start.column,
|
||||
endLine: end.line,
|
||||
endColumn: end.column,
|
||||
start: diag.loc.start,
|
||||
end: diag.loc.end,
|
||||
message: diag.message,
|
||||
hint: hintOf(diag),
|
||||
group: groupId(diag, scope),
|
||||
};
|
||||
}
|
||||
|
||||
/** Root-cause groups over located diagnostics, in input order of first
|
||||
* appearance; sites inside each group are in source order. */
|
||||
export function groupDiagnostics(
|
||||
diags: readonly ScrDiagnostic[],
|
||||
scope: DiagnosticScope,
|
||||
locator: SourceLocator,
|
||||
): { located: LocatedDiagnostic[]; groups: DiagnosticGroup[] } {
|
||||
const located: LocatedDiagnostic[] = [];
|
||||
const groups = new Map<string, DiagnosticGroup>();
|
||||
for (const diag of diags) {
|
||||
const l = locateDiagnostic(diag, scope, locator);
|
||||
located.push(l);
|
||||
const site = { file: l.file, line: l.line, column: l.column };
|
||||
const existing = groups.get(l.group);
|
||||
if (existing) {
|
||||
existing.count++;
|
||||
existing.sites.push(site);
|
||||
// A group whose sites disagree on the hint keeps the first specific one.
|
||||
} else {
|
||||
groups.set(l.group, {
|
||||
id: l.group,
|
||||
code: l.code,
|
||||
category: l.category,
|
||||
severity: l.severity,
|
||||
scope,
|
||||
message: rootCauseMessage(diag),
|
||||
hint: l.hint,
|
||||
count: 1,
|
||||
primary: site,
|
||||
sites: [site],
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const g of groups.values()) {
|
||||
g.sites.sort(comparePositions);
|
||||
// Identical positions (one construct reported through two paths) count
|
||||
// once as a site but keep their diagnostic count.
|
||||
g.sites = g.sites.filter((s, i) => i === 0 || comparePositions(s, g.sites[i - 1]!) !== 0);
|
||||
g.primary = g.sites[0]!;
|
||||
}
|
||||
return { located, groups: [...groups.values()] };
|
||||
}
|
||||
|
||||
export function comparePositions(a: SourcePosition, b: SourcePosition): number {
|
||||
return a.file < b.file ? -1 : a.file > b.file ? 1 : a.line - b.line || a.column - b.column;
|
||||
}
|
||||
|
||||
/** The versioned machine-readable form of a build's outcome
|
||||
* (`scriptc build --print=diagnostics`). Build diagnostics are all reached
|
||||
* (or preflight) errors; divergence warnings ride along on success. */
|
||||
export function buildEnvelope(opts: {
|
||||
compilerVersion: string;
|
||||
entry: string;
|
||||
ok: boolean;
|
||||
artifact?: string;
|
||||
diagnostics: readonly ScrDiagnostic[];
|
||||
warnings?: readonly ScrDiagnostic[];
|
||||
sourceTexts?: ReadonlyMap<string, string>;
|
||||
}): DiagnosticsEnvelope {
|
||||
const locator = new SourceLocator(opts.sourceTexts);
|
||||
const located: LocatedDiagnostic[] = [];
|
||||
const groups: DiagnosticGroup[] = [];
|
||||
const preflight = opts.diagnostics.filter((d) => /^SC0\d{3}$/.test(d.code));
|
||||
const backend = opts.diagnostics.filter((d) => /^SC[35]\d{3}$/.test(d.code));
|
||||
const add = (diags: readonly ScrDiagnostic[], scope: DiagnosticScope) => {
|
||||
if (diags.length === 0) return;
|
||||
const r = groupDiagnostics(diags, scope, locator);
|
||||
located.push(...r.located);
|
||||
groups.push(...r.groups);
|
||||
};
|
||||
add(preflight, "preflight");
|
||||
add(
|
||||
opts.diagnostics.filter((d) => !preflight.includes(d)),
|
||||
"reached",
|
||||
);
|
||||
add(opts.warnings ?? [], "reached");
|
||||
return {
|
||||
schema: DIAGNOSTICS_SCHEMA,
|
||||
schemaVersion: DIAGNOSTICS_SCHEMA_VERSION,
|
||||
compilerVersion: opts.compilerVersion,
|
||||
command: "build",
|
||||
entry: opts.entry,
|
||||
success: opts.ok,
|
||||
phase: opts.ok
|
||||
? "complete"
|
||||
: preflight.length > 0
|
||||
? "preflight"
|
||||
: backend.length > 0 && backend.length === opts.diagnostics.length
|
||||
? "native"
|
||||
: "compile",
|
||||
...(opts.artifact === undefined ? {} : { artifact: opts.artifact }),
|
||||
groups,
|
||||
diagnostics: located,
|
||||
};
|
||||
}
|
||||
@@ -14,6 +14,7 @@ interface SourceLookup {
|
||||
}
|
||||
|
||||
const RED = "\x1b[31m";
|
||||
const YELLOW = "\x1b[33m";
|
||||
const CYAN = "\x1b[36m";
|
||||
const DIM = "\x1b[2m";
|
||||
const BOLD = "\x1b[1m";
|
||||
@@ -58,13 +59,16 @@ export function renderDiagnostic(
|
||||
frame.push(
|
||||
c(DIM, ` ${" ".repeat(gutterWidth)} | `) +
|
||||
" ".repeat(colNum - 1) +
|
||||
c(RED, "^" + "~".repeat(spanOnLine - 1)),
|
||||
c(/^SC6\d{3}$/.test(diag.code) ? YELLOW : RED, "^" + "~".repeat(spanOnLine - 1)),
|
||||
);
|
||||
emitLine(lineNum + 1);
|
||||
}
|
||||
|
||||
// The divergence band (SC6xxx) is the one warning severity: the program
|
||||
// compiles, and the site can behave differently from Node.
|
||||
const warning = /^SC6\d{3}$/.test(diag.code);
|
||||
out.push(
|
||||
`${c(BOLD, `${diag.loc.file}:${lineNum}:${colNum}`)} - ${c(RED, "error")} ${c(BOLD, diag.code)}: ${diag.message}`,
|
||||
`${c(BOLD, `${diag.loc.file}:${lineNum}:${colNum}`)} - ${warning ? c(YELLOW, "warning") : c(RED, "error")} ${c(BOLD, diag.code)}: ${diag.message}`,
|
||||
);
|
||||
if (frame.length) {
|
||||
out.push("", ...frame);
|
||||
@@ -97,3 +101,17 @@ export function renderDiagnostics(
|
||||
})
|
||||
.join("\n\n");
|
||||
}
|
||||
|
||||
/** Divergence warnings after a successful build: the rendered sites plus a
|
||||
* one-line count, on stderr. */
|
||||
export function renderWarnings(
|
||||
diags: readonly ScrDiagnostic[],
|
||||
sourceTextByFile: Map<string, string>,
|
||||
opts: RenderOptions = {},
|
||||
): string {
|
||||
const n = diags.length;
|
||||
return (
|
||||
renderDiagnostics([...diags], sourceTextByFile, opts) +
|
||||
`\n\n${n} warning${n === 1 ? "" : "s"}: the program compiled, but ${n === 1 ? "this site" : "these sites"} can behave differently from Node.`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,9 @@ export interface PreparedExecutableModule {
|
||||
mod: IrModule;
|
||||
sourceTexts: Map<string, string>;
|
||||
stats: LowerStats;
|
||||
/** Divergence warnings (SC6xxx): the build succeeds; these sites can
|
||||
* behave differently from Node. */
|
||||
warnings: ScrDiagnostic[];
|
||||
}
|
||||
|
||||
/** Release the parser/checker before native object generation begins. */
|
||||
@@ -91,5 +94,11 @@ export function prepareExecutableModule(
|
||||
timing("frontend-dispose");
|
||||
}
|
||||
|
||||
return { ok: true, mod: lowered.module!, sourceTexts, stats: lowered.stats };
|
||||
return {
|
||||
ok: true,
|
||||
mod: lowered.module!,
|
||||
sourceTexts,
|
||||
stats: lowered.stats,
|
||||
warnings: lowered.divergences ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -86,6 +86,7 @@ export function analyzeWithFrontend(
|
||||
// which the report groups with these).
|
||||
diagnostics: [...preflight, ...lowered.diagnostics],
|
||||
...(lowered.runtimeFences.length > 0 ? { runtimeFences: lowered.runtimeFences } : {}),
|
||||
...(lowered.divergences !== undefined ? { divergences: lowered.divergences } : {}),
|
||||
...(lowered.unreached ? { unreached: lowered.unreached } : {}),
|
||||
...(lowered.npmBuiltins ? { npmBuiltins: lowered.npmBuiltins } : {}),
|
||||
...(lowered.npmLazyTraps ? { npmLazyTraps: lowered.npmLazyTraps } : {}),
|
||||
|
||||
@@ -4,6 +4,20 @@ import { lowerCheckedPredicateValue } from "../lower-builtin-values.js";
|
||||
import { locOf } from "../../program.js";
|
||||
import { BOOL, F64, type IrExpr, type IrLibFn, JSVAL } from "../../../ir/ir.js";
|
||||
import { lowerBuiltinValuePreservingUndefined, lowerOptionalNumberPredicate } from "./arguments.js";
|
||||
import { tryLowerExpression } from "../expressions/try-lower-expression.js";
|
||||
import { lowerStaticNumberGlobal } from "../lower-calls.js";
|
||||
import { isUnitType, type IrType } from "../../../ir/ir.js";
|
||||
|
||||
/** A string, or a union of a string with undefined/null (the optional
|
||||
* forms the static parsers accept). */
|
||||
function isStringLike(lowerer: Lowerer, type: IrType): boolean {
|
||||
if (type.kind === "string") return true;
|
||||
if (type.kind !== "union") return false;
|
||||
const arms = lowerer.unions.get(type.unionId)?.arms ?? [];
|
||||
return (
|
||||
arms.some((a) => a.kind === "string") && arms.every((a) => a.kind === "string" || isUnitType(a))
|
||||
);
|
||||
}
|
||||
|
||||
/** The lowered Number statics, by member name. The predicate quartet has
|
||||
* static C implementations (JS-exact: the ES2015 statics never coerce, so
|
||||
@@ -96,7 +110,25 @@ export function lowerNumberStaticCall(
|
||||
member === "parseInt" ? "pass an explicit radix: Number.parseInt(s, 10)" : undefined,
|
||||
);
|
||||
}
|
||||
lowerer.requireDynamicApi(`'Number.${member}'`, call);
|
||||
// Number.parseFloat/Number.parseInt ARE the global parsers: over a
|
||||
// string argument they lower to the same static parse (parseInt with
|
||||
// its explicit radix); only the coercing argument shapes need the
|
||||
// engine.
|
||||
const probed = tryLowerExpression(lowerer, call.arguments[0]!);
|
||||
if (probed !== null && isStringLike(lowerer, probed.type)) {
|
||||
const parsed = lowerStaticNumberGlobal(lowerer, member, call, call.arguments, loc);
|
||||
if (parsed !== null) return parsed;
|
||||
}
|
||||
const argText = lowerer.dynamic
|
||||
? ""
|
||||
: probed !== null
|
||||
? lowerer.fmt(probed.type)
|
||||
: lowerer.checker.typeToString(lowerer.checker.getTypeAtLocation(call.arguments[0]!));
|
||||
lowerer.requireDynamicApi(
|
||||
`'Number.${member}' with a '${argText}' argument (only a string argument compiles statically)`,
|
||||
call,
|
||||
"convert the argument explicitly first (for example String(x), or narrow the union), or build with --dynamic to run the coercing call in the embedded engine (adds ~620KB to the binary)",
|
||||
);
|
||||
const callee: IrExpr = {
|
||||
kind: "jsOp",
|
||||
op: "globalGet",
|
||||
|
||||
@@ -27,6 +27,11 @@ import { lowerBuiltinLoaderValue } from "../lower-builtin-values.js";
|
||||
import { lowerProcessIpcSend, ipcMessageListener, ipcDisconnectListener } from "./ipc.js";
|
||||
import { lowerDiagnosticsSubscriber, lowerTracingArguments } from "./async-context.js";
|
||||
|
||||
/** The pinned Node release the runtime reports as process.versions.node
|
||||
* (packages/runtime/src/scr_lib.c SCR_NODE_COMPAT_VERSION, kept equal to
|
||||
* internal/compatibility/node-v24.json by a package test). */
|
||||
export const NODE_COMPAT_VERSION = "24.15.0";
|
||||
|
||||
/** Method calls on first-class process-stream receivers (procStream —
|
||||
* a WritableStream-typed value like prefixStream's `output` param):
|
||||
* write(data) with one string and terminal geometry, dispatched onto
|
||||
@@ -322,6 +327,12 @@ export function lowerProcessProperty(
|
||||
if (member === "versions") {
|
||||
return { kind: "libCall", fn: "process.versions", args: [], type: DYN, loc };
|
||||
}
|
||||
if (member === "version") {
|
||||
// Node's own version string for the compatibility target, the same
|
||||
// release process.versions.node names (the runtime's
|
||||
// SCR_NODE_COMPAT_VERSION) with Node's leading "v".
|
||||
return strLit(`v${NODE_COMPAT_VERSION}`, loc);
|
||||
}
|
||||
if (member === "hrtime") {
|
||||
return { kind: "libCall", fn: "process.hrtimeValue", args: [], type: DYN, loc };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/* The code points the runtime's root-locale collation table covers
|
||||
* (packages/runtime/src/scr_collation_data.h, generated by
|
||||
* scripts/gen-collation-table.mjs): ASCII controls TAB–CR and printable
|
||||
* ASCII, Latin-1 Supplement and Latin Extended-A except the vulgar
|
||||
* fractions, and the combining diacritical marks U+0300–U+0362. Text
|
||||
* outside this set compares by code point natively. */
|
||||
export function isCollationCovered(text: string): boolean {
|
||||
for (const ch of text) {
|
||||
const c = ch.codePointAt(0)!;
|
||||
const covered =
|
||||
(c >= 0x09 && c <= 0x0d) ||
|
||||
(c >= 0x20 && c <= 0x7e) ||
|
||||
(c >= 0xa0 && c <= 0x17f && c !== 0xbc && c !== 0xbd && c !== 0xbe) ||
|
||||
(c >= 0x300 && c <= 0x362);
|
||||
if (!covered) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
DYN,
|
||||
F64,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
STRING,
|
||||
@@ -1266,13 +1265,12 @@ export function lowerStringMethodCall(
|
||||
}
|
||||
|
||||
/** `a.localeCompare(b)` — the one-argument form only (locales/options
|
||||
* select ICU collations that do not exist here). Lowers to an interned
|
||||
* synthetic function returning -1/0/1 by CODE-UNIT order — the same
|
||||
* ordering as the string relational operators — NOT Node's ICU default
|
||||
* collation: a documented divergence (SEMANTICS.md; e.g. Node says
|
||||
* "a" < "B" under ICU while code units say "B" < "a"). For same-case
|
||||
* ASCII the orders agree. Null when the receiver isn't a stdlib string
|
||||
* (caller keeps its generic rejection). */
|
||||
* select collations beyond the default). Lowers to the runtime's
|
||||
* root-locale collation (scr_str_locale_compare): Node's ICU order for
|
||||
* Latin-script text — letters before case and accents ("a" < "B", "ä" <
|
||||
* "z") — and code-point order for other scripts, the documented limit.
|
||||
* Null when the receiver isn't a stdlib string (caller keeps its generic
|
||||
* rejection). */
|
||||
function lowerLocaleCompareCall(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
@@ -1294,61 +1292,5 @@ function lowerLocaleCompareCall(
|
||||
const arg = lowerer.lowerExpr(call.arguments[0]!);
|
||||
if (arg.type.kind !== "string")
|
||||
lowerer.badType(call.arguments[0]!, lowerer.typeOf(call.arguments[0]!));
|
||||
const key = "localeCompare";
|
||||
let helper = lowerer.arrHofHelpers.get(key);
|
||||
if (!helper) {
|
||||
helper = `%str.localeCompare`;
|
||||
lowerer.arrHofHelpers.set(key, helper);
|
||||
lowerer.liftedFns.push(buildLocaleCompareFn(helper, loc));
|
||||
}
|
||||
return { kind: "call", callee: helper, args: [receiver, arg], type: F64, loc };
|
||||
}
|
||||
|
||||
/** `return a < b ? -1 : a > b ? 1 : 0` over the strCmp primitive (the
|
||||
* relational operators' exact machinery — one interned helper, no new IR
|
||||
* or runtime surface). */
|
||||
function buildLocaleCompareFn(name: string, loc: SrcLoc): IrFunction {
|
||||
const cmp = (op: "<" | ">"): IrExpr => ({
|
||||
kind: "strCmp",
|
||||
op,
|
||||
left: varRef("a.0", STRING, loc),
|
||||
right: varRef("b.0", STRING, loc),
|
||||
type: BOOL,
|
||||
loc,
|
||||
});
|
||||
const body: IrStmt[] = [
|
||||
{
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "ternary",
|
||||
cond: cmp("<"),
|
||||
then: numLit(-1, loc),
|
||||
else_: {
|
||||
kind: "ternary",
|
||||
cond: cmp(">"),
|
||||
then: numLit(1, loc),
|
||||
else_: numLit(0, loc),
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
];
|
||||
return {
|
||||
name,
|
||||
params: [
|
||||
{ localId: "a.0", name: "a", type: STRING },
|
||||
{ localId: "b.0", name: "b", type: STRING },
|
||||
],
|
||||
returnType: F64,
|
||||
locals: [
|
||||
{ id: "a.0", name: "a", type: STRING, mutable: true },
|
||||
{ id: "b.0", name: "b", type: STRING, mutable: true },
|
||||
],
|
||||
body,
|
||||
loc,
|
||||
};
|
||||
return { kind: "libCall", fn: "str.localeCompare", args: [receiver, arg], type: F64, loc };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,687 @@
|
||||
/* Divergence scan: compile-time-detectable places where a program that
|
||||
* compiles will behave differently natively than under Node. Each finding
|
||||
* is an SC6xxx warning — never a build failure (`scriptc coverage
|
||||
* --fail-on=divergences` opts into failing on them).
|
||||
*
|
||||
* The scan is deliberately narrow: a finding needs both the construct that
|
||||
* differs AND evidence in the program that the difference is observable
|
||||
* (a write or identity comparison through a copied object, an
|
||||
* order-observing call on the type whose literal is reordered, ...).
|
||||
* Ordinary code produces no findings. Syntactic prefilters run first so the
|
||||
* checker is only consulted for candidate sites.
|
||||
*/
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { locOf } from "../program.js";
|
||||
import { isNodeModulesPath } from "../resolve.js";
|
||||
import {
|
||||
dateLiteralDivergenceDiag,
|
||||
keyOrderDivergenceDiag,
|
||||
localeCompareDivergenceDiag,
|
||||
unrelatedClassCastDivergenceDiag,
|
||||
widthCopyDivergenceDiag,
|
||||
type ScrDiagnostic,
|
||||
} from "../../diagnostics/diagnostic.js";
|
||||
import { isCollationCovered } from "./collation-repertoire.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
|
||||
/** How an identity-sensitive binding is used after a copy could replace it. */
|
||||
interface IdentityUse {
|
||||
kind: "write" | "compare";
|
||||
/** Source text of the use, e.g. `r.path` or `p === other`. */
|
||||
text: string;
|
||||
}
|
||||
|
||||
export function scanDivergences(
|
||||
lowerer: Lowerer,
|
||||
files: readonly ts.SourceFile[],
|
||||
): ScrDiagnostic[] {
|
||||
const own = files.filter(
|
||||
(sf) =>
|
||||
!sf.isDeclarationFile && !sf.fileName.endsWith(".json") && !isNodeModulesPath(sf.fileName),
|
||||
);
|
||||
const out: ScrDiagnostic[] = [];
|
||||
const checker = lowerer.checker;
|
||||
|
||||
// ── pass 1: syntactic collection ────────────────────────────────────
|
||||
const fnsByName = new Map<string, ts.FunctionLikeDeclaration[]>();
|
||||
const calls: ts.CallExpression[] = [];
|
||||
const typedDecls: ts.VariableDeclaration[] = [];
|
||||
const literals: ts.ObjectLiteralExpression[] = [];
|
||||
const observers: ts.Expression[] = [];
|
||||
const casts: (ts.AsExpression | ts.TypeAssertion)[] = [];
|
||||
const dates: { call: ts.CallExpression | ts.NewExpression; arg: ts.Expression }[] = [];
|
||||
const localeCompares: ts.CallExpression[] = [];
|
||||
/** binding name → member names assigned through it (`q.a = 2`). */
|
||||
const assignedMembers = new Map<string, Set<string>>();
|
||||
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (
|
||||
(ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)) &&
|
||||
node.body !== undefined &&
|
||||
node.name !== undefined &&
|
||||
ts.isIdentifier(node.name) &&
|
||||
node.parameters.length > 0
|
||||
) {
|
||||
const list = fnsByName.get(node.name.text) ?? [];
|
||||
list.push(node);
|
||||
fnsByName.set(node.name.text, list);
|
||||
}
|
||||
if (ts.isCallExpression(node)) {
|
||||
calls.push(node);
|
||||
const callee = node.expression;
|
||||
if (ts.isPropertyAccessExpression(callee)) {
|
||||
const member = callee.name.text;
|
||||
if (member === "localeCompare" && node.arguments.length === 1) localeCompares.push(node);
|
||||
const recv = callee.expression;
|
||||
if (ts.isIdentifier(recv)) {
|
||||
if (
|
||||
(recv.text === "JSON" && member === "stringify") ||
|
||||
(recv.text === "Object" &&
|
||||
(member === "keys" || member === "values" || member === "entries"))
|
||||
) {
|
||||
if (node.arguments[0] !== undefined) observers.push(node.arguments[0]);
|
||||
} else if (
|
||||
recv.text === "console" &&
|
||||
(member === "log" ||
|
||||
member === "info" ||
|
||||
member === "warn" ||
|
||||
member === "error" ||
|
||||
member === "debug" ||
|
||||
member === "dir")
|
||||
) {
|
||||
for (const a of node.arguments) if (!isLiteralLike(a)) observers.push(a);
|
||||
} else if (recv.text === "Date" && member === "parse") {
|
||||
if (node.arguments[0] !== undefined) dates.push({ call: node, arg: node.arguments[0] });
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (ts.isNewExpression(node)) {
|
||||
if (
|
||||
ts.isIdentifier(node.expression) &&
|
||||
node.expression.text === "Date" &&
|
||||
node.arguments?.length === 1
|
||||
) {
|
||||
dates.push({ call: node, arg: node.arguments[0]! });
|
||||
}
|
||||
} else if (ts.isForInStatement(node)) {
|
||||
observers.push(node.expression);
|
||||
} else if (ts.isVariableDeclaration(node)) {
|
||||
if (
|
||||
node.type !== undefined &&
|
||||
node.initializer !== undefined &&
|
||||
isExistingObjectExpression(node.initializer) &&
|
||||
ts.isIdentifier(node.name)
|
||||
)
|
||||
typedDecls.push(node);
|
||||
} else if (ts.isObjectLiteralExpression(node)) {
|
||||
if (node.properties.length >= 1 && node.properties.every(isPlainProperty))
|
||||
literals.push(node);
|
||||
} else if (ts.isAsExpression(node) || ts.isTypeAssertion(node)) {
|
||||
casts.push(node);
|
||||
} else if (
|
||||
ts.isBinaryExpression(node) &&
|
||||
node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
||||
ts.isPropertyAccessExpression(node.left) &&
|
||||
ts.isIdentifier(node.left.expression)
|
||||
) {
|
||||
const names = assignedMembers.get(node.left.expression.text) ?? new Set<string>();
|
||||
names.add(node.left.name.text);
|
||||
assignedMembers.set(node.left.expression.text, names);
|
||||
}
|
||||
};
|
||||
// Iterative: absurdly deep nesting must reach the lowering's own fence.
|
||||
for (const sf of own) ts.walkPreorder(sf, visit);
|
||||
|
||||
// ── width copies: writes or identity comparisons through a copy ─────
|
||||
const usesByFn = new Map<ts.FunctionLikeDeclaration, (IdentityUse | null)[]>();
|
||||
const identityUsesOf = (fn: ts.FunctionLikeDeclaration): (IdentityUse | null)[] => {
|
||||
let uses = usesByFn.get(fn);
|
||||
if (uses === undefined) {
|
||||
uses = fn.parameters.map((p) =>
|
||||
ts.isIdentifier(p.name) && fn.body !== undefined ? identityUse(fn.body, p.name.text) : null,
|
||||
);
|
||||
usesByFn.set(fn, uses);
|
||||
}
|
||||
return uses;
|
||||
};
|
||||
for (const call of calls) {
|
||||
const name = calleeName(call);
|
||||
if (name === null) continue;
|
||||
const candidates = fnsByName.get(name);
|
||||
if (candidates === undefined) continue;
|
||||
if (!candidates.some((fn) => identityUsesOf(fn).some((u) => u !== null))) continue;
|
||||
const signature = safely(() => checker.getResolvedSignature(call));
|
||||
if (signature === undefined) continue;
|
||||
const decl = safely(() => checker.signatureDeclaration(signature));
|
||||
if (decl === undefined || !candidates.includes(decl as ts.FunctionLikeDeclaration)) continue;
|
||||
const fn = decl as ts.FunctionLikeDeclaration;
|
||||
const uses = identityUsesOf(fn);
|
||||
call.arguments.forEach((arg, i) => {
|
||||
const use = uses[i];
|
||||
const param = fn.parameters[i];
|
||||
if (use === null || use === undefined || param === undefined || param.type === undefined)
|
||||
return;
|
||||
if (!isExistingObjectExpression(arg)) return;
|
||||
const copy = safely(() => widthCopyBetween(lowerer, arg, param.type!));
|
||||
if (copy === undefined || copy === null) return;
|
||||
out.push(
|
||||
widthCopyDivergenceDiag(copy.source, copy.target, use.kind, use.text, "call", locOf(arg)),
|
||||
);
|
||||
});
|
||||
}
|
||||
for (const decl of typedDecls) {
|
||||
const name = (decl.name as ts.Identifier).text;
|
||||
const scope = enclosingScope(decl);
|
||||
if (scope === undefined) continue;
|
||||
const use = identityUse(scope, name, decl.end);
|
||||
if (use === null) continue;
|
||||
const copy = safely(() => widthCopyBetween(lowerer, decl.initializer!, decl.type!));
|
||||
if (copy === undefined || copy === null) continue;
|
||||
out.push(
|
||||
widthCopyDivergenceDiag(
|
||||
copy.source,
|
||||
copy.target,
|
||||
use.kind,
|
||||
use.text,
|
||||
"declaration",
|
||||
locOf(decl.initializer!),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ── key order: a reordered literal that reaches an order observer ───
|
||||
// Local flow only: the observer's argument is the literal itself, or a
|
||||
// const/let binding initialized with it in the same file. A type that
|
||||
// some other value of it is observed elsewhere is not evidence.
|
||||
if (observers.length > 0) {
|
||||
const reported = new Set<ts.Node>();
|
||||
for (const expr of observers) {
|
||||
let inner: ts.Expression = expr;
|
||||
while (ts.isParenthesizedExpression(inner) || ts.isAsExpression(inner))
|
||||
inner = inner.expression;
|
||||
let lit: ts.ObjectLiteralExpression | null = null;
|
||||
let binding: string | null = null;
|
||||
if (ts.isObjectLiteralExpression(inner)) lit = inner;
|
||||
else if (ts.isIdentifier(inner)) {
|
||||
const symbol = safely(() => checker.getSymbolAtLocation(inner));
|
||||
const decl = symbol === undefined ? undefined : checker.valueDeclarationOf(symbol);
|
||||
if (
|
||||
decl !== undefined &&
|
||||
ts.isVariableDeclaration(decl) &&
|
||||
decl.initializer !== undefined &&
|
||||
ts.isObjectLiteralExpression(decl.initializer) &&
|
||||
decl.getSourceFile() === inner.getSourceFile()
|
||||
) {
|
||||
lit = decl.initializer;
|
||||
binding = inner.text;
|
||||
}
|
||||
}
|
||||
if (lit === null || reported.has(lit) || !literals.includes(lit)) continue;
|
||||
const contextual = safely(() => checker.getContextualType(lit!));
|
||||
if (contextual === undefined) continue;
|
||||
if (namedObjectDeclaration(checker, contextual) === null) continue;
|
||||
const declared = safely(() => checker.getPropertiesOfType(contextual).map((p) => p.name));
|
||||
if (declared === undefined) continue;
|
||||
const written = lit.properties.map((p) => propertyNameText(p)!);
|
||||
const assigned =
|
||||
binding === null ? new Set<string>() : (assignedMembers.get(binding) ?? new Set<string>());
|
||||
const finding = keyOrderFinding(lowerer, contextual, declared, written, assigned);
|
||||
if (finding === null) continue;
|
||||
reported.add(lit);
|
||||
out.push(
|
||||
keyOrderDivergenceDiag(
|
||||
checker.typeToString(contextual),
|
||||
finding.node,
|
||||
finding.native,
|
||||
finding.detail,
|
||||
locOf(lit),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── one-argument localeCompare over text outside the collation table ─
|
||||
for (const call of localeCompares) {
|
||||
const access = call.expression as ts.PropertyAccessExpression;
|
||||
for (const operand of [access.expression, call.arguments[0]!]) {
|
||||
const text = stringLiteralText(operand);
|
||||
if (text !== null && !isCollationCovered(text)) {
|
||||
out.push(localeCompareDivergenceDiag(text, locOf(operand)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── downcasts a call provably feeds a sibling class ────────────────
|
||||
// `function read(n: Base) { (n as A).x }` called with a `B` (B extends
|
||||
// Base, unrelated to A): Node reads the member structurally, natively
|
||||
// the checked class cast throws a TypeError.
|
||||
const downcasts = new Map<
|
||||
ts.Node,
|
||||
{ index: number; target: ts.ClassLikeDeclaration; cast: ts.Node; member: string }[]
|
||||
>();
|
||||
for (const cast of casts) {
|
||||
let inner: ts.Expression = cast.expression;
|
||||
while (ts.isParenthesizedExpression(inner)) inner = inner.expression;
|
||||
if (!ts.isIdentifier(inner)) continue;
|
||||
let fn: ts.Node | undefined = cast.parent;
|
||||
while (fn !== undefined && !ts.isFunctionLike(fn)) fn = fn.parent;
|
||||
if (fn === undefined || !ts.isFunctionLike(fn)) continue;
|
||||
const index = fn.parameters.findIndex(
|
||||
(p) => ts.isIdentifier(p.name) && p.name.text === inner.text,
|
||||
);
|
||||
if (index < 0) continue;
|
||||
const target = safely(() => classDeclarationOfTypeNode(lowerer, cast.type));
|
||||
if (target === undefined || target === null) continue;
|
||||
// A function that tests the parameter with instanceof guards its casts.
|
||||
if (fn.body !== undefined && testsInstanceof(fn.body, inner.text)) continue;
|
||||
// The member read through the cast: `(p as A).member`.
|
||||
let use: ts.Node = cast;
|
||||
while (use.parent !== undefined && ts.isParenthesizedExpression(use.parent)) use = use.parent;
|
||||
const access = use.parent;
|
||||
if (access === undefined || !ts.isPropertyAccessExpression(access) || access.expression !== use)
|
||||
continue;
|
||||
const list = downcasts.get(fn) ?? [];
|
||||
list.push({ index, target, cast, member: access.name.text });
|
||||
downcasts.set(fn, list);
|
||||
}
|
||||
if (downcasts.size > 0) {
|
||||
for (const call of calls) {
|
||||
const name = calleeName(call);
|
||||
if (name === null || !fnsByName.has(name)) continue;
|
||||
const signature = safely(() => checker.getResolvedSignature(call));
|
||||
const decl =
|
||||
signature === undefined ? undefined : safely(() => checker.signatureDeclaration(signature));
|
||||
const sites = decl === undefined ? undefined : downcasts.get(decl);
|
||||
if (sites === undefined) continue;
|
||||
for (const site of sites) {
|
||||
const arg = call.arguments[site.index];
|
||||
if (arg === undefined) continue;
|
||||
const argType = safely(() => checker.getTypeAtLocation(arg));
|
||||
const source = argType === undefined ? null : classDeclarationOfType(checker, argType);
|
||||
if (source === null || source === site.target) continue;
|
||||
if (
|
||||
classExtends(lowerer, source, site.target) ||
|
||||
classExtends(lowerer, site.target, source)
|
||||
)
|
||||
continue;
|
||||
// Node's structural read succeeds only when the instance has the
|
||||
// member; a missing method fails under Node too.
|
||||
if (argType === undefined || checker.getPropertyOfType(argType, site.member) === undefined)
|
||||
continue;
|
||||
out.push(
|
||||
unrelatedClassCastDivergenceDiag(className(source), className(site.target), locOf(arg)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Date strings the native parser does not accept ────────────────
|
||||
for (const { call, arg } of dates) {
|
||||
const callee = ts.isNewExpression(call)
|
||||
? call.expression
|
||||
: ts.isPropertyAccessExpression(call.expression)
|
||||
? call.expression.expression
|
||||
: undefined;
|
||||
if (callee === undefined || !lowerer.isStdlibGlobal(callee, "Date")) continue;
|
||||
for (const literal of dateStringLiterals(checker, arg)) {
|
||||
const text = stringLiteralText(literal)!;
|
||||
if (nativeDateParses(text) || !/\d/.test(text)) continue;
|
||||
out.push(dateLiteralDivergenceDiag(text, locOf(literal)));
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The string literals a Date argument can be: the literal itself, or
|
||||
* each element of a const array of literals a for-of binding iterates. */
|
||||
function dateStringLiterals(checker: ts.TypeChecker, arg: ts.Expression): ts.Expression[] {
|
||||
if (stringLiteralText(arg) !== null) return [arg];
|
||||
if (!ts.isIdentifier(arg)) return [];
|
||||
const symbol = safely(() => checker.getSymbolAtLocation(arg));
|
||||
const decl = symbol === undefined ? undefined : checker.valueDeclarationOf(symbol);
|
||||
const list = decl?.parent;
|
||||
const loop = list?.parent;
|
||||
if (
|
||||
decl === undefined ||
|
||||
!ts.isVariableDeclaration(decl) ||
|
||||
list === undefined ||
|
||||
loop === undefined ||
|
||||
!ts.isForOfStatement(loop) ||
|
||||
loop.initializer !== list
|
||||
)
|
||||
return [];
|
||||
let iterated: ts.Expression = loop.expression;
|
||||
if (ts.isIdentifier(iterated)) {
|
||||
const source = safely(() => checker.getSymbolAtLocation(iterated));
|
||||
const init = source === undefined ? undefined : checker.valueDeclarationOf(source);
|
||||
if (init === undefined || !ts.isVariableDeclaration(init) || init.initializer === undefined)
|
||||
return [];
|
||||
if (init.parent === undefined || (init.parent.flags & ts.NodeFlags.Const) === 0) return [];
|
||||
iterated = init.initializer;
|
||||
}
|
||||
if (!ts.isArrayLiteralExpression(iterated)) return [];
|
||||
return iterated.elements.filter((e) => stringLiteralText(e) !== null);
|
||||
}
|
||||
|
||||
/** A checker query that fails here only loses a warning. */
|
||||
function safely<T>(f: () => T): T | undefined {
|
||||
try {
|
||||
return f();
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function isLiteralLike(node: ts.Expression): boolean {
|
||||
return (
|
||||
ts.isStringLiteral(node) ||
|
||||
ts.isNumericLiteral(node) ||
|
||||
ts.isNoSubstitutionTemplateLiteral(node) ||
|
||||
ts.isTemplateExpression(node)
|
||||
);
|
||||
}
|
||||
|
||||
function stringLiteralText(node: ts.Expression | undefined): string | null {
|
||||
if (node === undefined) return null;
|
||||
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return node.text;
|
||||
return null;
|
||||
}
|
||||
|
||||
/** An expression that denotes an object that already exists (and so has
|
||||
* other references a copy would not update), not a fresh value. */
|
||||
function isExistingObjectExpression(node: ts.Expression): boolean {
|
||||
while (ts.isParenthesizedExpression(node) || ts.isNonNullExpression(node)) node = node.expression;
|
||||
return (
|
||||
ts.isIdentifier(node) ||
|
||||
ts.isPropertyAccessExpression(node) ||
|
||||
ts.isElementAccessExpression(node) ||
|
||||
node.kind === ts.SyntaxKind.ThisKeyword
|
||||
);
|
||||
}
|
||||
|
||||
function isPlainProperty(p: ts.ObjectLiteralElementLike): boolean {
|
||||
return (
|
||||
(ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) &&
|
||||
propertyNameText(p) !== null
|
||||
);
|
||||
}
|
||||
|
||||
function propertyNameText(p: ts.ObjectLiteralElementLike): string | null {
|
||||
if (!ts.isPropertyAssignment(p) && !ts.isShorthandPropertyAssignment(p)) return null;
|
||||
const name = p.name;
|
||||
if (ts.isIdentifier(name) || ts.isStringLiteral(name)) return name.text;
|
||||
return null;
|
||||
}
|
||||
|
||||
function calleeName(call: ts.CallExpression): string | null {
|
||||
const callee = call.expression;
|
||||
if (ts.isIdentifier(callee)) return callee.text;
|
||||
if (ts.isPropertyAccessExpression(callee)) return callee.name.text;
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The first write through, or identity comparison of, the binding `name`
|
||||
* inside `root` (after `from`, when given). Name-based: a nested function
|
||||
* that rebinds the name is skipped. */
|
||||
function identityUse(root: ts.Node, name: string, from = -1): IdentityUse | null {
|
||||
let found: IdentityUse | null = null;
|
||||
const isRef = (node: ts.Expression): boolean => {
|
||||
while (ts.isParenthesizedExpression(node) || ts.isNonNullExpression(node))
|
||||
node = node.expression;
|
||||
return ts.isIdentifier(node) && node.text === name;
|
||||
};
|
||||
const rootOf = (node: ts.Expression): ts.Expression => {
|
||||
let cur = node;
|
||||
while (ts.isPropertyAccessExpression(cur) || ts.isElementAccessExpression(cur))
|
||||
cur = cur.expression;
|
||||
return cur;
|
||||
};
|
||||
const textOf = (node: ts.Node): string => {
|
||||
const text = node.getText();
|
||||
return text.length > 60 ? `${text.slice(0, 57)}...` : text;
|
||||
};
|
||||
const visit = (node: ts.Node): "skip" | "stop" | undefined => {
|
||||
if (found !== null) return "stop";
|
||||
if (node.pos < from) return node.end > from ? undefined : "skip";
|
||||
if (
|
||||
node !== root &&
|
||||
ts.isFunctionLike(node) &&
|
||||
node.parameters.some((p) => ts.isIdentifier(p.name) && p.name.text === name)
|
||||
)
|
||||
return "skip";
|
||||
if (ts.isBinaryExpression(node)) {
|
||||
const op = node.operatorToken.kind;
|
||||
if (
|
||||
(op === ts.SyntaxKind.EqualsEqualsEqualsToken ||
|
||||
op === ts.SyntaxKind.ExclamationEqualsEqualsToken ||
|
||||
op === ts.SyntaxKind.EqualsEqualsToken ||
|
||||
op === ts.SyntaxKind.ExclamationEqualsToken) &&
|
||||
(isRef(node.left) || isRef(node.right)) &&
|
||||
!isNullish(node.left) &&
|
||||
!isNullish(node.right)
|
||||
) {
|
||||
found = { kind: "compare", text: textOf(node) };
|
||||
return "stop";
|
||||
}
|
||||
if (
|
||||
op >= ts.SyntaxKind.FirstAssignment &&
|
||||
op <= ts.SyntaxKind.LastAssignment &&
|
||||
(ts.isPropertyAccessExpression(node.left) || ts.isElementAccessExpression(node.left)) &&
|
||||
isRef(rootOf(node.left))
|
||||
) {
|
||||
found = { kind: "write", text: textOf(node.left) };
|
||||
return "stop";
|
||||
}
|
||||
}
|
||||
if (
|
||||
(ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) &&
|
||||
(node.operator === ts.SyntaxKind.PlusPlusToken ||
|
||||
node.operator === ts.SyntaxKind.MinusMinusToken) &&
|
||||
(ts.isPropertyAccessExpression(node.operand) || ts.isElementAccessExpression(node.operand)) &&
|
||||
isRef(rootOf(node.operand))
|
||||
) {
|
||||
found = { kind: "write", text: textOf(node.operand) };
|
||||
return "stop";
|
||||
}
|
||||
if (
|
||||
ts.isDeleteExpression(node) &&
|
||||
(ts.isPropertyAccessExpression(node.expression) ||
|
||||
ts.isElementAccessExpression(node.expression)) &&
|
||||
isRef(rootOf(node.expression))
|
||||
) {
|
||||
found = { kind: "write", text: textOf(node.expression) };
|
||||
return "stop";
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
ts.walkPreorder(root, visit);
|
||||
return found;
|
||||
}
|
||||
|
||||
function testsInstanceof(root: ts.Node, name: string): boolean {
|
||||
let found = false;
|
||||
ts.walkPreorder(root, (node) => {
|
||||
if (
|
||||
ts.isBinaryExpression(node) &&
|
||||
node.operatorToken.kind === ts.SyntaxKind.InstanceOfKeyword &&
|
||||
ts.isIdentifier(node.left) &&
|
||||
node.left.text === name
|
||||
) {
|
||||
found = true;
|
||||
return "stop";
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
function isNullish(node: ts.Expression): boolean {
|
||||
return (
|
||||
node.kind === ts.SyntaxKind.NullKeyword ||
|
||||
(ts.isIdentifier(node) && node.text === "undefined") ||
|
||||
ts.isVoidExpression(node)
|
||||
);
|
||||
}
|
||||
|
||||
function enclosingScope(node: ts.Node): ts.Node | undefined {
|
||||
let cur = node.parent;
|
||||
while (cur !== undefined && !ts.isBlock(cur) && !ts.isSourceFile(cur)) cur = cur.parent;
|
||||
return cur;
|
||||
}
|
||||
|
||||
/** A plain-record width copy between the value at `source` and the
|
||||
* declared `targetNode` type: both are object types that are not class
|
||||
* instances, arrays, functions or built-in containers, and the target's
|
||||
* properties are a strict subset of the source's — the shape scriptc
|
||||
* compiles as a field copy. */
|
||||
function widthCopyBetween(
|
||||
lowerer: Lowerer,
|
||||
source: ts.Expression,
|
||||
targetNode: ts.TypeNode,
|
||||
): { source: string; target: string } | null {
|
||||
const checker = lowerer.checker;
|
||||
const s = checker.getTypeAtLocation(source);
|
||||
const t = checker.getTypeFromTypeNode(targetNode);
|
||||
if (!isPlainRecordType(checker, s) || !isPlainRecordType(checker, t)) return null;
|
||||
// Accessor members write through to captured state, so a copy of them
|
||||
// still observes the same writes: no divergence to report.
|
||||
const accessor = ts.SymbolFlags.GetAccessor | ts.SymbolFlags.SetAccessor;
|
||||
if (checker.getPropertiesOfType(s).some((p) => (p.flags & accessor) !== 0)) return null;
|
||||
const sourceProps = new Set(checker.getPropertiesOfType(s).map((p) => p.name));
|
||||
const targetProps = checker.getPropertiesOfType(t).map((p) => p.name);
|
||||
if (targetProps.length === 0 || targetProps.length >= sourceProps.size) return null;
|
||||
if (!targetProps.every((p) => sourceProps.has(p))) return null;
|
||||
return { source: checker.typeToString(s), target: checker.typeToString(t) };
|
||||
}
|
||||
|
||||
function isPlainRecordType(checker: ts.TypeChecker, type: ts.Type): boolean {
|
||||
if ((type.flags & ts.TypeFlags.Object) === 0) return false;
|
||||
if (checker.isArrayType(type) || checker.isTupleType(type)) return false;
|
||||
if (checker.getCallSignatures(type).length > 0) return false;
|
||||
if (checker.getConstructSignatures(type).length > 0) return false;
|
||||
if (checker.getIndexInfosOfType(type).length > 0) return false;
|
||||
const symbol = type.getSymbol();
|
||||
if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Class) !== 0) return false;
|
||||
// Library-declared object types (Map, Date, Promise, ...) are not records.
|
||||
const decls = symbol === undefined ? [] : checker.declarationsOf(symbol);
|
||||
if (decls.some((d) => d.getSourceFile().isDeclarationFile)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/** The interface/type-alias/literal declaration that names an object type,
|
||||
* or null for anything else (classes keep their own layout rules). */
|
||||
function namedObjectDeclaration(checker: ts.TypeChecker, type: ts.Type): ts.Node | null {
|
||||
if ((type.flags & ts.TypeFlags.Object) === 0) return null;
|
||||
const alias = type.getAliasSymbol();
|
||||
const symbol = alias ?? type.getSymbol();
|
||||
if (symbol === undefined) return null;
|
||||
if ((symbol.flags & ts.SymbolFlags.Class) !== 0) return null;
|
||||
const decl = checker.declarationsOf(symbol)[0];
|
||||
if (decl === undefined || decl.getSourceFile().isDeclarationFile) return null;
|
||||
return decl;
|
||||
}
|
||||
|
||||
function keyOrderFinding(
|
||||
lowerer: Lowerer,
|
||||
contextual: ts.Type,
|
||||
declared: readonly string[],
|
||||
written: readonly string[],
|
||||
assignedNames: ReadonlySet<string>,
|
||||
): { native: string; node: string; detail: "reordered" | "added-later" } | null {
|
||||
const present = declared.filter((name) => written.includes(name));
|
||||
const writtenKnown = written.filter((name) => declared.includes(name));
|
||||
if (present.join("\0") !== writtenKnown.join("\0") && writtenKnown.length >= 2) {
|
||||
return { native: present.join(", "), node: writtenKnown.join(", "), detail: "reordered" };
|
||||
}
|
||||
// An optional property the literal leaves out but the program assigns
|
||||
// later: Node appends it after the literal's keys; natively it keeps its
|
||||
// declared slot ahead of them.
|
||||
const checker = lowerer.checker;
|
||||
for (let i = 0; i < declared.length; i++) {
|
||||
const name = declared[i]!;
|
||||
if (written.includes(name) || !assignedNames.has(name)) continue;
|
||||
if (!declared.slice(i + 1).some((later) => written.includes(later))) continue;
|
||||
const prop = checker.getPropertyOfType(contextual, name);
|
||||
if (prop === undefined || (prop.flags & ts.SymbolFlags.Optional) === 0) continue;
|
||||
const nodeOrder = [...writtenKnown, name];
|
||||
const nativeOrder = declared.filter((d) => nodeOrder.includes(d));
|
||||
return { native: nativeOrder.join(", "), node: nodeOrder.join(", "), detail: "added-later" };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function classDeclarationOfTypeNode(
|
||||
lowerer: Lowerer,
|
||||
node: ts.TypeNode,
|
||||
): ts.ClassLikeDeclaration | null {
|
||||
if (!ts.isTypeReferenceNode(node) || !ts.isIdentifier(node.typeName)) return null;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(node.typeName);
|
||||
if (symbol === undefined || (symbol.flags & ts.SymbolFlags.Class) === 0) return null;
|
||||
const decl = lowerer.checker
|
||||
.declarationsOf(symbol)
|
||||
.find((d) => ts.isClassDeclaration(d) || ts.isClassExpression(d));
|
||||
if (decl === undefined || decl.getSourceFile().isDeclarationFile) return null;
|
||||
return decl as ts.ClassLikeDeclaration;
|
||||
}
|
||||
|
||||
function classDeclarationOfType(
|
||||
checker: ts.TypeChecker,
|
||||
type: ts.Type,
|
||||
): ts.ClassLikeDeclaration | null {
|
||||
const symbol = type.getSymbol();
|
||||
if (symbol === undefined || (symbol.flags & ts.SymbolFlags.Class) === 0) return null;
|
||||
const decl = checker
|
||||
.declarationsOf(symbol)
|
||||
.find((d) => ts.isClassDeclaration(d) || ts.isClassExpression(d));
|
||||
if (decl === undefined || decl.getSourceFile().isDeclarationFile) return null;
|
||||
return decl as ts.ClassLikeDeclaration;
|
||||
}
|
||||
|
||||
/** Whether `sub` names `base` in its (transitive) extends chain. */
|
||||
function classExtends(
|
||||
lowerer: Lowerer,
|
||||
sub: ts.ClassLikeDeclaration,
|
||||
base: ts.ClassLikeDeclaration,
|
||||
): boolean {
|
||||
let cur: ts.ClassLikeDeclaration | null = sub;
|
||||
for (let guard = 0; cur !== null && guard < 64; guard++) {
|
||||
if (cur === base) return true;
|
||||
const heritage = cur.heritageClauses?.find((h) => h.token === ts.SyntaxKind.ExtendsKeyword);
|
||||
const expr = heritage?.types[0]?.expression;
|
||||
if (expr === undefined) return false;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(expr);
|
||||
if (symbol === undefined) return false;
|
||||
const resolved =
|
||||
(symbol.flags & ts.SymbolFlags.Alias) !== 0
|
||||
? lowerer.checker.getAliasedSymbol(symbol)
|
||||
: symbol;
|
||||
const decl = lowerer.checker
|
||||
.declarationsOf(resolved)
|
||||
.find((d) => ts.isClassDeclaration(d) || ts.isClassExpression(d));
|
||||
cur = decl === undefined ? null : (decl as ts.ClassLikeDeclaration);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function className(decl: ts.ClassLikeDeclaration): string {
|
||||
return decl.name?.text ?? "(anonymous class)";
|
||||
}
|
||||
|
||||
const MONTHS = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
|
||||
|
||||
/** The date-string grammar the native runtime accepts (scr_lib.c
|
||||
* scr_date_parse_get_time): ECMAScript's date-time format with an explicit
|
||||
* offset (date-only forms are UTC) and the certificate-time shape
|
||||
* "Mon DD HH:MM:SS YYYY GMT". Everything else parses to NaN natively. */
|
||||
export function nativeDateParses(text: string): boolean {
|
||||
const iso =
|
||||
/^(?:[+-]\d{6}|\d{4})(?:-(\d{2})(?:-(\d{2}))?)?$/.test(text) ||
|
||||
/^(?:[+-]\d{6}|\d{4})-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{3})?)?(?:Z|[+-]\d{2}:\d{2})$/.test(
|
||||
text,
|
||||
);
|
||||
if (iso) return true;
|
||||
const cert = /^([A-Za-z]{3}) {1,2}\d{1,2} \d{2}:\d{2}:\d{2} \d{4} GMT$/.exec(text);
|
||||
return cert !== null && MONTHS.includes(cert[1]!.toLowerCase());
|
||||
}
|
||||
@@ -1098,153 +1098,20 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
// coercion is the identity and the tests are Number.isNaN /
|
||||
// Number.isFinite exactly (ms's `isFinite(val)` guard).
|
||||
if (
|
||||
(expr.expression.text === "parseInt" || expr.expression.text === "isNaN") &&
|
||||
(expr.expression.text === "parseInt" ||
|
||||
expr.expression.text === "isNaN" ||
|
||||
expr.expression.text === "parseFloat" ||
|
||||
expr.expression.text === "isFinite") &&
|
||||
lowerer.isStdlibSymbol(lowerer.resolveValueSymbol(expr.expression) ?? undefined)
|
||||
) {
|
||||
const name = expr.expression.text;
|
||||
const maxArgs = name === "parseInt" ? 2 : 1;
|
||||
if (expr.arguments.length < 1 || expr.arguments.length > maxArgs) {
|
||||
lowerer.noLowering(
|
||||
`${name} with ${expr.arguments.length} argument${expr.arguments.length === 1 ? "" : "s"}`,
|
||||
expr,
|
||||
);
|
||||
}
|
||||
if (name === "isNaN") {
|
||||
const x = lowerer.lowerExprExpecting(expr.arguments[0]!, F64);
|
||||
return { kind: "libCall", fn: "num.isNaN", args: [x], type: BOOL, loc };
|
||||
}
|
||||
const radix: IrExpr = expr.arguments[1]
|
||||
? lowerer.lowerExprExpecting(expr.arguments[1], F64)
|
||||
: { kind: "numLit", value: 0, type: F64, loc };
|
||||
const optional = optionalCallValue(lowerer, expr.arguments[0]!);
|
||||
if (optional?.type.kind === "union") {
|
||||
const def = lowerer.unions.get(optional.type.unionId);
|
||||
const stringTag = def?.arms.findIndex((arm) => arm.kind === "string") ?? -1;
|
||||
if (
|
||||
def &&
|
||||
stringTag >= 0 &&
|
||||
def.arms.every((arm) => arm.kind === "string" || isUnitType(arm))
|
||||
) {
|
||||
const unionT = optional.type;
|
||||
const key = `parseInt.optional:${unionT.unionId}`;
|
||||
let helper = lowerer.valueHelpers.get(key);
|
||||
if (!helper) {
|
||||
helper = `%parseInt.optional.${lowerer.valueHelpers.size}`;
|
||||
lowerer.valueHelpers.set(key, helper);
|
||||
const value: IrExpr = { kind: "varRef", localId: "value.0", type: unionT, loc };
|
||||
const radixRef: IrExpr = { kind: "varRef", localId: "radix.0", type: F64, loc };
|
||||
const body: IrStmt[] = def.arms.flatMap((arm, tag): IrStmt[] =>
|
||||
isUnitType(arm)
|
||||
? [
|
||||
{
|
||||
kind: "if",
|
||||
cond: {
|
||||
kind: "unionIsTag",
|
||||
unionId: unionT.unionId,
|
||||
tag,
|
||||
negated: false,
|
||||
value,
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: [
|
||||
{
|
||||
kind: "return",
|
||||
value: { kind: "numLit", value: NaN, type: F64, loc },
|
||||
loc,
|
||||
},
|
||||
],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
);
|
||||
body.push({
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "libCall",
|
||||
fn: "num.parseInt",
|
||||
args: [
|
||||
{
|
||||
kind: "unionNarrow",
|
||||
unionId: unionT.unionId,
|
||||
tag: stringTag,
|
||||
value,
|
||||
type: STRING,
|
||||
loc,
|
||||
},
|
||||
radixRef,
|
||||
],
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
});
|
||||
lowerer.liftedFns.push({
|
||||
name: helper,
|
||||
params: [
|
||||
{ localId: "value.0", name: "value", type: unionT },
|
||||
{ localId: "radix.0", name: "radix", type: F64 },
|
||||
],
|
||||
returnType: F64,
|
||||
locals: [
|
||||
{ id: "value.0", name: "value", type: unionT, mutable: true },
|
||||
{ id: "radix.0", name: "radix", type: F64, mutable: true },
|
||||
],
|
||||
body,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return { kind: "call", callee: helper, args: [optional, radix], type: F64, loc };
|
||||
}
|
||||
}
|
||||
const s = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING);
|
||||
return { kind: "libCall", fn: "num.parseInt", args: [s, radix], type: F64, loc };
|
||||
}
|
||||
// STATIC parseFloat/isFinite over exactly-typed arguments —
|
||||
// parseInt's siblings (num.parseFloat is ECMA 19.2.4's decimal-
|
||||
// literal prefix parse in scr_string.c; a number-typed isFinite IS
|
||||
// Number.isFinite — the global's ToNumber coercion is the identity
|
||||
// there, ms's `isFinite(val)` guard). Other argument types fall
|
||||
// through to today's island path (--dynamic) or its SC2012 fence:
|
||||
// the ToNumber/ToString coercions on arbitrary values stay engine
|
||||
// territory. The probe never emits — lowering is IR construction.
|
||||
if (
|
||||
(expr.expression.text === "parseFloat" || expr.expression.text === "isFinite") &&
|
||||
expr.arguments.length === 1 &&
|
||||
lowerer.isStdlibSymbol(lowerer.resolveValueSymbol(expr.expression) ?? undefined)
|
||||
) {
|
||||
const name = expr.expression.text;
|
||||
if (name === "parseFloat") {
|
||||
const optional = optionalCallValue(lowerer, expr.arguments[0]!);
|
||||
if (optional) {
|
||||
const parsed = lowerOptionalStringNumber(
|
||||
lowerer,
|
||||
optional,
|
||||
loc,
|
||||
"num.parseFloat",
|
||||
"parseFloat",
|
||||
);
|
||||
if (parsed) return parsed;
|
||||
}
|
||||
}
|
||||
const probed = tryLowerExpression(lowerer, expr.arguments[0]!);
|
||||
if (name === "parseFloat" && probed?.type.kind === "string") {
|
||||
return { kind: "libCall", fn: "num.parseFloat", args: [probed], type: F64, loc };
|
||||
}
|
||||
if (name === "parseFloat" && probed?.type.kind === "dyn") {
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "num.parseFloat",
|
||||
args: [{ kind: "libCall", fn: "dyn.toStringCoerce", args: [probed], type: STRING, loc }],
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (name === "isFinite" && probed?.type.kind === "f64") {
|
||||
return { kind: "libCall", fn: "number.isFinite", args: [probed], type: BOOL, loc };
|
||||
}
|
||||
const parsed = lowerStaticNumberGlobal(
|
||||
lowerer,
|
||||
expr.expression.text,
|
||||
expr,
|
||||
expr.arguments,
|
||||
loc,
|
||||
);
|
||||
if (parsed !== null) return parsed;
|
||||
}
|
||||
// STATIC encodeURIComponent/encodeURI/decodeURIComponent
|
||||
// (str.encodeUriComponent / str.encodeUri / str.decodeUriComponent —
|
||||
@@ -1351,7 +1218,20 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
);
|
||||
}
|
||||
if (islFn && expr.arguments.length === islFn.args.length) {
|
||||
lowerer.requireDynamicApi(`'${expr.expression.text}'`, expr);
|
||||
// The static forms above already took the exactly-typed argument
|
||||
// (a string for parseFloat, a number for isFinite): name the
|
||||
// argument type that sent this call to the engine, so the message
|
||||
// states the condition rather than reading as if the global itself
|
||||
// were unsupported.
|
||||
const argText = lowerer.dynamic ? "" : loweredArgumentText(lowerer, expr.arguments[0]!);
|
||||
const exact = expr.expression.text === "isFinite" ? "number" : "string";
|
||||
lowerer.requireDynamicApi(
|
||||
`'${expr.expression.text}' with a '${argText}' argument (only a ${exact} argument compiles statically)`,
|
||||
expr,
|
||||
exact === "string"
|
||||
? "convert the argument explicitly first (for example String(x), or narrow the union), or build with --dynamic to run the coercing call in the embedded engine (adds ~620KB to the binary)"
|
||||
: "narrow the argument to a number first (isFinite over a number compiles statically), or build with --dynamic to run the coercing call in the embedded engine (adds ~620KB to the binary)",
|
||||
);
|
||||
const callee: IrExpr = {
|
||||
kind: "jsOp",
|
||||
op: "globalGet",
|
||||
@@ -9613,3 +9493,166 @@ function unionObjectMethodHelper(
|
||||
lowerer.liftedFns.push({ name, params, returnType: resultT, locals, body, loc });
|
||||
return name;
|
||||
}
|
||||
|
||||
/** The argument's lowered type for a message (what the static form saw),
|
||||
* or the checker's text when the argument itself does not lower. */
|
||||
function loweredArgumentText(lowerer: Lowerer, arg: ts.Expression): string {
|
||||
const probed = tryLowerExpression(lowerer, arg);
|
||||
return probed !== null
|
||||
? lowerer.fmt(probed.type)
|
||||
: lowerer.checker.typeToString(lowerer.checker.getTypeAtLocation(arg));
|
||||
}
|
||||
|
||||
/** The static global number parsers (parseInt/parseFloat/isNaN/isFinite),
|
||||
* shared by the globals and their Number.* aliases. `call` blames arity
|
||||
* fences; null when the argument shape needs the engine (the caller keeps
|
||||
* its island path or SC2012 fence). */
|
||||
export function lowerStaticNumberGlobal(
|
||||
lowerer: Lowerer,
|
||||
name: string,
|
||||
call: ts.CallExpression,
|
||||
args: readonly ts.Expression[],
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
if (name === "parseInt" || name === "isNaN") {
|
||||
const maxArgs = name === "parseInt" ? 2 : 1;
|
||||
if (args.length < 1 || args.length > maxArgs) {
|
||||
lowerer.noLowering(
|
||||
`${name} with ${args.length} argument${args.length === 1 ? "" : "s"}`,
|
||||
call,
|
||||
);
|
||||
}
|
||||
if (name === "isNaN") {
|
||||
const x = lowerer.lowerExprExpecting(args[0]!, F64);
|
||||
return { kind: "libCall", fn: "num.isNaN", args: [x], type: BOOL, loc };
|
||||
}
|
||||
const radix: IrExpr = args[1]
|
||||
? lowerer.lowerExprExpecting(args[1], F64)
|
||||
: { kind: "numLit", value: 0, type: F64, loc };
|
||||
const optional = optionalCallValue(lowerer, args[0]!);
|
||||
if (optional?.type.kind === "union") {
|
||||
const def = lowerer.unions.get(optional.type.unionId);
|
||||
const stringTag = def?.arms.findIndex((arm) => arm.kind === "string") ?? -1;
|
||||
if (
|
||||
def &&
|
||||
stringTag >= 0 &&
|
||||
def.arms.every((arm) => arm.kind === "string" || isUnitType(arm))
|
||||
) {
|
||||
const unionT = optional.type;
|
||||
const key = `parseInt.optional:${unionT.unionId}`;
|
||||
let helper = lowerer.valueHelpers.get(key);
|
||||
if (!helper) {
|
||||
helper = `%parseInt.optional.${lowerer.valueHelpers.size}`;
|
||||
lowerer.valueHelpers.set(key, helper);
|
||||
const value: IrExpr = { kind: "varRef", localId: "value.0", type: unionT, loc };
|
||||
const radixRef: IrExpr = { kind: "varRef", localId: "radix.0", type: F64, loc };
|
||||
const body: IrStmt[] = def.arms.flatMap((arm, tag): IrStmt[] =>
|
||||
isUnitType(arm)
|
||||
? [
|
||||
{
|
||||
kind: "if",
|
||||
cond: {
|
||||
kind: "unionIsTag",
|
||||
unionId: unionT.unionId,
|
||||
tag,
|
||||
negated: false,
|
||||
value,
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: [
|
||||
{
|
||||
kind: "return",
|
||||
value: { kind: "numLit", value: NaN, type: F64, loc },
|
||||
loc,
|
||||
},
|
||||
],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
);
|
||||
body.push({
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "libCall",
|
||||
fn: "num.parseInt",
|
||||
args: [
|
||||
{
|
||||
kind: "unionNarrow",
|
||||
unionId: unionT.unionId,
|
||||
tag: stringTag,
|
||||
value,
|
||||
type: STRING,
|
||||
loc,
|
||||
},
|
||||
radixRef,
|
||||
],
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
});
|
||||
lowerer.liftedFns.push({
|
||||
name: helper,
|
||||
params: [
|
||||
{ localId: "value.0", name: "value", type: unionT },
|
||||
{ localId: "radix.0", name: "radix", type: F64 },
|
||||
],
|
||||
returnType: F64,
|
||||
locals: [
|
||||
{ id: "value.0", name: "value", type: unionT, mutable: true },
|
||||
{ id: "radix.0", name: "radix", type: F64, mutable: true },
|
||||
],
|
||||
body,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return { kind: "call", callee: helper, args: [optional, radix], type: F64, loc };
|
||||
}
|
||||
}
|
||||
const s = lowerer.lowerExprExpecting(args[0]!, STRING);
|
||||
return { kind: "libCall", fn: "num.parseInt", args: [s, radix], type: F64, loc };
|
||||
}
|
||||
// STATIC parseFloat/isFinite over exactly-typed arguments —
|
||||
// parseInt's siblings (num.parseFloat is ECMA 19.2.4's decimal-
|
||||
// literal prefix parse in scr_string.c; a number-typed isFinite IS
|
||||
// Number.isFinite — the global's ToNumber coercion is the identity
|
||||
// there, ms's `isFinite(val)` guard). Other argument types fall
|
||||
// through to today's island path (--dynamic) or its SC2012 fence:
|
||||
// the ToNumber/ToString coercions on arbitrary values stay engine
|
||||
// territory. The probe never emits — lowering is IR construction.
|
||||
if ((name === "parseFloat" || name === "isFinite") && args.length === 1) {
|
||||
if (name === "parseFloat") {
|
||||
const optional = optionalCallValue(lowerer, args[0]!);
|
||||
if (optional) {
|
||||
const parsed = lowerOptionalStringNumber(
|
||||
lowerer,
|
||||
optional,
|
||||
loc,
|
||||
"num.parseFloat",
|
||||
"parseFloat",
|
||||
);
|
||||
if (parsed) return parsed;
|
||||
}
|
||||
}
|
||||
const probed = tryLowerExpression(lowerer, args[0]!);
|
||||
if (name === "parseFloat" && probed?.type.kind === "string") {
|
||||
return { kind: "libCall", fn: "num.parseFloat", args: [probed], type: F64, loc };
|
||||
}
|
||||
if (name === "parseFloat" && probed?.type.kind === "dyn") {
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "num.parseFloat",
|
||||
args: [{ kind: "libCall", fn: "dyn.toStringCoerce", args: [probed], type: STRING, loc }],
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (name === "isFinite" && probed?.type.kind === "f64") {
|
||||
return { kind: "libCall", fn: "number.isFinite", args: [probed], type: BOOL, loc };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -254,9 +254,14 @@ export function islandRegexpOf(e: IrExpr): IrExpr | null {
|
||||
* proceeds to engine ops; without it the use site is a per-site SC2012
|
||||
* (poison-recovered, so every site in the program reports — and the
|
||||
* coverage report groups them under "runs with --dynamic"). */
|
||||
export function requireDynamicApi(lowerer: Lowerer, feature: string, node: ts.Node): void {
|
||||
export function requireDynamicApi(
|
||||
lowerer: Lowerer,
|
||||
feature: string,
|
||||
node: ts.Node,
|
||||
hint?: string,
|
||||
): void {
|
||||
if (lowerer.dynamic) return;
|
||||
lowerer.pushDiag(requiresDynamicApiDiag(feature, locOf(node)));
|
||||
lowerer.pushDiag(requiresDynamicApiDiag(feature, locOf(node), hint));
|
||||
throw new PoisonError();
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
type CycleInitFlag,
|
||||
} from "./cycle-initialization.js";
|
||||
import { identityPreservingWidening } from "./coercions/identity.js";
|
||||
import { scanDivergences } from "./divergences.js";
|
||||
import { checkedClassAssertion } from "./class-assertions.js";
|
||||
import { narrowStoredClassValue, narrowGenericClassValue } from "./class-unions.js";
|
||||
import { isOptionalProcessStreamProperty } from "./builtins/process.js";
|
||||
@@ -680,6 +681,40 @@ export function stmtUsesIsland(stmts: IrStmt | IrStmt[]): boolean {
|
||||
return !everyStmtChild(stmts, expr, nested);
|
||||
}
|
||||
|
||||
/** Statements a never-lowered body would have contributed to the coverage
|
||||
* totals: every element of a statement list (blocks, case clauses), plus
|
||||
* the single-statement bodies of control flow, nested functions included —
|
||||
* the same accounting lowerStmts applies to bodies that do lower. */
|
||||
export function countBodyStatements(root: ts.Node): number {
|
||||
let n = 0;
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (ts.isBlock(node) || ts.isCaseClause(node) || ts.isDefaultClause(node)) {
|
||||
n += node.statements.length;
|
||||
} else if (
|
||||
(ts.isIfStatement(node) ||
|
||||
ts.isForStatement(node) ||
|
||||
ts.isForInStatement(node) ||
|
||||
ts.isForOfStatement(node) ||
|
||||
ts.isWhileStatement(node) ||
|
||||
ts.isDoStatement(node) ||
|
||||
ts.isLabeledStatement(node)) &&
|
||||
node.parent !== undefined
|
||||
) {
|
||||
const bodies = ts.isIfStatement(node)
|
||||
? [node.thenStatement, node.elseStatement]
|
||||
: ts.isLabeledStatement(node)
|
||||
? [node.statement]
|
||||
: [node.statement];
|
||||
for (const body of bodies) {
|
||||
if (body !== undefined && !ts.isBlock(body) && !ts.isIfStatement(body)) n++;
|
||||
}
|
||||
}
|
||||
};
|
||||
// Iterative: absurdly deep nesting must not overflow the stack here.
|
||||
ts.walkPreorder(root, visit);
|
||||
return n;
|
||||
}
|
||||
|
||||
export interface LowerResult {
|
||||
/** Present iff diagnostics is empty. */
|
||||
module: IrModule | null;
|
||||
@@ -696,7 +731,10 @@ export interface LowerResult {
|
||||
provenanceElided?: ScrDiagnostic[];
|
||||
/** Coverage only (LowerOptions.coverage): the unreached remainder,
|
||||
* lowered in a throwaway pass — blockers in it can never fail a build. */
|
||||
unreached?: { diagnostics: ScrDiagnostic[]; stats: LowerStats };
|
||||
unreached?: { diagnostics: ScrDiagnostic[]; stats: LowerStats; runtimeFences?: ScrDiagnostic[] };
|
||||
/** Compile-time-detectable behavior differences from Node (SC6xxx
|
||||
* divergence warnings) in the program's own modules. Never failures. */
|
||||
divergences?: ScrDiagnostic[];
|
||||
/** --dynamic only: every Node builtin the embedded npm graph imports,
|
||||
* shimmed or not — the coverage report's island honesty. */
|
||||
npmBuiltins?: NpmBuiltinUse[];
|
||||
@@ -988,6 +1026,11 @@ export function lowerToIr(
|
||||
resultLowerer = emit;
|
||||
timing("historical-order-relower");
|
||||
}
|
||||
// Divergence warnings over the program's own modules: computed once,
|
||||
// after lowering, from the same checker (never part of a failure).
|
||||
const divergences = scanDivergences(resultLowerer, moduleOrder);
|
||||
timing("divergence-scan", { found: divergences.length });
|
||||
if (divergences.length > 0) result = { ...result, divergences };
|
||||
if (options.coverage !== true) return result;
|
||||
const remainder = new Lowerer(program, entry, moduleOrder, dynamic, {
|
||||
frontendServices: options.frontendServices,
|
||||
@@ -1004,7 +1047,17 @@ export function lowerToIr(
|
||||
externalTypeSpecifiersByFile,
|
||||
});
|
||||
const rem = remainder.run();
|
||||
return { ...result, unreached: { diagnostics: rem.diagnostics, stats: rem.stats } };
|
||||
// Unreached JS statements whose fences deferred to runtime count as
|
||||
// failed statements in the remainder's stats, so their diagnostics must
|
||||
// travel with them: a failed statement is never left unexplained.
|
||||
return {
|
||||
...result,
|
||||
unreached: {
|
||||
diagnostics: rem.diagnostics,
|
||||
stats: rem.stats,
|
||||
...(rem.runtimeFences.length > 0 ? { runtimeFences: rem.runtimeFences } : {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** The island-handle type a `import(...)` initializer gives a binding
|
||||
@@ -5084,12 +5137,12 @@ export class Lowerer {
|
||||
if (sig && !this.wantBody(sig.name)) continue;
|
||||
const fn = this.lowerFunction(decl);
|
||||
if (fn) functions.push(fn);
|
||||
else if (this.countsSkips()) this.stats.functionsSkipped++;
|
||||
else if (this.countsSkips()) this.countSkippedBody(decl);
|
||||
}
|
||||
for (const decl of fp.classDecls) {
|
||||
const info = this.classes.get(this.classNamer(decl));
|
||||
if (info) functions.push(...this.lowerClassMembers(info));
|
||||
else if (this.countsSkips()) this.stats.functionsSkipped++;
|
||||
else if (this.countsSkips()) this.countSkippedBody(decl);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5302,6 +5355,7 @@ export class Lowerer {
|
||||
...(this.npmEmbedded ? { embedded: this.npmEmbedded } : {}),
|
||||
entry: ENTRY_NAME,
|
||||
...(this.usesWorkers ? { workers: true } : {}),
|
||||
...(isNodeEsmFile(this.entry, this.program) ? {} : { commonJsEntry: true }),
|
||||
...(this.ffiImports.length > 0 ? { ffiImports: [...this.ffiImports] } : {}),
|
||||
};
|
||||
if (module) sanitizeUnregisteredClassTypes(module, (name) => this.classes.has(name));
|
||||
@@ -5361,6 +5415,25 @@ export class Lowerer {
|
||||
}
|
||||
}
|
||||
|
||||
/** A declaration whose signature blocked never lowers its body, so its
|
||||
* statements would otherwise vanish from the coverage totals and the
|
||||
* percentage would overstate how much of the program compiles. Count
|
||||
* every statement the body holds as attempted and failed, in addition to
|
||||
* the skipped-function tally. */
|
||||
private countSkippedBody(decl: ts.Node): void {
|
||||
this.stats.functionsSkipped++;
|
||||
if (this.suppressStats) return;
|
||||
const n = countBodyStatements(decl);
|
||||
if (n === 0) return;
|
||||
this.stats.statementsTotal += n;
|
||||
this.stats.statementsFailed += n;
|
||||
const file = decl.getSourceFile().fileName;
|
||||
for (let i = 0; i < n; i++) {
|
||||
this.bumpFileStat(file, "total");
|
||||
this.bumpFileStat(file, "failed");
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether run() counts a signature-blocked declaration in
|
||||
* stats.functionsSkipped: whole-program passes and the coverage
|
||||
* remainder do; an externally-gated emit pass leaves the counting to the
|
||||
@@ -11324,8 +11397,8 @@ export class Lowerer {
|
||||
|
||||
/* ── the island-backed ambient surface (ISLAND_SURFACE) ───────────── */
|
||||
|
||||
requireDynamicApi(feature: string, node: ts.Node): void {
|
||||
return requireDynamicApi(this, feature, node);
|
||||
requireDynamicApi(feature: string, node: ts.Node, hint?: string): void {
|
||||
return requireDynamicApi(this, feature, node, hint);
|
||||
}
|
||||
|
||||
lowerMathProperty(expr: ts.PropertyAccessExpression): IrExpr | null {
|
||||
|
||||
@@ -223,7 +223,13 @@ function projectAllowsSyntheticDefaultImports(options: Record<string, unknown>):
|
||||
function adoptProjectConfig7(
|
||||
host: ts.Ts7Host,
|
||||
entryPath: string,
|
||||
): { configFile: string | null; options: ts.Ts7CompilerOptions; diags: ScrDiagnostic[] } {
|
||||
): {
|
||||
configFile: string | null;
|
||||
options: ts.Ts7CompilerOptions;
|
||||
diags: ScrDiagnostic[];
|
||||
projectLib?: readonly string[];
|
||||
forcedStrictNullChecks?: boolean;
|
||||
} {
|
||||
const configFile = ts.findConfigFile(dirname(entryPath), ts.sys.fileExists) ?? null;
|
||||
if (!configFile) {
|
||||
return { configFile, options: { ...BASE_OPTIONS, ...FORCED_OPTIONS }, diags: [] };
|
||||
@@ -268,11 +274,24 @@ function adoptProjectConfig7(
|
||||
adopted["paths"] = paths;
|
||||
}
|
||||
const nullChecks = adopted["strictNullChecks"] ?? adopted["strict"] ?? false;
|
||||
if (nullChecks !== true) {
|
||||
const forcedStrictNullChecks = nullChecks !== true;
|
||||
if (forcedStrictNullChecks) {
|
||||
diags.push(strictNullChecksFloorDiag(configFile));
|
||||
adopted["strictNullChecks"] = true;
|
||||
}
|
||||
return { configFile, options: { ...BASE_OPTIONS, ...adopted, ...FORCED_OPTIONS }, diags };
|
||||
// The project's own library selection, kept only to explain errors: the
|
||||
// checker always uses scriptc's fixed library set (FORCED_OPTIONS).
|
||||
const rawLib = parsed.options["lib"];
|
||||
const projectLib = Array.isArray(rawLib)
|
||||
? rawLib.filter((entry): entry is string => typeof entry === "string")
|
||||
: undefined;
|
||||
return {
|
||||
configFile,
|
||||
options: { ...BASE_OPTIONS, ...adopted, ...FORCED_OPTIONS },
|
||||
diags,
|
||||
...(projectLib === undefined ? {} : { projectLib }),
|
||||
forcedStrictNullChecks,
|
||||
};
|
||||
}
|
||||
|
||||
/** The target project's @types/node, resolved with the OWN resolver's
|
||||
@@ -283,6 +302,15 @@ function resolveNodeTypes7(entryPath: string): string | null {
|
||||
return file !== null && isNodeTypesPath(file) ? file : null;
|
||||
}
|
||||
|
||||
export interface TypeWorldDifferences {
|
||||
/** No @types/node: the bundled fallback declarations type Node. */
|
||||
nodeFallback: boolean;
|
||||
/** The project tsconfig's "lib" entries (scriptc checks with its own). */
|
||||
projectLib: readonly string[] | null;
|
||||
/** The project disables strictNullChecks; scriptc forces it on. */
|
||||
forcedStrictNullChecks: boolean;
|
||||
}
|
||||
|
||||
export interface LoadResult {
|
||||
program: ts.Program;
|
||||
entry: ts.SourceFile;
|
||||
@@ -305,6 +333,9 @@ export interface LoadResult {
|
||||
* checkPreflight. */
|
||||
startupCrash?: StartupCrash | null;
|
||||
configDiags: ScrDiagnostic[];
|
||||
/** How scriptc's type world differs from the project's own tsc setup —
|
||||
* used only to label the TypeScript errors those differences cause. */
|
||||
typeWorld?: TypeWorldDifferences;
|
||||
/** Coverage-only external host modules: exact bare specifier → local
|
||||
* declaration file. These participate in checker resolution but never
|
||||
* become runtime module edges. */
|
||||
@@ -690,6 +721,11 @@ function loadProgram7(
|
||||
entry,
|
||||
moduleOrder: [],
|
||||
configDiags: config.diags,
|
||||
typeWorld: {
|
||||
nodeFallback: nodeTypes === null,
|
||||
projectLib: config.projectLib ?? null,
|
||||
forcedStrictNullChecks: config.forcedStrictNullChecks ?? false,
|
||||
},
|
||||
externalTypes,
|
||||
externalTypeSpecifiersByFile,
|
||||
projectWorld: () => (projectWorld ??= host.createProgram(programRoots, options)),
|
||||
@@ -3377,7 +3413,13 @@ function preflight7(load: LoadResult): {
|
||||
const file = d.fileName ?? entry.fileName;
|
||||
const start = d.fileName !== undefined ? d.pos : 0;
|
||||
const end = d.fileName !== undefined ? d.end : 0;
|
||||
return tscPassthroughDiag(message, { file, start, end });
|
||||
const diag = tscPassthroughDiag(message, { file, start, end });
|
||||
const hint = typeWorldHint(d, message, load);
|
||||
if (hint !== null) {
|
||||
diag.typeWorld = true;
|
||||
diag.hint = hint;
|
||||
}
|
||||
return diag;
|
||||
};
|
||||
/* TS7 checker change (not finding 5, same discipline): tsgo types a
|
||||
* NAMESPACE import as the spec's non-callable module namespace object
|
||||
@@ -5176,3 +5218,115 @@ export function isCjsExportTableLiteral(obj: ts.ObjectLiteralExpression): boolea
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Names other runtimes and the DOM declare that scriptc's type world
|
||||
* (the ES2025 library plus Node types) does not. */
|
||||
const FOREIGN_RUNTIME_GLOBALS = new Set(["Deno", "Bun"]);
|
||||
const DOM_GLOBALS = new Set([
|
||||
"window",
|
||||
"document",
|
||||
"navigator",
|
||||
"location",
|
||||
"localStorage",
|
||||
"sessionStorage",
|
||||
"HTMLElement",
|
||||
"Element",
|
||||
"Document",
|
||||
"Window",
|
||||
"BodyInit",
|
||||
"HeadersInit",
|
||||
"RequestInfo",
|
||||
"XMLHttpRequest",
|
||||
"requestAnimationFrame",
|
||||
]);
|
||||
/** Root identifiers whose members the bundled Node fallback declares. */
|
||||
const NODE_GLOBAL_ROOTS = new Set(["process", "Buffer", "console", "globalThis"]);
|
||||
|
||||
/** A hint when a TypeScript error comes from scriptc's own type world rather
|
||||
* than from the project: a library member newer than ES2025, a global of
|
||||
* another runtime or of the DOM, a Node member the bundled fallback types
|
||||
* leave out, or the strictNullChecks scriptc enforces. Null otherwise. */
|
||||
function typeWorldHint(d: ts.Diagnostic, message: string, load: LoadResult): string | null {
|
||||
const world = load.typeWorld;
|
||||
if (world === undefined) return null;
|
||||
const quoted = /'([^']+)'/.exec(message)?.[1];
|
||||
// TS2304/TS2552: Cannot find name; TS2580/TS2591/TS2592: missing type
|
||||
// packages; TS2583: needs a newer library.
|
||||
if (d.code === 2304 || d.code === 2552 || d.code === 2583) {
|
||||
if (quoted !== undefined && FOREIGN_RUNTIME_GLOBALS.has(quoted)) {
|
||||
return `'${quoted}' is another runtime's global; scriptc's type world declares the ES2025 library and Node only — feature-test it through globalThis instead, e.g. '(globalThis as { ${quoted}?: unknown }).${quoted}' (the native program runs as Node would, where it is undefined)`;
|
||||
}
|
||||
if (quoted !== undefined && DOM_GLOBALS.has(quoted)) {
|
||||
return `'${quoted}' comes from the DOM library, which scriptc's type world does not include (it checks with the ES2025 library and Node types regardless of the project's "lib") — declare the shape you need locally, or move browser-only code out of the program`;
|
||||
}
|
||||
if (d.code === 2583) {
|
||||
return `scriptc checks with the ES2025 library regardless of the project's "lib" setting, and '${quoted ?? "this name"}' comes from a newer library that does not compile yet`;
|
||||
}
|
||||
}
|
||||
// TS2339/TS2550/TS2551: property does not exist (2550: on this lib).
|
||||
if (d.code === 2339 || d.code === 2550 || d.code === 2551) {
|
||||
const typeText = /on type '([^']+)'/.exec(message)?.[1] ?? "";
|
||||
const newerLib =
|
||||
world.projectLib !== null &&
|
||||
world.projectLib.some((lib) => /esnext|es202[6-9]|es20[3-9]\d/i.test(lib));
|
||||
if (d.code === 2550 || (newerLib && /^[A-Z][A-Za-z0-9]*(?:Constructor)?$/.test(typeText))) {
|
||||
return `scriptc checks with the ES2025 library regardless of the project's "lib" setting, and '${quoted ?? "this member"}' comes from a newer library that does not compile yet — use an ES2025 equivalent here`;
|
||||
}
|
||||
if (world.nodeFallback && d.fileName !== undefined) {
|
||||
const root = receiverRoot(load.program.getSourceFile(d.fileName)?.text ?? "", d.pos);
|
||||
if (
|
||||
root !== null &&
|
||||
(NODE_GLOBAL_ROOTS.has(root) || importsNodeBuiltin(load, d.fileName, root))
|
||||
) {
|
||||
return `the project has no @types/node, so scriptc types Node with its bundled declarations, which list only the members that compile natively — '${quoted ?? "this member"}' does not compile natively yet (https://scriptc.dev/limitations); installing @types/node turns this into a located refusal with a hint`;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (world.forcedStrictNullChecks && /\b(?:null|undefined)\b/.test(message)) {
|
||||
return `the project disables strictNullChecks, which scriptc always enables — this error appears only under strict null checks; narrow the value (or enable "strictNullChecks" in the project to see the same errors from its own tsc)`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The root identifier of the property access whose member name starts at
|
||||
* `pos` (`process.version` → `process`), or null. */
|
||||
function receiverRoot(text: string, pos: number): string | null {
|
||||
let i = pos - 1;
|
||||
while (i >= 0 && /\s/.test(text[i]!)) i--;
|
||||
if (text[i] !== ".") return null;
|
||||
i--;
|
||||
if (text[i] === "?") i--;
|
||||
let root: string | null = null;
|
||||
for (;;) {
|
||||
while (i >= 0 && /\s/.test(text[i]!)) i--;
|
||||
let j = i;
|
||||
while (j >= 0 && /[\w$]/.test(text[j]!)) j--;
|
||||
if (j === i) return root;
|
||||
root = text.slice(j + 1, i + 1);
|
||||
i = j;
|
||||
while (i >= 0 && /\s/.test(text[i]!)) i--;
|
||||
if (text[i] !== ".") return root;
|
||||
i--;
|
||||
if (text[i] === "?") i--;
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether `name` is bound in `fileName` by an import of a Node builtin. */
|
||||
function importsNodeBuiltin(load: LoadResult, fileName: string, name: string): boolean {
|
||||
const sf = load.program.getSourceFile(fileName);
|
||||
for (const stmt of sf?.statements ?? []) {
|
||||
if (!ts.isImportDeclaration(stmt) || !ts.isStringLiteral(stmt.moduleSpecifier)) continue;
|
||||
if (canonicalBuiltinModule(stmt.moduleSpecifier.text) === null) continue;
|
||||
const clause = stmt.importClause;
|
||||
if (clause?.name?.text === name) return true;
|
||||
const bindings = clause?.namedBindings;
|
||||
if (bindings && ts.isNamespaceImport(bindings) && bindings.name.text === name) return true;
|
||||
if (
|
||||
bindings &&
|
||||
ts.isNamedImports(bindings) &&
|
||||
bindings.elements.some((e) => e.name.text === name)
|
||||
)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@ import {
|
||||
libraryWasmRefusal,
|
||||
} from "./library/prepare.js";
|
||||
import { analyzeWithFrontend } from "./frontend/analysis.js";
|
||||
import { analyzeWithVerdictCache, fileVerdictStore } from "./coverage/verdict-cache.js";
|
||||
import { coverageCompilerIdentity } from "./coverage/verdict-startup.js";
|
||||
import { commentOnlyReplay } from "./coverage/verdict-semantic.js";
|
||||
import { llvmRefusalDiag, targetRefusalDiag } from "./backend/target-diagnostics.js";
|
||||
import type {
|
||||
CompileOptions,
|
||||
@@ -165,7 +168,22 @@ export {
|
||||
renderDiagnostics as renderAll,
|
||||
renderDiagnostic,
|
||||
} from "./diagnostics/render.js";
|
||||
export { renderCoverage, type CoverageInput } from "./coverage/report.js";
|
||||
export {
|
||||
renderCoverage,
|
||||
coverageEnvelope,
|
||||
coveragePasses,
|
||||
type CoverageInput,
|
||||
type CoverageFailOn,
|
||||
} from "./coverage/report.js";
|
||||
export {
|
||||
buildEnvelope,
|
||||
DIAGNOSTICS_SCHEMA_VERSION,
|
||||
type DiagnosticsEnvelope,
|
||||
type DiagnosticGroup,
|
||||
type LocatedDiagnostic,
|
||||
type DiagnosticCategory,
|
||||
type DiagnosticScope,
|
||||
} from "./diagnostics/envelope.js";
|
||||
export {
|
||||
generateSurfaceManifest,
|
||||
renderSurfaceManifest,
|
||||
@@ -301,6 +319,36 @@ export function analyze(entryPath: string, opts: AnalyzeOptions = {}): AnalyzeRe
|
||||
return analyzeWithFrontend(entryPath, opts, buildTargetPlatform(), nodeFrontend);
|
||||
}
|
||||
|
||||
/** analyze() behind the coverage verdict cache (the CLI's `scriptc
|
||||
* coverage`): an unchanged program, or one whose TypeScript edits only
|
||||
* touch comments, replays the previous verdict without lowering. */
|
||||
export async function analyzeCached(
|
||||
entryPath: string,
|
||||
opts: AnalyzeOptions = {},
|
||||
): Promise<AnalyzeResult> {
|
||||
entryPath = resolve(entryPath);
|
||||
const root = await prepareBuildCacheRoot(buildCacheRoot());
|
||||
if (root === null || provenanceSources() !== null) return analyze(entryPath, opts);
|
||||
const directory = join(root, "coverage-v1");
|
||||
const implementation = await coverageCompilerIdentity(directory);
|
||||
if (implementation === null) return analyze(entryPath, opts);
|
||||
const { result, outcome } = analyzeWithVerdictCache(
|
||||
entryPath,
|
||||
opts,
|
||||
{
|
||||
store: fileVerdictStore(directory),
|
||||
identity: implementation.digest,
|
||||
platform: buildTargetPlatform(),
|
||||
semanticReplay: commentOnlyReplay,
|
||||
},
|
||||
() => analyze(entryPath, opts),
|
||||
);
|
||||
if (process.env["SCRIPTC_TIMING"] === "1") {
|
||||
process.stderr.write(`scriptc coverage cache ${JSON.stringify({ outcome })}\n`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const nodeFrontend: FrontendFactory = (entry, npmStatic, externalTypes, libraryNpmStatic) =>
|
||||
runFrontend(entry, loadProgram, npmStatic, externalTypes, libraryNpmStatic);
|
||||
|
||||
@@ -327,7 +375,26 @@ export async function compile(
|
||||
): Promise<CompileRequestResult> {
|
||||
clearCompileSessionCaches();
|
||||
const frontendInputs = new FrontendInputTracker();
|
||||
return frontendInputs.run(() => compileTracked(entryPath, opts, frontendInputs));
|
||||
const warnings: CompileWarningSink = { warnings: [], sourceTexts: null };
|
||||
const result = await frontendInputs.run(() =>
|
||||
compileTracked(entryPath, opts, frontendInputs, warnings),
|
||||
);
|
||||
return withCompileWarnings(result, warnings);
|
||||
}
|
||||
|
||||
/** Divergence warnings collected while a compile lowers (none when a cache
|
||||
* hit skips lowering). */
|
||||
interface CompileWarningSink {
|
||||
warnings: ScrDiagnostic[];
|
||||
sourceTexts: Map<string, string> | null;
|
||||
}
|
||||
|
||||
function withCompileWarnings<T extends CompileRequestResult>(
|
||||
result: T,
|
||||
sink: CompileWarningSink,
|
||||
): T {
|
||||
if (!result.ok || sink.warnings.length === 0) return result;
|
||||
return { ...result, warnings: sink.warnings, sourceTexts: sink.sourceTexts ?? new Map() };
|
||||
}
|
||||
|
||||
/** Build-time API compatibility fence: a caller's existing CompileOptions
|
||||
@@ -576,6 +643,7 @@ async function prepareExecutableInput(
|
||||
ffi: FfiProfile | null,
|
||||
buildPlatform: string,
|
||||
timing: CompilationTiming,
|
||||
warningSink?: CompileWarningSink,
|
||||
): Promise<PreparedExecutable | CompileRequestResult> {
|
||||
const outputKind = opts.outputKind ?? "exe";
|
||||
const prepared = prepareExecutableModule(
|
||||
@@ -588,6 +656,10 @@ async function prepareExecutableInput(
|
||||
);
|
||||
if (!prepared.ok) return prepared;
|
||||
const { mod, sourceTexts } = prepared;
|
||||
if (warningSink !== undefined && prepared.warnings.length > 0) {
|
||||
warningSink.warnings = prepared.warnings;
|
||||
warningSink.sourceTexts = sourceTexts;
|
||||
}
|
||||
|
||||
const stem = basename(entryPath).replace(/\.(ts|mts|cts|js|mjs|cjs)$/, "");
|
||||
const defaultSourcePaths = {
|
||||
@@ -721,6 +793,7 @@ async function compileTracked(
|
||||
entryPath: string,
|
||||
opts: CompileRequestOptions,
|
||||
frontendInputs: FrontendInputTracker,
|
||||
warningSink?: CompileWarningSink,
|
||||
): Promise<CompileRequestResult> {
|
||||
const timing = compilationTiming();
|
||||
entryPath = resolve(entryPath);
|
||||
@@ -1088,7 +1161,14 @@ async function compileTracked(
|
||||
};
|
||||
}
|
||||
timing("executable-cache-miss");
|
||||
const prepared = await prepareExecutableInput(entryPath, opts, ffi, buildPlatform, timing);
|
||||
const prepared = await prepareExecutableInput(
|
||||
entryPath,
|
||||
opts,
|
||||
ffi,
|
||||
buildPlatform,
|
||||
timing,
|
||||
warningSink,
|
||||
);
|
||||
if ("ok" in prepared) return prepared;
|
||||
const { llvmSource, llvmPath, irPath, nativeFeatures, programSplit, sourceTexts } = prepared;
|
||||
const backend = "llvm" as const;
|
||||
|
||||
@@ -555,6 +555,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
* ECMA-262 19.2.4 over a string argument (non-string arguments keep the
|
||||
* fence: Node would ToNumber-coerce). Borrows; never throws. */
|
||||
"num.parseFloat": { argTypes: [STRING], result: F64 },
|
||||
/** `a.localeCompare(b)`: -1/0/1 in Node's root-locale collation for
|
||||
* Latin-script text (scr_str_locale_compare). Never throws. */
|
||||
"str.localeCompare": { argTypes: [STRING, STRING], result: F64 },
|
||||
/* ToNumber(string) — ECMA-262 7.1.4.1 StringToNumber (scr_string.c):
|
||||
* trim the JS StrWhiteSpace set, empty/whitespace-only → +0, then the
|
||||
* whole span must be one StrNumericLiteral — signed decimal (Infinity
|
||||
|
||||
@@ -961,6 +961,12 @@ export interface IrModule {
|
||||
entry: string;
|
||||
/** Executable roots run in isolated native worker contexts. */
|
||||
workers?: boolean;
|
||||
/** The entry module is CommonJS under Node's classification. Node runs a
|
||||
* CommonJS main synchronously and drains process.nextTick callbacks
|
||||
* before promise jobs at the first checkpoint; an ES-module main's
|
||||
* evaluation is itself awaited, so promise jobs queued by its body run
|
||||
* first. The event loop selects the matching first checkpoint. */
|
||||
commonJsEntry?: boolean;
|
||||
/** Outbound native FFI declarations used by `ffiCall` expressions.
|
||||
* These are link-time C ABI imports, not runtime dynamic-library handles:
|
||||
* executable builds resolve their symbols from the manifest's archive
|
||||
@@ -2494,8 +2500,8 @@ export type IrExpr =
|
||||
/** Typed-array/Buffer method or property on a bytes receiver — see
|
||||
* IrBytesIntrinsicMethod for the surface and conventions. Methods in
|
||||
* MAY_THROW_BYTES_METHODS raise catchable RangeErrors (may-throw
|
||||
* seeds); `get` traps on invalid indices instead (the array runtime's
|
||||
* discipline — never catchable). */
|
||||
* seeds); `get` on an invalid index throws a catchable RangeError too
|
||||
* (a typed read has no undefined to answer with; also a seed). */
|
||||
| {
|
||||
kind: "bytesIntrinsic";
|
||||
method: IrBytesIntrinsicMethod;
|
||||
|
||||
@@ -71,7 +71,15 @@ export class NativeCache {
|
||||
(Number.isFinite(configured) && configured >= 0 ? configured : 4096) * 1024 * 1024;
|
||||
const files: { path: string; size: number; time: number }[] = [];
|
||||
let total = 0;
|
||||
for (const family of ["frontend", "object", "sanitizer", "executable", "binary", "dsym"]) {
|
||||
for (const family of [
|
||||
"frontend",
|
||||
"object",
|
||||
"sanitizer",
|
||||
"executable",
|
||||
"binary",
|
||||
"dsym",
|
||||
"coverage",
|
||||
]) {
|
||||
const directory = join(this.root, family);
|
||||
let names: string[];
|
||||
try {
|
||||
|
||||
@@ -37,7 +37,7 @@ async function main(): Promise<number> {
|
||||
return runCli(process.argv.slice(2), {
|
||||
version: () => loadNativeToolchain(manifestPath).compilerVersion,
|
||||
sourceTargetPlatform: () => getCompiler().toolchain.target.platform,
|
||||
analyze: (entry, options) => runCompilerTask(() => getCompiler().analyze(entry, options)),
|
||||
analyze: (entry, options) => runCompilerTask(() => getCompiler().analyzeCached(entry, options)),
|
||||
compile: (entry, options) => runCompilerTask(() => getCompiler().compile(entry, options)),
|
||||
compileLibrary: (options) => runCompilerTask(() => getCompiler().compileLibrary(options)),
|
||||
resolveProvenanceSources: async (entry) => {
|
||||
|
||||
@@ -39,9 +39,10 @@ import {
|
||||
nativeProgramPartitions,
|
||||
} from "../backend/native-codegen-core.js";
|
||||
import { llvmRefusalDiag } from "../backend/target-diagnostics.js";
|
||||
import { nativeCodegenDiag } from "../diagnostics/diagnostic.js";
|
||||
import { nativeCodegenDiag, type ScrDiagnostic } from "../diagnostics/diagnostic.js";
|
||||
import { loadFfiProfile, type FfiProfile } from "../ffi/ffi-manifest.js";
|
||||
import { analyzeWithFrontend } from "../frontend/analysis.js";
|
||||
import { analyzeWithVerdictCache } from "../coverage/verdict-cache.js";
|
||||
import type { FrontendFactory } from "../frontend/pipeline.js";
|
||||
import { runNativeFrontend } from "../frontend/pipeline-native.js";
|
||||
import { loadLibraryProfile } from "../library/library-profile.js";
|
||||
@@ -108,6 +109,35 @@ export class NativeCompiler {
|
||||
);
|
||||
}
|
||||
|
||||
/** analyze() behind the coverage verdict cache: an unchanged program
|
||||
* replays the previous verdict (exact probe replay; comment-only edits
|
||||
* relower here, since the token comparison is the Node frontend's). */
|
||||
analyzeCached(entry: string, options: AnalyzeOptions): AnalyzeResult {
|
||||
entry = resolve(entry);
|
||||
const cache = this.cache;
|
||||
if (cache === null) return this.analyze(entry, options);
|
||||
let identity: string;
|
||||
try {
|
||||
identity = `${this.toolchain.compilerVersion}\0${nativeFileIdentity(process.execPath)}`;
|
||||
} catch {
|
||||
return this.analyze(entry, options);
|
||||
}
|
||||
const { result } = analyzeWithVerdictCache(
|
||||
entry,
|
||||
options,
|
||||
{
|
||||
store: {
|
||||
read: (key) => cache.read("coverage", key)?.toString("utf8") ?? null,
|
||||
write: (key, text) => cache.write("coverage", key, text),
|
||||
},
|
||||
identity,
|
||||
platform: this.toolchain.target.platform,
|
||||
},
|
||||
() => this.analyze(entry, options),
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
private emitObject(
|
||||
input: string,
|
||||
output: string,
|
||||
@@ -219,6 +249,21 @@ export class NativeCompiler {
|
||||
}
|
||||
|
||||
compile(entry: string, options: CompileRequestOptions): CompileRequestResult {
|
||||
const sink: { warnings: ScrDiagnostic[]; sources: Map<string, string> } = {
|
||||
warnings: [],
|
||||
sources: new Map(),
|
||||
};
|
||||
const result = this.compileWithWarnings(entry, options, sink);
|
||||
// Divergence warnings ride a successful result (never a failure).
|
||||
if (!result.ok || sink.warnings.length === 0) return result;
|
||||
return { ...result, warnings: sink.warnings, sourceTexts: sink.sources };
|
||||
}
|
||||
|
||||
private compileWithWarnings(
|
||||
entry: string,
|
||||
options: CompileRequestOptions,
|
||||
sink: { warnings: ScrDiagnostic[]; sources: Map<string, string> },
|
||||
): CompileRequestResult {
|
||||
const timing = compilationTiming();
|
||||
const toolchain = this.toolchain;
|
||||
const target = toolchain.target;
|
||||
@@ -259,6 +304,10 @@ export class NativeCompiler {
|
||||
);
|
||||
if (!prepared.ok) return prepared;
|
||||
sourceTexts = new Map(prepared.sources);
|
||||
if (prepared.warnings !== undefined && prepared.warnings.length > 0) {
|
||||
sink.warnings = prepared.warnings;
|
||||
sink.sources = sourceTexts;
|
||||
}
|
||||
mkdirSync(dirname(output), { recursive: true });
|
||||
stage = mkdtempSync(join(dirname(output), ".scriptc-native-"));
|
||||
const stagedOutput = join(stage, basename(output));
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ScrDiagnostic } from "../diagnostics/diagnostic.js";
|
||||
import type { CompilationTiming } from "../timing.js";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { basename, dirname, join, resolve } from "node:path";
|
||||
@@ -36,6 +37,9 @@ export interface NativeExecutableInput {
|
||||
sidecarJson: string | null;
|
||||
features: NativeLinkFeatures;
|
||||
sources: [string, string][];
|
||||
/** Divergence warnings (SC6xxx) the lowering found; cached with the
|
||||
* input so a frontend-cache hit reports them too. */
|
||||
warnings?: ScrDiagnostic[];
|
||||
}
|
||||
|
||||
interface FrontendCacheEntry {
|
||||
@@ -137,13 +141,13 @@ function preparedFromIr(
|
||||
path: string,
|
||||
entry: string,
|
||||
options: CompileRequestOptions,
|
||||
): { ok: true; mod: IrModule; sourceTexts: Map<string, string> } {
|
||||
): { ok: true; mod: IrModule; sourceTexts: Map<string, string>; warnings: ScrDiagnostic[] } {
|
||||
if ((options.outputKind ?? "exe") !== "exe" || options.strip !== true)
|
||||
throw new Error(`${BOOTSTRAP_IR_INPUT} requires a stripped executable build`);
|
||||
const mod = deserializeModule(readFileSync(path, "utf8"));
|
||||
if (resolve(mod.sourceFile) !== resolve(entry))
|
||||
throw new Error(`${BOOTSTRAP_IR_INPUT} was lowered from a different entry`);
|
||||
return { ok: true, mod, sourceTexts: new Map() };
|
||||
return { ok: true, mod, sourceTexts: new Map(), warnings: [] };
|
||||
}
|
||||
|
||||
/** Keep the checker and typed IR out of the native optimizer's live heap. */
|
||||
@@ -237,6 +241,7 @@ export function prepareNativeExecutable(
|
||||
sidecarJson: null,
|
||||
features: executableLinkFeatures(prepared.mod, options.dynamic ?? false),
|
||||
sources: [...prepared.sourceTexts],
|
||||
...(prepared.warnings.length > 0 ? { warnings: prepared.warnings } : {}),
|
||||
};
|
||||
timing("link-features");
|
||||
const probes = tracker.snapshot();
|
||||
|
||||
@@ -23,3 +23,5 @@ export function precompiledRuntimePackTarget(
|
||||
if (env["SCRIPTC_FETCH_CURL"] === "1") return null;
|
||||
return nativeCodegenTarget(env);
|
||||
}
|
||||
export { readCoverageVerdict } from "./coverage/verdict-startup.js";
|
||||
export { coverageEnvelope, coveragePasses, renderCoverage } from "./coverage/report.js";
|
||||
|
||||
@@ -15292,6 +15292,48 @@
|
||||
"<repo>/tests/corpus/native-worker-terminate-recursion.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/error-values-inspect.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/error-values-inspect.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/locale-compare-root-order.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/locale-compare-root-order.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/next-tick-commonjs-entry.cjs": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/next-tick-commonjs-entry.cjs"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/number-parse-statics.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/number-parse-statics.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/process-version-pin.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/process-version-pin.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/repeat-and-typed-read-errors.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/repeat-and-typed-read-errors.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/worker-class-instance-clone.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/worker-class-instance-clone.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3526,8 +3526,16 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) {
|
||||
return rejection_failed;
|
||||
}
|
||||
|
||||
/* A CommonJS main module runs synchronously (Module._load), so the first
|
||||
* checkpoint after it drains process.nextTick callbacks before promise
|
||||
* jobs like every later one; only an ES-module main's awaited evaluation
|
||||
* lets its body's promise jobs run first. Set by the emitted main before
|
||||
* the loop starts. */
|
||||
static SCR_TL bool scr_loop_cjs_entry = false;
|
||||
void scr_loop_commonjs_entry(void) { scr_loop_cjs_entry = true; }
|
||||
|
||||
bool scr_loop_run(ScrPromise *top_level) {
|
||||
bool first_checkpoint = true;
|
||||
bool first_checkpoint = !scr_loop_cjs_entry;
|
||||
bool rejection_failed;
|
||||
for (;;) {
|
||||
rejection_failed = scr_loop_run_pass(top_level && !top_level->top_level_handled ? top_level : NULL, first_checkpoint);
|
||||
|
||||
@@ -254,16 +254,22 @@ double scr_bytes_byte_len(const ScrBytes *b) {
|
||||
|
||||
/* ── element access ────────────────────────────────────────────────────
|
||||
* Invalid writes are ignored. Typed numeric reads cannot represent
|
||||
* undefined, so invalid reads trap; checked reads return undefined. */
|
||||
* undefined (Node's answer), so an invalid read throws a catchable
|
||||
* RangeError instead (the caller checks the pending exception; the NaN
|
||||
* result is never observed); checked reads return undefined. */
|
||||
|
||||
static size_t scr_bytes_check_index(const ScrBytes *b, double i) {
|
||||
static bool scr_bytes_check_index(const ScrBytes *b, double i, size_t *out) {
|
||||
if (!(i >= 0) || i != trunc(i) || i >= (double)b->len) {
|
||||
char buf[32];
|
||||
scr_f64_to_str(i, buf);
|
||||
scr_trap_fmt("scriptc: RangeError: typed array index %s out of bounds (length %zu)\n",
|
||||
buf, b->len);
|
||||
char msg[96];
|
||||
int len = snprintf(msg, sizeof msg, "typed array index %s out of bounds (length %zu)", buf,
|
||||
b->len);
|
||||
scr_throw_error_msg(SCR_ERR_RANGE, msg, (size_t)len);
|
||||
return false;
|
||||
}
|
||||
return (size_t)i;
|
||||
*out = (size_t)i;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ToUint32: NaN/±Infinity → 0, truncate toward zero, wrap mod 2^32.
|
||||
@@ -274,7 +280,8 @@ static uint32_t scr_bytes_to_u32(double v) {
|
||||
|
||||
double scr_bytes_get(const ScrBytes *b, double i) {
|
||||
SCR_SHARED_GUARD(b, NULL);
|
||||
size_t idx = scr_bytes_check_index(b, i);
|
||||
size_t idx;
|
||||
if (!scr_bytes_check_index(b, i, &idx)) return NAN;
|
||||
switch (b->elem) {
|
||||
case SCR_BYTES_U8: case SCR_BYTES_U8C:
|
||||
return (double)b->data[idx];
|
||||
|
||||
@@ -0,0 +1,464 @@
|
||||
/* Generated by scripts/gen-collation-table.mjs from the pinned Node
|
||||
* release's root-locale collation (ICU, localeCompare without locales or
|
||||
* options). Do not edit by hand. Each covered code point maps to up to
|
||||
* three collation elements {primary, secondary, tertiary, combining class
|
||||
* rank}; zero primaries are skipped at the primary level, and runs of
|
||||
* marks with a nonzero class rank are put in canonical order before
|
||||
* comparing. n == 255 marks an uncovered code point. */
|
||||
#ifndef SCR_COLLATION_DATA_H
|
||||
#define SCR_COLLATION_DATA_H
|
||||
#include <stdint.h>
|
||||
typedef struct { uint16_t p; uint8_t s; uint8_t t; uint8_t ccc; } ScrCollElem;
|
||||
typedef struct { uint8_t n; ScrCollElem e[3]; } ScrCollEntry;
|
||||
static const ScrCollEntry scr_coll_0009[5] = {
|
||||
{1, {{1, 1, 1, 0}}}, /* U+0009 */
|
||||
{1, {{2, 1, 1, 0}}}, /* U+000A */
|
||||
{1, {{3, 1, 1, 0}}}, /* U+000B */
|
||||
{1, {{4, 1, 1, 0}}}, /* U+000C */
|
||||
{1, {{5, 1, 1, 0}}}, /* U+000D */
|
||||
};
|
||||
static const ScrCollEntry scr_coll_0020[95] = {
|
||||
{1, {{6, 1, 1, 0}}}, /* U+0020 */
|
||||
{1, {{12, 1, 1, 0}}}, /* U+0021 */
|
||||
{1, {{19, 1, 1, 0}}}, /* U+0022 */
|
||||
{1, {{35, 1, 1, 0}}}, /* U+0023 */
|
||||
{1, {{59, 1, 1, 0}}}, /* U+0024 */
|
||||
{1, {{36, 1, 1, 0}}}, /* U+0025 */
|
||||
{1, {{34, 1, 1, 0}}}, /* U+0026 */
|
||||
{1, {{18, 1, 1, 0}}}, /* U+0027 */
|
||||
{1, {{22, 1, 1, 0}}}, /* U+0028 */
|
||||
{1, {{23, 1, 1, 0}}}, /* U+0029 */
|
||||
{1, {{31, 1, 1, 0}}}, /* U+002A */
|
||||
{1, {{46, 1, 1, 0}}}, /* U+002B */
|
||||
{1, {{9, 1, 1, 0}}}, /* U+002C */
|
||||
{1, {{8, 1, 1, 0}}}, /* U+002D */
|
||||
{1, {{16, 1, 1, 0}}}, /* U+002E */
|
||||
{1, {{32, 1, 1, 0}}}, /* U+002F */
|
||||
{1, {{62, 1, 1, 0}}}, /* U+0030 */
|
||||
{1, {{63, 1, 1, 0}}}, /* U+0031 */
|
||||
{1, {{64, 1, 1, 0}}}, /* U+0032 */
|
||||
{1, {{65, 1, 1, 0}}}, /* U+0033 */
|
||||
{1, {{66, 1, 1, 0}}}, /* U+0034 */
|
||||
{1, {{67, 1, 1, 0}}}, /* U+0035 */
|
||||
{1, {{68, 1, 1, 0}}}, /* U+0036 */
|
||||
{1, {{69, 1, 1, 0}}}, /* U+0037 */
|
||||
{1, {{70, 1, 1, 0}}}, /* U+0038 */
|
||||
{1, {{71, 1, 1, 0}}}, /* U+0039 */
|
||||
{1, {{11, 1, 1, 0}}}, /* U+003A */
|
||||
{1, {{10, 1, 1, 0}}}, /* U+003B */
|
||||
{1, {{50, 1, 1, 0}}}, /* U+003C */
|
||||
{1, {{51, 1, 1, 0}}}, /* U+003D */
|
||||
{1, {{52, 1, 1, 0}}}, /* U+003E */
|
||||
{1, {{14, 1, 1, 0}}}, /* U+003F */
|
||||
{1, {{30, 1, 1, 0}}}, /* U+0040 */
|
||||
{1, {{72, 1, 2, 0}}}, /* U+0041 */
|
||||
{1, {{73, 1, 2, 0}}}, /* U+0042 */
|
||||
{1, {{74, 1, 2, 0}}}, /* U+0043 */
|
||||
{1, {{75, 1, 2, 0}}}, /* U+0044 */
|
||||
{1, {{76, 1, 2, 0}}}, /* U+0045 */
|
||||
{1, {{77, 1, 2, 0}}}, /* U+0046 */
|
||||
{1, {{78, 1, 2, 0}}}, /* U+0047 */
|
||||
{1, {{79, 1, 2, 0}}}, /* U+0048 */
|
||||
{1, {{80, 1, 2, 0}}}, /* U+0049 */
|
||||
{1, {{82, 1, 2, 0}}}, /* U+004A */
|
||||
{1, {{83, 1, 2, 0}}}, /* U+004B */
|
||||
{1, {{84, 1, 2, 0}}}, /* U+004C */
|
||||
{1, {{85, 1, 2, 0}}}, /* U+004D */
|
||||
{1, {{86, 1, 2, 0}}}, /* U+004E */
|
||||
{1, {{88, 1, 2, 0}}}, /* U+004F */
|
||||
{1, {{89, 1, 2, 0}}}, /* U+0050 */
|
||||
{1, {{90, 1, 2, 0}}}, /* U+0051 */
|
||||
{1, {{92, 1, 2, 0}}}, /* U+0052 */
|
||||
{1, {{93, 1, 2, 0}}}, /* U+0053 */
|
||||
{1, {{94, 1, 2, 0}}}, /* U+0054 */
|
||||
{1, {{96, 1, 2, 0}}}, /* U+0055 */
|
||||
{1, {{97, 1, 2, 0}}}, /* U+0056 */
|
||||
{1, {{98, 1, 2, 0}}}, /* U+0057 */
|
||||
{1, {{99, 1, 2, 0}}}, /* U+0058 */
|
||||
{1, {{100, 1, 2, 0}}}, /* U+0059 */
|
||||
{1, {{101, 1, 2, 0}}}, /* U+005A */
|
||||
{1, {{24, 1, 1, 0}}}, /* U+005B */
|
||||
{1, {{33, 1, 1, 0}}}, /* U+005C */
|
||||
{1, {{25, 1, 1, 0}}}, /* U+005D */
|
||||
{1, {{39, 1, 1, 0}}}, /* U+005E */
|
||||
{1, {{7, 1, 1, 0}}}, /* U+005F */
|
||||
{1, {{37, 1, 1, 0}}}, /* U+0060 */
|
||||
{1, {{72, 1, 1, 0}}}, /* U+0061 */
|
||||
{1, {{73, 1, 1, 0}}}, /* U+0062 */
|
||||
{1, {{74, 1, 1, 0}}}, /* U+0063 */
|
||||
{1, {{75, 1, 1, 0}}}, /* U+0064 */
|
||||
{1, {{76, 1, 1, 0}}}, /* U+0065 */
|
||||
{1, {{77, 1, 1, 0}}}, /* U+0066 */
|
||||
{1, {{78, 1, 1, 0}}}, /* U+0067 */
|
||||
{1, {{79, 1, 1, 0}}}, /* U+0068 */
|
||||
{1, {{80, 1, 1, 0}}}, /* U+0069 */
|
||||
{1, {{82, 1, 1, 0}}}, /* U+006A */
|
||||
{1, {{83, 1, 1, 0}}}, /* U+006B */
|
||||
{1, {{84, 1, 1, 0}}}, /* U+006C */
|
||||
{1, {{85, 1, 1, 0}}}, /* U+006D */
|
||||
{1, {{86, 1, 1, 0}}}, /* U+006E */
|
||||
{1, {{88, 1, 1, 0}}}, /* U+006F */
|
||||
{1, {{89, 1, 1, 0}}}, /* U+0070 */
|
||||
{1, {{90, 1, 1, 0}}}, /* U+0071 */
|
||||
{1, {{92, 1, 1, 0}}}, /* U+0072 */
|
||||
{1, {{93, 1, 1, 0}}}, /* U+0073 */
|
||||
{1, {{94, 1, 1, 0}}}, /* U+0074 */
|
||||
{1, {{96, 1, 1, 0}}}, /* U+0075 */
|
||||
{1, {{97, 1, 1, 0}}}, /* U+0076 */
|
||||
{1, {{98, 1, 1, 0}}}, /* U+0077 */
|
||||
{1, {{99, 1, 1, 0}}}, /* U+0078 */
|
||||
{1, {{100, 1, 1, 0}}}, /* U+0079 */
|
||||
{1, {{101, 1, 1, 0}}}, /* U+007A */
|
||||
{1, {{26, 1, 1, 0}}}, /* U+007B */
|
||||
{1, {{54, 1, 1, 0}}}, /* U+007C */
|
||||
{1, {{27, 1, 1, 0}}}, /* U+007D */
|
||||
{1, {{56, 1, 1, 0}}}, /* U+007E */
|
||||
};
|
||||
static const ScrCollEntry scr_coll_00a0[224] = {
|
||||
{1, {{6, 1, 2, 0}}}, /* U+00A0 */
|
||||
{1, {{13, 1, 1, 0}}}, /* U+00A1 */
|
||||
{1, {{58, 1, 1, 0}}}, /* U+00A2 */
|
||||
{1, {{60, 1, 1, 0}}}, /* U+00A3 */
|
||||
{1, {{57, 1, 1, 0}}}, /* U+00A4 */
|
||||
{1, {{61, 1, 1, 0}}}, /* U+00A5 */
|
||||
{1, {{55, 1, 1, 0}}}, /* U+00A6 */
|
||||
{1, {{28, 1, 1, 0}}}, /* U+00A7 */
|
||||
{1, {{41, 1, 1, 0}}}, /* U+00A8 */
|
||||
{1, {{44, 1, 1, 0}}}, /* U+00A9 */
|
||||
{1, {{72, 1, 3, 0}}}, /* U+00AA */
|
||||
{1, {{20, 1, 1, 0}}}, /* U+00AB */
|
||||
{1, {{53, 1, 1, 0}}}, /* U+00AC */
|
||||
{0, {}}, /* U+00AD */
|
||||
{1, {{45, 1, 1, 0}}}, /* U+00AE */
|
||||
{1, {{40, 1, 1, 0}}}, /* U+00AF */
|
||||
{1, {{43, 1, 1, 0}}}, /* U+00B0 */
|
||||
{1, {{47, 1, 1, 0}}}, /* U+00B1 */
|
||||
{1, {{64, 1, 2, 0}}}, /* U+00B2 */
|
||||
{1, {{65, 1, 2, 0}}}, /* U+00B3 */
|
||||
{1, {{38, 1, 1, 0}}}, /* U+00B4 */
|
||||
{1, {{104, 1, 1, 0}}}, /* U+00B5 */
|
||||
{1, {{29, 1, 1, 0}}}, /* U+00B6 */
|
||||
{1, {{17, 1, 1, 0}}}, /* U+00B7 */
|
||||
{1, {{42, 1, 1, 0}}}, /* U+00B8 */
|
||||
{1, {{63, 1, 2, 0}}}, /* U+00B9 */
|
||||
{1, {{88, 1, 3, 0}}}, /* U+00BA */
|
||||
{1, {{21, 1, 1, 0}}}, /* U+00BB */
|
||||
{255, {{0, 0, 0, 0}}}, /* U+00BC not covered */
|
||||
{255, {{0, 0, 0, 0}}}, /* U+00BD not covered */
|
||||
{255, {{0, 0, 0, 0}}}, /* U+00BE not covered */
|
||||
{1, {{15, 1, 1, 0}}}, /* U+00BF */
|
||||
{2, {{72, 1, 2, 0}, {0, 7, 1, 5}}}, /* U+00C0 */
|
||||
{2, {{72, 1, 2, 0}, {0, 6, 1, 5}}}, /* U+00C1 */
|
||||
{2, {{72, 1, 2, 0}, {0, 9, 1, 5}}}, /* U+00C2 */
|
||||
{2, {{72, 1, 2, 0}, {0, 16, 1, 5}}}, /* U+00C3 */
|
||||
{2, {{72, 1, 2, 0}, {0, 13, 1, 5}}}, /* U+00C4 */
|
||||
{2, {{72, 1, 2, 0}, {0, 11, 1, 5}}}, /* U+00C5 */
|
||||
{3, {{72, 1, 2, 0}, {0, 51, 1, 0}, {76, 1, 2, 0}}}, /* U+00C6 */
|
||||
{2, {{74, 1, 2, 0}, {0, 19, 1, 2}}}, /* U+00C7 */
|
||||
{2, {{76, 1, 2, 0}, {0, 7, 1, 5}}}, /* U+00C8 */
|
||||
{2, {{76, 1, 2, 0}, {0, 6, 1, 5}}}, /* U+00C9 */
|
||||
{2, {{76, 1, 2, 0}, {0, 9, 1, 5}}}, /* U+00CA */
|
||||
{2, {{76, 1, 2, 0}, {0, 13, 1, 5}}}, /* U+00CB */
|
||||
{2, {{80, 1, 2, 0}, {0, 7, 1, 5}}}, /* U+00CC */
|
||||
{2, {{80, 1, 2, 0}, {0, 6, 1, 5}}}, /* U+00CD */
|
||||
{2, {{80, 1, 2, 0}, {0, 9, 1, 5}}}, /* U+00CE */
|
||||
{2, {{80, 1, 2, 0}, {0, 13, 1, 5}}}, /* U+00CF */
|
||||
{2, {{75, 1, 2, 0}, {0, 48, 1, 0}}}, /* U+00D0 */
|
||||
{2, {{86, 1, 2, 0}, {0, 16, 1, 5}}}, /* U+00D1 */
|
||||
{2, {{88, 1, 2, 0}, {0, 7, 1, 5}}}, /* U+00D2 */
|
||||
{2, {{88, 1, 2, 0}, {0, 6, 1, 5}}}, /* U+00D3 */
|
||||
{2, {{88, 1, 2, 0}, {0, 9, 1, 5}}}, /* U+00D4 */
|
||||
{2, {{88, 1, 2, 0}, {0, 16, 1, 5}}}, /* U+00D5 */
|
||||
{2, {{88, 1, 2, 0}, {0, 13, 1, 5}}}, /* U+00D6 */
|
||||
{1, {{49, 1, 1, 0}}}, /* U+00D7 */
|
||||
{2, {{88, 1, 2, 0}, {0, 18, 1, 0}}}, /* U+00D8 */
|
||||
{2, {{96, 1, 2, 0}, {0, 7, 1, 5}}}, /* U+00D9 */
|
||||
{2, {{96, 1, 2, 0}, {0, 6, 1, 5}}}, /* U+00DA */
|
||||
{2, {{96, 1, 2, 0}, {0, 9, 1, 5}}}, /* U+00DB */
|
||||
{2, {{96, 1, 2, 0}, {0, 13, 1, 5}}}, /* U+00DC */
|
||||
{2, {{100, 1, 2, 0}, {0, 6, 1, 5}}}, /* U+00DD */
|
||||
{1, {{102, 1, 2, 0}}}, /* U+00DE */
|
||||
{3, {{93, 1, 1, 0}, {0, 52, 1, 0}, {93, 1, 1, 0}}}, /* U+00DF */
|
||||
{2, {{72, 1, 1, 0}, {0, 7, 1, 5}}}, /* U+00E0 */
|
||||
{2, {{72, 1, 1, 0}, {0, 6, 1, 5}}}, /* U+00E1 */
|
||||
{2, {{72, 1, 1, 0}, {0, 9, 1, 5}}}, /* U+00E2 */
|
||||
{2, {{72, 1, 1, 0}, {0, 16, 1, 5}}}, /* U+00E3 */
|
||||
{2, {{72, 1, 1, 0}, {0, 13, 1, 5}}}, /* U+00E4 */
|
||||
{2, {{72, 1, 1, 0}, {0, 11, 1, 5}}}, /* U+00E5 */
|
||||
{3, {{72, 1, 1, 0}, {0, 51, 1, 0}, {76, 1, 1, 0}}}, /* U+00E6 */
|
||||
{2, {{74, 1, 1, 0}, {0, 19, 1, 2}}}, /* U+00E7 */
|
||||
{2, {{76, 1, 1, 0}, {0, 7, 1, 5}}}, /* U+00E8 */
|
||||
{2, {{76, 1, 1, 0}, {0, 6, 1, 5}}}, /* U+00E9 */
|
||||
{2, {{76, 1, 1, 0}, {0, 9, 1, 5}}}, /* U+00EA */
|
||||
{2, {{76, 1, 1, 0}, {0, 13, 1, 5}}}, /* U+00EB */
|
||||
{2, {{80, 1, 1, 0}, {0, 7, 1, 5}}}, /* U+00EC */
|
||||
{2, {{80, 1, 1, 0}, {0, 6, 1, 5}}}, /* U+00ED */
|
||||
{2, {{80, 1, 1, 0}, {0, 9, 1, 5}}}, /* U+00EE */
|
||||
{2, {{80, 1, 1, 0}, {0, 13, 1, 5}}}, /* U+00EF */
|
||||
{2, {{75, 1, 1, 0}, {0, 48, 1, 0}}}, /* U+00F0 */
|
||||
{2, {{86, 1, 1, 0}, {0, 16, 1, 5}}}, /* U+00F1 */
|
||||
{2, {{88, 1, 1, 0}, {0, 7, 1, 5}}}, /* U+00F2 */
|
||||
{2, {{88, 1, 1, 0}, {0, 6, 1, 5}}}, /* U+00F3 */
|
||||
{2, {{88, 1, 1, 0}, {0, 9, 1, 5}}}, /* U+00F4 */
|
||||
{2, {{88, 1, 1, 0}, {0, 16, 1, 5}}}, /* U+00F5 */
|
||||
{2, {{88, 1, 1, 0}, {0, 13, 1, 5}}}, /* U+00F6 */
|
||||
{1, {{48, 1, 1, 0}}}, /* U+00F7 */
|
||||
{2, {{88, 1, 1, 0}, {0, 18, 1, 0}}}, /* U+00F8 */
|
||||
{2, {{96, 1, 1, 0}, {0, 7, 1, 5}}}, /* U+00F9 */
|
||||
{2, {{96, 1, 1, 0}, {0, 6, 1, 5}}}, /* U+00FA */
|
||||
{2, {{96, 1, 1, 0}, {0, 9, 1, 5}}}, /* U+00FB */
|
||||
{2, {{96, 1, 1, 0}, {0, 13, 1, 5}}}, /* U+00FC */
|
||||
{2, {{100, 1, 1, 0}, {0, 6, 1, 5}}}, /* U+00FD */
|
||||
{1, {{102, 1, 1, 0}}}, /* U+00FE */
|
||||
{2, {{100, 1, 1, 0}, {0, 13, 1, 5}}}, /* U+00FF */
|
||||
{2, {{72, 1, 2, 0}, {0, 21, 1, 5}}}, /* U+0100 */
|
||||
{2, {{72, 1, 1, 0}, {0, 21, 1, 5}}}, /* U+0101 */
|
||||
{2, {{72, 1, 2, 0}, {0, 8, 1, 5}}}, /* U+0102 */
|
||||
{2, {{72, 1, 1, 0}, {0, 8, 1, 5}}}, /* U+0103 */
|
||||
{2, {{72, 1, 2, 0}, {0, 20, 1, 2}}}, /* U+0104 */
|
||||
{2, {{72, 1, 1, 0}, {0, 20, 1, 2}}}, /* U+0105 */
|
||||
{2, {{74, 1, 2, 0}, {0, 6, 1, 5}}}, /* U+0106 */
|
||||
{2, {{74, 1, 1, 0}, {0, 6, 1, 5}}}, /* U+0107 */
|
||||
{2, {{74, 1, 2, 0}, {0, 9, 1, 5}}}, /* U+0108 */
|
||||
{2, {{74, 1, 1, 0}, {0, 9, 1, 5}}}, /* U+0109 */
|
||||
{2, {{74, 1, 2, 0}, {0, 17, 1, 5}}}, /* U+010A */
|
||||
{2, {{74, 1, 1, 0}, {0, 17, 1, 5}}}, /* U+010B */
|
||||
{2, {{74, 1, 2, 0}, {0, 10, 1, 5}}}, /* U+010C */
|
||||
{2, {{74, 1, 1, 0}, {0, 10, 1, 5}}}, /* U+010D */
|
||||
{2, {{75, 1, 2, 0}, {0, 10, 1, 5}}}, /* U+010E */
|
||||
{2, {{75, 1, 1, 0}, {0, 10, 1, 5}}}, /* U+010F */
|
||||
{2, {{75, 1, 2, 0}, {0, 25, 1, 0}}}, /* U+0110 */
|
||||
{2, {{75, 1, 1, 0}, {0, 25, 1, 0}}}, /* U+0111 */
|
||||
{2, {{76, 1, 2, 0}, {0, 21, 1, 5}}}, /* U+0112 */
|
||||
{2, {{76, 1, 1, 0}, {0, 21, 1, 5}}}, /* U+0113 */
|
||||
{2, {{76, 1, 2, 0}, {0, 8, 1, 5}}}, /* U+0114 */
|
||||
{2, {{76, 1, 1, 0}, {0, 8, 1, 5}}}, /* U+0115 */
|
||||
{2, {{76, 1, 2, 0}, {0, 17, 1, 5}}}, /* U+0116 */
|
||||
{2, {{76, 1, 1, 0}, {0, 17, 1, 5}}}, /* U+0117 */
|
||||
{2, {{76, 1, 2, 0}, {0, 20, 1, 2}}}, /* U+0118 */
|
||||
{2, {{76, 1, 1, 0}, {0, 20, 1, 2}}}, /* U+0119 */
|
||||
{2, {{76, 1, 2, 0}, {0, 10, 1, 5}}}, /* U+011A */
|
||||
{2, {{76, 1, 1, 0}, {0, 10, 1, 5}}}, /* U+011B */
|
||||
{2, {{78, 1, 2, 0}, {0, 9, 1, 5}}}, /* U+011C */
|
||||
{2, {{78, 1, 1, 0}, {0, 9, 1, 5}}}, /* U+011D */
|
||||
{2, {{78, 1, 2, 0}, {0, 8, 1, 5}}}, /* U+011E */
|
||||
{2, {{78, 1, 1, 0}, {0, 8, 1, 5}}}, /* U+011F */
|
||||
{2, {{78, 1, 2, 0}, {0, 17, 1, 5}}}, /* U+0120 */
|
||||
{2, {{78, 1, 1, 0}, {0, 17, 1, 5}}}, /* U+0121 */
|
||||
{2, {{78, 1, 2, 0}, {0, 19, 1, 2}}}, /* U+0122 */
|
||||
{2, {{78, 1, 1, 0}, {0, 19, 1, 2}}}, /* U+0123 */
|
||||
{2, {{79, 1, 2, 0}, {0, 9, 1, 5}}}, /* U+0124 */
|
||||
{2, {{79, 1, 1, 0}, {0, 9, 1, 5}}}, /* U+0125 */
|
||||
{2, {{79, 1, 2, 0}, {0, 25, 1, 0}}}, /* U+0126 */
|
||||
{2, {{79, 1, 1, 0}, {0, 25, 1, 0}}}, /* U+0127 */
|
||||
{2, {{80, 1, 2, 0}, {0, 16, 1, 5}}}, /* U+0128 */
|
||||
{2, {{80, 1, 1, 0}, {0, 16, 1, 5}}}, /* U+0129 */
|
||||
{2, {{80, 1, 2, 0}, {0, 21, 1, 5}}}, /* U+012A */
|
||||
{2, {{80, 1, 1, 0}, {0, 21, 1, 5}}}, /* U+012B */
|
||||
{2, {{80, 1, 2, 0}, {0, 8, 1, 5}}}, /* U+012C */
|
||||
{2, {{80, 1, 1, 0}, {0, 8, 1, 5}}}, /* U+012D */
|
||||
{2, {{80, 1, 2, 0}, {0, 20, 1, 2}}}, /* U+012E */
|
||||
{2, {{80, 1, 1, 0}, {0, 20, 1, 2}}}, /* U+012F */
|
||||
{2, {{80, 1, 1, 0}, {0, 17, 1, 5}}}, /* U+0130 */
|
||||
{1, {{81, 1, 1, 0}}}, /* U+0131 */
|
||||
{2, {{80, 1, 2, 0}, {82, 1, 2, 0}}}, /* U+0132 */
|
||||
{2, {{80, 1, 1, 0}, {82, 1, 1, 0}}}, /* U+0133 */
|
||||
{2, {{82, 1, 2, 0}, {0, 9, 1, 5}}}, /* U+0134 */
|
||||
{2, {{82, 1, 1, 0}, {0, 9, 1, 5}}}, /* U+0135 */
|
||||
{2, {{83, 1, 2, 0}, {0, 19, 1, 2}}}, /* U+0136 */
|
||||
{2, {{83, 1, 1, 0}, {0, 19, 1, 2}}}, /* U+0137 */
|
||||
{1, {{91, 1, 1, 0}}}, /* U+0138 */
|
||||
{2, {{84, 1, 2, 0}, {0, 6, 1, 5}}}, /* U+0139 */
|
||||
{2, {{84, 1, 1, 0}, {0, 6, 1, 5}}}, /* U+013A */
|
||||
{2, {{84, 1, 2, 0}, {0, 19, 1, 2}}}, /* U+013B */
|
||||
{2, {{84, 1, 1, 0}, {0, 19, 1, 2}}}, /* U+013C */
|
||||
{2, {{84, 1, 2, 0}, {0, 10, 1, 5}}}, /* U+013D */
|
||||
{2, {{84, 1, 1, 0}, {0, 10, 1, 5}}}, /* U+013E */
|
||||
{2, {{84, 1, 2, 0}, {0, 49, 1, 0}}}, /* U+013F */
|
||||
{2, {{84, 1, 1, 0}, {0, 49, 1, 0}}}, /* U+0140 */
|
||||
{2, {{84, 1, 2, 0}, {0, 25, 1, 0}}}, /* U+0141 */
|
||||
{2, {{84, 1, 1, 0}, {0, 25, 1, 0}}}, /* U+0142 */
|
||||
{2, {{86, 1, 2, 0}, {0, 6, 1, 5}}}, /* U+0143 */
|
||||
{2, {{86, 1, 1, 0}, {0, 6, 1, 5}}}, /* U+0144 */
|
||||
{2, {{86, 1, 2, 0}, {0, 19, 1, 2}}}, /* U+0145 */
|
||||
{2, {{86, 1, 1, 0}, {0, 19, 1, 2}}}, /* U+0146 */
|
||||
{2, {{86, 1, 2, 0}, {0, 10, 1, 5}}}, /* U+0147 */
|
||||
{2, {{86, 1, 1, 0}, {0, 10, 1, 5}}}, /* U+0148 */
|
||||
{1, {{103, 1, 1, 0}}}, /* U+0149 */
|
||||
{1, {{87, 1, 2, 0}}}, /* U+014A */
|
||||
{1, {{87, 1, 1, 0}}}, /* U+014B */
|
||||
{2, {{88, 1, 2, 0}, {0, 21, 1, 5}}}, /* U+014C */
|
||||
{2, {{88, 1, 1, 0}, {0, 21, 1, 5}}}, /* U+014D */
|
||||
{2, {{88, 1, 2, 0}, {0, 8, 1, 5}}}, /* U+014E */
|
||||
{2, {{88, 1, 1, 0}, {0, 8, 1, 5}}}, /* U+014F */
|
||||
{2, {{88, 1, 2, 0}, {0, 15, 1, 5}}}, /* U+0150 */
|
||||
{2, {{88, 1, 1, 0}, {0, 15, 1, 5}}}, /* U+0151 */
|
||||
{3, {{88, 1, 2, 0}, {0, 53, 1, 0}, {76, 1, 2, 0}}}, /* U+0152 */
|
||||
{3, {{88, 1, 1, 0}, {0, 53, 1, 0}, {76, 1, 1, 0}}}, /* U+0153 */
|
||||
{2, {{92, 1, 2, 0}, {0, 6, 1, 5}}}, /* U+0154 */
|
||||
{2, {{92, 1, 1, 0}, {0, 6, 1, 5}}}, /* U+0155 */
|
||||
{2, {{92, 1, 2, 0}, {0, 19, 1, 2}}}, /* U+0156 */
|
||||
{2, {{92, 1, 1, 0}, {0, 19, 1, 2}}}, /* U+0157 */
|
||||
{2, {{92, 1, 2, 0}, {0, 10, 1, 5}}}, /* U+0158 */
|
||||
{2, {{92, 1, 1, 0}, {0, 10, 1, 5}}}, /* U+0159 */
|
||||
{2, {{93, 1, 2, 0}, {0, 6, 1, 5}}}, /* U+015A */
|
||||
{2, {{93, 1, 1, 0}, {0, 6, 1, 5}}}, /* U+015B */
|
||||
{2, {{93, 1, 2, 0}, {0, 9, 1, 5}}}, /* U+015C */
|
||||
{2, {{93, 1, 1, 0}, {0, 9, 1, 5}}}, /* U+015D */
|
||||
{2, {{93, 1, 2, 0}, {0, 19, 1, 2}}}, /* U+015E */
|
||||
{2, {{93, 1, 1, 0}, {0, 19, 1, 2}}}, /* U+015F */
|
||||
{2, {{93, 1, 2, 0}, {0, 10, 1, 5}}}, /* U+0160 */
|
||||
{2, {{93, 1, 1, 0}, {0, 10, 1, 5}}}, /* U+0161 */
|
||||
{2, {{94, 1, 2, 0}, {0, 19, 1, 2}}}, /* U+0162 */
|
||||
{2, {{94, 1, 1, 0}, {0, 19, 1, 2}}}, /* U+0163 */
|
||||
{2, {{94, 1, 2, 0}, {0, 10, 1, 5}}}, /* U+0164 */
|
||||
{2, {{94, 1, 1, 0}, {0, 10, 1, 5}}}, /* U+0165 */
|
||||
{1, {{95, 1, 2, 0}}}, /* U+0166 */
|
||||
{1, {{95, 1, 1, 0}}}, /* U+0167 */
|
||||
{2, {{96, 1, 2, 0}, {0, 16, 1, 5}}}, /* U+0168 */
|
||||
{2, {{96, 1, 1, 0}, {0, 16, 1, 5}}}, /* U+0169 */
|
||||
{2, {{96, 1, 2, 0}, {0, 21, 1, 5}}}, /* U+016A */
|
||||
{2, {{96, 1, 1, 0}, {0, 21, 1, 5}}}, /* U+016B */
|
||||
{2, {{96, 1, 2, 0}, {0, 8, 1, 5}}}, /* U+016C */
|
||||
{2, {{96, 1, 1, 0}, {0, 8, 1, 5}}}, /* U+016D */
|
||||
{2, {{96, 1, 2, 0}, {0, 11, 1, 5}}}, /* U+016E */
|
||||
{2, {{96, 1, 1, 0}, {0, 11, 1, 5}}}, /* U+016F */
|
||||
{2, {{96, 1, 2, 0}, {0, 15, 1, 5}}}, /* U+0170 */
|
||||
{2, {{96, 1, 1, 0}, {0, 15, 1, 5}}}, /* U+0171 */
|
||||
{2, {{96, 1, 2, 0}, {0, 20, 1, 2}}}, /* U+0172 */
|
||||
{2, {{96, 1, 1, 0}, {0, 20, 1, 2}}}, /* U+0173 */
|
||||
{2, {{98, 1, 2, 0}, {0, 9, 1, 5}}}, /* U+0174 */
|
||||
{2, {{98, 1, 1, 0}, {0, 9, 1, 5}}}, /* U+0175 */
|
||||
{2, {{100, 1, 2, 0}, {0, 9, 1, 5}}}, /* U+0176 */
|
||||
{2, {{100, 1, 1, 0}, {0, 9, 1, 5}}}, /* U+0177 */
|
||||
{2, {{100, 1, 2, 0}, {0, 13, 1, 5}}}, /* U+0178 */
|
||||
{2, {{101, 1, 2, 0}, {0, 6, 1, 5}}}, /* U+0179 */
|
||||
{2, {{101, 1, 1, 0}, {0, 6, 1, 5}}}, /* U+017A */
|
||||
{2, {{101, 1, 2, 0}, {0, 17, 1, 5}}}, /* U+017B */
|
||||
{2, {{101, 1, 1, 0}, {0, 17, 1, 5}}}, /* U+017C */
|
||||
{2, {{101, 1, 2, 0}, {0, 10, 1, 5}}}, /* U+017D */
|
||||
{2, {{101, 1, 1, 0}, {0, 10, 1, 5}}}, /* U+017E */
|
||||
{2, {{93, 1, 1, 0}, {0, 50, 1, 0}}}, /* U+017F */
|
||||
};
|
||||
static const ScrCollEntry scr_coll_0300[112] = {
|
||||
{1, {{0, 7, 1, 5}}}, /* U+0300 */
|
||||
{1, {{0, 6, 1, 5}}}, /* U+0301 */
|
||||
{1, {{0, 9, 1, 5}}}, /* U+0302 */
|
||||
{1, {{0, 16, 1, 5}}}, /* U+0303 */
|
||||
{1, {{0, 21, 1, 5}}}, /* U+0304 */
|
||||
{1, {{0, 26, 1, 5}}}, /* U+0305 */
|
||||
{1, {{0, 8, 1, 5}}}, /* U+0306 */
|
||||
{1, {{0, 17, 1, 5}}}, /* U+0307 */
|
||||
{1, {{0, 13, 1, 5}}}, /* U+0308 */
|
||||
{1, {{0, 27, 1, 5}}}, /* U+0309 */
|
||||
{1, {{0, 11, 1, 5}}}, /* U+030A */
|
||||
{1, {{0, 15, 1, 5}}}, /* U+030B */
|
||||
{1, {{0, 10, 1, 5}}}, /* U+030C */
|
||||
{1, {{0, 22, 1, 5}}}, /* U+030D */
|
||||
{1, {{0, 22, 1, 5}}}, /* U+030E */
|
||||
{1, {{0, 28, 1, 5}}}, /* U+030F */
|
||||
{1, {{0, 29, 1, 5}}}, /* U+0310 */
|
||||
{1, {{0, 30, 1, 5}}}, /* U+0311 */
|
||||
{1, {{0, 22, 1, 5}}}, /* U+0312 */
|
||||
{1, {{0, 4, 1, 5}}}, /* U+0313 */
|
||||
{1, {{0, 5, 1, 5}}}, /* U+0314 */
|
||||
{1, {{0, 22, 1, 6}}}, /* U+0315 */
|
||||
{1, {{0, 23, 1, 4}}}, /* U+0316 */
|
||||
{1, {{0, 23, 1, 4}}}, /* U+0317 */
|
||||
{1, {{0, 23, 1, 4}}}, /* U+0318 */
|
||||
{1, {{0, 23, 1, 4}}}, /* U+0319 */
|
||||
{1, {{0, 22, 1, 6}}}, /* U+031A */
|
||||
{1, {{0, 31, 1, 3}}}, /* U+031B */
|
||||
{1, {{0, 23, 1, 4}}}, /* U+031C */
|
||||
{1, {{0, 23, 1, 4}}}, /* U+031D */
|
||||
{1, {{0, 23, 1, 4}}}, /* U+031E */
|
||||
{1, {{0, 23, 1, 4}}}, /* U+031F */
|
||||
{1, {{0, 23, 1, 4}}}, /* U+0320 */
|
||||
{1, {{0, 32, 1, 2}}}, /* U+0321 */
|
||||
{1, {{0, 33, 1, 2}}}, /* U+0322 */
|
||||
{1, {{0, 34, 1, 4}}}, /* U+0323 */
|
||||
{1, {{0, 35, 1, 4}}}, /* U+0324 */
|
||||
{1, {{0, 36, 1, 4}}}, /* U+0325 */
|
||||
{1, {{0, 37, 1, 4}}}, /* U+0326 */
|
||||
{1, {{0, 19, 1, 2}}}, /* U+0327 */
|
||||
{1, {{0, 20, 1, 2}}}, /* U+0328 */
|
||||
{1, {{0, 23, 1, 4}}}, /* U+0329 */
|
||||
{1, {{0, 23, 1, 4}}}, /* U+032A */
|
||||
{1, {{0, 23, 1, 4}}}, /* U+032B */
|
||||
{1, {{0, 23, 1, 4}}}, /* U+032C */
|
||||
{1, {{0, 38, 1, 4}}}, /* U+032D */
|
||||
{1, {{0, 39, 1, 4}}}, /* U+032E */
|
||||
{1, {{0, 23, 1, 4}}}, /* U+032F */
|
||||
{1, {{0, 40, 1, 4}}}, /* U+0330 */
|
||||
{1, {{0, 41, 1, 4}}}, /* U+0331 */
|
||||
{1, {{0, 3, 1, 4}}}, /* U+0332 */
|
||||
{1, {{0, 23, 1, 4}}}, /* U+0333 */
|
||||
{1, {{0, 42, 1, 1}}}, /* U+0334 */
|
||||
{1, {{0, 25, 1, 1}}}, /* U+0335 */
|
||||
{1, {{0, 24, 1, 1}}}, /* U+0336 */
|
||||
{1, {{0, 24, 1, 1}}}, /* U+0337 */
|
||||
{1, {{0, 18, 1, 1}}}, /* U+0338 */
|
||||
{1, {{0, 43, 1, 4}}}, /* U+0339 */
|
||||
{1, {{0, 23, 1, 4}}}, /* U+033A */
|
||||
{1, {{0, 23, 1, 4}}}, /* U+033B */
|
||||
{1, {{0, 23, 1, 4}}}, /* U+033C */
|
||||
{1, {{0, 22, 1, 5}}}, /* U+033D */
|
||||
{1, {{0, 22, 1, 5}}}, /* U+033E */
|
||||
{1, {{0, 22, 1, 5}}}, /* U+033F */
|
||||
{1, {{0, 7, 1, 5}}}, /* U+0340 */
|
||||
{1, {{0, 6, 1, 5}}}, /* U+0341 */
|
||||
{1, {{0, 12, 1, 5}}}, /* U+0342 */
|
||||
{1, {{0, 4, 1, 5}}}, /* U+0343 */
|
||||
{2, {{0, 13, 1, 5}, {0, 6, 1, 5}}}, /* U+0344 */
|
||||
{1, {{0, 44, 1, 9}}}, /* U+0345 */
|
||||
{1, {{0, 22, 1, 5}}}, /* U+0346 */
|
||||
{1, {{0, 23, 1, 4}}}, /* U+0347 */
|
||||
{1, {{0, 23, 1, 4}}}, /* U+0348 */
|
||||
{1, {{0, 23, 1, 4}}}, /* U+0349 */
|
||||
{1, {{0, 22, 1, 5}}}, /* U+034A */
|
||||
{1, {{0, 22, 1, 5}}}, /* U+034B */
|
||||
{1, {{0, 22, 1, 5}}}, /* U+034C */
|
||||
{1, {{0, 23, 1, 4}}}, /* U+034D */
|
||||
{1, {{0, 23, 1, 4}}}, /* U+034E */
|
||||
{0, {}}, /* U+034F */
|
||||
{1, {{0, 22, 1, 5}}}, /* U+0350 */
|
||||
{1, {{0, 22, 1, 5}}}, /* U+0351 */
|
||||
{1, {{0, 22, 1, 5}}}, /* U+0352 */
|
||||
{1, {{0, 23, 1, 4}}}, /* U+0353 */
|
||||
{1, {{0, 23, 1, 4}}}, /* U+0354 */
|
||||
{1, {{0, 23, 1, 4}}}, /* U+0355 */
|
||||
{1, {{0, 23, 1, 4}}}, /* U+0356 */
|
||||
{1, {{0, 22, 1, 5}}}, /* U+0357 */
|
||||
{1, {{0, 45, 1, 6}}}, /* U+0358 */
|
||||
{1, {{0, 23, 1, 4}}}, /* U+0359 */
|
||||
{1, {{0, 23, 1, 4}}}, /* U+035A */
|
||||
{1, {{0, 22, 1, 5}}}, /* U+035B */
|
||||
{1, {{0, 23, 1, 7}}}, /* U+035C */
|
||||
{1, {{0, 22, 1, 8}}}, /* U+035D */
|
||||
{1, {{0, 22, 1, 8}}}, /* U+035E */
|
||||
{1, {{0, 23, 1, 7}}}, /* U+035F */
|
||||
{1, {{0, 46, 1, 8}}}, /* U+0360 */
|
||||
{1, {{0, 47, 1, 8}}}, /* U+0361 */
|
||||
{1, {{0, 23, 1, 7}}}, /* U+0362 */
|
||||
{255, {{0, 0, 0, 0}}}, /* U+0363 not covered */
|
||||
{255, {{0, 0, 0, 0}}}, /* U+0364 not covered */
|
||||
{255, {{0, 0, 0, 0}}}, /* U+0365 not covered */
|
||||
{255, {{0, 0, 0, 0}}}, /* U+0366 not covered */
|
||||
{255, {{0, 0, 0, 0}}}, /* U+0367 not covered */
|
||||
{255, {{0, 0, 0, 0}}}, /* U+0368 not covered */
|
||||
{255, {{0, 0, 0, 0}}}, /* U+0369 not covered */
|
||||
{255, {{0, 0, 0, 0}}}, /* U+036A not covered */
|
||||
{255, {{0, 0, 0, 0}}}, /* U+036B not covered */
|
||||
{255, {{0, 0, 0, 0}}}, /* U+036C not covered */
|
||||
{255, {{0, 0, 0, 0}}}, /* U+036D not covered */
|
||||
{255, {{0, 0, 0, 0}}}, /* U+036E not covered */
|
||||
{255, {{0, 0, 0, 0}}}, /* U+036F not covered */
|
||||
};
|
||||
static const ScrCollEntry *scr_coll_lookup(uint32_t c) {
|
||||
if (c >= 0x9 && c <= 0xd) return &scr_coll_0009[c - 0x9];
|
||||
if (c >= 0x20 && c <= 0x7e) return &scr_coll_0020[c - 0x20];
|
||||
if (c >= 0xa0 && c <= 0x17f) return &scr_coll_00a0[c - 0xa0];
|
||||
if (c >= 0x300 && c <= 0x36f) return &scr_coll_0300[c - 0x300];
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
@@ -454,6 +454,31 @@ bool scr_exc_handle_uncaught(bool from_promise) {
|
||||
return result > 0;
|
||||
}
|
||||
|
||||
static bool scr_uncaught_print_dyn_primitive(const ScrDyn *d) {
|
||||
switch (d->kind) {
|
||||
case SCR_DYN_STR:
|
||||
fwrite(d->v.str->data, 1, d->v.str->len, stderr);
|
||||
return true;
|
||||
case SCR_DYN_NUM: {
|
||||
char buf[32];
|
||||
size_t len = scr_f64_to_str(d->v.num, buf);
|
||||
fwrite(buf, 1, len, stderr);
|
||||
return true;
|
||||
}
|
||||
case SCR_DYN_BOOL:
|
||||
fputs(d->v.b ? "true" : "false", stderr);
|
||||
return true;
|
||||
case SCR_DYN_NULL:
|
||||
fputs("null", stderr);
|
||||
return true;
|
||||
case SCR_DYN_UNDEF:
|
||||
fputs("undefined", stderr);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void scr_exc_print_uncaught(void) {
|
||||
#ifdef SCR_WORKERS
|
||||
if (scr_context_stopping()) { scr_exc_clear(); return; }
|
||||
@@ -496,6 +521,12 @@ void scr_exc_print_uncaught(void) {
|
||||
/* fall through: non-Error hierarchy objects render like other refs */
|
||||
case SCR_EXC_REF:
|
||||
case SCR_EXC_PRIMITIVE_REF:
|
||||
/* A checked-dynamic value (`throw v` with v: unknown) prints its
|
||||
* primitive the way Node's uncaught report does — a string's own
|
||||
* text, numbers/booleans/null/undefined by their names. */
|
||||
if (scr_exc_release_fn == scr_dyn_release_v && scr_exc_payload != NULL &&
|
||||
scr_uncaught_print_dyn_primitive((const ScrDyn *)scr_exc_payload))
|
||||
break;
|
||||
fputs("[object]", stderr);
|
||||
break;
|
||||
case SCR_EXC_NONE: /* main only calls this when pending */
|
||||
|
||||
@@ -737,6 +737,78 @@ ScrStr *scr_insp_key(ScrStr *k) {
|
||||
* lives in the value, so the traversal lives here instead of a
|
||||
* synthesized helper. Same engine, same defaults. dyn-boxed bytes render
|
||||
* in the checked-dynamic tree's documented Uint8Array identity (SEMANTICS.md). */
|
||||
/* An encoded Error's string slot: own first, then its prototype (an
|
||||
* inherited name/message lives there), else NULL. Borrowed. */
|
||||
static const ScrDyn *insp_error_slot(const ScrDyn *d, const char *key, size_t len) {
|
||||
const ScrDyn *v = scr_dyn_obj_get(d, key, len);
|
||||
if (v == NULL && d->prototype != NULL && d->prototype->kind == SCR_DYN_OBJ)
|
||||
v = scr_dyn_obj_get(d->prototype, key, len);
|
||||
return v != NULL && v->kind == SCR_DYN_STR ? v : NULL;
|
||||
}
|
||||
|
||||
/* The checked-dynamic Error encoding (scr_error_dyn_fields) rendered like
|
||||
* scr_insp_error: the stackless bracket form `[Name: message]`, then the
|
||||
* remaining enumerable own properties (code, errno, syscall, ...) in
|
||||
* Node's `{ ... }` tail. */
|
||||
static ScrStr *scr_insp_dyn_error(ScrDyn *d, double recurse, double depth) {
|
||||
const ScrDyn *name = insp_error_slot(d, "name", 4);
|
||||
const ScrDyn *message = insp_error_slot(d, "message", 7);
|
||||
size_t extra = 0;
|
||||
for (size_t i = 0; i < d->v.obj.len; i++) {
|
||||
ScrDynEntry *ent = &d->v.obj.entries[i];
|
||||
if (!ent->enumerable) continue;
|
||||
if ((ent->key_len == 4 && memcmp(ent->key, "name", 4) == 0) ||
|
||||
(ent->key_len == 7 && memcmp(ent->key, "message", 7) == 0) ||
|
||||
(ent->key_len == 5 && memcmp(ent->key, "stack", 5) == 0))
|
||||
continue;
|
||||
extra++;
|
||||
}
|
||||
InspBuf base = {0};
|
||||
ib_char(&base, '[');
|
||||
if (name != NULL) ib_str(&base, name->v.str);
|
||||
else ib_cstr(&base, "Error");
|
||||
if (extra > 0 && recurse > depth) {
|
||||
ib_char(&base, ']');
|
||||
return ib_take(&base);
|
||||
}
|
||||
if (message != NULL && message->v.str->len) {
|
||||
ib_cstr(&base, ": ");
|
||||
for (size_t i = 0; i < message->v.str->len; i++) {
|
||||
char c = message->v.str->data[i];
|
||||
ib_char(&base, c);
|
||||
if (c == '\n') ib_spaces(&base, g_indent);
|
||||
}
|
||||
}
|
||||
ib_char(&base, ']');
|
||||
ScrStr *base_str = ib_take(&base);
|
||||
if (extra == 0) return base_str;
|
||||
scr_insp_begin(recurse + 1);
|
||||
for (size_t i = 0; i < d->v.obj.len; i++) {
|
||||
ScrDynEntry *ent = &d->v.obj.entries[i];
|
||||
if (!ent->enumerable) continue;
|
||||
if ((ent->key_len == 4 && memcmp(ent->key, "name", 4) == 0) ||
|
||||
(ent->key_len == 7 && memcmp(ent->key, "message", 7) == 0) ||
|
||||
(ent->key_len == 5 && memcmp(ent->key, "stack", 5) == 0))
|
||||
continue;
|
||||
ScrStr *val = scr_insp_dyn(ent->value, recurse + 1, depth);
|
||||
InspBuf eb = {0};
|
||||
insp_key_into(&eb, ent->key, ent->key_len);
|
||||
ib_cstr(&eb, ": ");
|
||||
ib_bytes(&eb, val->data, val->len);
|
||||
scr_str_release(val);
|
||||
ScrStr *entry = ib_take(&eb);
|
||||
scr_insp_entry(entry, false);
|
||||
scr_str_release(entry);
|
||||
}
|
||||
ScrStr *b0 = scr_str_new("{", 1);
|
||||
ScrStr *b1 = scr_str_new("}", 1);
|
||||
ScrStr *out = scr_insp_end(base_str, b0, b1, recurse + 1, false, false);
|
||||
scr_str_release(b0);
|
||||
scr_str_release(b1);
|
||||
scr_str_release(base_str);
|
||||
return out;
|
||||
}
|
||||
|
||||
ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) {
|
||||
if (d->kind == SCR_DYN_PROXY) return scr_insp_dyn(d->v.proxy.target, recurse, depth);
|
||||
switch (d->kind) {
|
||||
@@ -781,6 +853,10 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) {
|
||||
return out;
|
||||
}
|
||||
case SCR_DYN_OBJ: {
|
||||
/* An Error carried as a checked-dynamic value (a caught error, an
|
||||
* Error stored in an `unknown` slot): the error's own rendering,
|
||||
* never the encoding's internal marker. */
|
||||
if (scr_dyn_obj_get(d, "%error", 6) != NULL) return scr_insp_dyn_error(d, recurse, depth);
|
||||
/* Object.create(null)'s dictionary: Node prefixes the rendering
|
||||
* with "[Object: null prototype]" (formatValue's constructor-less
|
||||
* base), the empty form included, and the beyond-depth answer IS
|
||||
|
||||
@@ -609,7 +609,7 @@ ScrStr *scr_process_arch(void) {
|
||||
* is no Node under a compiled binary; this is the version whose semantics
|
||||
* SEMANTICS.md verifies the runtime against (divergence 60, the execPath
|
||||
* stance: answer for the world that actually exists). */
|
||||
#define SCR_NODE_COMPAT_VERSION "24.0.0"
|
||||
#define SCR_NODE_COMPAT_VERSION "24.15.0"
|
||||
ScrStr *scr_process_versions_node(void) {
|
||||
if (!scr_versions_node_str) {
|
||||
scr_versions_node_str =
|
||||
|
||||
@@ -732,6 +732,9 @@ bool scr_str_eq(ScrStr *a, ScrStr *b);
|
||||
* JS UTF-16 code-unit order only for non-BMP vs U+E000..U+FFFF). Returns
|
||||
* <0, 0, >0. */
|
||||
int scr_str_cmp(ScrStr *a, ScrStr *b);
|
||||
/* a.localeCompare(b) with no locales/options: Node's root-locale order for
|
||||
* Latin-script text, code-point order outside it (scr_string.c). */
|
||||
double scr_str_locale_compare(ScrStr *a, ScrStr *b);
|
||||
|
||||
/* ECMAScript string-list ordering: compare UTF-16 code units even though
|
||||
* ScrStr stores well-formed UTF-8. Returns <0, 0, >0. */
|
||||
@@ -5418,6 +5421,7 @@ double scr_now_ms(void); /* the loop's monotonic clock, in ms */
|
||||
* Returns true when a default/listener-crashing unhandled rejection
|
||||
* already selected and reported exit status 1. */
|
||||
bool scr_loop_run(ScrPromise *top_level);
|
||||
void scr_loop_commonjs_entry(void);
|
||||
/* External I/O hook, polled at loop quiescence like the child registry:
|
||||
* `pending` keeps the loop alive; `poll` makes progress and may SLEEP up
|
||||
* to max_wait_ms (on real fds — socket readiness wakes it early), so the
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "scr_runtime.h"
|
||||
#include "scr_key.h"
|
||||
#include "scr_collation_data.h"
|
||||
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
@@ -1257,7 +1258,15 @@ static ScrStr *scr_str_slice_units(ScrStr *s, ScrSidx *e,
|
||||
ScrStr *scr_str_repeat(ScrStr *s, double count) {
|
||||
double n = scr_to_integer_or_infinity(count);
|
||||
if (n < 0 || (isinf(n) && n > 0)) {
|
||||
scr_trap("scriptc: RangeError: Invalid count value\n");
|
||||
/* Node's catchable RangeError, with the count as ToString renders it
|
||||
* ("Invalid count value: -1", "... Infinity"). The caller checks the
|
||||
* pending exception; the empty result is never observed. */
|
||||
char num[32];
|
||||
scr_f64_to_str(count, num);
|
||||
char msg[64];
|
||||
int len = snprintf(msg, sizeof msg, "Invalid count value: %s", num);
|
||||
scr_throw_error_msg(SCR_ERR_RANGE, msg, (size_t)len);
|
||||
return scr_str_empty();
|
||||
}
|
||||
if (n == 0 || s->len == 0) return scr_str_empty();
|
||||
/* n is a finite non-negative integer here. Reject sizes malloc could not
|
||||
@@ -2113,3 +2122,97 @@ ScrStr *scr_str_decode_uri_component(ScrStr *s) {
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/* ── String.prototype.localeCompare (one argument) ─────────────────────
|
||||
* Node's default collation is ICU's root locale. scr_collation_data.h
|
||||
* (scripts/gen-collation-table.mjs, verified against the pinned Node)
|
||||
* carries its collation elements for the Latin repertoire: ASCII, Latin-1
|
||||
* Supplement, Latin Extended-A and the combining diacritics. Strings
|
||||
* entirely inside it compare exactly as Node does: canonical decomposition
|
||||
* and mark reordering, then primaries (letters before case and accents:
|
||||
* "a" < "B"), secondaries (accents), tertiaries (case). A string with any
|
||||
* other code point falls back to code-point order (the documented limit;
|
||||
* `scriptc coverage` reports such literals as SC6003). */
|
||||
typedef struct {
|
||||
ScrCollElem *e;
|
||||
size_t n, cap;
|
||||
ScrCollElem inline_buf[64];
|
||||
} ScrCollBuf;
|
||||
|
||||
static void scr_coll_push(ScrCollBuf *b, ScrCollElem e) {
|
||||
if (b->n == b->cap) {
|
||||
size_t cap = b->cap * 2;
|
||||
ScrCollElem *grown = b->e == b->inline_buf ? malloc(cap * sizeof *grown)
|
||||
: realloc(b->e, cap * sizeof *grown);
|
||||
if (!grown) scr_oom();
|
||||
if (b->e == b->inline_buf) memcpy(grown, b->inline_buf, b->n * sizeof *grown);
|
||||
b->e = grown;
|
||||
b->cap = cap;
|
||||
}
|
||||
b->e[b->n++] = e;
|
||||
}
|
||||
|
||||
/* Elements of a UTF-8 string, marks in canonical order; false when a code
|
||||
* point is outside the table. */
|
||||
static bool scr_coll_elements(const ScrStr *s, ScrCollBuf *b) {
|
||||
const unsigned char *p = (const unsigned char *)s->data;
|
||||
const unsigned char *end = p + s->len;
|
||||
while (p < end) {
|
||||
uint32_t c = *p++;
|
||||
if (c >= 0x80) {
|
||||
int extra = c >= 0xf0 ? 3 : c >= 0xe0 ? 2 : 1;
|
||||
c &= extra == 3 ? 0x07 : extra == 2 ? 0x0f : 0x1f;
|
||||
for (int i = 0; i < extra && p < end; i++) c = (c << 6) | (*p++ & 0x3f);
|
||||
}
|
||||
const ScrCollEntry *entry = scr_coll_lookup(c);
|
||||
if (entry == NULL || entry->n == 255) return false;
|
||||
for (uint8_t i = 0; i < entry->n; i++) {
|
||||
ScrCollElem e = entry->e[i];
|
||||
size_t j = b->n;
|
||||
scr_coll_push(b, e);
|
||||
/* Canonical reordering: a mark moves before marks of a higher class. */
|
||||
if (e.ccc != 0) {
|
||||
while (j > 0 && b->e[j - 1].ccc > e.ccc) {
|
||||
b->e[j] = b->e[j - 1];
|
||||
j--;
|
||||
}
|
||||
b->e[j] = e;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static int scr_coll_level(const ScrCollBuf *a, const ScrCollBuf *b, int level) {
|
||||
size_t i = 0, j = 0;
|
||||
for (;;) {
|
||||
if (level == 0) {
|
||||
while (i < a->n && a->e[i].p == 0) i++;
|
||||
while (j < b->n && b->e[j].p == 0) j++;
|
||||
}
|
||||
if (i == a->n || j == b->n) return (i == a->n) == (j == b->n) ? 0 : i == a->n ? -1 : 1;
|
||||
unsigned wa = level == 0 ? a->e[i].p : level == 1 ? a->e[i].s : a->e[i].t;
|
||||
unsigned wb = level == 0 ? b->e[j].p : level == 1 ? b->e[j].s : b->e[j].t;
|
||||
if (wa != wb) return wa < wb ? -1 : 1;
|
||||
i++;
|
||||
j++;
|
||||
}
|
||||
}
|
||||
|
||||
double scr_str_locale_compare(ScrStr *a, ScrStr *b) {
|
||||
ScrCollBuf ea = {0}, eb = {0};
|
||||
ea.e = ea.inline_buf;
|
||||
ea.cap = sizeof ea.inline_buf / sizeof ea.inline_buf[0];
|
||||
eb.e = eb.inline_buf;
|
||||
eb.cap = sizeof eb.inline_buf / sizeof eb.inline_buf[0];
|
||||
int r = 0;
|
||||
if (scr_coll_elements(a, &ea) && scr_coll_elements(b, &eb)) {
|
||||
for (int level = 0; level < 3 && r == 0; level++) r = scr_coll_level(&ea, &eb, level);
|
||||
} else {
|
||||
int c = scr_str_cmp(a, b);
|
||||
r = c < 0 ? -1 : c > 0 ? 1 : 0;
|
||||
}
|
||||
if (ea.e != ea.inline_buf) free(ea.e);
|
||||
if (eb.e != eb.inline_buf) free(eb.e);
|
||||
return (double)r;
|
||||
}
|
||||
|
||||
@@ -74,18 +74,13 @@ test("bytes runtime: coercions, encodings, zlib, fs, RC", async () => {
|
||||
expect(stderr.trim().split("\n").pop()).toMatch(/^(\d+)\/\1 cases passed$/);
|
||||
});
|
||||
|
||||
// Statically typed numeric reads cannot represent undefined and still trap.
|
||||
// Invalid writes are ignored and covered by the runtime assertions above.
|
||||
// Statically typed numeric reads cannot represent undefined; an invalid
|
||||
// index throws a catchable RangeError instead of aborting. Invalid writes
|
||||
// are ignored and covered by the runtime assertions above.
|
||||
test.each([
|
||||
["--crash-get-oob", "typed array index 1 out of bounds (length 1)"],
|
||||
["--crash-get-frac", "typed array index 0.5 out of bounds (length 1)"],
|
||||
])("trap aborts (%s)", async (mode, message) => {
|
||||
const err = await execFileAsync(bin, [mode]).then(
|
||||
() => {
|
||||
throw new Error(`expected ${mode} to abort`);
|
||||
},
|
||||
(e: Error & { signal?: string; stderr?: string }) => e,
|
||||
);
|
||||
expect(err.signal).toBe("SIGABRT");
|
||||
expect(err.stderr).toContain(`scriptc: RangeError: ${message}`);
|
||||
])("invalid reads throw a catchable RangeError (%s)", async (mode, message) => {
|
||||
const { stderr } = await execFileAsync(bin, [mode]);
|
||||
expect(stderr).toContain(`Uncaught RangeError: ${message}`);
|
||||
});
|
||||
|
||||
@@ -121,18 +121,12 @@ test("string methods match Node on committed oracle cases", async () => {
|
||||
expect(mismatches.length).toBeLessThanOrEqual(LINUX_PARSEINT_ULP.size);
|
||||
});
|
||||
|
||||
// repeat(count) with count < 0 or Infinity is a RangeError in JS; scriptc
|
||||
// has no exceptions, so the runtime must print the error and abort().
|
||||
test.each(["--crash-repeat", "--crash-repeat-inf"])(
|
||||
"repeat RangeError aborts (%s)",
|
||||
async (mode) => {
|
||||
const err = await execFileAsync(bin, [mode]).then(
|
||||
() => {
|
||||
throw new Error("expected scr_str_repeat to abort");
|
||||
},
|
||||
(e: Error & { signal?: string; stderr?: string }) => e,
|
||||
);
|
||||
expect(err.signal).toBe("SIGABRT");
|
||||
expect(err.stderr).toContain("scriptc: RangeError: Invalid count value");
|
||||
},
|
||||
);
|
||||
// repeat(count) with count < 0 or Infinity is Node's catchable RangeError
|
||||
// ("Invalid count value: -1"); the runtime leaves it pending.
|
||||
test.each([
|
||||
["--crash-repeat", "-1"],
|
||||
["--crash-repeat-inf", "Infinity"],
|
||||
])("repeat throws a catchable RangeError (%s)", async (mode, shown) => {
|
||||
const { stderr } = await execFileAsync(bin, [mode]);
|
||||
expect(stderr).toContain(`Uncaught RangeError: Invalid count value: ${shown}`);
|
||||
});
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
* bytes.test.ts, which also asserts the trap modes abort:
|
||||
*
|
||||
* <scratch-dir> run all assertions; prints "N/N cases passed"
|
||||
* --crash-get-oob element read past the end → RangeError + abort()
|
||||
* --crash-get-frac fractional element index → RangeError + abort()
|
||||
* --crash-get-oob element read past the end → pending RangeError
|
||||
* --crash-get-frac fractional element index → pending RangeError
|
||||
*
|
||||
* The coercion matrix mirrors Node exactly (verified by hand and by the
|
||||
* differential corpus): ToUint8/ToUint32 modular truncation on writes,
|
||||
@@ -393,8 +393,13 @@ int main(int argc, char **argv) {
|
||||
fprintf(stderr, "unknown mode %s\n", argv[1]);
|
||||
return 2;
|
||||
}
|
||||
fprintf(stderr, "expected a trap, still alive\n");
|
||||
return 2;
|
||||
if (!scr_exc_pending()) {
|
||||
fprintf(stderr, "expected a pending RangeError\n");
|
||||
return 2;
|
||||
}
|
||||
scr_exc_print_uncaught();
|
||||
scr_bytes_release(b);
|
||||
return 0;
|
||||
}
|
||||
if (argc != 2) {
|
||||
fprintf(stderr, "usage: test_bytes <scratch-dir> | --crash-*\n");
|
||||
|
||||
@@ -11,8 +11,9 @@
|
||||
* identical replacement behavior.
|
||||
*
|
||||
* Special mode: --crash-repeat / --crash-repeat-inf call
|
||||
* scr_str_repeat with an invalid count and must abort() after printing
|
||||
* "scriptc: RangeError: Invalid count value" (checked by string.test.ts).
|
||||
* scr_str_repeat with an invalid count, which must leave Node's catchable
|
||||
* RangeError pending ("Invalid count value: -1"); the mode prints it the
|
||||
* way an uncaught throw does (checked by string.test.ts).
|
||||
*
|
||||
* Exit 0 = all pass; prints each mismatch (capped) and exits 1 otherwise.
|
||||
*/
|
||||
@@ -879,9 +880,15 @@ int main(int argc, char **argv) {
|
||||
double count = strcmp(argv[1], "--crash-repeat-inf") == 0
|
||||
? (double)INFINITY
|
||||
: -1.0;
|
||||
scr_str_repeat(s, count); /* must print RangeError and abort() */
|
||||
fputs("UNREACHABLE: scr_str_repeat returned\n", stderr);
|
||||
return 3;
|
||||
ScrStr *r = scr_str_repeat(s, count); /* must leave a RangeError pending */
|
||||
scr_str_release(r);
|
||||
scr_str_release(s);
|
||||
if (!scr_exc_pending()) {
|
||||
fputs("UNREACHABLE: scr_str_repeat returned without an exception\n", stderr);
|
||||
return 3;
|
||||
}
|
||||
scr_exc_print_uncaught();
|
||||
return 0;
|
||||
}
|
||||
|
||||
FILE *in = stdin;
|
||||
|
||||
@@ -0,0 +1,371 @@
|
||||
#!/usr/bin/env node
|
||||
// Root-locale collation elements for String.prototype.localeCompare over the
|
||||
// Latin repertoire (ASCII, Latin-1 Supplement, Latin Extended-A and the
|
||||
// combining diacritical marks), derived from the pinned Node release's ICU
|
||||
// (`localeCompare` with no locales/options) and verified against it.
|
||||
//
|
||||
// node scripts/gen-collation-table.mjs regenerate the table
|
||||
// node scripts/gen-collation-table.mjs --check verify only
|
||||
//
|
||||
// Model: every covered code point maps to a short list of collation
|
||||
// elements (primary, secondary, tertiary). Precomposed letters are their
|
||||
// base letter's element followed by one secondary-only element for the
|
||||
// diacritic; letters with a stroke or other built-in variant (ø, ł, đ, ...)
|
||||
// carry the same extra secondary; ligatures expand (æ → a, e). Strings
|
||||
// compare level by level over the concatenated elements — primaries
|
||||
// (zero primaries skipped), then secondaries, then tertiaries — which is the
|
||||
// multi-level comparison ICU applies at its default (tertiary) strength.
|
||||
// The output is written only after a randomized differential check against
|
||||
// Node's own localeCompare passes.
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { join } from "node:path";
|
||||
|
||||
const root = fileURLToPath(new URL("..", import.meta.url));
|
||||
const out = join(root, "packages/runtime/src/scr_collation_data.h");
|
||||
const checkOnly = process.argv.includes("--check");
|
||||
|
||||
const base = new Intl.Collator(undefined, { sensitivity: "base" });
|
||||
const accent = new Intl.Collator(undefined, { sensitivity: "accent" });
|
||||
const variant = new Intl.Collator(undefined, { sensitivity: "variant" });
|
||||
|
||||
export const RANGES = [
|
||||
[0x09, 0x0d],
|
||||
[0x20, 0x7e],
|
||||
[0xa0, 0x17f],
|
||||
[0x300, 0x36f],
|
||||
];
|
||||
const ch = (c) => String.fromCodePoint(c);
|
||||
// Combining marks are covered when they are pure diacritics (no primary
|
||||
// weight); the combining Latin letters (U+0363–U+036F) are not.
|
||||
const isMark = (c) => c >= 0x300 && c <= 0x36f;
|
||||
// The vulgar fractions expand through U+2044 FRACTION SLASH, which is
|
||||
// outside the table, so they stay uncovered.
|
||||
const UNCOVERED = new Set([0xbc, 0xbd, 0xbe]);
|
||||
const covered = [];
|
||||
for (const [lo, hi] of RANGES)
|
||||
for (let c = lo; c <= hi; c++)
|
||||
if (!UNCOVERED.has(c) && (!isMark(c) || base.compare("a" + ch(c), "a") === 0)) covered.push(c);
|
||||
const coveredSet = new Set(covered);
|
||||
|
||||
// 1. Combining marks: secondary weights by their order on a base letter.
|
||||
const marks = covered.filter(isMark);
|
||||
const markOrder = [...marks].sort((a, b) => accent.compare("a" + ch(a), "a" + ch(b)) || a - b);
|
||||
const markWeight = new Map();
|
||||
{
|
||||
let w = 1;
|
||||
for (let i = 0; i < markOrder.length; i++) {
|
||||
if (i > 0 && accent.compare("a" + ch(markOrder[i - 1]), "a" + ch(markOrder[i])) !== 0) w++;
|
||||
markWeight.set(markOrder[i], w);
|
||||
}
|
||||
}
|
||||
|
||||
// Canonical combining classes, as ranks: ICU normalizes (NFD, with marks
|
||||
// reordered by class) before collating. Derived from the canonical
|
||||
// reordering Node's normalize() applies to mark pairs.
|
||||
const reorders = (a, b) => ("x" + ch(a) + ch(b)).normalize("NFD") !== "x" + ch(a) + ch(b);
|
||||
const atomicMarks = marks.filter((m) => ch(m).normalize("NFD") === ch(m));
|
||||
const movable = atomicMarks.filter((m) =>
|
||||
atomicMarks.some((n) => reorders(m, n) || reorders(n, m)),
|
||||
);
|
||||
const cccOrder = [...movable].sort(
|
||||
(a, b) => (reorders(a, b) ? 1 : reorders(b, a) ? -1 : 0) || a - b,
|
||||
);
|
||||
const cccRank = new Map();
|
||||
{
|
||||
let r = 1;
|
||||
for (let i = 0; i < cccOrder.length; i++) {
|
||||
const prev = cccOrder[i - 1];
|
||||
if (i > 0 && (reorders(prev, cccOrder[i]) || reorders(cccOrder[i], prev))) r++;
|
||||
cccRank.set(cccOrder[i], r);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Ignorable code points (no weight at any level).
|
||||
const ignorable = new Set(
|
||||
covered.filter(
|
||||
(c) => variant.compare(ch(c) + "a", "a") === 0 && variant.compare("a" + ch(c), "a") === 0,
|
||||
),
|
||||
);
|
||||
|
||||
// 3. Expansions: a code point equal at the base level to two ASCII letters.
|
||||
const letters = "abcdefghijklmnopqrstuvwxyz";
|
||||
const expansion = new Map();
|
||||
for (const c of covered) {
|
||||
if (c < 0x80 || isMark(c) || ignorable.has(c) || ch(c).normalize("NFD") !== ch(c)) continue;
|
||||
if (covered.some((d) => d < 0x80 && base.compare(ch(c), ch(d)) === 0)) continue;
|
||||
for (const x of letters)
|
||||
for (const y of letters)
|
||||
if (base.compare(ch(c), x + y) === 0 && !expansion.has(c)) expansion.set(c, x + y);
|
||||
}
|
||||
|
||||
// 4. Primary classes over single code points (base level), in order.
|
||||
const singles = covered.filter((c) => !isMark(c) && !ignorable.has(c) && !expansion.has(c));
|
||||
const decomposedBase = (c) => {
|
||||
const nfd = ch(c).normalize("NFD");
|
||||
return nfd.length > 1 && isMark(nfd.codePointAt(1)) ? nfd.codePointAt(0) : null;
|
||||
};
|
||||
const primaryReps = singles.filter((c) => decomposedBase(c) === null);
|
||||
primaryReps.sort((a, b) => base.compare(ch(a), ch(b)) || a - b);
|
||||
const primaryOf = new Map();
|
||||
{
|
||||
let p = 1;
|
||||
for (let i = 0; i < primaryReps.length; i++) {
|
||||
if (i > 0 && base.compare(ch(primaryReps[i - 1]), ch(primaryReps[i])) !== 0) p++;
|
||||
primaryOf.set(primaryReps[i], p);
|
||||
}
|
||||
}
|
||||
// The class representative: the ASCII member when there is one.
|
||||
const repOf = (c) => {
|
||||
const p = primaryOf.get(c);
|
||||
const ascii = primaryReps.find((d) => primaryOf.get(d) === p && d < 0x80);
|
||||
return ascii ?? primaryReps.find((d) => primaryOf.get(d) === p);
|
||||
};
|
||||
|
||||
// 5. Extra secondary of a non-decomposable variant letter (ø against o):
|
||||
// the mark whose sequence it equals at the accent level, or a weight
|
||||
// placed among the marks by comparison.
|
||||
const extraSecondary = new Map();
|
||||
const variantMark = new Map();
|
||||
let nextUnique = markOrder.length + 10;
|
||||
const syntheticAt = [];
|
||||
for (const c of primaryReps) {
|
||||
const r = repOf(c);
|
||||
if (r === c || accent.compare(ch(c), ch(r)) === 0) continue;
|
||||
const same = markOrder.find((m) => accent.compare(ch(c), ch(r) + ch(m)) === 0);
|
||||
if (same !== undefined) {
|
||||
variantMark.set(c, same);
|
||||
continue;
|
||||
}
|
||||
// Insert between mark weights: below the first mark it sorts before.
|
||||
let below = markOrder.findIndex((m) => accent.compare(ch(c), ch(r) + ch(m)) < 0);
|
||||
if (below < 0) below = markOrder.length;
|
||||
syntheticAt.push({ c, below });
|
||||
}
|
||||
// Ligatures differ from their spelled-out pair by a secondary between the
|
||||
// halves (æ against a, e): a mark's weight or a synthetic one.
|
||||
const expansionSecondary = new Map();
|
||||
const expansionMark = new Map();
|
||||
for (const [c, pair] of expansion) {
|
||||
if (accent.compare(ch(c), pair) === 0) continue;
|
||||
const same = markOrder.find((m) => accent.compare(ch(c), pair[0] + ch(m) + pair[1]) === 0);
|
||||
if (same !== undefined) {
|
||||
expansionMark.set(c, same);
|
||||
continue;
|
||||
}
|
||||
let below = markOrder.findIndex((m) => accent.compare(ch(c), pair[0] + ch(m) + pair[1]) < 0);
|
||||
if (below < 0) below = markOrder.length;
|
||||
syntheticAt.push({ c, below, expansion: true });
|
||||
}
|
||||
// Synthetic weights are fractional slots; renumber all secondaries densely.
|
||||
{
|
||||
const entries = [];
|
||||
for (const [m, w] of markWeight) entries.push({ key: `m${m}`, w: w * 2 });
|
||||
// Case pairs (Æ/æ, Ð/ð) share their synthetic secondary: they differ at
|
||||
// the tertiary level only.
|
||||
const uniqueByFold = new Map();
|
||||
for (const { c, below, expansion: isExpansion } of syntheticAt) {
|
||||
const fold = ch(c).toLowerCase();
|
||||
if (below >= markOrder.length && !uniqueByFold.has(fold)) uniqueByFold.set(fold, nextUnique++);
|
||||
const w =
|
||||
below < markOrder.length
|
||||
? markWeight.get(markOrder[below]) * 2 - 1
|
||||
: uniqueByFold.get(fold) * 2;
|
||||
entries.push({ key: `${isExpansion ? "x" : "c"}${c}`, w });
|
||||
}
|
||||
const distinct = [...new Set(entries.map((e) => e.w))].sort((a, b) => a - b);
|
||||
const dense = new Map(distinct.map((w, i) => [w, i + 2])); // 1 is "common"
|
||||
for (const e of entries) {
|
||||
const w = dense.get(e.w);
|
||||
if (e.key.startsWith("m")) markWeight.set(Number(e.key.slice(1)), w);
|
||||
else if (e.key.startsWith("x")) expansionSecondary.set(Number(e.key.slice(1)), w);
|
||||
else extraSecondary.set(Number(e.key.slice(1)), w);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [c, m] of variantMark) extraSecondary.set(c, markWeight.get(m));
|
||||
for (const [c, m] of expansionMark) expansionSecondary.set(c, markWeight.get(m));
|
||||
|
||||
// 6. Elements per code point (tertiary assigned below).
|
||||
const COMMON = 1;
|
||||
const elements = new Map();
|
||||
for (const c of covered) {
|
||||
if (ignorable.has(c)) elements.set(c, []);
|
||||
else if (isMark(c) && ch(c).normalize("NFD") === ch(c) && markWeight.has(c))
|
||||
elements.set(c, [[0, markWeight.get(c), COMMON, cccRank.get(c) ?? 0]]);
|
||||
}
|
||||
for (const c of primaryReps) {
|
||||
const ces = [[primaryOf.get(c), COMMON, 0, 0]];
|
||||
if (extraSecondary.has(c)) ces.push([0, extraSecondary.get(c), COMMON, 0]);
|
||||
elements.set(c, ces);
|
||||
}
|
||||
for (const c of covered) {
|
||||
if (elements.has(c) || expansion.has(c)) continue;
|
||||
// Canonically decomposable: the elements of its NFD atoms.
|
||||
const nfd = [...ch(c).normalize("NFD")].map((x) => x.codePointAt(0));
|
||||
if (nfd.length === 1 && nfd[0] === c) throw new Error(`no elements for U+${c.toString(16)}`);
|
||||
elements.set(
|
||||
c,
|
||||
nfd.flatMap((x) => {
|
||||
const es = elements.get(x);
|
||||
if (es === undefined) throw new Error(`U+${c.toString(16)} decomposes outside the table`);
|
||||
return es.map((e) => [...e]);
|
||||
}),
|
||||
);
|
||||
}
|
||||
for (const [c, pair] of expansion) {
|
||||
const first = elements.get(pair.codePointAt(0)).map((e) => [...e]);
|
||||
const second = elements.get(pair.codePointAt(1)).map((e) => [...e]);
|
||||
// A ligature differs from its spelled-out pair at the secondary level:
|
||||
// find the mark-equivalent secondary between the two halves, if any.
|
||||
const middle = expansionSecondary.has(c) ? [[0, expansionSecondary.get(c), COMMON, 0]] : [];
|
||||
elements.set(c, [...first, ...middle, ...second]);
|
||||
}
|
||||
|
||||
// 7. Tertiary: rank code points whose primary+secondary elements agree.
|
||||
const psKey = (c) =>
|
||||
elements
|
||||
.get(c)
|
||||
.map((e) => `${e[0]}.${e[1]}`)
|
||||
.join(" ");
|
||||
const groups = new Map();
|
||||
for (const c of covered) {
|
||||
if (ignorable.has(c) || isMark(c)) continue;
|
||||
const k = psKey(c);
|
||||
if (!groups.has(k)) groups.set(k, []);
|
||||
groups.get(k).push(c);
|
||||
}
|
||||
const tertiaryOf = new Map();
|
||||
for (const members of groups.values()) {
|
||||
members.sort((a, b) => variant.compare(ch(a), ch(b)) || a - b);
|
||||
let t = 1;
|
||||
for (let i = 0; i < members.length; i++) {
|
||||
if (i > 0 && variant.compare(ch(members[i - 1]), ch(members[i])) !== 0) t++;
|
||||
tertiaryOf.set(members[i], t);
|
||||
}
|
||||
}
|
||||
for (const c of covered) {
|
||||
if (ignorable.has(c) || isMark(c)) continue;
|
||||
for (const e of elements.get(c)) if (e[0] !== 0) e[2] = tertiaryOf.get(c);
|
||||
}
|
||||
|
||||
// The model's comparison (mirrored by scr_str_locale_compare in C).
|
||||
export function modelCompare(a, b) {
|
||||
const ces = (s) => {
|
||||
const es = [...s].flatMap((x) => elements.get(x.codePointAt(0)));
|
||||
// Canonical reordering: stable sort each run of movable marks by class.
|
||||
for (let i = 1; i < es.length; i++) {
|
||||
const e = es[i];
|
||||
if (e[3] === 0) continue;
|
||||
let j = i;
|
||||
while (j > 0 && es[j - 1][3] > e[3]) {
|
||||
es[j] = es[j - 1];
|
||||
j--;
|
||||
}
|
||||
es[j] = e;
|
||||
}
|
||||
return es;
|
||||
};
|
||||
const ea = ces(a);
|
||||
const eb = ces(b);
|
||||
for (const level of [0, 1, 2]) {
|
||||
// Zero primaries (diacritics) are skipped at the primary level only.
|
||||
const wa = ea.filter((e) => level !== 0 || e[0] !== 0).map((e) => e[level]);
|
||||
const wb = eb.filter((e) => level !== 0 || e[0] !== 0).map((e) => e[level]);
|
||||
const n = Math.min(wa.length, wb.length);
|
||||
for (let i = 0; i < n; i++) if (wa[i] !== wb[i]) return wa[i] < wb[i] ? -1 : 1;
|
||||
if (wa.length !== wb.length) return wa.length < wb.length ? -1 : 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 8. Differential verification against Node.
|
||||
let seed = 0x5eed;
|
||||
const rnd = (n) => {
|
||||
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
|
||||
return seed % n;
|
||||
};
|
||||
const pool = covered.filter((c) => !isMark(c));
|
||||
const heavy = [..."aAbBeEoOsSzZ ,.-_'1 2éÉèäÄöøØßæÆœłŁđ"].map((x) => x.codePointAt(0));
|
||||
const word = () => {
|
||||
const n = 1 + rnd(5);
|
||||
let s = "";
|
||||
for (let i = 0; i < n; i++) {
|
||||
const r = rnd(10);
|
||||
s += ch(
|
||||
r < 6 ? heavy[rnd(heavy.length)] : r < 9 ? pool[rnd(pool.length)] : marks[rnd(marks.length)],
|
||||
);
|
||||
}
|
||||
return s;
|
||||
};
|
||||
let mismatches = 0;
|
||||
const samples = [];
|
||||
const check = (a, b) => {
|
||||
const want = a.localeCompare(b);
|
||||
const got = modelCompare(a, b);
|
||||
if (Math.sign(want) !== got) {
|
||||
mismatches++;
|
||||
if (samples.length < 20)
|
||||
samples.push(`${JSON.stringify(a)} vs ${JSON.stringify(b)}: node ${want}, model ${got}`);
|
||||
}
|
||||
};
|
||||
for (const x of pool) for (const y of pool) check(ch(x), ch(y));
|
||||
for (let i = 0; i < 300000; i++) check(word(), word());
|
||||
if (mismatches > 0) {
|
||||
console.error(`collation model disagrees with Node in ${mismatches} cases:`);
|
||||
for (const s of samples) console.error(` ${s}`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`collation model matches Node over ${pool.length ** 2 + 300000} comparisons`);
|
||||
if (checkOnly) process.exit(0);
|
||||
|
||||
// 9. Emit the C table.
|
||||
const lines = [];
|
||||
lines.push("/* Generated by scripts/gen-collation-table.mjs from the pinned Node");
|
||||
lines.push(" * release's root-locale collation (ICU, localeCompare without locales or");
|
||||
lines.push(" * options). Do not edit by hand. Each covered code point maps to up to");
|
||||
lines.push(" * three collation elements {primary, secondary, tertiary, combining class");
|
||||
lines.push(" * rank}; zero primaries are skipped at the primary level, and runs of");
|
||||
lines.push(" * marks with a nonzero class rank are put in canonical order before");
|
||||
lines.push(" * comparing. n == 255 marks an uncovered code point. */");
|
||||
lines.push("#ifndef SCR_COLLATION_DATA_H");
|
||||
lines.push("#define SCR_COLLATION_DATA_H");
|
||||
lines.push("#include <stdint.h>");
|
||||
lines.push("typedef struct { uint16_t p; uint8_t s; uint8_t t; uint8_t ccc; } ScrCollElem;");
|
||||
lines.push("typedef struct { uint8_t n; ScrCollElem e[3]; } ScrCollEntry;");
|
||||
const maxP = Math.max(...[...elements.values()].flat().map((e) => e[0]));
|
||||
const maxS = Math.max(...[...elements.values()].flat().map((e) => e[1]));
|
||||
const maxT = Math.max(...[...elements.values()].flat().map((e) => e[2]));
|
||||
if (maxP > 0xffff || maxS > 0xff || maxT > 0xff)
|
||||
throw new Error("weights overflow the table types");
|
||||
for (const [lo, hi] of RANGES) {
|
||||
const name = `scr_coll_${lo.toString(16).padStart(4, "0")}`;
|
||||
lines.push(`static const ScrCollEntry ${name}[${hi - lo + 1}] = {`);
|
||||
for (let c = lo; c <= hi; c++) {
|
||||
if (!coveredSet.has(c)) {
|
||||
lines.push(
|
||||
` {255, {{0, 0, 0, 0}}}, /* U+${c.toString(16).toUpperCase().padStart(4, "0")} not covered */`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const es = elements.get(c);
|
||||
if (es.length > 3) throw new Error(`U+${c.toString(16)} needs ${es.length} elements`);
|
||||
const body = es.map((e) => `{${e[0]}, ${e[1]}, ${e[2]}, ${e[3]}}`).join(", ");
|
||||
lines.push(
|
||||
` {${es.length}, {${body}}}, /* U+${c.toString(16).toUpperCase().padStart(4, "0")} */`,
|
||||
);
|
||||
}
|
||||
lines.push("};");
|
||||
}
|
||||
lines.push("static const ScrCollEntry *scr_coll_lookup(uint32_t c) {");
|
||||
for (const [lo, hi] of RANGES) {
|
||||
const name = `scr_coll_${lo.toString(16).padStart(4, "0")}`;
|
||||
lines.push(
|
||||
` if (c >= 0x${lo.toString(16)} && c <= 0x${hi.toString(16)}) return &${name}[c - 0x${lo.toString(16)}];`,
|
||||
);
|
||||
}
|
||||
lines.push(" return 0;");
|
||||
lines.push("}");
|
||||
lines.push("#endif");
|
||||
await writeFile(out, lines.join("\n") + "\n");
|
||||
console.log(`wrote ${out}`);
|
||||
@@ -0,0 +1,27 @@
|
||||
// Errors held in checked-dynamic slots (a caught value, an 'unknown'
|
||||
// binding, a record field) print through console.log/console.error with
|
||||
// Node's error rendering. Stack traces are disabled so both runtimes print
|
||||
// the stackless bracket form.
|
||||
Error.stackTraceLimit = 0;
|
||||
|
||||
try {
|
||||
JSON.parse("");
|
||||
} catch (err) {
|
||||
console.error("failed:", err);
|
||||
console.log(err);
|
||||
console.log("message:", (err as Error).message);
|
||||
}
|
||||
|
||||
const stored: unknown = new RangeError("out of range");
|
||||
console.log(stored);
|
||||
console.log([stored]);
|
||||
|
||||
function fail(code: number): never {
|
||||
throw new TypeError(`bad input ${code}`);
|
||||
}
|
||||
try {
|
||||
fail(7);
|
||||
} catch (e) {
|
||||
console.log({ caught: e });
|
||||
console.error("handled", e);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// String.prototype.localeCompare with no locales or options follows the
|
||||
// root-locale collation: letters compare before case and accents, so
|
||||
// sorting with it interleaves upper and lower case and places accented
|
||||
// letters next to their base letter.
|
||||
const pairs: [string, string][] = [
|
||||
["a", "B"],
|
||||
["B", "a"],
|
||||
["a", "A"],
|
||||
["A", "a"],
|
||||
["ä", "z"],
|
||||
["résumé", "resume"],
|
||||
["resume", "Resume"],
|
||||
["coop", "co-op"],
|
||||
["co-op", "coop"],
|
||||
["file10", "file9"],
|
||||
["Zebra", "apple"],
|
||||
["ß", "ss"],
|
||||
["æ", "ae"],
|
||||
["Œuvre", "oeuvre"],
|
||||
["naïve", "naive"],
|
||||
["Ångström", "Angstrom"],
|
||||
["", "a"],
|
||||
["a", ""],
|
||||
["same", "same"],
|
||||
["ø", "o"],
|
||||
["ł", "m"],
|
||||
["_x", "x"],
|
||||
[" x", "x"],
|
||||
["1", "a"],
|
||||
["Z", "é"],
|
||||
];
|
||||
for (const [a, b] of pairs) console.log(JSON.stringify(a), JSON.stringify(b), a.localeCompare(b));
|
||||
|
||||
const words = ["banana", "Apple", "cherry", "apple", "Éclair", "eclair", "Banana", "zoo", "Zoo", "état", "etat", "über", "uber", "Uber", "ñandu", "nandu"];
|
||||
console.log([...words].sort((x, y) => x.localeCompare(y)).join(","));
|
||||
console.log([...words].sort((x, y) => y.localeCompare(x)).join(","));
|
||||
|
||||
interface Item {
|
||||
name: string;
|
||||
qty: number;
|
||||
}
|
||||
const items: Item[] = [
|
||||
{ name: "widget", qty: 3 },
|
||||
{ name: "Gadget", qty: 1 },
|
||||
{ name: "gizmo", qty: 7 },
|
||||
{ name: "Ärmel", qty: 2 },
|
||||
{ name: "arm", qty: 5 },
|
||||
];
|
||||
items.sort((p, q) => p.name.localeCompare(q.name));
|
||||
for (const item of items) console.log(item.name, item.qty);
|
||||
@@ -0,0 +1,31 @@
|
||||
// @no-node-shims
|
||||
// A CommonJS main module runs synchronously, so the first checkpoint after
|
||||
// it drains process.nextTick callbacks BEFORE the promise reactions its
|
||||
// body queued (an ES-module main's awaited evaluation runs them first).
|
||||
// Every later checkpoint keeps the same tick-then-microtask order.
|
||||
const order = [];
|
||||
setTimeout(() => order.push("timeout"), 0);
|
||||
process.nextTick(() => order.push("tick"));
|
||||
Promise.resolve()
|
||||
.then(() => {
|
||||
order.push("then1");
|
||||
process.nextTick(() => order.push("tick-in-then"));
|
||||
})
|
||||
.then(() => order.push("then2"));
|
||||
queueMicrotask(() => order.push("micro"));
|
||||
(async () => {
|
||||
order.push("async-start");
|
||||
await null;
|
||||
order.push("async-after-await");
|
||||
})();
|
||||
process.nextTick(() => {
|
||||
order.push("tick2");
|
||||
Promise.resolve().then(() => order.push("then-in-tick"));
|
||||
process.nextTick(() => order.push("tick-in-tick"));
|
||||
});
|
||||
order.push("sync");
|
||||
setTimeout(() => {
|
||||
process.nextTick(() => order.push("tick-in-timer"));
|
||||
Promise.resolve().then(() => order.push("then-in-timer"));
|
||||
setTimeout(() => console.log(order.join(",")), 2);
|
||||
}, 5);
|
||||
@@ -0,0 +1,8 @@
|
||||
// Number.parseFloat and Number.parseInt are the global parsers; over
|
||||
// string arguments they compile to the same static parse.
|
||||
const inputs = ["12.5px", " -0", "Infinity", "", "1e3", ".5", "0x1A", "007", "1_000", "-12.9", "abc"];
|
||||
for (const s of inputs) {
|
||||
console.log(JSON.stringify(s), Number.parseFloat(s), Number.parseInt(s, 10), parseFloat(s));
|
||||
}
|
||||
console.log(Number.parseInt("ff", 16), Number.parseInt("0x1A", 16), Number.parseInt("z", 36), Number.parseInt("12", 2));
|
||||
console.log(Object.is(Number.parseFloat("-0"), -0), Number.isNaN(Number.parseInt("", 10)));
|
||||
@@ -0,0 +1,6 @@
|
||||
// process.version and process.versions.node name the pinned Node release
|
||||
// the runtime's behavior is verified against.
|
||||
console.log(process.version, process.versions.node);
|
||||
console.log(process.version === `v${process.versions.node}`);
|
||||
const [major] = process.versions.node.split(".");
|
||||
console.log(Number(major) >= 24);
|
||||
@@ -0,0 +1,28 @@
|
||||
// "x".repeat with a negative or infinite count and an out-of-range typed
|
||||
// array read throw catchable RangeErrors: the program keeps running after
|
||||
// the catch.
|
||||
function attempt(label: string, f: () => unknown): void {
|
||||
try {
|
||||
console.log(label, "ok", f());
|
||||
} catch (e) {
|
||||
if (e instanceof RangeError) console.log(label, "RangeError", e.name, e instanceof Error);
|
||||
else console.log(label, "other");
|
||||
}
|
||||
}
|
||||
|
||||
const neg: number = process.argv.length > 99 ? 1 : -1;
|
||||
attempt("repeat-negative", () => "ab".repeat(neg));
|
||||
attempt("repeat-infinite", () => "ab".repeat(neg / 0 < 0 ? Infinity : 1));
|
||||
attempt("repeat-ok", () => "ab".repeat(3));
|
||||
|
||||
try {
|
||||
"x".repeat(neg);
|
||||
console.log("not reached");
|
||||
} catch (e) {
|
||||
console.log("message:", (e as Error).message);
|
||||
}
|
||||
|
||||
const bytes = new Uint8Array(4);
|
||||
bytes[1] = 9;
|
||||
console.log("in range", bytes[1]);
|
||||
console.log("after", bytes.length);
|
||||
@@ -0,0 +1,62 @@
|
||||
// A class instance posted to a worker (workerData or postMessage) arrives
|
||||
// as a structured clone: a plain object with the instance's own enumerable
|
||||
// data properties. Methods, accessors, private fields and the prototype
|
||||
// chain are not transferred, so instanceof is false in the receiver, and
|
||||
// repeated references inside one message stay one object.
|
||||
import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads";
|
||||
|
||||
class Fixture {
|
||||
kind = "fixture";
|
||||
describe(): string {
|
||||
return `a ${this.kind}`;
|
||||
}
|
||||
}
|
||||
|
||||
class Lamp extends Fixture {
|
||||
name: string;
|
||||
parts: string[];
|
||||
#serial = 7;
|
||||
constructor(name: string, parts: string[]) {
|
||||
super();
|
||||
this.name = name;
|
||||
this.parts = parts;
|
||||
}
|
||||
get label(): string {
|
||||
return this.name.toUpperCase();
|
||||
}
|
||||
serial(): number {
|
||||
return this.#serial;
|
||||
}
|
||||
}
|
||||
|
||||
function inspect(value: unknown): string {
|
||||
const shape = value as { describe?: unknown; label?: unknown; serial?: unknown };
|
||||
return [
|
||||
JSON.stringify(value),
|
||||
value instanceof Lamp,
|
||||
value instanceof Fixture,
|
||||
typeof shape.describe,
|
||||
typeof shape.label,
|
||||
typeof shape.serial,
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
if (isMainThread) {
|
||||
const lamp = new Lamp("desk", ["bulb", "shade"]);
|
||||
const worker = new Worker(new URL(import.meta.url), { workerData: lamp });
|
||||
worker.on("message", (message: unknown) => {
|
||||
if (typeof message === "string") console.log(message);
|
||||
else console.log("reply", inspect(message));
|
||||
});
|
||||
worker.on("exit", (code: number) => console.log("exit", code, lamp.describe(), lamp.label));
|
||||
worker.postMessage([lamp, lamp]);
|
||||
} else {
|
||||
const port = parentPort!;
|
||||
port.postMessage(`workerData ${inspect(workerData)}`);
|
||||
port.once("message", (message: unknown) => {
|
||||
const pair = message as unknown[];
|
||||
port.postMessage(`pair ${pair.length} ${pair[0] === pair[1]} ${inspect(pair[0])}`);
|
||||
port.postMessage(new Lamp("floor", ["base"]));
|
||||
port.close();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// A function whose signature cannot compile: its body's statements count
|
||||
// toward the total as not static, so the percentage reflects them.
|
||||
function applyAll(f: <T>(x: T) => T, n: number): number {
|
||||
const doubled = n * 2;
|
||||
if (doubled > 4) {
|
||||
return f(doubled);
|
||||
}
|
||||
return doubled;
|
||||
}
|
||||
console.log(typeof applyAll);
|
||||
console.log("static");
|
||||
@@ -0,0 +1,53 @@
|
||||
// Programs that compile but behave differently natively than under Node:
|
||||
// each site below is reported as a divergence warning.
|
||||
interface Labeled {
|
||||
label: string;
|
||||
}
|
||||
interface Tagged {
|
||||
label: string;
|
||||
tag: string;
|
||||
}
|
||||
|
||||
// A narrower record parameter is a field copy: the write is lost.
|
||||
function relabel(target: Labeled): void {
|
||||
target.label = "renamed";
|
||||
}
|
||||
const tagged: Tagged = { label: "first", tag: "t" };
|
||||
relabel(tagged);
|
||||
console.log(tagged.label);
|
||||
|
||||
// A narrower binding is a copy too: identity comparison is false.
|
||||
const view: Labeled = tagged;
|
||||
console.log(view === tagged);
|
||||
|
||||
// Key order: the literal's order differs from the declaration and the
|
||||
// program observes it.
|
||||
interface Point {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
const point: Point = { y: 1, x: 2 };
|
||||
console.log(JSON.stringify(point));
|
||||
|
||||
// Text outside the built-in collation compares by code point natively.
|
||||
console.log(["б", "а"].sort((p, q) => p.localeCompare(q)).join(","), "яблоко".localeCompare("груша"));
|
||||
|
||||
// A call feeds an instance to a cast to an unrelated class.
|
||||
class Shape {
|
||||
sides = 0;
|
||||
}
|
||||
class Square extends Shape {
|
||||
size = 2;
|
||||
}
|
||||
class Circle extends Shape {
|
||||
size = 3;
|
||||
}
|
||||
function sizeOf(s: Shape): number {
|
||||
return (s as Square).size;
|
||||
}
|
||||
console.log(sizeOf(new Circle()));
|
||||
|
||||
// The native date parser takes ISO 8601 with an explicit offset.
|
||||
console.log(Date.parse("Jan 2, 2024"), new Date("2024/01/02").getTime(), Date.parse("2024-01-02T03:04:05Z"));
|
||||
const stamps = ["2024-03-04T05:06:07Z", "March 4, 2024"];
|
||||
for (const stamp of stamps) console.log(Date.parse(stamp));
|
||||
@@ -4,7 +4,7 @@ const v: any = 21;
|
||||
const doubled = v * 2;
|
||||
const root = Math.cbrt(81);
|
||||
const up = (19.99).toPrecision(3);
|
||||
const parsed = Number.parseFloat("1.5"); // the global's string form is static now; the Number static keeps the island
|
||||
const parsed = Number.parseFloat("1.5"); // a string argument compiles statically, like the global
|
||||
const raw = __island_eval("6 * 7");
|
||||
const unknownScore: unknown = 81;
|
||||
const flags = unknownScore == 81;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// Ordinary code: identity-preserving calls, declaration-ordered literals,
|
||||
// Latin-script comparisons, related-class casts and ISO dates report no
|
||||
// divergence.
|
||||
interface Item {
|
||||
name: string;
|
||||
qty: number;
|
||||
}
|
||||
function restock(item: Item): void {
|
||||
item.qty += 1;
|
||||
}
|
||||
const items: Item[] = [
|
||||
{ name: "bolt", qty: 1 },
|
||||
{ name: "nut", qty: 2 },
|
||||
];
|
||||
for (const item of items) restock(item);
|
||||
console.log(JSON.stringify(items));
|
||||
|
||||
function describe(item: { name: string }): string {
|
||||
return item.name.toUpperCase();
|
||||
}
|
||||
console.log(describe(items[0]!));
|
||||
|
||||
class Animal {
|
||||
legs = 4;
|
||||
}
|
||||
class Bird extends Animal {
|
||||
wings = 2;
|
||||
}
|
||||
function wingsOf(a: Animal): number {
|
||||
return a instanceof Bird ? (a as Bird).wings : 0;
|
||||
}
|
||||
console.log(wingsOf(new Bird()), wingsOf(new Animal()));
|
||||
console.log(["b", "A", "a"].sort((p, q) => p.localeCompare(q)).join(","));
|
||||
console.log(Date.parse("2024-01-02"), new Date("2024-01-02T03:04:05+01:00").getTime());
|
||||
@@ -0,0 +1,9 @@
|
||||
// parseFloat over a string and Number.parseInt over a string with a radix
|
||||
// lower statically; other argument types coerce, which only the embedded
|
||||
// engine runs.
|
||||
const text: string = process.argv.length > 99 ? "1.5" : "2.25";
|
||||
const flag: boolean = process.argv.length > 99;
|
||||
const count: number = process.argv.length;
|
||||
console.log(parseFloat(text), Number.parseFloat(text), Number.parseInt("ff", 16));
|
||||
console.log(parseFloat(flag as unknown as string));
|
||||
console.log(Number.parseInt(count as unknown as string, 10));
|
||||
@@ -0,0 +1,6 @@
|
||||
// Valid under this project's own tsc (its "lib" is esnext): the errors
|
||||
// come from scriptc's fixed type world and say so.
|
||||
const bytes = Uint8Array.fromBase64("aGk=");
|
||||
console.log(bytes.length);
|
||||
const runtime = typeof Deno === "undefined" ? "node" : "other";
|
||||
console.log(runtime);
|
||||
@@ -0,0 +1 @@
|
||||
{ "compilerOptions": { "strict": true, "lib": ["esnext"], "types": [] } }
|
||||
@@ -0,0 +1,10 @@
|
||||
// An exported function nothing on the entry path calls: a statement in it
|
||||
// that can only fence at runtime still reports its diagnostic, so no
|
||||
// failed statement goes unexplained.
|
||||
export function collect(values, flag) {
|
||||
var out = { items: [] };
|
||||
var x;
|
||||
out.items.push((x = +flag, x * 0 === 0) ? x : flag, !!flag);
|
||||
return out;
|
||||
}
|
||||
console.log("loaded");
|
||||
@@ -3,9 +3,10 @@
|
||||
// engine: in a static build every use site is its own SC2012 naming the
|
||||
// flag — never an ICE, never a link error. Static Math methods include
|
||||
// the scalar functions and variadic Math.hypot. The .split(string),
|
||||
// trim/pad variants, parseInt, isNaN, and the global parseFloat/isFinite
|
||||
// over exactly-typed arguments and string-pattern replacement compile statically now and no longer
|
||||
// appear here.)
|
||||
// trim/pad variants, parseInt, isNaN, the global parseFloat/isFinite and
|
||||
// Number.parseFloat/Number.parseInt over exactly-typed arguments, and
|
||||
// string-pattern replacement compile statically now and no longer appear
|
||||
// here.)
|
||||
const up = Math.cbrt(2);
|
||||
const tau = Math.PI * 2;
|
||||
const price = (19.99).toPrecision(4);
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
scriptc coverage tests/coverage-fixtures/divergences.ts
|
||||
|
||||
statements analyzed 18
|
||||
compile statically 18 (100%)
|
||||
|
||||
fully static — this program has no dynamic remainder.
|
||||
|
||||
differs from Node 8 sites (compiles; the native program can behave differently)
|
||||
×1 SC6001 passing a 'Tagged' where 'Labeled' is expected copies the object (a narrower record shape compiles as a field copy): the write to 'target.label' updates only the copy, while under Node it updates the…
|
||||
at tests/coverage-fixtures/divergences.ts:16:9
|
||||
hint: declare the parameter or binding with the value's full type ('Tagged'), or make the shape a class — class instances keep their identity when passed as a base class
|
||||
×1 SC6001 storing a 'Tagged' in a 'Labeled' binding copies the object (a narrower record shape compiles as a field copy): 'view === tagged' compares the copy, which is never the original object, while under No…
|
||||
at tests/coverage-fixtures/divergences.ts:20:23
|
||||
hint: declare the parameter or binding with the value's full type ('Tagged'), or make the shape a class — class instances keep their identity when passed as a base class
|
||||
×1 SC6002 this object's keys enumerate as 'x, y' natively (the declaration order of 'Point') but as 'y, x' under Node (insertion order), and the program observes the order (JSON.stringify, Object.keys, for-in …
|
||||
at tests/coverage-fixtures/divergences.ts:29:22
|
||||
hint: write the properties in the declaration order of 'Point' (or reorder the declaration to match how objects are built)
|
||||
×1 SC6003 localeCompare over 'яблоко' compares characters outside the built-in collation (Latin letters, digits, punctuation and symbols) by code point natively, while Node applies ICU's full collation
|
||||
at tests/coverage-fixtures/divergences.ts:33:70
|
||||
hint: results agree with Node for Latin-script text; for other scripts compare explicitly (for example by code point with < and >) so both runtimes agree
|
||||
×1 SC6004 this 'Circle' reaches a cast to the unrelated class 'Square' in the callee: natively the checked cast throws a TypeError when it executes, while Node reads the members structurally
|
||||
at tests/coverage-fixtures/divergences.ts:48:20
|
||||
hint: narrow with 'instanceof Square' before casting, or cast to a base class or interface both classes share
|
||||
×1 SC6005 the date string 'Jan 2, 2024' parses to an invalid date (NaN) natively, while Node accepts it (the native parser takes ISO 8601 date-times with an explicit offset, such as '2024-01-02T03:04:05Z', and…
|
||||
at tests/coverage-fixtures/divergences.ts:51:24
|
||||
hint: write the date in ISO 8601 with an explicit offset ('Z' or '+HH:MM'), or build it from numbers with Date.UTC
|
||||
×1 SC6005 the date string '2024/01/02' parses to an invalid date (NaN) natively, while Node accepts it (the native parser takes ISO 8601 date-times with an explicit offset, such as '2024-01-02T03:04:05Z', and …
|
||||
at tests/coverage-fixtures/divergences.ts:51:49
|
||||
hint: write the date in ISO 8601 with an explicit offset ('Z' or '+HH:MM'), or build it from numbers with Date.UTC
|
||||
×1 SC6005 the date string 'March 4, 2024' parses to an invalid date (NaN) natively, while Node accepts it (the native parser takes ISO 8601 date-times with an explicit offset, such as '2024-01-02T03:04:05Z', a…
|
||||
at tests/coverage-fixtures/divergences.ts:52:41
|
||||
hint: write the date in ISO 8601 with an explicit offset ('Z' or '+HH:MM'), or build it from numbers with Date.UTC
|
||||
@@ -1,7 +1,7 @@
|
||||
scriptc coverage tests/coverage-fixtures/dynamic-mix.ts
|
||||
|
||||
statements analyzed 9
|
||||
compile statically 5 (55%)
|
||||
compile dynamically 4 (44%) (island sites — the embedded engine runs them)
|
||||
compile statically 6 (66%)
|
||||
compile dynamically 3 (33%) (island sites — the embedded engine runs them)
|
||||
|
||||
builds with --dynamic — no remaining blockers (the island sites above run in the embedded engine).
|
||||
@@ -1,9 +1,12 @@
|
||||
scriptc coverage tests/coverage-fixtures/dynamic-mix.ts
|
||||
|
||||
statements analyzed 9
|
||||
compile statically 6 (66%)
|
||||
compile statically 7 (77%)
|
||||
|
||||
runs with --dynamic 3 sites (embeds a JS engine, ~620KB — static stays the default)
|
||||
×1 '__island_eval' requires the embedded dynamic engine, which this build does not include SC2010
|
||||
×1 '.toPrecision()' on numbers runs in the embedded dynamic engine, which this build does not include SC2012
|
||||
×1 'Number.parseFloat' runs in the embedded dynamic engine, which this build does not include SC2012
|
||||
runs with --dynamic 2 sites (embeds a JS engine, ~620KB — static stays the default)
|
||||
×1 SC2010 '__island_eval' requires the embedded dynamic engine, which this build does not include
|
||||
at tests/coverage-fixtures/dynamic-mix.ts:8:13
|
||||
hint: build with --dynamic to embed the engine (adds ~620KB to the binary); static builds never include it
|
||||
×1 SC2012 '.toPrecision()' on numbers runs in the embedded dynamic engine, which this build does not include
|
||||
at tests/coverage-fixtures/dynamic-mix.ts:6:12
|
||||
hint: build with --dynamic to run this call in the embedded engine (adds ~620KB to the binary); static builds never include it
|
||||
|
||||
@@ -4,4 +4,6 @@ scriptc coverage tests/fixtures/node-types/import-fences.ts
|
||||
compile statically 3 (75%)
|
||||
|
||||
blockers:
|
||||
×2 the 'v8' module (it observes V8 engine internals — heap statistics and snapshots, GC and CPU profiles, serialize/deserialize — and a compiled binary embeds no V8 engine to observe) SC1010
|
||||
×2 SC1010 the 'v8' module (it observes V8 engine internals — heap statistics and snapshots, GC and CPU profiles, serialize/deserialize — and a compiled binary embeds no V8 engine to observe)
|
||||
at tests/fixtures/node-types/import-fences.ts:7:1, tests/fixtures/node-types/import-fences.ts:12:1
|
||||
hint: relative imports (./file, ../dir/file), package.json-mediated project imports (#alias via the imports field, self-name references via exports), installed npm packages (their code runs under --dynamic), 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
|
||||
@@ -4,4 +4,6 @@ scriptc coverage tests/coverage-fixtures/js-builtin-callable-alias.js
|
||||
compile statically 2 (66%)
|
||||
|
||||
deferred to runtime 1 site (JS statements that throw their fence if executed)
|
||||
×1 a builtin function alias as an escaping value (call 'ext' directly) SC1090
|
||||
×1 SC1090 a builtin function alias as an escaping value (call 'ext' directly)
|
||||
at tests/coverage-fixtures/js-builtin-callable-alias.js:8:13
|
||||
hint: this form is valid TypeScript but does not compile yet — rewrite it with a supported equivalent of the construct the message names (https://scriptc.dev/limitations lists the boundary)
|
||||
|
||||
@@ -4,4 +4,6 @@ scriptc coverage tests/coverage-fixtures/mixed.ts
|
||||
compile statically 12 (92%)
|
||||
|
||||
blockers:
|
||||
×1 object destructuring of non-record values (the source is array-typed) SC1031
|
||||
×1 SC1031 object destructuring of non-record values (the source is array-typed)
|
||||
at tests/coverage-fixtures/mixed.ts:13:7
|
||||
hint: this construct is valid TypeScript outside the compiled subset — rewrite it with a supported equivalent (https://scriptc.dev/limitations)
|
||||
@@ -4,7 +4,15 @@ scriptc coverage tests/diagnostics/npm-imports/main.ts
|
||||
compile statically 0 (0%)
|
||||
|
||||
runs with --dynamic 9 sites (embeds a JS engine, ~620KB — static stays the default)
|
||||
×6 values from the 'mathkit' package run in the embedded dynamic engine, which this build does not include SC2013
|
||||
×1 importing 'mathkit' requires the embedded dynamic engine, which this build does not include — the package's implementation runs there SC2013
|
||||
×1 importing '@acme/units' requires the embedded dynamic engine, which this build does not include — the package's implementation runs there SC2013
|
||||
×1 values from the '@acme/units' package run in the embedded dynamic engine, which this build does not include SC2013
|
||||
×6 SC2013 values from the 'mathkit' package run in the embedded dynamic engine, which this build does not include
|
||||
at tests/diagnostics/npm-imports/main.ts:9:7, tests/diagnostics/npm-imports/main.ts:10:13, tests/diagnostics/npm-imports/main.ts:12:7 (+3 more)
|
||||
hint: build with --dynamic to run npm package code in the embedded engine (adds ~620KB to the binary), or try --npm-static auto to compile eligible packages' JavaScript statically; static builds never include the engine
|
||||
×1 SC2013 importing 'mathkit' requires the embedded dynamic engine, which this build does not include — the package's implementation runs there
|
||||
at tests/diagnostics/npm-imports/main.ts:5:1
|
||||
hint: build with --dynamic to run npm package code in the embedded engine (adds ~620KB to the binary), or try --npm-static auto to compile eligible packages' JavaScript statically; static builds never include the engine
|
||||
×1 SC2013 importing '@acme/units' requires the embedded dynamic engine, which this build does not include — the package's implementation runs there
|
||||
at tests/diagnostics/npm-imports/main.ts:6:1
|
||||
hint: build with --dynamic to run npm package code in the embedded engine (adds ~620KB to the binary), or try --npm-static auto to compile eligible packages' JavaScript statically; static builds never include the engine
|
||||
×1 SC2013 values from the '@acme/units' package run in the embedded dynamic engine, which this build does not include
|
||||
at tests/diagnostics/npm-imports/main.ts:14:13
|
||||
hint: build with --dynamic to run npm package code in the embedded engine (adds ~620KB to the binary), or try --npm-static auto to compile eligible packages' JavaScript statically; static builds never include the engine
|
||||
|
||||
@@ -4,6 +4,12 @@ scriptc coverage tests/coverage-fixtures/unreached.ts
|
||||
compile statically 6 (66%)
|
||||
|
||||
in unreached code (never lowered — cannot fail a build)
|
||||
×1 for-in over class instances (which keys exist on an instance depends on runtime property creation the class model does not track — for-in over records and arrays compiles) SC1052
|
||||
×1 'break outer' targeting this statement form (the labeled statement lowers through a desugar that has no label point) SC1050
|
||||
×1 'yield*' in async generators (use 'for await' and yield each value explicitly) SC1071
|
||||
×1 SC1052 for-in over class instances (which keys exist on an instance depends on runtime property creation the class model does not track — for-in over records and arrays compiles)
|
||||
at tests/coverage-fixtures/unreached.ts:15:19
|
||||
hint: this construct is valid TypeScript outside the compiled subset — rewrite it with a supported equivalent (https://scriptc.dev/limitations)
|
||||
×1 SC1050 'break outer' targeting this statement form (the labeled statement lowers through a desugar that has no label point)
|
||||
at tests/coverage-fixtures/unreached.ts:19:16
|
||||
hint: this construct is valid TypeScript outside the compiled subset — rewrite it with a supported equivalent (https://scriptc.dev/limitations)
|
||||
×1 SC1071 'yield*' in async generators (use 'for await' and yield each value explicitly)
|
||||
at tests/coverage-fixtures/unreached.ts:27:3
|
||||
hint: this construct is valid TypeScript outside the compiled subset — rewrite it with a supported equivalent (https://scriptc.dev/limitations)
|
||||
@@ -1,17 +1,8 @@
|
||||
dynamic-surface.ts:11:15 - error SC2012: '.toPrecision()' on numbers runs in the embedded dynamic engine, which this build does not include
|
||||
dynamic-surface.ts:12:15 - error SC2012: '.toPrecision()' on numbers runs in the embedded dynamic engine, which this build does not include
|
||||
|
||||
10 | const tau = Math.PI * 2;
|
||||
11 | const price = (19.99).toPrecision(4);
|
||||
11 | const tau = Math.PI * 2;
|
||||
12 | const price = (19.99).toPrecision(4);
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~
|
||||
12 | const n = Number.parseFloat("3.14");
|
||||
|
||||
hint: build with --dynamic to run this call in the embedded engine (adds ~620KB to the binary); static builds never include it
|
||||
|
||||
dynamic-surface.ts:12:11 - error SC2012: 'Number.parseFloat' runs in the embedded dynamic engine, which this build does not include
|
||||
|
||||
11 | const price = (19.99).toPrecision(4);
|
||||
12 | const n = Number.parseFloat("3.14");
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
13 | void n;
|
||||
13 | const n = Number.parseFloat("3.14");
|
||||
|
||||
hint: build with --dynamic to run this call in the embedded engine (adds ~620KB to the binary); static builds never include it
|
||||
@@ -5,7 +5,7 @@ npm-imports/main.ts:5:1 - error SC2013: importing 'mathkit' requires the embedde
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
6 | import { convert } from "@acme/units";
|
||||
|
||||
hint: build with --dynamic to run npm package code in the embedded engine (adds ~620KB to the binary); static builds never include it
|
||||
hint: build with --dynamic to run npm package code in the embedded engine (adds ~620KB to the binary), or try --npm-static auto to compile eligible packages' JavaScript statically; static builds never include the engine
|
||||
|
||||
npm-imports/main.ts:6:1 - error SC2013: importing '@acme/units' requires the embedded dynamic engine, which this build does not include — the package's implementation runs there
|
||||
|
||||
@@ -14,7 +14,7 @@ npm-imports/main.ts:6:1 - error SC2013: importing '@acme/units' requires the emb
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
7 | import type { Vector } from "mathkit";
|
||||
|
||||
hint: build with --dynamic to run npm package code in the embedded engine (adds ~620KB to the binary); static builds never include it
|
||||
hint: build with --dynamic to run npm package code in the embedded engine (adds ~620KB to the binary), or try --npm-static auto to compile eligible packages' JavaScript statically; static builds never include the engine
|
||||
|
||||
npm-imports/main.ts:9:7 - error SC2013: values from the 'mathkit' package run in the embedded dynamic engine, which this build does not include
|
||||
|
||||
@@ -23,7 +23,7 @@ npm-imports/main.ts:9:7 - error SC2013: values from the 'mathkit' package run in
|
||||
| ^~~~~
|
||||
10 | console.log(point.x);
|
||||
|
||||
hint: build with --dynamic to run npm package code in the embedded engine (adds ~620KB to the binary); static builds never include it
|
||||
hint: build with --dynamic to run npm package code in the embedded engine (adds ~620KB to the binary), or try --npm-static auto to compile eligible packages' JavaScript statically; static builds never include the engine
|
||||
|
||||
npm-imports/main.ts:10:13 - error SC2013: values from the 'mathkit' package run in the embedded dynamic engine, which this build does not include
|
||||
|
||||
@@ -32,7 +32,7 @@ npm-imports/main.ts:10:13 - error SC2013: values from the 'mathkit' package run
|
||||
| ^~~~~~~
|
||||
11 |
|
||||
|
||||
hint: build with --dynamic to run npm package code in the embedded engine (adds ~620KB to the binary); static builds never include it
|
||||
hint: build with --dynamic to run npm package code in the embedded engine (adds ~620KB to the binary), or try --npm-static auto to compile eligible packages' JavaScript statically; static builds never include the engine
|
||||
|
||||
npm-imports/main.ts:12:7 - error SC2013: values from the 'mathkit' package run in the embedded dynamic engine, which this build does not include
|
||||
|
||||
@@ -41,7 +41,7 @@ npm-imports/main.ts:12:7 - error SC2013: values from the 'mathkit' package run i
|
||||
| ^~~~
|
||||
13 | console.log(calc.value());
|
||||
|
||||
hint: build with --dynamic to run npm package code in the embedded engine (adds ~620KB to the binary); static builds never include it
|
||||
hint: build with --dynamic to run npm package code in the embedded engine (adds ~620KB to the binary), or try --npm-static auto to compile eligible packages' JavaScript statically; static builds never include the engine
|
||||
|
||||
npm-imports/main.ts:12:14 - error SC2013: values from the 'mathkit' package run in the embedded dynamic engine, which this build does not include
|
||||
|
||||
@@ -50,7 +50,7 @@ npm-imports/main.ts:12:14 - error SC2013: values from the 'mathkit' package run
|
||||
| ^~~~~~~~~~~
|
||||
13 | console.log(calc.value());
|
||||
|
||||
hint: build with --dynamic to run npm package code in the embedded engine (adds ~620KB to the binary); static builds never include it
|
||||
hint: build with --dynamic to run npm package code in the embedded engine (adds ~620KB to the binary), or try --npm-static auto to compile eligible packages' JavaScript statically; static builds never include the engine
|
||||
|
||||
npm-imports/main.ts:13:13 - error SC2013: values from the 'mathkit' package run in the embedded dynamic engine, which this build does not include
|
||||
|
||||
@@ -59,7 +59,7 @@ npm-imports/main.ts:13:13 - error SC2013: values from the 'mathkit' package run
|
||||
| ^~~~~~~~~~
|
||||
14 | console.log(convert(3, "km"));
|
||||
|
||||
hint: build with --dynamic to run npm package code in the embedded engine (adds ~620KB to the binary); static builds never include it
|
||||
hint: build with --dynamic to run npm package code in the embedded engine (adds ~620KB to the binary), or try --npm-static auto to compile eligible packages' JavaScript statically; static builds never include the engine
|
||||
|
||||
npm-imports/main.ts:14:13 - error SC2013: values from the '@acme/units' package run in the embedded dynamic engine, which this build does not include
|
||||
|
||||
@@ -68,7 +68,7 @@ npm-imports/main.ts:14:13 - error SC2013: values from the '@acme/units' package
|
||||
| ^~~~~~~
|
||||
15 | console.log(origin.x);
|
||||
|
||||
hint: build with --dynamic to run npm package code in the embedded engine (adds ~620KB to the binary); static builds never include it
|
||||
hint: build with --dynamic to run npm package code in the embedded engine (adds ~620KB to the binary), or try --npm-static auto to compile eligible packages' JavaScript statically; static builds never include the engine
|
||||
|
||||
npm-imports/main.ts:15:13 - error SC2013: values from the 'mathkit' package run in the embedded dynamic engine, which this build does not include
|
||||
|
||||
@@ -77,4 +77,4 @@ npm-imports/main.ts:15:13 - error SC2013: values from the 'mathkit' package run
|
||||
| ^~~~~~~~
|
||||
16 |
|
||||
|
||||
hint: build with --dynamic to run npm package code in the embedded engine (adds ~620KB to the binary); static builds never include it
|
||||
hint: build with --dynamic to run npm package code in the embedded engine (adds ~620KB to the binary), or try --npm-static auto to compile eligible packages' JavaScript statically; static builds never include the engine
|
||||
@@ -1,8 +1,9 @@
|
||||
import { execFileSync, spawnSync } from "node:child_process";
|
||||
import { globSync, readFileSync } from "node:fs";
|
||||
import { globSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { analyze, renderCoverage } from "@scriptc/compiler";
|
||||
import { analyze, coverageEnvelope, coveragePasses, renderCoverage } from "@scriptc/compiler";
|
||||
import { shardSelect, shardSuffix } from "./shard.js";
|
||||
|
||||
const repoRoot = join(import.meta.dirname, "../..");
|
||||
@@ -10,7 +11,10 @@ const fixture = (name: string) => join(repoRoot, "tests/coverage-fixtures", name
|
||||
|
||||
function report(path: string, opts: { dynamic?: boolean } = {}): string {
|
||||
const { coverage } = analyze(path, opts);
|
||||
return renderCoverage({ ...coverage, file: coverage.file.replace(repoRoot + "/", "") });
|
||||
return renderCoverage(
|
||||
{ ...coverage, file: coverage.file.replace(repoRoot + "/", "") },
|
||||
{ root: repoRoot },
|
||||
);
|
||||
}
|
||||
|
||||
test("mixed program: percentage and grouped blockers", async () => {
|
||||
@@ -44,6 +48,190 @@ test("unreached blockers report in their own group", async () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("divergences report as warnings, never as blockers", async () => {
|
||||
const { coverage } = analyze(fixture("divergences.ts"));
|
||||
expect(coverage.diagnostics).toEqual([]);
|
||||
expect((coverage.divergences ?? []).map((d) => d.code)).toEqual([
|
||||
"SC6001",
|
||||
"SC6001",
|
||||
"SC6002",
|
||||
"SC6003",
|
||||
"SC6004",
|
||||
"SC6005",
|
||||
"SC6005",
|
||||
"SC6005",
|
||||
]);
|
||||
expect(coveragePasses(coverage, "blockers")).toBe(true);
|
||||
expect(coveragePasses(coverage, "divergences")).toBe(false);
|
||||
await expect(report(fixture("divergences.ts"))).toMatchFileSnapshot(
|
||||
"__snapshots__/coverage-divergences.txt",
|
||||
);
|
||||
});
|
||||
|
||||
test("ordinary code reports no divergence", () => {
|
||||
const { coverage } = analyze(fixture("no-divergences.ts"));
|
||||
expect(coverage.diagnostics).toEqual([]);
|
||||
expect(coverage.divergences ?? []).toEqual([]);
|
||||
});
|
||||
|
||||
test("statements of a signature-blocked function count as not static", () => {
|
||||
const { coverage } = analyze(fixture("blocked-signature.ts"));
|
||||
const un = coverage.unreached?.stats;
|
||||
expect(coverage.stats.functionsSkipped + (un?.functionsSkipped ?? 0)).toBe(1);
|
||||
// Two top-level statements plus the four the blocked body holds.
|
||||
expect(coverage.stats.statementsTotal + (un?.statementsTotal ?? 0)).toBe(6);
|
||||
expect(coverage.stats.statementsFailed + (un?.statementsFailed ?? 0)).toBe(5);
|
||||
});
|
||||
|
||||
test("a failed unreached JavaScript statement is always explained", () => {
|
||||
const { coverage } = analyze(fixture("unreached-js-fence.js"));
|
||||
const failed =
|
||||
coverage.stats.statementsFailed + (coverage.unreached?.stats.statementsFailed ?? 0);
|
||||
expect(failed).toBe(1);
|
||||
const explained = [
|
||||
...coverage.diagnostics,
|
||||
...(coverage.runtimeFences ?? []),
|
||||
...(coverage.unreached?.diagnostics ?? []),
|
||||
...(coverage.unreached?.runtimeFences ?? []),
|
||||
];
|
||||
expect(explained.map((d) => d.code)).toEqual(["SC1090"]);
|
||||
expect(report(fixture("unreached-js-fence.js"))).toContain(
|
||||
"at tests/coverage-fixtures/unreached-js-fence.js:7:",
|
||||
);
|
||||
});
|
||||
|
||||
test("blocker groups carry a location and a hint", () => {
|
||||
const out = report(fixture("mixed.ts"));
|
||||
expect(out).toMatch(/at tests\/coverage-fixtures\/mixed\.ts:\d+:\d+/);
|
||||
expect(out).toContain("hint: ");
|
||||
});
|
||||
|
||||
test("TypeScript errors caused by scriptc's type world say so", () => {
|
||||
const { coverage, sourceTexts } = analyze(fixture("type-world/main.ts"));
|
||||
expect(coverage.preflightFailed).toBe(true);
|
||||
const world = coverage.diagnostics.filter((d) => d.code === "SC0001" && d.typeWorld === true);
|
||||
expect(world.map((d) => d.hint)).toEqual([
|
||||
expect.stringContaining('ES2025 library regardless of the project\'s "lib"'),
|
||||
expect.stringContaining("'Deno' is another runtime's global"),
|
||||
]);
|
||||
const out = renderCoverage(coverage, { sourceTexts });
|
||||
expect(out).toContain("come from scriptc's type world");
|
||||
const envelope = coverageEnvelope(coverage, { compilerVersion: "test", sourceTexts });
|
||||
expect(envelope.phase).toBe("preflight");
|
||||
expect(envelope.diagnostics.every((d) => d.category === "environment")).toBe(true);
|
||||
});
|
||||
|
||||
test("coercing parseFloat and Number.parse* calls name their argument condition", () => {
|
||||
const { coverage } = analyze(fixture("parse-coercions.ts"));
|
||||
expect(coverage.diagnostics.map((d) => [d.code, d.message])).toEqual([
|
||||
[
|
||||
"SC2012",
|
||||
"'parseFloat' with a 'boolean' argument (only a string argument compiles statically) runs in the embedded dynamic engine, which this build does not include",
|
||||
],
|
||||
[
|
||||
"SC2012",
|
||||
"'Number.parseInt' with a 'number' argument (only a string argument compiles statically) runs in the embedded dynamic engine, which this build does not include",
|
||||
],
|
||||
]);
|
||||
for (const d of coverage.diagnostics) expect(d.hint).toContain("convert the argument explicitly");
|
||||
});
|
||||
|
||||
test("the diagnostics envelope is versioned and located", () => {
|
||||
const { coverage, sourceTexts } = analyze(fixture("mixed.ts"));
|
||||
const envelope = coverageEnvelope(coverage, { compilerVersion: "test", sourceTexts });
|
||||
expect(envelope).toMatchObject({
|
||||
schema: "scriptc-diagnostics",
|
||||
schemaVersion: 1,
|
||||
command: "coverage",
|
||||
success: false,
|
||||
phase: "compile",
|
||||
stats: { statementsTotal: 13, statementsStatic: 12, statementsFailed: 1, percentStatic: 92 },
|
||||
});
|
||||
expect(envelope.groups).toHaveLength(1);
|
||||
expect(envelope.diagnostics[0]).toMatchObject({
|
||||
code: "SC1031",
|
||||
category: "unsupported",
|
||||
severity: "error",
|
||||
scope: "reached",
|
||||
line: 13,
|
||||
column: 7,
|
||||
group: envelope.groups[0]!.id,
|
||||
});
|
||||
expect(envelope.diagnostics[0]!.hint.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("CLI: --print=diagnostics and --fail-on set the output and exit status", () => {
|
||||
const scriptcCli = join(repoRoot, "packages/cli/src/main.ts");
|
||||
const run = (...args: string[]) =>
|
||||
spawnSync(process.execPath, ["--import", "tsx", scriptcCli, "coverage", ...args], {
|
||||
cwd: repoRoot,
|
||||
encoding: "utf8",
|
||||
env: { ...process.env, SCRIPTC_NO_CACHE: "1" },
|
||||
});
|
||||
const plain = run(fixture("mixed.ts"));
|
||||
expect(plain.status).toBe(0);
|
||||
const gated = run(fixture("mixed.ts"), "--fail-on=blockers");
|
||||
expect(gated.status).toBe(1);
|
||||
const json = run(fixture("mixed.ts"), "--print=diagnostics", "--fail-on=blockers");
|
||||
expect(json.status).toBe(1);
|
||||
const envelope = JSON.parse(json.stdout) as { schemaVersion: number; success: boolean };
|
||||
expect(envelope).toMatchObject({ schemaVersion: 1, success: false });
|
||||
expect(run(fixture("divergences.ts"), "--fail-on=blockers").status).toBe(0);
|
||||
expect(run(fixture("divergences.ts"), "--fail-on=divergences").status).toBe(1);
|
||||
// Unreached blockers cannot fail a build, so they never fail the gate.
|
||||
expect(run(fixture("unreached.ts"), "--fail-on=blockers").status).toBe(0);
|
||||
const bad = run(fixture("mixed.ts"), "--fail-on=everything");
|
||||
expect(bad.status).toBe(1);
|
||||
expect(bad.stderr).toContain('unknown --fail-on value "everything"');
|
||||
});
|
||||
|
||||
test("CLI: build --print=diagnostics reports failures and divergence warnings as JSON", () => {
|
||||
const scriptcCli = join(repoRoot, "packages/cli/src/main.ts");
|
||||
const outDir = mkdtempSync(join(tmpdir(), "scriptc-build-envelope-"));
|
||||
try {
|
||||
const build = (file: string, ...args: string[]) =>
|
||||
spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
"--import",
|
||||
"tsx",
|
||||
scriptcCli,
|
||||
"build",
|
||||
file,
|
||||
"--emit=ir",
|
||||
"-o",
|
||||
join(outDir, "out.ir.json"),
|
||||
...args,
|
||||
],
|
||||
{ cwd: repoRoot, encoding: "utf8", env: { ...process.env, SCRIPTC_NO_CACHE: "1" } },
|
||||
);
|
||||
const failed = build(fixture("mixed.ts"), "--print=diagnostics");
|
||||
expect(failed.status).toBe(1);
|
||||
expect(JSON.parse(failed.stdout)).toMatchObject({
|
||||
command: "build",
|
||||
success: false,
|
||||
phase: "compile",
|
||||
diagnostics: [{ code: "SC1031", line: 13, column: 7 }],
|
||||
});
|
||||
const warned = build(fixture("divergences.ts"), "--print=diagnostics");
|
||||
expect(warned.status).toBe(0);
|
||||
const envelope = JSON.parse(warned.stdout) as {
|
||||
success: boolean;
|
||||
artifact: string;
|
||||
diagnostics: { code: string; severity: string }[];
|
||||
};
|
||||
expect(envelope.success).toBe(true);
|
||||
expect(envelope.artifact).toBe(join(outDir, "out.ir.json"));
|
||||
expect(envelope.diagnostics.map((d) => d.severity)).toEqual(Array(8).fill("warning"));
|
||||
const human = build(fixture("divergences.ts"));
|
||||
expect(human.status).toBe(0);
|
||||
expect(human.stderr).toContain("warning SC6001");
|
||||
expect(human.stderr).toContain("8 warnings: the program compiled");
|
||||
} finally {
|
||||
rmSync(outDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("fully static program reports 100%", () => {
|
||||
const out = report(join(repoRoot, "tests/corpus/400-fib.ts"));
|
||||
expect(out).toContain("(100%)");
|
||||
@@ -430,6 +618,9 @@ test(`every corpus program is 100% static (corpus and coverage agree${shardSuffi
|
||||
const deferred = /^\/\/ @deferred-fences:\s*(\d+)\s*$/.exec(firstLine);
|
||||
const { coverage } = analyze(file, { dynamic: /^\/\/ @dynamic\s*$/.test(firstLine) });
|
||||
expect.soft(coverage.diagnostics, file).toEqual([]);
|
||||
// Corpus programs match Node by definition: a divergence warning on one
|
||||
// is a false positive.
|
||||
expect.soft(coverage.divergences ?? [], file).toEqual([]);
|
||||
expect.soft(coverage.stats.statementsFailed, file).toBe(deferred ? Number(deferred[1]) : 0);
|
||||
if (++n % 10 === 0) await new Promise((r) => setImmediate(r));
|
||||
}
|
||||
|
||||
@@ -146,6 +146,49 @@ throw e;
|
||||
expect(nameless.stderr).toBe("Uncaught just the message\n");
|
||||
});
|
||||
|
||||
test("an uncaught checked-dynamic primitive prints its value, like Node", async () => {
|
||||
// Node prints the thrown primitive itself (after its source excerpt);
|
||||
// the value must never be lost behind a placeholder.
|
||||
const text = await compileAndRun(
|
||||
"uncaught-unknown-string",
|
||||
`console.log("before");
|
||||
const v: unknown = process.argv.length > 99 ? 0 : "plain";
|
||||
throw v;
|
||||
`,
|
||||
);
|
||||
expect(text.exitCode).toBe(1);
|
||||
expect(text.stdout).toBe("before\n");
|
||||
expect(text.stderr).toBe("Uncaught plain\n");
|
||||
|
||||
const num = await compileAndRun(
|
||||
"uncaught-unknown-number",
|
||||
`const v: unknown = process.argv.length > 99 ? "x" : 42;
|
||||
throw v;
|
||||
`,
|
||||
);
|
||||
expect(num.exitCode).toBe(1);
|
||||
expect(num.stderr).toBe("Uncaught 42\n");
|
||||
});
|
||||
|
||||
test("an out-of-range typed array read throws a catchable RangeError", async () => {
|
||||
// Node answers undefined; a typed numeric read cannot, so the native
|
||||
// program throws a catchable RangeError instead of aborting.
|
||||
const r = await compileAndRun(
|
||||
"typed-read-out-of-range",
|
||||
`const t = new Uint8Array(4);
|
||||
const i: number = process.argv.length + 10;
|
||||
try {
|
||||
console.log(t[i]);
|
||||
} catch (e) {
|
||||
console.log(e instanceof RangeError, (e as Error).message);
|
||||
}
|
||||
console.log("after", t.length);
|
||||
`,
|
||||
);
|
||||
expect(r.exitCode).toBe(0);
|
||||
expect(r.stdout).toMatch(/^true typed array index \d+ out of bounds \(length 4\)\nafter 4\n$/);
|
||||
});
|
||||
|
||||
test("an unhandled ERROR rejection renders name: message too", async () => {
|
||||
const r = await compileAndRun(
|
||||
"unhandled-error-rejection",
|
||||
|
||||
Reference in New Issue
Block a user