mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Merge main
This commit is contained in:
@@ -8,7 +8,9 @@ node_modules/
|
||||
!tests/fixtures/npm/node_modules/
|
||||
!tests/corpus/*/node_modules/
|
||||
!tests/fixtures/npm-static/node_modules/
|
||||
!tests/coverage-fixtures/wasm/node_modules/
|
||||
!tests/fixtures/npm-static/nested/inner/node_modules/
|
||||
!tests/fixtures/npm-static/node_modules/*/node_modules/
|
||||
!tests/fixtures/npm/store/*/node_modules/
|
||||
!tests/fixtures/npm/cases/*/node_modules/
|
||||
!tests/library-mode/*/node_modules/
|
||||
@@ -32,6 +34,7 @@ output/
|
||||
!tests/fixtures/npm/cases/workspace-copied/members/*/dist/
|
||||
!tests/fixtures/npm/cases/workspace-copied/node_modules/*/dist/
|
||||
!tests/fixtures/provenance/node_modules/*/dist/
|
||||
!tests/fixtures/npm-static/node_modules/*/dist/
|
||||
.scriptc/
|
||||
*.log
|
||||
.DS_Store
|
||||
|
||||
@@ -270,6 +270,7 @@
|
||||
"packages/compiler/src/backend/llvm/**",
|
||||
"packages/compiler/src/backend/loop-polls.test.ts",
|
||||
"packages/compiler/src/backend/may-throw.test.ts",
|
||||
"packages/compiler/src/backend/stack-observations.test.ts",
|
||||
"packages/compiler/src/ir/**",
|
||||
"packages/compiler/src/library/*.test.ts",
|
||||
"packages/compiler/test/**",
|
||||
|
||||
@@ -133,6 +133,9 @@ Verifies and loads the installed precompiled runtime objects and vendored librar
|
||||
<dt><code>--explain-dynamic[=<name>]</code></dt>
|
||||
<dd>Coverage only. After the report, list the functions that already operate on values typed only at runtime and the bindings, fields, functions, classes and modules that unsupported constructs would move to the dynamic tier, each with the diagnostic behind it. With a name such as <code>x</code>, <code>Point</code>, or <code>Point.x</code>, show only the matching units. See <a href="/docs/coverage#dynamic-tier">Dynamic tier</a>.</dd>
|
||||
|
||||
<dt><code>--wasm <file.wasm></code></dt>
|
||||
<dd>Coverage only. Report a WebAssembly module the program loads from a path its sources do not spell out. The report already lists modules whose paths the compiler resolves, modules inlined as base64 literals, and <code>.wasm</code> files shipped by packages that use WebAssembly; a <code>scriptc.wasm</code> array in <code>package.json</code> adds files the same way as this flag. Each module is listed with its size, whether it validates, and the WebAssembly proposals it uses. scriptc does not compile WebAssembly modules to native code yet. Repeatable.</dd>
|
||||
|
||||
<dt><code>--sanitize</code></dt>
|
||||
<dd>Build an executable with AddressSanitizer and a reference-count audit at exit, the same checks scriptc's own test suite uses. Requires a C compiler and is available only for host builds. Not supported with <code>--emit=asm</code> or <code>--emit=obj</code>.</dd>
|
||||
|
||||
|
||||
@@ -120,6 +120,8 @@ title: "Limitations"
|
||||
|
||||
<include>../limitations/builtin-apis/synchronous-iteration.mdx</include>
|
||||
|
||||
<include>../limitations/builtin-apis/iterator-helpers.mdx</include>
|
||||
|
||||
<include>../limitations/builtin-apis/regular-expressions.mdx</include>
|
||||
|
||||
<include>../limitations/builtin-apis/object-freezing.mdx</include>
|
||||
@@ -238,6 +240,10 @@ title: "Limitations"
|
||||
|
||||
<include>../limitations/native-addons.mdx</include>
|
||||
|
||||
## WebAssembly modules
|
||||
|
||||
<include>../limitations/webassembly-modules.mdx</include>
|
||||
|
||||
## WASI target
|
||||
|
||||
<include>../limitations/wasi-target.mdx</include>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
- **Iterator helpers:** `map`, `filter`, `take`, `drop`, `flatMap`, `toArray`, `forEach`, `reduce`, `some`, `every` and `find` compile on generators, on array, Map and Set iterators, and on the objects the helpers return. Helper objects support `.next()`, `for...of`, spreads, `Array.from` and `yield*`, pull their source lazily, and close it (run its `return()`) at the same points as Node: when `take` reaches its count, when a short-circuiting method finds its answer, when a callback throws, and when the loop consuming the helper exits early. `flatMap` callbacks return arrays or generators. Iterator helpers on string iterators, `Iterator.from`, the `Iterator` global, calling `return()` on a helper object in TypeScript, printing helper objects, and helper calls on untyped iterators in JavaScript are unsupported.
|
||||
@@ -1 +1 @@
|
||||
- **Map and Set:** Keys and elements support numbers and strings by value, and records, classes, arrays, functions, symbols, server handles, and unions of these reference types by identity. Map values may also be functions. `unknown` slots use native checked values, comparing supported primitives by value and retaining supported object references. Typed nested collections other than `Map<unknown, unknown>` and `Set<unknown>`, and promises requiring payload adapters, cannot pass into these slots. `Promise<unknown>` retains its native representation. Stored `keys()`, `values()`, and `entries()` iterators compile, retain element identity, and observe live collection mutations until exhausted. They support `.next()`, `for...of`, array spreads, and mapper-less `Array.from`. Detached collection methods and iterator helper methods such as `.map()` and `.take()` are unsupported. The Set composition methods (`union`, `intersection`, `difference`, `symmetricDifference`, `isSubsetOf`, `isSupersetOf`, `isDisjointFrom`) accept a Set of the same element type or a Map keyed by it; other set-like arguments are unsupported.
|
||||
- **Map and Set:** Keys and elements support numbers and strings by value, and records, classes, arrays, functions, symbols, server handles, and unions of these reference types by identity. Map values may also be functions. `unknown` slots use native checked values, comparing supported primitives by value and retaining supported object references. Typed nested collections other than `Map<unknown, unknown>` and `Set<unknown>`, and promises requiring payload adapters, cannot pass into these slots. `Promise<unknown>` retains its native representation. Stored `keys()`, `values()`, and `entries()` iterators compile, retain element identity, and observe live collection mutations until exhausted. They support `.next()`, `for...of`, array spreads, and mapper-less `Array.from`. They also support the iterator helper methods, such as `.map()` and `.take()`. Detached collection methods are unsupported. The Set composition methods (`union`, `intersection`, `difference`, `symmetricDifference`, `isSubsetOf`, `isSupersetOf`, `isDisjointFrom`) accept a Set of the same element type or a Map keyed by it; other set-like arguments are unsupported.
|
||||
|
||||
@@ -1 +1 @@
|
||||
- **Local class expressions:** Static members, decorators, private brands, generic members, explicitly generic factories returning local constructors, and references through the class's internal name are unsupported. Supported expressions preserve fresh constructor identity and shared captures across constructors, methods, accessors, and arrow fields. Local inheritance requires a compiled base class. Constructors retain identity, `typeof`, `name`, and `length` through `unknown`; cast back to their exact type before construction. Construction and static-member access directly through `unknown` are unsupported.
|
||||
- **Local class expressions:** Static members, decorators, private brands, generic members, and references through the class's internal name are unsupported. Supported expressions preserve fresh constructor identity and shared captures across constructors, methods, accessors, and arrow fields. Local inheritance requires a compiled base class. Constructors retain identity, `typeof`, `name`, and `length` through `unknown`; cast back to their exact type before construction. Construction and static-member access directly through `unknown` are unsupported.
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
Compiled programs can't load WebAssembly modules yet. Code that only mentions the `WebAssembly` types type-checks, including packages whose declarations name them, and `typeof WebAssembly` answers as it does in Node.js. Every use of the API itself is rejected: `WebAssembly.compile`, `instantiate` and `validate`, `new WebAssembly.Module` and the other constructors, `instanceof` checks, and reads of `globalThis.WebAssembly`. TypeScript sources fail to build with SC2020 at each use, and JavaScript sources throw that diagnostic when the statement runs.
|
||||
|
||||
`scriptc coverage` lists the modules a program would load, with their size, whether they validate, and the WebAssembly proposals they use. It finds modules at paths the compiler resolves, modules inlined as base64 literals, `.wasm` files shipped by packages that use WebAssembly, and files named with [`--wasm`](/docs/cli) or a `scriptc.wasm` array in `package.json`.
|
||||
@@ -2,6 +2,6 @@
|
||||
"schemaVersion": 3,
|
||||
"nodeVersion": "24.22.0",
|
||||
"nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
|
||||
"artifactVersion": "4421a7b808191ac7f410",
|
||||
"artifactVersion": "efc18bcd8a7a03fd8622",
|
||||
"rowCount": 3880
|
||||
}
|
||||
|
||||
@@ -330,8 +330,8 @@
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 12,
|
||||
"refused": 0,
|
||||
"not-implemented": 80,
|
||||
"refused": 1,
|
||||
"not-implemented": 79,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
@@ -36590,9 +36590,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "refused",
|
||||
"detail": "Explicitly rejected by a named compiler diagnostic.",
|
||||
"verification": "explicit-refusal"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
"tiers": {
|
||||
"static": {
|
||||
"audit-partial": 1162,
|
||||
"verify-gap": 2167,
|
||||
"replace-refusal": 40,
|
||||
"verify-gap": 2166,
|
||||
"replace-refusal": 41,
|
||||
"classify": 2,
|
||||
"implement": 85
|
||||
}
|
||||
@@ -34178,11 +34178,11 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:globals",
|
||||
"status": "refused",
|
||||
"action": "replace-refusal",
|
||||
"verification": "explicit-refusal",
|
||||
"confidence": "high",
|
||||
"source": "surface-manifest:unsupported:stdlib.webassembly",
|
||||
"tests": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,8 +342,8 @@
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 12,
|
||||
"refused": 0,
|
||||
"not-implemented": 80,
|
||||
"refused": 1,
|
||||
"not-implemented": 79,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
@@ -53521,8 +53521,8 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:globals"
|
||||
"status": "refused",
|
||||
"evidence": "surface-manifest:unsupported:stdlib.webassembly"
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
|
||||
@@ -203,6 +203,10 @@ function featureOf(row, entries, parentSignature) {
|
||||
|
||||
function symbolCandidates(row) {
|
||||
const result = new Set([row.apiSymbol]);
|
||||
// A global's own row (`WebAssembly`) matches the compiler's
|
||||
// `globalThis.<name>` entries.
|
||||
if (row.chapter === "globals" && row.apiSymbol && !row.apiSymbol.includes("."))
|
||||
result.add(`globalThis.${row.apiSymbol}`);
|
||||
const aliases = [
|
||||
["fsPromises.", "fs/promises."],
|
||||
["dnsPromises.", "dns/promises."],
|
||||
|
||||
+134
@@ -13,6 +13,9 @@
|
||||
* they MERGE as overloads instead of colliding — the Timeout handle maps
|
||||
* to the numeric timer id either way, and .unref()/.ref()/.hasRef() are
|
||||
* loop-liveness bookkeeping).
|
||||
* 4. The WebAssembly namespace and BufferSource, which the DOM library
|
||||
* supplies in ordinary TypeScript projects (types only; every reached
|
||||
* use of the API is refused at its site).
|
||||
*
|
||||
* This file ships to EVERY program scriptc builds — the lowering program
|
||||
* and preflight's project-world second-chance program alike (a project
|
||||
@@ -108,3 +111,134 @@ declare function clearTimeout(handle?: Timeout | number | null | undefined): voi
|
||||
* baked result is exactly what Node would compute at runtime (the
|
||||
* differential harness verifies this with a `comptime = (f) => f()` shim). */
|
||||
declare function comptime<T>(compute: () => T): T;
|
||||
|
||||
/* The JavaScript WebAssembly API, as the DOM library declares it (scriptc's
|
||||
* ES-only type world never loads lib.dom, and @types/node leaves it to
|
||||
* lib.dom). Packages whose declarations name these types (quickjs-wasi,
|
||||
* photon-node, ...) typecheck against them. These are types only: the
|
||||
* type mapper gives every type declared here the checked-dynamic
|
||||
* representation, and the lowerer refuses each reached use of the API with
|
||||
* a located diagnostic (frontend/lowering/builtins/webassembly.ts).
|
||||
* BufferSource admits every ArrayBufferView, as lib.dom did before
|
||||
* TypeScript 5.7: Node's Buffer types are views over ArrayBufferLike. */
|
||||
type BufferSource = ArrayBufferView | ArrayBuffer;
|
||||
declare namespace WebAssembly {
|
||||
interface CompileError extends Error {}
|
||||
var CompileError: {
|
||||
prototype: CompileError;
|
||||
new (message?: string): CompileError;
|
||||
(message?: string): CompileError;
|
||||
};
|
||||
interface Global<T extends ValueType = ValueType> {
|
||||
value: ValueTypeMap[T];
|
||||
valueOf(): ValueTypeMap[T];
|
||||
}
|
||||
var Global: {
|
||||
prototype: Global;
|
||||
new <T extends ValueType = ValueType>(
|
||||
descriptor: GlobalDescriptor<T>,
|
||||
v?: ValueTypeMap[T],
|
||||
): Global<T>;
|
||||
};
|
||||
interface Instance {
|
||||
readonly exports: Exports;
|
||||
}
|
||||
var Instance: {
|
||||
prototype: Instance;
|
||||
new (module: Module, importObject?: Imports): Instance;
|
||||
};
|
||||
interface LinkError extends Error {}
|
||||
var LinkError: {
|
||||
prototype: LinkError;
|
||||
new (message?: string): LinkError;
|
||||
(message?: string): LinkError;
|
||||
};
|
||||
interface Memory {
|
||||
readonly buffer: ArrayBuffer;
|
||||
grow(delta: number): number;
|
||||
}
|
||||
var Memory: {
|
||||
prototype: Memory;
|
||||
new (descriptor: MemoryDescriptor): Memory;
|
||||
};
|
||||
interface Module {}
|
||||
var Module: {
|
||||
prototype: Module;
|
||||
new (bytes: BufferSource): Module;
|
||||
customSections(moduleObject: Module, sectionName: string): ArrayBuffer[];
|
||||
exports(moduleObject: Module): ModuleExportDescriptor[];
|
||||
imports(moduleObject: Module): ModuleImportDescriptor[];
|
||||
};
|
||||
interface RuntimeError extends Error {}
|
||||
var RuntimeError: {
|
||||
prototype: RuntimeError;
|
||||
new (message?: string): RuntimeError;
|
||||
(message?: string): RuntimeError;
|
||||
};
|
||||
interface Table {
|
||||
readonly length: number;
|
||||
get(index: number): any;
|
||||
grow(delta: number, value?: any): number;
|
||||
set(index: number, value?: any): void;
|
||||
}
|
||||
var Table: {
|
||||
prototype: Table;
|
||||
new (descriptor: TableDescriptor, value?: any): Table;
|
||||
};
|
||||
interface GlobalDescriptor<T extends ValueType = ValueType> {
|
||||
mutable?: boolean;
|
||||
value: T;
|
||||
}
|
||||
interface MemoryDescriptor {
|
||||
initial: number;
|
||||
maximum?: number;
|
||||
shared?: boolean;
|
||||
}
|
||||
interface ModuleExportDescriptor {
|
||||
kind: ImportExportKind;
|
||||
name: string;
|
||||
}
|
||||
interface ModuleImportDescriptor {
|
||||
kind: ImportExportKind;
|
||||
module: string;
|
||||
name: string;
|
||||
}
|
||||
interface TableDescriptor {
|
||||
element: TableKind;
|
||||
initial: number;
|
||||
maximum?: number;
|
||||
}
|
||||
interface ValueTypeMap {
|
||||
anyfunc: Function;
|
||||
externref: any;
|
||||
f32: number;
|
||||
f64: number;
|
||||
i32: number;
|
||||
i64: bigint;
|
||||
v128: never;
|
||||
}
|
||||
interface WebAssemblyInstantiatedSource {
|
||||
instance: Instance;
|
||||
module: Module;
|
||||
}
|
||||
type ImportExportKind = "function" | "global" | "memory" | "table";
|
||||
type TableKind = "anyfunc" | "externref";
|
||||
type ExportValue = Function | Global | Memory | Table;
|
||||
type Exports = Record<string, ExportValue>;
|
||||
type ImportValue = ExportValue | number;
|
||||
type Imports = Record<string, ModuleImports>;
|
||||
type ModuleImports = Record<string, ImportValue>;
|
||||
type ValueType = keyof ValueTypeMap;
|
||||
function compile(bytes: BufferSource): Promise<Module>;
|
||||
function compileStreaming(source: Response | PromiseLike<Response>): Promise<Module>;
|
||||
function instantiate(
|
||||
bytes: BufferSource,
|
||||
importObject?: Imports,
|
||||
): Promise<WebAssemblyInstantiatedSource>;
|
||||
function instantiate(moduleObject: Module, importObject?: Imports): Promise<Instance>;
|
||||
function instantiateStreaming(
|
||||
source: Response | PromiseLike<Response>,
|
||||
importObject?: Imports,
|
||||
): Promise<WebAssemblyInstantiatedSource>;
|
||||
function validate(bytes: BufferSource): boolean;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { type AllocHost, emitObjectAlloc, emitObjectFree, inlineAllocSupported } from "./alloc.js";
|
||||
import {
|
||||
type AllocHost,
|
||||
disposeHookLoadLines,
|
||||
emitObjectAlloc,
|
||||
emitObjectFree,
|
||||
inlineAllocSupported,
|
||||
} from "./alloc.js";
|
||||
|
||||
function host(inlineAlloc: boolean): AllocHost & { decls: Set<string>; oom: boolean } {
|
||||
const h = {
|
||||
@@ -63,7 +69,12 @@ describe("emitted object allocation", () => {
|
||||
]);
|
||||
expect(emitObjectFree(h, false)).toEqual([
|
||||
" call void @scr_obj_free_note()",
|
||||
" %sf.obs = load i1, ptr @sc_weak_keys",
|
||||
" br i1 %sf.obs, label %sf.observe, label %sf.block",
|
||||
"sf.observe:",
|
||||
" call void @scr_weak_dispose(ptr %o)",
|
||||
" br label %sf.block",
|
||||
"sf.block:",
|
||||
" call void @scr_rt_free(ptr %o)",
|
||||
]);
|
||||
expect([...h.decls].some((d) => d.includes("@scr_sa"))).toBe(false);
|
||||
@@ -167,4 +178,30 @@ describe("emitted object allocation", () => {
|
||||
expect(lines.at(-1)).toBe("sf.done:");
|
||||
}
|
||||
});
|
||||
|
||||
test("frees test the dispose hook only where the program can observe them", () => {
|
||||
const h = host(true);
|
||||
const weak = emitObjectFree(h, false);
|
||||
expectSingleDefinitions(weak);
|
||||
expect(weak.slice(0, 2)).toEqual([
|
||||
" %sf.obs = load i1, ptr @sc_weak_keys",
|
||||
" br i1 %sf.obs, label %sf.hooked, label %sf.block",
|
||||
]);
|
||||
const records = emitObjectFree(h, true, "weak-or-source");
|
||||
expectSingleDefinitions(records);
|
||||
expect(records.slice(0, 4)).toEqual([
|
||||
" %sf.obsw = load i1, ptr @sc_weak_keys",
|
||||
" %sf.obss = load i1, ptr @sc_record_sources",
|
||||
" %sf.obs = or i1 %sf.obsw, %sf.obss",
|
||||
" br i1 %sf.obs, label %sf.hooked, label %sf.block",
|
||||
]);
|
||||
expect(records).toContain(" call void %sf.hook(ptr %o)");
|
||||
});
|
||||
|
||||
test("loads the dispose hook from emitted code only with the inline allocator", () => {
|
||||
expect(disposeHookLoadLines(host(false), "%t1")).toBeNull();
|
||||
expect(disposeHookLoadLines(host(true), "%t1")).toEqual([
|
||||
" %t1 = load ptr, ptr @scr_weak_dispose_hook",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -79,6 +79,17 @@ function threadLocalState(host: AllocHost, prefix: string, lines: string[]): str
|
||||
return [...preamble, ...text.split("\n")];
|
||||
}
|
||||
|
||||
/** Lines that load the runtime's dispose hook into `dest`, or null on
|
||||
* targets without the inline allocator, where the hook's storage is not
|
||||
* addressed from emitted code. The hook is NULL until a weak collection
|
||||
* or a kept record source installs it, and then never NULL again. */
|
||||
export function disposeHookLoadLines(host: AllocHost, dest: string): string[] | null {
|
||||
if (host.inlineAlloc !== true) return null;
|
||||
declareGlobal(host, `@scr_weak_dispose_hook = external global ptr`);
|
||||
const prefix = dest.replace(/^%/, "").replace(/[^-$._A-Za-z0-9]/g, "_");
|
||||
return threadLocalState(host, prefix, [` ${dest} = load ptr, ptr @scr_weak_dispose_hook`]);
|
||||
}
|
||||
|
||||
/** Worker executables: the calling script thread's ScrThreadHot block into
|
||||
* `dest`. Its address also identifies the thread (the owner word of a
|
||||
* lifetime-region chunk; llvm/regions.ts). */
|
||||
@@ -325,20 +336,56 @@ function allocLines(
|
||||
return lines;
|
||||
}
|
||||
|
||||
/* ── dispose observers ────────────────────────────────────────────────
|
||||
* The runtime's dispose hook (scr_weak_dispose_hook) has two clients: weak
|
||||
* collections, whose keys can be any object, and the kept sources of
|
||||
* checked records (scr_record_source_note and the JSON schema parser),
|
||||
* keyed by closed records. A program without either can never install the
|
||||
* hook, so its frees need not test it. Whether the program has them is
|
||||
* known only once every helper is emitted (dynCheck builders and JSON
|
||||
* schemas are requested lazily), so the emitted frees and record writers
|
||||
* test these two module constants, which the emitter defines last
|
||||
* (LlvmEmitter.emitParts) and LLVM folds away. */
|
||||
|
||||
/** True when the program constructs a WeakMap or WeakSet. */
|
||||
export const WEAK_KEYS_GATE = "@sc_weak_keys";
|
||||
/** True when a checked conversion can keep a record's undeclared source. */
|
||||
export const RECORD_SOURCES_GATE = "@sc_record_sources";
|
||||
|
||||
/** Which dispose observers an object type can have: weak-collection keys
|
||||
* only, or (closed record layouts) kept record sources too. */
|
||||
export type FreeObservers = "weak" | "weak-or-source";
|
||||
|
||||
/**
|
||||
* Lines that free the object `%o` after its members were released: the
|
||||
* RC-audit free note, the weak-collection dispose hook, and the block —
|
||||
* `scr_cyc_free` for collector-headered objects, `scr_rt_free` otherwise.
|
||||
* Labels use the `sf.` prefix; the lines end in an open block.
|
||||
* RC-audit free note, the dispose hook (when the program can observe this
|
||||
* type's disposals), and the block — `scr_cyc_free` for collector-headered
|
||||
* objects, `scr_rt_free` otherwise. Labels use the `sf.` prefix; the lines
|
||||
* end in an open block.
|
||||
*/
|
||||
export function emitObjectFree(host: AllocHost, cyc: boolean): string[] {
|
||||
const lines = threadLocalState(host, "sf", freeLines(host, cyc));
|
||||
export function emitObjectFree(
|
||||
host: AllocHost,
|
||||
cyc: boolean,
|
||||
observers: FreeObservers = "weak",
|
||||
): string[] {
|
||||
const lines = threadLocalState(host, "sf", freeLines(host, cyc, observers));
|
||||
if (host.objectAudit === false) return lines.filter((line) => line !== FREE_NOTE);
|
||||
host.declare(`declare void @scr_obj_free_note()`);
|
||||
return lines;
|
||||
}
|
||||
|
||||
function freeLines(host: AllocHost, cyc: boolean): string[] {
|
||||
/** Lines that leave in `%<prefix>.obs` whether the program can observe a
|
||||
* disposal of this type (folds to a constant). */
|
||||
function observedLines(prefix: string, observers: FreeObservers): string[] {
|
||||
if (observers === "weak") return [` %${prefix}.obs = load i1, ptr ${WEAK_KEYS_GATE}`];
|
||||
return [
|
||||
` %${prefix}.obsw = load i1, ptr ${WEAK_KEYS_GATE}`,
|
||||
` %${prefix}.obss = load i1, ptr ${RECORD_SOURCES_GATE}`,
|
||||
` %${prefix}.obs = or i1 %${prefix}.obsw, %${prefix}.obss`,
|
||||
];
|
||||
}
|
||||
|
||||
function freeLines(host: AllocHost, cyc: boolean, observers: FreeObservers): string[] {
|
||||
if (host.inlineAlloc !== true) {
|
||||
if (cyc) {
|
||||
host.declare(`declare void @scr_cyc_free(ptr)`);
|
||||
@@ -348,7 +395,12 @@ function freeLines(host: AllocHost, cyc: boolean): string[] {
|
||||
host.declare(`declare void @scr_weak_dispose(ptr)`);
|
||||
return [
|
||||
` call void @scr_obj_free_note()`,
|
||||
...observedLines("sf", observers),
|
||||
` br i1 %sf.obs, label %sf.observe, label %sf.block`,
|
||||
`sf.observe:`,
|
||||
` call void @scr_weak_dispose(ptr %o)`,
|
||||
` br label %sf.block`,
|
||||
`sf.block:`,
|
||||
` call void @scr_rt_free(ptr %o)`,
|
||||
];
|
||||
}
|
||||
@@ -356,11 +408,14 @@ function freeLines(host: AllocHost, cyc: boolean): string[] {
|
||||
host.declare(`declare void @scr_rt_free(ptr)`);
|
||||
declareGlobal(host, `@scr_weak_dispose_hook = external global ptr`);
|
||||
const lines = [
|
||||
...observedLines("sf", observers),
|
||||
` br i1 %sf.obs, label %sf.hooked, label %sf.block`,
|
||||
`sf.hooked:`,
|
||||
` %sf.hook = load ptr, ptr @scr_weak_dispose_hook`,
|
||||
` %sf.has = icmp ne ptr %sf.hook, null`,
|
||||
` %sf.hasx = call i1 @llvm.expect.i1(i1 %sf.has, i1 false)`,
|
||||
` br i1 %sf.hasx, label %sf.observe, label %sf.block`,
|
||||
`sf.observe: ; weak collections observe the disposal`,
|
||||
`sf.observe: ; a weak collection or a kept record source observes the disposal`,
|
||||
` call void %sf.hook(ptr %o)`,
|
||||
` br label %sf.block`,
|
||||
`sf.block:`,
|
||||
|
||||
@@ -1106,6 +1106,10 @@ export class LlDyn {
|
||||
private readonly jsonSchemas = new Map<string, string>();
|
||||
private readonly jsonParsers = new Map<string, string>();
|
||||
private readonly recordNameTables = new Map<string, { names: string; lens: string }>();
|
||||
/** Some emitted helper can keep a record's undeclared source: a checked
|
||||
* record builder that notes it, or a JSON schema of a closed record
|
||||
* (alloc.ts RECORD_SOURCES_GATE). */
|
||||
keepsRecordSources = false;
|
||||
|
||||
/** Targets the one-pass parser (scr_json_parse_schema) can build
|
||||
* directly: number, boolean and string leaves inside arrays and records
|
||||
@@ -1221,6 +1225,7 @@ export class LlDyn {
|
||||
const index = new Map(shape.fields.map((f, i) => [f.name, i + base]));
|
||||
// The runtime names kept-source keys by this index (record-sources.ts).
|
||||
const order = recordSourceFieldOrder(shape);
|
||||
if (recordMayKeepSource(shape)) this.keepsRecordSources = true;
|
||||
const fieldTy = `{ ptr, ${S}, ${S}, ptr }`;
|
||||
const entries = order.map((fieldName) => {
|
||||
const field = shape.fields[index.get(fieldName)! - base]!;
|
||||
@@ -1981,6 +1986,7 @@ export class LlDyn {
|
||||
// that has them stays beside the record so its reflective
|
||||
// surfaces still observe them (scr_record_source_note).
|
||||
if (recordMayKeepSource(shape)) {
|
||||
this.keepsRecordSources = true;
|
||||
const names = this.recordNameTable(t.shapeId, shape);
|
||||
host.declare(
|
||||
`declare void @scr_record_source_note(ptr, ptr, ptr, ptr, ${host.sizeType})`,
|
||||
|
||||
@@ -2,7 +2,14 @@ import { type ArraySearchKind, emitArraySearchHelpers } from "./array-search.js"
|
||||
import { emitStringInputs } from "./string-lifetimes.js";
|
||||
import { emitLlvmLayouts } from "./layouts.js";
|
||||
import { compileRegexMatcher, type RegexMatcherCode } from "./regex-matchers.js";
|
||||
import { CYC_BUF, CYC_HDR, FIRST_EMITTED_CYC_KIND, inlineAllocSupported } from "./alloc.js";
|
||||
import {
|
||||
CYC_BUF,
|
||||
CYC_HDR,
|
||||
FIRST_EMITTED_CYC_KIND,
|
||||
inlineAllocSupported,
|
||||
RECORD_SOURCES_GATE,
|
||||
WEAK_KEYS_GATE,
|
||||
} from "./alloc.js";
|
||||
import { hasLoneSurrogate, llvmBytes as llBytes, wtf8Bytes } from "../literals.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { createHash } from "node:crypto";
|
||||
@@ -50,6 +57,7 @@ import {
|
||||
VOID,
|
||||
} from "../../ir/ir.js";
|
||||
import { moduleRuntimeFeatures, moduleUsesMockTimers } from "../../ir/runtime-features.js";
|
||||
import { captureFreeFunctions, observesStacks } from "../stack-observations.js";
|
||||
import {
|
||||
matchIntegerArrayForLoop,
|
||||
matchIntegerBytesForLoop,
|
||||
@@ -158,6 +166,7 @@ import {
|
||||
mangleFnClosure,
|
||||
mangleFunction,
|
||||
mangleGenDrop,
|
||||
mangleGenSourceClose,
|
||||
mangleGenSpawn,
|
||||
mangleGlobal,
|
||||
mangleLocal,
|
||||
@@ -1172,18 +1181,14 @@ export class LlEmitter {
|
||||
readonly chainSlots = new Map<string, LlValue>();
|
||||
logArgSlots = 0;
|
||||
private readonly stackTraces: boolean;
|
||||
/** Stack-trace mode: functions no capture can include (no frame push). */
|
||||
private readonly unframedFunctions: ReadonlySet<string>;
|
||||
|
||||
constructor(
|
||||
readonly mod: IrModule,
|
||||
options: LlvmTargetOptions,
|
||||
) {
|
||||
this.stackTraces = mod.functions.some(
|
||||
(fn) =>
|
||||
!everyStmtList(fn.body, {
|
||||
stmt: () => true,
|
||||
expr: (expr) => !(expr.kind === "libCall" && expr.fn === "error.stack"),
|
||||
}),
|
||||
);
|
||||
this.stackTraces = observesStacks(mod);
|
||||
this.nullableUnions = new NullableUnions(mod, options.debugSources === undefined);
|
||||
this.mockTimers = moduleUsesMockTimers(mod);
|
||||
this.debug =
|
||||
@@ -1265,6 +1270,9 @@ export class LlEmitter {
|
||||
// which Windows lacks.
|
||||
this.stacklessAsyncAllowed = this.stacklessGenerators && mod.lib === undefined && !windows;
|
||||
this.publishedTypes = computePublishedTypes(mod);
|
||||
this.unframedFunctions = this.stackTraces
|
||||
? captureFreeFunctions(mod, this.stackChecks, this.publishedTypes)
|
||||
: new Set();
|
||||
this.immutableFields = computeImmutableFields(mod);
|
||||
this.constructorFunctions = new Set((mod.classes ?? []).map((c) => `%${c.name}.constructor`));
|
||||
this.indirectMayThrow = mt.indirect || mod.workers === true;
|
||||
@@ -1964,7 +1972,10 @@ export class LlEmitter {
|
||||
}
|
||||
|
||||
emitParts(): string[] {
|
||||
const parts = limitInlineDynReleases(this.emitModuleParts());
|
||||
const parts = [
|
||||
...limitInlineDynReleases(this.emitModuleParts()),
|
||||
...this.disposeObserverGates(),
|
||||
];
|
||||
if (!this.executableTls) return parts;
|
||||
// A worker executable's thread-locals all live in the executable's own
|
||||
// TLS block: the program's internal ones, and the runtime's external
|
||||
@@ -1987,6 +1998,21 @@ export class LlEmitter {
|
||||
);
|
||||
}
|
||||
|
||||
/** The dispose-observer gates the emitted frees and record writers test
|
||||
* (alloc.ts): defined after every helper is emitted, because dynCheck
|
||||
* builders, JSON schemas and weak collection constructors are requested
|
||||
* lazily. */
|
||||
private disposeObserverGates(): string[] {
|
||||
const weak = [...this.decls].some(
|
||||
(d) => d.includes("@scr_weak_map_new(") || d.includes("@scr_weak_set_new("),
|
||||
);
|
||||
return [
|
||||
`${WEAK_KEYS_GATE} = internal constant i1 ${weak}`,
|
||||
`${RECORD_SOURCES_GATE} = internal constant i1 ${this.dyn.keepsRecordSources}`,
|
||||
``,
|
||||
];
|
||||
}
|
||||
|
||||
private emitModuleParts(): string[] {
|
||||
// Function bodies first (the literal/unit/fn-value tables fill as they
|
||||
// emit), then the file assembles around them — the runtime ABI’s order.
|
||||
@@ -4598,6 +4624,25 @@ export class LlEmitter {
|
||||
dr.push(` call void @free(ptr %ap)`, ` ret void`, `}`, ``);
|
||||
for (const line of dr) out.push(line);
|
||||
|
||||
// An iterator helper's return() before its first next() closes the
|
||||
// source generator in its first argument slot.
|
||||
const closesSource = fn.generator.closesSource === true;
|
||||
if (closesSource) {
|
||||
const field = lifted ? 1 : 0;
|
||||
this.declare(`declare void @scr_gen_close(ptr)`);
|
||||
this.declare(`declare void @scr_gen_set_unstarted_return(ptr, ptr)`);
|
||||
out.push(
|
||||
`define internal void @${mangleGenSourceClose(fn.name)}(ptr %ap) ${FN_ATTRS} {`,
|
||||
`entry:`,
|
||||
` %sp = getelementptr inbounds %${pack}, ptr %ap, i64 0, i32 ${field}`,
|
||||
` %sv = load ptr, ptr %sp`,
|
||||
` call void @scr_gen_close(ptr %sv)`,
|
||||
` ret void`,
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
}
|
||||
|
||||
// Spawn wrapper: pack the args (+1 moves in), allocate the
|
||||
// SUSPENDED fiber — nothing runs until the first .next().
|
||||
if (lifted) {
|
||||
@@ -4614,6 +4659,11 @@ export class LlEmitter {
|
||||
settleAsync === null
|
||||
? ` %gg = call ptr @${genNew}(ptr @${mangleTrampoline(fn.name)}, ptr %ap, ptr @${mangleGenDrop(fn.name)})`
|
||||
: ` %gg = call ptr @scr_async_gen_new(ptr @${mangleTrampoline(fn.name)}, ptr %ap, ptr @${mangleGenDrop(fn.name)}, ptr @${settleAsync})`,
|
||||
...(closesSource
|
||||
? [
|
||||
` call void @scr_gen_set_unstarted_return(ptr %gg, ptr @${mangleGenSourceClose(fn.name)})`,
|
||||
]
|
||||
: []),
|
||||
` ret ptr %gg`,
|
||||
`}`,
|
||||
``,
|
||||
@@ -7125,13 +7175,39 @@ export class LlEmitter {
|
||||
// Stackful native fibers and native coroutine frames retain these frames
|
||||
// while suspended. The active exception context owns the chain, so
|
||||
// concurrent fibers isolate it.
|
||||
if (this.stackTraces && !this.wasi && fn.sourceName !== undefined) {
|
||||
this.declare(`declare void @scr_stack_enter(ptr, ptr)`);
|
||||
this.declare(`declare void @scr_stack_leave(ptr)`);
|
||||
if (
|
||||
this.stackTraces &&
|
||||
!this.wasi &&
|
||||
fn.sourceName !== undefined &&
|
||||
!this.unframedFunctions.has(fn.name)
|
||||
) {
|
||||
B.entryAllocas.push(`%source_frame = alloca { ptr, ptr }`);
|
||||
const frame = ` at ${fn.sourceName} (${fn.loc.file})`;
|
||||
B.line(`call void @scr_stack_enter(ptr %source_frame, ptr ${this.cstr(frame)})`);
|
||||
B.returnEpilogue = `call void @scr_stack_leave(ptr %source_frame)`;
|
||||
const frame = this.cstr(` at ${fn.sourceName} (${fn.loc.file})`);
|
||||
if (
|
||||
B.excCellInvariant &&
|
||||
this.mod.lib === undefined &&
|
||||
this.mod.workers !== true &&
|
||||
this.sizeType === "i64"
|
||||
) {
|
||||
// Executables link the active-cell pointer as a plain global, and a
|
||||
// synchronous body sees one cell from entry to every return: push
|
||||
// and pop the frame inline (ScrExcCell.stack is at byte 56 on
|
||||
// 64-bit targets) instead of calling the runtime twice per call.
|
||||
this.declare(`@scr_exc_active = external global ptr`);
|
||||
B.line(`%source_frame.cell = load ptr, ptr @scr_exc_active`);
|
||||
B.line(`%source_frame.chain = getelementptr inbounds i8, ptr %source_frame.cell, i64 56`);
|
||||
B.line(`%source_frame.previous = load ptr, ptr %source_frame.chain`);
|
||||
B.line(`store ptr %source_frame.previous, ptr %source_frame`);
|
||||
B.line(`%source_frame.text = getelementptr inbounds i8, ptr %source_frame, i64 8`);
|
||||
B.line(`store ptr ${frame}, ptr %source_frame.text`);
|
||||
B.line(`store ptr %source_frame, ptr %source_frame.chain`);
|
||||
B.returnEpilogue = `store ptr %source_frame.previous, ptr %source_frame.chain`;
|
||||
} else {
|
||||
this.declare(`declare void @scr_stack_enter(ptr, ptr)`);
|
||||
this.declare(`declare void @scr_stack_leave(ptr)`);
|
||||
B.line(`call void @scr_stack_enter(ptr %source_frame, ptr ${frame})`);
|
||||
B.returnEpilogue = `call void @scr_stack_leave(ptr %source_frame)`;
|
||||
}
|
||||
}
|
||||
if (this.stackChecks?.has(fn.name)) this.emitStackCheck();
|
||||
if (this.workerEntryPolls?.has(fn.name)) this.emitPendingCheck(undefined, true);
|
||||
@@ -9492,14 +9568,15 @@ export class LlEmitter {
|
||||
B.line(`${stash} = call ptr @scr_exc_take() ; stash across finally`);
|
||||
B.line(`store ptr ${stash}, ptr ${stashSlot}`);
|
||||
this.scopes.push([{ slot: stashSlot, type: CAUGHT }]);
|
||||
const suppressHandler = s.suppressFinallyErrors
|
||||
? {
|
||||
label: B.newLabel("try.fs"),
|
||||
used: false,
|
||||
frameDepth: this.frames.length,
|
||||
scopeDepth: this.scopes.length,
|
||||
}
|
||||
: null;
|
||||
const suppressHandler =
|
||||
s.suppressFinallyErrors || s.dropFinallyErrors
|
||||
? {
|
||||
label: B.newLabel("try.fs"),
|
||||
used: false,
|
||||
frameDepth: this.frames.length,
|
||||
scopeDepth: this.scopes.length,
|
||||
}
|
||||
: null;
|
||||
if (suppressHandler) this.tryStack.push(suppressHandler);
|
||||
this.emitBlock(s.finallyBody!);
|
||||
if (suppressHandler) this.tryStack.pop();
|
||||
@@ -9509,8 +9586,9 @@ export class LlEmitter {
|
||||
this.emitUnwind();
|
||||
if (suppressHandler?.used) {
|
||||
B.startBlock(suppressHandler.label);
|
||||
this.declare(`declare void @scr_exc_suppress(ptr)`);
|
||||
B.line(`call void @scr_exc_suppress(ptr ${stash}) ; consumes stash`);
|
||||
const cleanup = s.dropFinallyErrors ? "scr_exc_keep_first" : "scr_exc_suppress";
|
||||
this.declare(`declare void @${cleanup}(ptr)`);
|
||||
B.line(`call void @${cleanup}(ptr ${stash}) ; consumes stash`);
|
||||
this.emitUnwind();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { emitTestLibCall } from "./lib-test.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
|
||||
import { MAY_THROW_LIB_FNS } from "../../ir/builtin-effects.js";
|
||||
import type { IrExpr, IrType } from "../../ir/ir.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
import type { LlvmEmitterContext, LibCallExpr, LibCallPrefix, LlValue } from "./expr-context.js";
|
||||
import {
|
||||
@@ -21,6 +22,7 @@ import { isDynUndefined } from "../../ir/runtime-features.js";
|
||||
import { DYN_KIND } from "./dyn.js";
|
||||
import { rawBytes } from "./lib-abi.js";
|
||||
import { emitInlineAtomicsOp } from "./expr-bytes.js";
|
||||
import { regexLiteralCaptures } from "./regex-matchers.js";
|
||||
|
||||
export function emitAssertInspectLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
const B = host.B;
|
||||
@@ -642,8 +644,66 @@ function emitAssertFailing(
|
||||
* `a.name.localeCompare(b.name)` reads both fields without retaining). */
|
||||
const READ_ONLY_STRING_LIB_FNS: ReadonlySet<string> = new Set(["str.localeCompare"]);
|
||||
|
||||
/** The parameter count of a replace callback the runtime can call through
|
||||
* the closure ABI (scr_regex_replace_fn), or null when it needs the dyn
|
||||
* box. Each declared parameter must receive a value of its declared kind
|
||||
* in every match: the match string at 0; with more than one parameter the
|
||||
* pattern must be a literal whose captures are known, so position i is a
|
||||
* capture that every match sets (a string), the offset (a number) right
|
||||
* after the captures, or the subject (a string) after that. */
|
||||
function typedReplacerArity(callback: IrType, pattern: IrExpr): number | null {
|
||||
if (callback.kind !== "func" || callback.rest || callback.argumentsAll) return null;
|
||||
if (callback.ret.kind !== "string") return null;
|
||||
const params = callback.params;
|
||||
if (params.length > 4) return null;
|
||||
if (params.length === 0) return 0;
|
||||
if (params[0]!.kind !== "string") return null;
|
||||
if (params.length === 1) return 1;
|
||||
if (pattern.kind !== "regexLit") return null;
|
||||
const captures = regexLiteralCaptures(pattern.pattern, pattern.flags);
|
||||
if (!captures) return null;
|
||||
for (let i = 1; i < params.length; i++) {
|
||||
const kind = params[i]!.kind;
|
||||
const ok =
|
||||
i < captures.count
|
||||
? kind === "string" && captures.always[i] === true
|
||||
: i === captures.count
|
||||
? kind === "f64"
|
||||
: i === captures.count + 1 && kind === "string";
|
||||
if (!ok) return null;
|
||||
}
|
||||
return params.length;
|
||||
}
|
||||
|
||||
/** `s.replace(re, fn)` / `replaceAll` with a typed callback the runtime can
|
||||
* call directly: the dyn box the IR wraps it in is never observable (the
|
||||
* runtime only calls it, with `this` undefined), so the closure goes to
|
||||
* scr_regex_replace_fn as it is. Null keeps the generic path. */
|
||||
function emitTypedRegexReplace(host: LlvmEmitterContext, e: LibCallExpr): LlValue | null {
|
||||
const [subject, pattern, callback, all] = e.args;
|
||||
if (!subject || !pattern || callback?.kind !== "dynFrom" || all?.kind !== "boolLit") return null;
|
||||
const arity = typedReplacerArity(callback.value.type, pattern);
|
||||
if (arity === null) return null;
|
||||
const B = host.B;
|
||||
const s = host.emitExpr(subject);
|
||||
const re = host.emitExpr(pattern);
|
||||
const fn = host.emitExpr(callback.value);
|
||||
host.declare(`declare ptr @scr_regex_replace_fn(ptr, ptr, ptr, i32, i1 zeroext)`);
|
||||
const result = B.tmp();
|
||||
B.line(
|
||||
`${result} = call ptr @scr_regex_replace_fn(ptr ${s.name}, ptr ${re.name}, ptr ${fn.name}, i32 ${arity}, i1 zeroext ${all.value})`,
|
||||
);
|
||||
const output = host.own({ name: result, type: e.type });
|
||||
host.emitPendingCheck();
|
||||
return output;
|
||||
}
|
||||
|
||||
export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
|
||||
if (USES_TIMERS_LIB_FNS.has(e.fn)) host.usesTimers = true;
|
||||
if (e.fn === "regex.replaceCallback") {
|
||||
const typed = emitTypedRegexReplace(host, e);
|
||||
if (typed) return typed;
|
||||
}
|
||||
if (e.fn === "process.nextTick") return host.emitAsyncContextLibCall(e);
|
||||
if (
|
||||
e.fn === "sp.pipeline" ||
|
||||
|
||||
@@ -144,6 +144,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"num.toFixed0": "scr_num_to_fixed0",
|
||||
"num.toFixed": "scr_num_to_fixed",
|
||||
"num.toStringRadix": "scr_num_to_string_radix",
|
||||
"num.toStringRadixF64": "scr_num_to_string_radix_f64",
|
||||
"num.sameValue": "scr_num_same_value",
|
||||
"intl.numFormatEnUs": "scr_intl_num_format_en_us",
|
||||
"number.isFinite": "scr_num_is_finite",
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, test } from "vitest";
|
||||
import {
|
||||
compileRegexMatcher,
|
||||
regexLiteralBailReason,
|
||||
regexLiteralCaptures,
|
||||
regexLiteralCompiles,
|
||||
} from "./regex-matchers.js";
|
||||
|
||||
@@ -11,6 +12,30 @@ function body(pattern: string, flags = "", vector = true): string {
|
||||
return code.lines.join("\n");
|
||||
}
|
||||
|
||||
describe("regex literal captures", () => {
|
||||
test("counts captures with the whole match and marks the ones every match sets", () => {
|
||||
expect(regexLiteralCaptures("[&<>]", "g")).toEqual({ count: 1, always: [true] });
|
||||
expect(regexLiteralCaptures("(\\w+)=(\\d)+", "")).toEqual({
|
||||
count: 3,
|
||||
always: [true, true, true],
|
||||
});
|
||||
expect(regexLiteralCaptures("(?<k>a)(b)?|(c)", "")).toEqual({
|
||||
count: 4,
|
||||
always: [true, false, false, false],
|
||||
});
|
||||
expect(regexLiteralCaptures("x(?:(a)|(b))y(c)*(d){0,2}(e){1,3}", "")).toEqual({
|
||||
count: 6,
|
||||
always: [true, false, false, false, false, true],
|
||||
});
|
||||
});
|
||||
|
||||
test("answers null outside the parsed subset", () => {
|
||||
expect(regexLiteralCaptures("(?=a)(b)", "")).toBeNull();
|
||||
expect(regexLiteralCaptures("(a)\\1", "")).toBeNull();
|
||||
expect(regexLiteralCaptures("a", "v")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("regex literal matcher subset", () => {
|
||||
test.each([
|
||||
['^(\\d+\\.\\d+\\.\\d+\\.\\d+) - - \\[([^\\]]+)\\] "(\\w+) ([^ ?"]+)(?:\\?([^ "]*))?$', ""],
|
||||
|
||||
@@ -1639,6 +1639,40 @@ function parseSubset(pattern: string, flags: string): { root: RxNode; ncap: numb
|
||||
}
|
||||
}
|
||||
|
||||
/** The capture count of a regex literal (the whole match included) and,
|
||||
* per capture, whether every match sets it: no alternation of several
|
||||
* branches and no repetition that can run zero times encloses it. Null
|
||||
* when the pattern is outside the parsed subset. */
|
||||
export function regexLiteralCaptures(
|
||||
pattern: string,
|
||||
flags: string,
|
||||
): { count: number; always: boolean[] } | null {
|
||||
const parsed = parseSubset(pattern, flags);
|
||||
if (!parsed) return null;
|
||||
const always = Array.from({ length: parsed.ncap }, (_, i) => i === 0);
|
||||
const walk = (node: RxNode, sure: boolean): void => {
|
||||
switch (node.kind) {
|
||||
case "cap":
|
||||
always[node.index] = sure;
|
||||
walk(node.body, sure);
|
||||
return;
|
||||
case "seq":
|
||||
for (const item of node.items) walk(item, sure);
|
||||
return;
|
||||
case "alt":
|
||||
for (const alt of node.alts) walk(alt, sure && node.alts.length === 1);
|
||||
return;
|
||||
case "rep":
|
||||
walk(node.body, sure && node.min > 0);
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
};
|
||||
walk(parsed.root, true);
|
||||
return { count: parsed.ncap, always };
|
||||
}
|
||||
|
||||
/** Whether a literal compiles to a native matcher (tests and size caps). */
|
||||
export function regexLiteralCompiles(pattern: string, flags: string): boolean {
|
||||
return parseSubset(pattern, flags) !== null;
|
||||
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
stackUnionBox,
|
||||
type InlineUnionFields,
|
||||
} from "./inline-unions.js";
|
||||
import { recordMayKeepSource } from "./record-sources.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
|
||||
/** What the tables need from the emitter: the extern-declaration ledger
|
||||
@@ -1524,7 +1525,9 @@ export function emitRecordShapes(
|
||||
}
|
||||
t++;
|
||||
}
|
||||
freeBody.push(...emitObjectFree(host, traced));
|
||||
// Closed layouts can carry a kept record source (record-sources.ts).
|
||||
const observers = recordMayKeepSource(shape) ? "weak-or-source" : "weak";
|
||||
freeBody.push(...emitObjectFree(host, traced, observers));
|
||||
defs.push(
|
||||
...releaseBody(
|
||||
host,
|
||||
@@ -1639,7 +1642,7 @@ export function emitRecordShapes(
|
||||
` call void ${releaseSym(host, m.type)}(ptr %v${i}) ; ${llvmCommentText(m.name)} (acyclic)`,
|
||||
);
|
||||
});
|
||||
gf.push(...emitObjectFree(host, true), ` ret void`, `}`, ``);
|
||||
gf.push(...emitObjectFree(host, true, observers), ` ret void`, `}`, ``);
|
||||
defs.push(...gf);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
traceAdapter,
|
||||
type ShapeHost,
|
||||
} from "./shapes.js";
|
||||
import { disposeHookLoadLines, RECORD_SOURCES_GATE } from "./alloc.js";
|
||||
import { recordMayKeepSource, recordSourceFieldOrder } from "./record-sources.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
import { emitUnionPeek, emitUnionTag } from "./union-repr.js";
|
||||
@@ -397,6 +398,24 @@ export class LlWalkers {
|
||||
const writer = this.recordFieldWriter(shapeId);
|
||||
const lSource = B.newLabel("jw.source");
|
||||
const lNative = B.newLabel("jw.native");
|
||||
// Only a program that can keep record sources looks one up (the gate
|
||||
// folds to a constant), and only once one was kept: keeping a source
|
||||
// installs the dispose hook, so a NULL hook means the table is empty.
|
||||
const lHooked = B.newLabel("jw.hooked");
|
||||
const kept = B.tmp();
|
||||
B.line(`${kept} = load i1, ptr ${RECORD_SOURCES_GATE}`);
|
||||
B.condBr(kept, lHooked, lNative);
|
||||
B.startBlock(lHooked);
|
||||
const hook = B.tmp();
|
||||
const hookLines = disposeHookLoadLines(host, hook);
|
||||
if (hookLines !== null) {
|
||||
const lProbe = B.newLabel("jw.probe");
|
||||
for (const line of hookLines) B.line(line.trimStart());
|
||||
const installed = B.tmp();
|
||||
B.line(`${installed} = icmp ne ptr ${hook}, null`);
|
||||
B.condBr(installed, lProbe, lNative);
|
||||
B.startBlock(lProbe);
|
||||
}
|
||||
if (traceAdapter(host, { kind: "record", shapeId }) === null) {
|
||||
// One lookup: the runtime writes the record when it kept a source.
|
||||
host.declare(`declare zeroext i1 @scr_jb_try_record_source(ptr, ptr, ptr)`);
|
||||
|
||||
@@ -204,6 +204,11 @@ export function mangleGenSpawn(fnName: string): string {
|
||||
export function mangleGenDrop(fnName: string): string {
|
||||
return `sc_gd_${sanitize(fnName)}`;
|
||||
}
|
||||
/** The never-started return() hook of a generator that closes its source
|
||||
* argument (an iterator helper). */
|
||||
export function mangleGenSourceClose(fnName: string): string {
|
||||
return `sc_gc_${sanitize(fnName)}`;
|
||||
}
|
||||
/** Emitted ref-kind resolve thunk, interned per inner-type key. */
|
||||
export function mangleResolveThunk(n: number): string {
|
||||
return `sc_rslv_${n}`;
|
||||
|
||||
@@ -580,7 +580,7 @@ function breaksTo(
|
||||
|
||||
/** Statements no execution reaches: those after a statement that cannot
|
||||
* complete normally in the same statement list. */
|
||||
function unreachableStatements(body: readonly IrStmt[]): Set<IrStmt> {
|
||||
export function unreachableStatements(body: readonly IrStmt[]): Set<IrStmt> {
|
||||
const dead = new Set<IrStmt>();
|
||||
const { statement } = completion();
|
||||
const list = (stmts: readonly IrStmt[]): void => {
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import { expect, test } from "vitest";
|
||||
import {
|
||||
F64,
|
||||
STRING,
|
||||
VOID,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrModule,
|
||||
type IrStmt,
|
||||
} from "../ir/ir.js";
|
||||
import { captureFreeFunctions, observesStacks } from "./stack-observations.js";
|
||||
|
||||
const loc = { file: "stack-observations.ts", start: 0, end: 0 };
|
||||
const ERROR = { kind: "object", className: "%Error" } as const;
|
||||
const err: IrExpr = { loc, kind: "varRef", localId: "e", type: ERROR };
|
||||
const stackRead: IrStmt = {
|
||||
loc,
|
||||
kind: "exprStmt",
|
||||
expr: { loc, kind: "libCall", fn: "error.stack", args: [err], type: STRING },
|
||||
};
|
||||
const call = (callee: string): IrStmt => ({
|
||||
loc,
|
||||
kind: "exprStmt",
|
||||
expr: { loc, kind: "call", callee, args: [], type: VOID },
|
||||
});
|
||||
const throwStmt: IrStmt = { loc, kind: "throw", value: err };
|
||||
const ret: IrStmt = { loc, kind: "return", value: null };
|
||||
const fn = (name: string, body: IrStmt[]): IrFunction => ({
|
||||
loc,
|
||||
name,
|
||||
params: [],
|
||||
locals: [],
|
||||
returnType: VOID,
|
||||
body,
|
||||
});
|
||||
const mod = (functions: IrFunction[], extra: Partial<IrModule> = {}): IrModule => ({
|
||||
irVersion: 16,
|
||||
sourceFile: "stack-observations.ts",
|
||||
entry: "%main",
|
||||
functions,
|
||||
classes: [],
|
||||
records: [],
|
||||
unions: [],
|
||||
globals: [],
|
||||
...extra,
|
||||
});
|
||||
|
||||
test("a reachable stack read is observed", () => {
|
||||
expect(observesStacks(mod([fn("%main", [call("parse")]), fn("parse", [stackRead])]))).toBe(true);
|
||||
});
|
||||
|
||||
test("programs without stack reads are not", () => {
|
||||
expect(observesStacks(mod([fn("%main", [call("helper")]), fn("helper", [ret])]))).toBe(false);
|
||||
});
|
||||
|
||||
test("reads after an unconditional throw or return never run", () => {
|
||||
expect(observesStacks(mod([fn("%main", [throwStmt, stackRead])]))).toBe(false);
|
||||
expect(observesStacks(mod([fn("%main", [ret, call("parse")]), fn("parse", [stackRead])]))).toBe(
|
||||
false,
|
||||
);
|
||||
// A terminator ends only its own list: the outer list continues.
|
||||
const cond: IrExpr = { loc, kind: "numLit", value: 1, type: F64 };
|
||||
expect(
|
||||
observesStacks(
|
||||
mod([fn("%main", [{ kind: "if", cond, then: [ret], else_: null, loc }, stackRead])]),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("functions referenced only from dead code are unreached", () => {
|
||||
// The production React shape: a DEV-only body behind a throw stub calls
|
||||
// the stack parser, and a closure created after the stub reaches it too.
|
||||
const closure: IrStmt = {
|
||||
loc,
|
||||
kind: "assign",
|
||||
localId: "replacer",
|
||||
value: {
|
||||
loc,
|
||||
kind: "closure",
|
||||
fnName: "replacer",
|
||||
captures: [],
|
||||
type: { kind: "func", params: [], ret: VOID },
|
||||
},
|
||||
};
|
||||
const program = mod([
|
||||
fn("%main", [call("outline")]),
|
||||
fn("outline", [throwStmt, closure, call("parse")]),
|
||||
fn("replacer", [call("parse")]),
|
||||
fn("parse", [stackRead]),
|
||||
]);
|
||||
expect(observesStacks(program)).toBe(false);
|
||||
// The same closure created before the stub keeps the read reachable.
|
||||
const live = mod([
|
||||
fn("%main", [call("outline")]),
|
||||
fn("outline", [closure, throwStmt]),
|
||||
fn("replacer", [call("parse")]),
|
||||
fn("parse", [stackRead]),
|
||||
]);
|
||||
expect(observesStacks(live)).toBe(true);
|
||||
});
|
||||
|
||||
test("class methods are roots", () => {
|
||||
const program = mod([fn("%main", [ret]), fn("%Parser.read", [stackRead])], {
|
||||
classes: [
|
||||
{ name: "Parser", fields: [], methods: ["read"] } as unknown as NonNullable<
|
||||
IrModule["classes"]
|
||||
>[number],
|
||||
],
|
||||
});
|
||||
expect(observesStacks(program)).toBe(true);
|
||||
});
|
||||
|
||||
test("leaf functions that cannot create an Error need no frame", () => {
|
||||
const num = (value: number): IrExpr => ({ loc, kind: "numLit", value, type: F64 });
|
||||
const add: IrStmt = {
|
||||
loc,
|
||||
kind: "return",
|
||||
value: { loc, kind: "bin", op: "+", left: num(1), right: num(2), type: F64 },
|
||||
};
|
||||
const construct: IrStmt = {
|
||||
loc,
|
||||
kind: "exprStmt",
|
||||
expr: { loc, kind: "libCall", fn: "error.new", args: [], type: ERROR },
|
||||
};
|
||||
const program = mod([
|
||||
fn("%main", [call("leaf"), call("caller"), call("creates"), call("viaCreates")]),
|
||||
fn("leaf", [add]),
|
||||
fn("caller", [call("leaf"), add]),
|
||||
fn("creates", [construct]),
|
||||
fn("viaCreates", [call("creates")]),
|
||||
fn("recursive", [call("recursive")]),
|
||||
]);
|
||||
const free = captureFreeFunctions(program, new Set(["recursive"]), null);
|
||||
expect([...free].sort()).toEqual(["caller", "leaf"]);
|
||||
// A worker program keeps every frame.
|
||||
expect(captureFreeFunctions({ ...program, workers: true }, null, null).size).toBe(0);
|
||||
});
|
||||
@@ -0,0 +1,237 @@
|
||||
/* Whole-program stack-trace gate (see observesStacks). Pure function of the
|
||||
* IR module; the emitter keeps a source frame in every function only when
|
||||
* it answers true. */
|
||||
import { exprFunctionReference, moduleFunctionReferences } from "../ir/function-references.js";
|
||||
import { MAY_THROW_LIB_FNS } from "../ir/builtin-effects.js";
|
||||
import type { IrExpr, IrFunction, IrModule, IrStmt } from "../ir/ir.js";
|
||||
import { MAY_THROW_ARR_METHODS } from "../ir/ir.js";
|
||||
import { everyExprChild, everyStmtChild, everyStmtList } from "../ir/traverse.js";
|
||||
import { unreachableStatements } from "./may-throw.js";
|
||||
import type { PublishedTypes } from "./publication.js";
|
||||
import { writesPublished } from "./publication.js";
|
||||
|
||||
/** Lib calls that read a captured stack trace. */
|
||||
const STACK_READS: ReadonlySet<string> = new Set(["error.stack"]);
|
||||
|
||||
function readsStack(expr: IrExpr): boolean {
|
||||
return expr.kind === "libCall" && STACK_READS.has(expr.fn);
|
||||
}
|
||||
|
||||
function containsStackRead(fn: IrFunction): boolean {
|
||||
return !everyStmtList(fn.body, { stmt: () => true, expr: (expr) => !readsStack(expr) });
|
||||
}
|
||||
|
||||
/** Visit every expression of `body` that execution can reach: statements
|
||||
* after one that cannot complete normally in the same list are skipped. */
|
||||
function visitLiveExprs(body: IrStmt[], visit: (expr: IrExpr) => void): void {
|
||||
const dead = unreachableStatements(body);
|
||||
const expr = (node: IrExpr): boolean => {
|
||||
visit(node);
|
||||
return everyExprChild(node, expr, stmt);
|
||||
};
|
||||
const stmt = (node: IrStmt): boolean => dead.has(node) || everyStmtChild(node, expr, stmt);
|
||||
body.every(stmt);
|
||||
}
|
||||
|
||||
/** Whether the program can read a captured stack trace, which is what makes
|
||||
* every function keep a source frame. A read counts when it can execute:
|
||||
* it sits in live code of a function reachable from the module's roots (its
|
||||
* entry, library exports, class prototype helpers and every class's
|
||||
* methods) through live code. Code after a statement that cannot complete
|
||||
* normally is not live: production builds of libraries such as React's
|
||||
* Flight server keep development-only stack parsing behind `throw` stubs,
|
||||
* and those reads must not cost every call of the program a frame.
|
||||
* Programs without any stack read answer false without the walk. */
|
||||
export function observesStacks(mod: IrModule): boolean {
|
||||
if (!mod.functions.some(containsStackRead)) return false;
|
||||
const byName = new Map(mod.functions.map((fn) => [fn.name, fn]));
|
||||
const classNames = new Set((mod.classes ?? []).map((cls) => cls.name));
|
||||
const pending: string[] = [...moduleFunctionReferences(mod)];
|
||||
// Class methods, accessors and constructors are reachable through
|
||||
// dispatch the IR does not name per site: all of them are roots.
|
||||
for (const fn of mod.functions) {
|
||||
if (!fn.name.startsWith("%")) continue;
|
||||
for (let dot = fn.name.indexOf("."); dot !== -1; dot = fn.name.indexOf(".", dot + 1)) {
|
||||
if (classNames.has(fn.name.slice(1, dot))) {
|
||||
pending.push(fn.name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const reached = new Set<string>();
|
||||
let observed = false;
|
||||
while (pending.length > 0 && !observed) {
|
||||
const name = pending.pop()!;
|
||||
if (reached.has(name)) continue;
|
||||
reached.add(name);
|
||||
const fn = byName.get(name);
|
||||
if (fn === undefined) continue;
|
||||
visitLiveExprs(fn.body, (expr) => {
|
||||
if (readsStack(expr)) observed = true;
|
||||
if (expr.kind === "call" && !reached.has(expr.callee)) pending.push(expr.callee);
|
||||
const referenced = exprFunctionReference(expr);
|
||||
if (referenced !== undefined && !reached.has(referenced)) pending.push(referenced);
|
||||
});
|
||||
}
|
||||
return observed;
|
||||
}
|
||||
|
||||
/** Expressions that neither create an Error nor throw one by themselves.
|
||||
* Calls, `new` and virtual calls are capture-free when their callees are. */
|
||||
const CAPTURE_FREE_EXPRS: ReadonlySet<IrExpr["kind"]> = new Set([
|
||||
"numLit",
|
||||
"boolLit",
|
||||
"strLit",
|
||||
"unitLit",
|
||||
"varRef",
|
||||
"incDec",
|
||||
"assignExpr",
|
||||
"bin",
|
||||
"unary",
|
||||
"logical",
|
||||
"ternary",
|
||||
"nullish",
|
||||
"toBool",
|
||||
"fieldGet",
|
||||
"recordGet",
|
||||
"arrayGet",
|
||||
"arrayHas",
|
||||
"arrayState",
|
||||
"strCmp",
|
||||
"strEq",
|
||||
"strConcat",
|
||||
"upcast",
|
||||
"unionDisc",
|
||||
"unionIsTag",
|
||||
"unionEq",
|
||||
"instanceOf",
|
||||
"recordLit",
|
||||
"arrayLit",
|
||||
"unionWrap",
|
||||
"unionNarrow",
|
||||
"libCall",
|
||||
"arrIntrinsic",
|
||||
"call",
|
||||
"new",
|
||||
"virtualCall",
|
||||
]);
|
||||
|
||||
/** Statements that neither create an Error nor throw one by themselves. */
|
||||
const CAPTURE_FREE_STMTS: ReadonlySet<IrStmt["kind"]> = new Set([
|
||||
"varDecl",
|
||||
"assign",
|
||||
"exprStmt",
|
||||
"if",
|
||||
"while",
|
||||
"doWhile",
|
||||
"for",
|
||||
"switch",
|
||||
"block",
|
||||
"return",
|
||||
"break",
|
||||
"continue",
|
||||
"fieldSet",
|
||||
"recordSet",
|
||||
"arraySet",
|
||||
]);
|
||||
|
||||
/** Functions whose frame no stack capture can ever include: a capture runs
|
||||
* where an Error is created (by the program or by a throwing runtime
|
||||
* operation), and these functions only perform operations from a fixed
|
||||
* list that never create one and only call functions of the same kind.
|
||||
* Their frames are therefore never observable and the emitter omits them,
|
||||
* which keeps small leaf methods and helpers free of the per-call frame
|
||||
* push in programs that read stacks. Recursive functions (the stack guard
|
||||
* throws RangeError), async and generator bodies, and every function of a
|
||||
* worker program keep their frames. */
|
||||
export function captureFreeFunctions(
|
||||
mod: IrModule,
|
||||
stackChecks: ReadonlySet<string> | null,
|
||||
published: PublishedTypes | null,
|
||||
): Set<string> {
|
||||
const free = new Set<string>();
|
||||
if (mod.workers === true) return free;
|
||||
const methodImpls = new Map<string, string[]>();
|
||||
for (const cls of mod.classes ?? []) {
|
||||
for (const m of cls.methods ?? []) {
|
||||
let list = methodImpls.get(m);
|
||||
if (!list) methodImpls.set(m, (list = []));
|
||||
list.push(`%${cls.name}.${m}`);
|
||||
}
|
||||
}
|
||||
const tdzGlobals = new Set((mod.globals ?? []).filter((g) => g.tdz).map((g) => g.id));
|
||||
const callees = new Map<string, string[]>();
|
||||
for (const fn of mod.functions) {
|
||||
if (fn.async === true || fn.generator !== undefined || stackChecks?.has(fn.name)) continue;
|
||||
const tdz = new Set([...tdzGlobals, ...fn.locals.filter((l) => l.tdz).map((l) => l.id)]);
|
||||
const calls: string[] = [];
|
||||
const bigint = (e: IrExpr): boolean => e.type.kind === "bigint";
|
||||
const expr = (e: IrExpr): boolean => {
|
||||
if (!CAPTURE_FREE_EXPRS.has(e.kind)) return false;
|
||||
switch (e.kind) {
|
||||
case "varRef":
|
||||
case "incDec":
|
||||
case "assignExpr":
|
||||
if (tdz.has(e.localId)) return false;
|
||||
break;
|
||||
case "bin":
|
||||
// BigInt division and remainder by zero throw RangeError.
|
||||
if (bigint(e) || bigint(e.left) || bigint(e.right)) return false;
|
||||
break;
|
||||
case "unary":
|
||||
if (bigint(e) || bigint(e.operand)) return false;
|
||||
break;
|
||||
case "libCall":
|
||||
// Math functions only: they neither throw nor call back.
|
||||
if (!e.fn.startsWith("math.") || MAY_THROW_LIB_FNS.has(e.fn)) return false;
|
||||
break;
|
||||
case "arrIntrinsic": {
|
||||
// Methods that cannot throw, over numbers, booleans or strings
|
||||
// (no element conversion can run program code) and without a
|
||||
// callback argument.
|
||||
const elem = e.receiver.type.kind === "array" ? e.receiver.type.elem.kind : null;
|
||||
if (
|
||||
MAY_THROW_ARR_METHODS.has(e.method) ||
|
||||
(elem !== "f64" && elem !== "bool" && elem !== "string") ||
|
||||
e.args.some((arg) => arg.type.kind === "func")
|
||||
)
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
case "call":
|
||||
calls.push(e.callee);
|
||||
break;
|
||||
case "new":
|
||||
calls.push(`%${e.className}.constructor`);
|
||||
break;
|
||||
case "virtualCall": {
|
||||
const impls = methodImpls.get(e.method);
|
||||
if (impls === undefined) return false;
|
||||
calls.push(...impls);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return everyExprChild(e, expr, stmt);
|
||||
};
|
||||
const stmt = (s: IrStmt): boolean => {
|
||||
if (!CAPTURE_FREE_STMTS.has(s.kind)) return false;
|
||||
if (s.kind === "assign" && tdz.has(s.localId)) return false;
|
||||
if (published !== null && writesPublished(published, s)) return false;
|
||||
return everyStmtChild(s, expr, stmt);
|
||||
};
|
||||
if (fn.body.every(stmt)) callees.set(fn.name, calls);
|
||||
}
|
||||
// Drop candidates that call a function outside the set until none do.
|
||||
for (const name of callees.keys()) free.add(name);
|
||||
let changed = true;
|
||||
while (changed) {
|
||||
changed = false;
|
||||
for (const [name, calls] of callees) {
|
||||
if (free.has(name) && !calls.every((callee) => free.has(callee))) {
|
||||
free.delete(name);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return free;
|
||||
}
|
||||
@@ -285,6 +285,11 @@ async function main(args: string[], host: CliHost): Promise<number> {
|
||||
}
|
||||
externalTypes[specifier] = declarationPath;
|
||||
}
|
||||
const wasmArgs = values.wasm ?? [];
|
||||
if (wasmArgs.length > 0 && command !== "coverage") {
|
||||
fail(`--wasm is a coverage-only option\n\n${USAGE}`);
|
||||
}
|
||||
const wasmFiles = wasmArgs.map((file) => resolve(file));
|
||||
const ffiProfilePath = values.ffi !== undefined ? resolve(values.ffi) : undefined;
|
||||
if (values.backend !== undefined && values.backend !== "llvm") {
|
||||
fail(`unknown backend "${values.backend}" (supported: llvm)\n\n${USAGE}`);
|
||||
@@ -364,6 +369,7 @@ async function main(args: string[], host: CliHost): Promise<number> {
|
||||
...(Object.keys(externalTypes).length > 0 ? { externalTypes } : {}),
|
||||
...(tier !== undefined ? { tier } : {}),
|
||||
...(explainDynamic !== undefined ? { explainDynamic: true } : {}),
|
||||
...(wasmFiles.length > 0 ? { wasm: wasmFiles } : {}),
|
||||
});
|
||||
if (printDiagnostics) {
|
||||
// Keep stdout pure JSON for tooling.
|
||||
|
||||
@@ -80,6 +80,11 @@ Options:
|
||||
values and the bindings, fields, functions, classes and
|
||||
modules the dynamic tier would take over, with the
|
||||
refusal behind each; a name shows only that unit's chain
|
||||
--wasm <file.wasm>
|
||||
coverage only: report a WebAssembly module the program
|
||||
loads whose path its sources do not spell (repeatable;
|
||||
modules the sources name, inline modules and packaged
|
||||
.wasm files are found without it)
|
||||
-h, --help show this help
|
||||
-v, --version print the version
|
||||
`;
|
||||
@@ -103,6 +108,7 @@ export const CLI_OPTIONS = {
|
||||
"external-types": { type: "string", multiple: true },
|
||||
tier: { type: "string" },
|
||||
"explain-dynamic": { type: "string" },
|
||||
wasm: { type: "string", multiple: true },
|
||||
lib: { type: "boolean", default: false },
|
||||
profile: { type: "string" },
|
||||
help: { type: "boolean", short: "h", default: false },
|
||||
|
||||
@@ -171,6 +171,9 @@ export interface AnalyzeOptions {
|
||||
/** `--explain-dynamic`: attach the tier-assignment report
|
||||
* (CoverageInput.tiers). Such analyses bypass the verdict cache. */
|
||||
explainDynamic?: boolean;
|
||||
/** `--wasm <file>`: WebAssembly module files the program loads that its
|
||||
* sources do not name (absolute paths); reported with the discovered ones. */
|
||||
wasm?: readonly string[];
|
||||
}
|
||||
|
||||
export interface AnalyzeResult {
|
||||
|
||||
@@ -24,6 +24,7 @@ import { renderDiagnostics } from "../diagnostics/render.js";
|
||||
import type { LowerStats } from "../frontend/lowering/lowerer.js";
|
||||
import type { ProvenanceSources } from "../frontend/provenance-registry.js";
|
||||
import type { DemotionUnit, TierReport } from "../frontend/tier-analysis.js";
|
||||
import type { WasmModuleReport } from "../wasm/discover.js";
|
||||
|
||||
export interface CoverageInput {
|
||||
file: string;
|
||||
@@ -57,6 +58,9 @@ export interface CoverageInput {
|
||||
/** --explain-dynamic: the report-only tier assignment (which bindings,
|
||||
* fields, functions, classes and modules the dynamic tier would take). */
|
||||
tiers?: TierReport;
|
||||
/** The WebAssembly modules the build can see (wasm/discover.ts): their
|
||||
* size, validity and the proposals they use. */
|
||||
wasmModules?: WasmModuleReport[];
|
||||
/** True when tsc preflight failed — no coverage can be computed. */
|
||||
preflightFailed: boolean;
|
||||
}
|
||||
@@ -217,6 +221,51 @@ export function renderCoverage(input: CoverageInput, opts: CoverageRenderOptions
|
||||
out.push("");
|
||||
}
|
||||
|
||||
// The WebAssembly modules the build can see: what translating them would
|
||||
// take (wasm/discover.ts). No module compiles to native code yet; the
|
||||
// sites that load one are blockers or deferred fences above.
|
||||
const wasm = input.wasmModules ?? [];
|
||||
if (wasm.length > 0) {
|
||||
out.push(
|
||||
` ${c(DIM, "WebAssembly modules:")} ${c(DIM, "(found at build time; none compile to native code yet)")}`,
|
||||
);
|
||||
for (const m of wasm) {
|
||||
const name = m.path !== undefined ? displayPath(m.path, opts.root) : "inline module";
|
||||
const state =
|
||||
m.status === "valid"
|
||||
? c(GREEN, "valid")
|
||||
: m.status === "unsupported"
|
||||
? c(YELLOW, `uses ${m.unsupported.join(", ")}`)
|
||||
: c(RED, `${m.status}: ${m.error ?? ""}`);
|
||||
out.push(` ${name} ${c(DIM, `${groupDigits(m.size)} bytes`)} ${state}`);
|
||||
const imports = m.importModules.length > 0 ? ` (${m.importModules.join(", ")})` : "";
|
||||
out.push(
|
||||
` ${c(DIM, `${counted(m.functions, "function")}, ${counted(m.imports, "import")}${imports}, ${counted(m.exports, "export")}`)}`,
|
||||
);
|
||||
if (m.features.length > 0) out.push(` ${c(DIM, `features: ${m.features.join(", ")}`)}`);
|
||||
const shown = m.sources.slice(0, SITES_SHOWN);
|
||||
for (const s of shown) {
|
||||
const at =
|
||||
s.line !== undefined
|
||||
? `${displayPath(s.file, opts.root)}:${s.line}`
|
||||
: displayPath(s.file, opts.root);
|
||||
const how =
|
||||
s.kind === "path"
|
||||
? `path at ${at}`
|
||||
: s.kind === "inline"
|
||||
? `inline at ${at}`
|
||||
: s.kind === "package"
|
||||
? `shipped by the package at ${displayPath(dirOf(s.file), opts.root)}`
|
||||
: `configured by ${at}`;
|
||||
out.push(` ${c(DIM, `found: ${how}`)}`);
|
||||
}
|
||||
if (m.sources.length > shown.length) {
|
||||
out.push(` ${c(DIM, `found: +${m.sources.length - shown.length} more`)}`);
|
||||
}
|
||||
}
|
||||
out.push("");
|
||||
}
|
||||
|
||||
// --provenance-sources: per-package attribution — the answer to "did
|
||||
// the DEPENDENCY's statements compile static?", computed by summing
|
||||
// statsByFile entries under each package's source dir. Elided
|
||||
@@ -335,6 +384,27 @@ export function renderCoverage(input: CoverageInput, opts: CoverageRenderOptions
|
||||
return out.join("\n");
|
||||
}
|
||||
|
||||
/** "1 function", "1,698 functions". */
|
||||
function counted(n: number, noun: string): string {
|
||||
return `${groupDigits(n)} ${noun}${n === 1 ? "" : "s"}`;
|
||||
}
|
||||
|
||||
/** 637405 → "637,405". */
|
||||
function groupDigits(n: number): string {
|
||||
const digits = String(n);
|
||||
let out = "";
|
||||
for (let i = 0; i < digits.length; i++) {
|
||||
if (i > 0 && (digits.length - i) % 3 === 0) out += ",";
|
||||
out += digits[i];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function dirOf(file: string): string {
|
||||
const slash = file.lastIndexOf("/");
|
||||
return slash <= 0 ? file : file.slice(0, slash);
|
||||
}
|
||||
|
||||
function DIMPath(file: string, color: boolean): string {
|
||||
return color ? DIM + file + RESET : file;
|
||||
}
|
||||
@@ -453,6 +523,7 @@ export function coverageEnvelope(
|
||||
},
|
||||
}),
|
||||
...(input.tiers !== undefined ? { tiers: tiersEnvelope(input.tiers, locator) } : {}),
|
||||
...(input.wasmModules !== undefined ? { wasmModules: input.wasmModules } : {}),
|
||||
groups,
|
||||
diagnostics: located,
|
||||
};
|
||||
|
||||
@@ -38,7 +38,7 @@ import { InternalCompilerError } from "../errors.js";
|
||||
* harness asserts refusals match entry codes. renderSurfaceManifest is
|
||||
* byte-deterministic: entries sort by id, keys are emitted in one fixed
|
||||
* order, and the output carries no timestamps or absolute paths. */
|
||||
import { FENCE_CODES, UNSUPPORTED } from "../diagnostics/diagnostic.js";
|
||||
import { FENCE_CODES, UNSUPPORTED, webAssemblyDiag } from "../diagnostics/diagnostic.js";
|
||||
import { NODE24_FETCH_COMPAT_PROFILE } from "../compat/fetch-profile.js";
|
||||
import { SUPPORTED_BUILTIN_MODULES, SUPPORTED_NODE_MODULES } from "../frontend/builtin-modules.js";
|
||||
import {
|
||||
@@ -275,7 +275,7 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes
|
||||
status: "static",
|
||||
...(COLLECTION_ITERATOR_METHODS.has(name)
|
||||
? {
|
||||
note: "live native iterators support next(), for...of, and drains; iterator helper methods are refused",
|
||||
note: "live native iterators support next(), for...of, drains, and the iterator helper methods",
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
@@ -288,7 +288,7 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes
|
||||
status: "static",
|
||||
...(COLLECTION_ITERATOR_METHODS.has(name)
|
||||
? {
|
||||
note: "live native iterators support next(), for...of, and drains; iterator helper methods are refused",
|
||||
note: "live native iterators support next(), for...of, drains, and the iterator helper methods",
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
@@ -444,6 +444,19 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes
|
||||
}
|
||||
}
|
||||
|
||||
// ── the WebAssembly global: every reached use refuses (webAssemblyDiag) ─
|
||||
{
|
||||
const refusal = webAssemblyDiag("WebAssembly", { file: "", start: 0, end: 0 });
|
||||
add({
|
||||
id: "stdlib.webassembly",
|
||||
kind: "stdlib",
|
||||
name: "globalThis.WebAssembly",
|
||||
status: "unsupported",
|
||||
code: refusal.code,
|
||||
...(refusal.hint !== undefined ? { note: refusal.hint } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
// ── fetch/Web platform: one explicit, versioned compatibility profile ─
|
||||
const fetchTarget =
|
||||
`Node ${NODE24_FETCH_COMPAT_PROFILE.target.node} / ` +
|
||||
|
||||
@@ -14,6 +14,7 @@ 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 type { WasmModuleReport } from "../wasm/discover.js";
|
||||
import {
|
||||
FrontendInputTracker,
|
||||
frontendInputsStillMatch,
|
||||
@@ -38,6 +39,7 @@ export interface CachedCoverage {
|
||||
unreached?: { stats: LowerStats; diagnostics: ScrDiagnostic[]; runtimeFences?: ScrDiagnostic[] };
|
||||
divergences?: ScrDiagnostic[];
|
||||
npmStatic?: NpmStaticStatus[];
|
||||
wasmModules?: WasmModuleReport[];
|
||||
preflightFailed: boolean;
|
||||
}
|
||||
|
||||
@@ -125,6 +127,8 @@ export function verdictKey(
|
||||
external.join("\n"),
|
||||
// Demotion (stage S6) makes the tier mode part of the verdict.
|
||||
...(opts.tier === "static-only" ? ["tier=static-only"] : []),
|
||||
// The module files' contents are probes; their names are options.
|
||||
...(opts.wasm !== undefined && opts.wasm.length > 0 ? [`wasm=${opts.wasm.join("\n")}`] : []),
|
||||
].join("\0"),
|
||||
);
|
||||
}
|
||||
@@ -146,6 +150,7 @@ function cacheable(coverage: AnalyzeResult["coverage"]): CachedCoverage | null {
|
||||
if (coverage.unreached !== undefined) out.unreached = coverage.unreached;
|
||||
if (coverage.divergences !== undefined) out.divergences = coverage.divergences;
|
||||
if (coverage.npmStatic !== undefined) out.npmStatic = coverage.npmStatic;
|
||||
if (coverage.wasmModules !== undefined) out.wasmModules = coverage.wasmModules;
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -162,6 +167,7 @@ function restore(cached: CachedCoverage, sources: CachedSource[]): AnalyzeResult
|
||||
if (cached.unreached !== undefined) coverage.unreached = cached.unreached;
|
||||
if (cached.divergences !== undefined) coverage.divergences = cached.divergences;
|
||||
if (cached.npmStatic !== undefined) coverage.npmStatic = cached.npmStatic;
|
||||
if (cached.wasmModules !== undefined) coverage.wasmModules = cached.wasmModules;
|
||||
return { coverage, sourceTexts };
|
||||
}
|
||||
|
||||
|
||||
@@ -751,6 +751,20 @@ export function noLoweringDiag(
|
||||
};
|
||||
}
|
||||
|
||||
/** A REACHED use of the WebAssembly API (SC2020): scriptc.d.ts declares
|
||||
* its types so packages that mention them typecheck, but no module
|
||||
* compiles to native code yet. `surface` names what was used
|
||||
* ('WebAssembly.compile', 'new WebAssembly.Module', 'globalThis.WebAssembly'). */
|
||||
export function webAssemblyDiag(surface: string, loc: SrcLoc): ScrDiagnostic {
|
||||
return {
|
||||
code: "SC2020",
|
||||
message: `'${surface}' is not supported yet: scriptc does not compile WebAssembly modules to native code yet`,
|
||||
loc,
|
||||
milestone: "later",
|
||||
hint: "WebAssembly support is planned as translation of each module whose bytes the build can see (an inline literal, or a file read from a path the compiler resolves) into native code; 'scriptc coverage' lists the modules this program would load (https://scriptc.dev/limitations)",
|
||||
};
|
||||
}
|
||||
|
||||
/** A resolved native addon needs Node's embedding ABI. The migration uses the
|
||||
* underlying native operation through an ordinary C ABI declaration. */
|
||||
export function nativeAddonDiag(specifier: string, loc: SrcLoc): ScrDiagnostic {
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
* bumps it. */
|
||||
import { readFileSync } from "node:fs";
|
||||
import type { ScrDiagnostic } from "./diagnostic.js";
|
||||
import type { WasmModuleReport } from "../wasm/discover.js";
|
||||
|
||||
export const DIAGNOSTICS_SCHEMA = "scriptc-diagnostics";
|
||||
export const DIAGNOSTICS_SCHEMA_VERSION = 1;
|
||||
@@ -138,6 +139,9 @@ export interface DiagnosticsEnvelope {
|
||||
artifact?: string;
|
||||
/** coverage --explain-dynamic only: the report-only tier assignment. */
|
||||
tiers?: TiersEnvelope;
|
||||
/** coverage only: the WebAssembly modules the build can see, with their
|
||||
* size, validity, proposals and the sources that found them. */
|
||||
wasmModules?: WasmModuleReport[];
|
||||
groups: DiagnosticGroup[];
|
||||
diagnostics: LocatedDiagnostic[];
|
||||
}
|
||||
|
||||
@@ -93,6 +93,7 @@ export function analyzeWithFrontend(
|
||||
...(ffi !== null ? { ffiImports: ffi.functions } : {}),
|
||||
...(opts.tier !== undefined ? { tierMode: opts.tier } : {}),
|
||||
...(opts.explainDynamic === true ? { tierReport: { preflight } } : {}),
|
||||
...(opts.wasm !== undefined && opts.wasm.length > 0 ? { wasmFiles: opts.wasm } : {}),
|
||||
});
|
||||
const provenance = provenanceSources();
|
||||
return {
|
||||
@@ -113,6 +114,7 @@ export function analyzeWithFrontend(
|
||||
...(lowered.statsByFile ? { statsByFile: lowered.statsByFile } : {}),
|
||||
...(lowered.provenanceElided ? { provenanceElided: lowered.provenanceElided } : {}),
|
||||
...(lowered.tiers !== undefined ? { tiers: lowered.tiers } : {}),
|
||||
...(lowered.wasmModules !== undefined ? { wasmModules: lowered.wasmModules } : {}),
|
||||
preflightFailed: false,
|
||||
},
|
||||
sourceTexts: fe.sourceTexts(),
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { type Lowerer, PoisonError } from "../lowerer.js";
|
||||
import { locOf } from "../../program.js";
|
||||
import { webAssemblyDiag } from "../../../diagnostics/diagnostic.js";
|
||||
import { STRING, type IrExpr } from "../../../ir/ir.js";
|
||||
import { stdlibGlobalNameOf } from "../surfaces.js";
|
||||
|
||||
/* The WebAssembly API. scriptc.d.ts declares its types (lib.dom's
|
||||
* namespace) so that packages whose declarations mention them typecheck,
|
||||
* and the type mapper gives those types the checked-dynamic representation.
|
||||
* No module compiles to native code yet, so every reached use of the API
|
||||
* value fences at its expression with webAssemblyDiag: a build error in
|
||||
* TypeScript sources, a located runtime fence in JavaScript ones. typeof
|
||||
* needs no value and folds to Node's answer. */
|
||||
|
||||
/** Strips the spellings that leave a value unchanged: parentheses, type
|
||||
* assertions, `satisfies` and non-null assertions. */
|
||||
function bare(expr: ts.Expression): ts.Expression {
|
||||
for (;;) {
|
||||
if (ts.isParenthesizedExpression(expr) || ts.isNonNullExpression(expr)) expr = expr.expression;
|
||||
else if (ts.isAsExpression(expr) || ts.isTypeAssertion(expr)) expr = expr.expression;
|
||||
else if (ts.isSatisfiesExpression(expr)) expr = expr.expression;
|
||||
else return expr;
|
||||
}
|
||||
}
|
||||
|
||||
/** The global object itself, as `globalThis` or `global` spells it. */
|
||||
function isGlobalThis(lowerer: Lowerer, expr: ts.Expression): boolean {
|
||||
const node = bare(expr);
|
||||
return (
|
||||
ts.isIdentifier(node) &&
|
||||
(node.text === "globalThis" || node.text === "global") &&
|
||||
stdlibGlobalNameOf(lowerer, node) === "globalThis"
|
||||
);
|
||||
}
|
||||
|
||||
/** THE WebAssembly namespace value: `WebAssembly` or `globalThis.WebAssembly`
|
||||
* (and the global object reached through a cast, as in
|
||||
* `(globalThis as any).WebAssembly`). */
|
||||
function isWebAssemblyGlobal(lowerer: Lowerer, expr: ts.Expression): boolean {
|
||||
expr = bare(expr);
|
||||
if (ts.isIdentifier(expr)) {
|
||||
return expr.text === "WebAssembly" && stdlibGlobalNameOf(lowerer, expr) === "WebAssembly";
|
||||
}
|
||||
if (ts.isPropertyAccessExpression(expr)) {
|
||||
return expr.name.text === "WebAssembly" && isGlobalThis(lowerer, expr.expression);
|
||||
}
|
||||
if (ts.isElementAccessExpression(expr)) {
|
||||
const key = expr.argumentExpression;
|
||||
return (
|
||||
(ts.isStringLiteral(key) || ts.isNoSubstitutionTemplateLiteral(key)) &&
|
||||
key.text === "WebAssembly" &&
|
||||
isGlobalThis(lowerer, expr.expression)
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** The surface a WebAssembly API expression names ('WebAssembly',
|
||||
* 'globalThis.WebAssembly', 'WebAssembly.compile'), or null. A cheap
|
||||
* spelling check runs first: almost every expression is rejected without a
|
||||
* checker query. */
|
||||
function webAssemblySurface(lowerer: Lowerer, expr: ts.Expression): string | null {
|
||||
// `globalThis as unknown as { WebAssembly: ... }`: the cast's target type
|
||||
// names the member, and the value it checks is the global object's
|
||||
// WebAssembly (the spelling pi's codemode uses).
|
||||
if (
|
||||
(ts.isAsExpression(expr) || ts.isTypeAssertion(expr)) &&
|
||||
isGlobalThis(lowerer, expr.expression)
|
||||
) {
|
||||
const target = lowerer.checker.getTypeAtLocation(expr.type);
|
||||
return lowerer.checker.getPropertyOfType(target, "WebAssembly") !== undefined
|
||||
? "globalThis.WebAssembly"
|
||||
: null;
|
||||
}
|
||||
const node = bare(expr);
|
||||
if (ts.isIdentifier(node)) {
|
||||
return node.text === "WebAssembly" && isWebAssemblyGlobal(lowerer, node) ? "WebAssembly" : null;
|
||||
}
|
||||
if (ts.isPropertyAccessExpression(node)) {
|
||||
if (node.name.text === "WebAssembly") {
|
||||
return isWebAssemblyGlobal(lowerer, node) ? "globalThis.WebAssembly" : null;
|
||||
}
|
||||
// WebAssembly.<member>, and members of members
|
||||
// (WebAssembly.Module.customSections).
|
||||
const names = [node.name.text];
|
||||
let receiver = bare(node.expression);
|
||||
while (ts.isPropertyAccessExpression(receiver) && receiver.name.text !== "WebAssembly") {
|
||||
names.unshift(receiver.name.text);
|
||||
receiver = bare(receiver.expression);
|
||||
}
|
||||
const spelled =
|
||||
(ts.isIdentifier(receiver) && receiver.text === "WebAssembly") ||
|
||||
(ts.isPropertyAccessExpression(receiver) && receiver.name.text === "WebAssembly");
|
||||
return spelled && isWebAssemblyGlobal(lowerer, receiver)
|
||||
? `WebAssembly.${names.join(".")}`
|
||||
: null;
|
||||
}
|
||||
if (ts.isElementAccessExpression(node)) {
|
||||
return isWebAssemblyGlobal(lowerer, node) ? "globalThis.WebAssembly" : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Fences a reached use of the WebAssembly API. `prefix` qualifies a
|
||||
* construction ('new '). Returns normally when `expr` is anything else. */
|
||||
export function refuseWebAssemblyUse(lowerer: Lowerer, expr: ts.Expression, prefix = ""): void {
|
||||
const surface = webAssemblySurface(lowerer, expr);
|
||||
if (surface === null) return;
|
||||
lowerer.pushDiag(webAssemblyDiag(`${prefix}${surface}`, locOf(expr)));
|
||||
throw new PoisonError();
|
||||
}
|
||||
|
||||
/** `typeof WebAssembly` is "object" and `typeof WebAssembly.<member>` is
|
||||
* "function" for every member the namespace declares, as in Node. The
|
||||
* operand is an effect-free global read, so nothing else evaluates. */
|
||||
export function lowerWebAssemblyTypeof(lowerer: Lowerer, operand: ts.Expression): IrExpr | null {
|
||||
const surface = webAssemblySurface(lowerer, operand);
|
||||
if (surface === null) return null;
|
||||
const loc = locOf(operand);
|
||||
if (surface === "WebAssembly" || surface === "globalThis.WebAssembly") {
|
||||
return { kind: "strLit", value: "object", type: STRING, loc };
|
||||
}
|
||||
const t = lowerer.typeOf(operand);
|
||||
if (
|
||||
lowerer.checker.getCallSignatures(t).length > 0 ||
|
||||
lowerer.checker.getConstructSignatures(t).length > 0
|
||||
) {
|
||||
return { kind: "strLit", value: "function", type: STRING, loc };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -510,6 +510,24 @@ export function requireExactArityValue(
|
||||
? lowerer.mapTypeOf(lowerer.typeOf(contextual))
|
||||
: null;
|
||||
if (mapped && typeEquals(mapped, funcType)) return;
|
||||
// An object-literal METHOD has no contextual type of its own: the
|
||||
// literal's contextual type names the member slot it fills. An
|
||||
// overloaded member's slot holds its erased function, which spells the
|
||||
// completed parameters of an implementation like `m(a?: string)`.
|
||||
if (
|
||||
contextual === null &&
|
||||
ts.isMethodDeclaration(blame) &&
|
||||
blame.parent !== undefined &&
|
||||
ts.isObjectLiteralExpression(blame.parent) &&
|
||||
(ts.isIdentifier(blame.name) || ts.isStringLiteral(blame.name))
|
||||
) {
|
||||
const literalType = lowerer.checker.getContextualType(blame.parent);
|
||||
const member = literalType
|
||||
? lowerer.checker.getPropertyOfType(literalType, blame.name.text)
|
||||
: undefined;
|
||||
const slot = member ? lowerer.memberSlotTypeOf(lowerer.checker.getTypeOfSymbol(member)) : null;
|
||||
if (slot && typeEquals(slot, funcType)) return;
|
||||
}
|
||||
// A union-typed slot (`runner || defaultRunner` under a
|
||||
// `CommandRunner | undefined` context): the value can only inhabit
|
||||
// the union's one func arm — judge by it.
|
||||
|
||||
@@ -0,0 +1,430 @@
|
||||
/* A CommonJS module's own exports object, seen from inside the module.
|
||||
*
|
||||
* The compiled module system has no reflective exports object: each export
|
||||
* is a module global (collectGlobals) or alias plumbing to a declaration.
|
||||
* Inside the module that is enough for
|
||||
*
|
||||
* - member reads and member calls (`exports.f`, `exports.f(x)`,
|
||||
* `module.exports.f(x)`): they are the member's export global, the
|
||||
* storage every `exports.f =` statement assigns;
|
||||
* - identity comparisons of the object itself (`root === module.exports`,
|
||||
* `exports === module.exports`): while the module never replaces
|
||||
* `module.exports` and never rebinds `exports`, both name the module's
|
||||
* original exports object, which top-level `this` already represents
|
||||
* (lowerCjsTopLevelThis). `typeof` and truthiness keep their existing
|
||||
* lowerings.
|
||||
*
|
||||
* Every other use of the object as a value (passing it to a function,
|
||||
* storing it, reading a member through an alias) would need a real object
|
||||
* whose properties are the exports; those uses keep a named refusal. The
|
||||
* dynamic CommonJS module object is the two-tier design's S9 stage. */
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import {
|
||||
cjsExportAssignmentOf,
|
||||
cjsExportTargetLiteral,
|
||||
isCjsJsFile,
|
||||
isModuleExportsAccess,
|
||||
isRequireStatement,
|
||||
locOf,
|
||||
} from "../program.js";
|
||||
import { isCjsModuleGlobal } from "./lower-node-module.js";
|
||||
import { DYN, type IrExpr, type IrGlobal } from "../../ir/ir.js";
|
||||
|
||||
/** True for the CommonJS wrapper's `exports` binding (not shadowed by any
|
||||
* source declaration) in a CommonJS JS file. */
|
||||
export function isCjsExportsIdentifier(lowerer: Lowerer, node: ts.Expression): boolean {
|
||||
if (!ts.isIdentifier(node) || node.text !== "exports") return false;
|
||||
if (!isCjsJsFile(node.getSourceFile(), lowerer.program)) return false;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(node);
|
||||
const decls = symbol ? lowerer.checker.declarationsOf(symbol) : [];
|
||||
return !decls.some(
|
||||
(d) =>
|
||||
!d.getSourceFile().isDeclarationFile &&
|
||||
(ts.isVariableDeclaration(d) ||
|
||||
ts.isParameter(d) ||
|
||||
ts.isBindingElement(d) ||
|
||||
ts.isFunctionDeclaration(d) ||
|
||||
ts.isClassDeclaration(d) ||
|
||||
ts.isImportSpecifier(d) ||
|
||||
ts.isNamespaceImport(d) ||
|
||||
ts.isImportEqualsDeclaration(d)),
|
||||
);
|
||||
}
|
||||
|
||||
/** `module.exports` over the CommonJS `module` global in a CommonJS file. */
|
||||
export function isCjsModuleExports(lowerer: Lowerer, node: ts.Expression): boolean {
|
||||
return (
|
||||
isModuleExportsAccess(node) &&
|
||||
isCjsModuleGlobal(lowerer, node.expression) &&
|
||||
isCjsJsFile(node.getSourceFile(), lowerer.program)
|
||||
);
|
||||
}
|
||||
|
||||
/** The export global behind a same-module member access `exports.<name>` /
|
||||
* `module.exports.<name>`, or null (identifier-valued members are alias
|
||||
* plumbing with no storage, and other receivers are not the exports
|
||||
* object). */
|
||||
export function cjsInFileExportGlobalOf(
|
||||
lowerer: Lowerer,
|
||||
access: ts.PropertyAccessExpression,
|
||||
): IrGlobal | null {
|
||||
if (access.questionDotToken || !ts.isIdentifier(access.name)) return null;
|
||||
const sf = access.getSourceFile();
|
||||
const recv =
|
||||
isModuleExportsAccess(access.expression) ||
|
||||
(ts.isIdentifier(access.expression) &&
|
||||
access.expression.text === "exports" &&
|
||||
!lowerer.resolveLocal(access.expression) &&
|
||||
!lowerer.globalOf(access.expression));
|
||||
if (!recv || !isCjsJsFile(sf, lowerer.program)) return null;
|
||||
const sym =
|
||||
lowerer.checker.getSymbolAtLocation(access.name) ??
|
||||
lowerer.cjsModuleExportSymbol(sf, access.name.text);
|
||||
const resolved =
|
||||
sym !== undefined && sym.flags & ts.SymbolFlags.Alias
|
||||
? lowerer.checker.getAliasedSymbol(sym)
|
||||
: sym;
|
||||
return (
|
||||
(sym ? lowerer.globalsBySymbol.get(sym) : undefined) ??
|
||||
(resolved ? lowerer.globalsBySymbol.get(resolved) : undefined) ??
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
/** Whether `node` is an operand of an equality operator: only its identity
|
||||
* is observed. */
|
||||
function identityUse(node: ts.Expression): boolean {
|
||||
let child: ts.Node = node;
|
||||
let parent = node.parent;
|
||||
while (parent !== undefined && ts.isParenthesizedExpression(parent)) {
|
||||
child = parent;
|
||||
parent = parent.parent;
|
||||
}
|
||||
if (parent === undefined || !ts.isBinaryExpression(parent)) return false;
|
||||
const op = parent.operatorToken.kind;
|
||||
return (
|
||||
(op === ts.SyntaxKind.EqualsEqualsEqualsToken ||
|
||||
op === ts.SyntaxKind.ExclamationEqualsEqualsToken ||
|
||||
op === ts.SyntaxKind.EqualsEqualsToken ||
|
||||
op === ts.SyntaxKind.ExclamationEqualsToken) &&
|
||||
(parent.left === child || parent.right === child)
|
||||
);
|
||||
}
|
||||
|
||||
/** Uses whose answer does not need the object: `typeof`, `void`, and
|
||||
* truthiness tests (their existing lowerings answer them). */
|
||||
function answeredWithoutObject(node: ts.Expression): boolean {
|
||||
let child: ts.Node = node;
|
||||
let parent = node.parent;
|
||||
while (parent !== undefined && ts.isParenthesizedExpression(parent)) {
|
||||
child = parent;
|
||||
parent = parent.parent;
|
||||
}
|
||||
if (parent === undefined) return false;
|
||||
if (ts.isTypeOfExpression(parent) || ts.isVoidExpression(parent)) return true;
|
||||
return truthinessOnly(child as ts.Expression);
|
||||
}
|
||||
|
||||
function truthinessOnly(node: ts.Expression): boolean {
|
||||
let child: ts.Node = node;
|
||||
for (let parent = node.parent; parent !== undefined; child = parent, parent = parent.parent) {
|
||||
if (ts.isParenthesizedExpression(parent)) continue;
|
||||
if (ts.isPrefixUnaryExpression(parent))
|
||||
return parent.operator === ts.SyntaxKind.ExclamationToken;
|
||||
if (
|
||||
ts.isBinaryExpression(parent) &&
|
||||
(parent.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ||
|
||||
parent.operatorToken.kind === ts.SyntaxKind.BarBarToken)
|
||||
)
|
||||
continue;
|
||||
if (ts.isIfStatement(parent) || ts.isWhileStatement(parent) || ts.isDoStatement(parent))
|
||||
return parent.expression === child;
|
||||
if (ts.isForStatement(parent)) return parent.condition === child;
|
||||
if (ts.isConditionalExpression(parent)) return parent.condition === child;
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Which of the module's exports bindings the file ever assigns (any
|
||||
* nesting, compound operators included), cached per file on the lowerer
|
||||
* (Lowerer.cjsExportsAssignments). */
|
||||
function exportsAssignments(
|
||||
lowerer: Lowerer,
|
||||
sf: ts.SourceFile,
|
||||
): { exports: boolean; moduleExports: boolean } {
|
||||
const cached = lowerer.cjsExportsAssignments.get(sf);
|
||||
if (cached !== undefined) return cached;
|
||||
const out = { exports: false, moduleExports: false };
|
||||
lowerer.cjsExportsAssignments.set(sf, out);
|
||||
ts.walkPreorder(sf, (node) => {
|
||||
// A fresh `exports = module.exports = {}` reset leaves both bindings
|
||||
// naming one (new, empty) exports object for the rest of the module.
|
||||
if (ts.isExpressionStatement(node) && isFreshExportsReset(lowerer, node)) return "skip";
|
||||
if (
|
||||
!ts.isBinaryExpression(node) ||
|
||||
node.operatorToken.kind < ts.SyntaxKind.FirstAssignment ||
|
||||
node.operatorToken.kind > ts.SyntaxKind.LastAssignment
|
||||
)
|
||||
return undefined;
|
||||
let target = node.left;
|
||||
while (ts.isParenthesizedExpression(target)) target = target.expression;
|
||||
if (isCjsModuleExports(lowerer, target)) out.moduleExports = true;
|
||||
if (isCjsExportsIdentifier(lowerer, target)) out.exports = true;
|
||||
return undefined;
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Whether `node` still names the module's ORIGINAL exports object wherever
|
||||
* it runs: `exports` unless the file rebinds it; `module.exports` unless the
|
||||
* file rebinds `exports` or replaces `module.exports`. */
|
||||
function namesOriginalExports(lowerer: Lowerer, node: ts.Expression): boolean {
|
||||
const assigned = exportsAssignments(lowerer, node.getSourceFile());
|
||||
if (assigned.exports) return false;
|
||||
return ts.isIdentifier(node) || !assigned.moduleExports;
|
||||
}
|
||||
|
||||
/** The uses this module answers with the original exports object: identity
|
||||
* comparisons and `typeof` of either spelling, and truthiness or `void` of
|
||||
* `exports` (`module.exports` truthiness keeps its existing fold). */
|
||||
function objectAnsweredUse(node: ts.Expression): boolean {
|
||||
if (identityUse(node)) return true;
|
||||
let parent = node.parent;
|
||||
while (parent !== undefined && ts.isParenthesizedExpression(parent)) parent = parent.parent;
|
||||
if (parent !== undefined && ts.isTypeOfExpression(parent)) return true;
|
||||
return ts.isIdentifier(node) && answeredWithoutObject(node);
|
||||
}
|
||||
|
||||
/** Whether `node` (`exports` or `module.exports`) is a use of the module's
|
||||
* exports object as a VALUE (not a member access receiver, not an
|
||||
* assignment target). */
|
||||
export function isCjsExportsObjectValue(lowerer: Lowerer, node: ts.Expression): boolean {
|
||||
if (!isCjsExportsIdentifier(lowerer, node) && !isCjsModuleExports(lowerer, node)) return false;
|
||||
let outer: ts.Node = node;
|
||||
while (outer.parent !== undefined && ts.isParenthesizedExpression(outer.parent))
|
||||
outer = outer.parent;
|
||||
const parent = outer.parent;
|
||||
if (parent === undefined) return false;
|
||||
if (
|
||||
(ts.isPropertyAccessExpression(parent) || ts.isElementAccessExpression(parent)) &&
|
||||
parent.expression === outer
|
||||
)
|
||||
return false;
|
||||
if (
|
||||
ts.isBinaryExpression(parent) &&
|
||||
parent.left === outer &&
|
||||
parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
|
||||
parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment
|
||||
)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Files whose exports object is observed by identity somewhere: their top-
|
||||
* level `this` object must exist (lowerFileInit creates it). */
|
||||
export function usesCjsExportsIdentity(lowerer: Lowerer, sf: ts.SourceFile): boolean {
|
||||
if (!isCjsJsFile(sf, lowerer.program)) return false;
|
||||
let found = false;
|
||||
ts.walkPreorder(sf, (node) => {
|
||||
if (
|
||||
(ts.isIdentifier(node) || ts.isPropertyAccessExpression(node)) &&
|
||||
isCjsExportsObjectValue(lowerer, node) &&
|
||||
objectAnsweredUse(node) &&
|
||||
namesOriginalExports(lowerer, node)
|
||||
) {
|
||||
found = true;
|
||||
return "stop";
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
/** `exports` / `module.exports` used as a value inside its own module: the
|
||||
* original exports object for identity, typeof and truthiness, or a named
|
||||
* refusal. Null when `node` is not such a use. */
|
||||
export function lowerCjsExportsObjectValue(lowerer: Lowerer, node: ts.Expression): IrExpr | null {
|
||||
if (!isCjsExportsObjectValue(lowerer, node)) return null;
|
||||
const sf = node.getSourceFile();
|
||||
// A module that replaces module.exports answers `module.exports` reads
|
||||
// through the replacement's own machinery (the class-expression whole
|
||||
// export, the replaced-exports reads).
|
||||
if (!ts.isIdentifier(node) && exportsAssignments(lowerer, sf).moduleExports) return null;
|
||||
const spelled = ts.isIdentifier(node) ? "exports" : "module.exports";
|
||||
const original = lowerer.cjsExportsThis.get(sf);
|
||||
if (objectAnsweredUse(node) && original !== undefined && namesOriginalExports(lowerer, node)) {
|
||||
return { kind: "varRef", localId: original.id, type: DYN, loc: locOf(node) };
|
||||
}
|
||||
// `module.exports` truthiness keeps its existing fold (and anything the
|
||||
// object is not needed for keeps whatever answered it before).
|
||||
if (answeredWithoutObject(node)) return null;
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
node,
|
||||
identityUse(node)
|
||||
? `comparing '${spelled}' in a module that replaces module.exports or rebinds exports (the compiled exports are a static table, not an object with an identity)`
|
||||
: `'${spelled}' as a value other than a member access, comparison, typeof or truthiness test (the compiled exports are a static table, not an object: read ${spelled}.<name>)`,
|
||||
);
|
||||
}
|
||||
|
||||
/** `exports = module.exports = {}` (either order) as a top-level statement
|
||||
* that runs before anything could observe or extend the original exports
|
||||
* object: only directives, require statements and function declarations
|
||||
* precede it, and the module assigns neither binding anywhere else. Node
|
||||
* swaps the original empty exports object for another empty one that both
|
||||
* bindings name; the compiled module's static exports start out exactly
|
||||
* the same, so the statement is a no-op (semver's internal/re.js). */
|
||||
export function isFreshExportsReset(lowerer: Lowerer, stmt: ts.ExpressionStatement): boolean {
|
||||
const sf = stmt.parent;
|
||||
if (!ts.isSourceFile(sf) || !isCjsJsFile(sf, lowerer.program)) return false;
|
||||
let e: ts.Expression = stmt.expression;
|
||||
const targets: ts.Expression[] = [];
|
||||
for (;;) {
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
if (!ts.isBinaryExpression(e) || e.operatorToken.kind !== ts.SyntaxKind.EqualsToken) break;
|
||||
targets.push(e.left);
|
||||
e = e.right;
|
||||
}
|
||||
if (!ts.isObjectLiteralExpression(e) || e.properties.length !== 0) return false;
|
||||
if (targets.length !== 2) return false;
|
||||
const hasExports = targets.some((t) => isCjsExportsIdentifier(lowerer, t));
|
||||
const hasModuleExports = targets.some((t) => isCjsModuleExports(lowerer, t));
|
||||
if (!hasExports || !hasModuleExports) return false;
|
||||
for (const prior of sf.statements) {
|
||||
if (prior === stmt) break;
|
||||
if (ts.isFunctionDeclaration(prior) || isRequireStatement(prior)) continue;
|
||||
if (ts.isExpressionStatement(prior) && ts.isStringLiteral(prior.expression)) continue;
|
||||
return false;
|
||||
}
|
||||
let others = 0;
|
||||
ts.walkPreorder(sf, (node) => {
|
||||
if (
|
||||
!ts.isBinaryExpression(node) ||
|
||||
node.operatorToken.kind < ts.SyntaxKind.FirstAssignment ||
|
||||
node.operatorToken.kind > ts.SyntaxKind.LastAssignment
|
||||
)
|
||||
return undefined;
|
||||
let target = node.left;
|
||||
while (ts.isParenthesizedExpression(target)) target = target.expression;
|
||||
if (isCjsModuleExports(lowerer, target) || isCjsExportsIdentifier(lowerer, target)) others++;
|
||||
return undefined;
|
||||
});
|
||||
return others === 2;
|
||||
}
|
||||
|
||||
/** The export global a WRITE `exports.<name> = v` / `module.exports.<name> =
|
||||
* v` updates when it is not a top-level export statement (inside a
|
||||
* function, or in value position): only while the module keeps its
|
||||
* original exports object, which is then the object every requirer reads
|
||||
* the member from. */
|
||||
export function cjsExportMemberWriteGlobalOf(
|
||||
lowerer: Lowerer,
|
||||
target: ts.PropertyAccessExpression,
|
||||
): IrGlobal | null {
|
||||
const sf = target.getSourceFile();
|
||||
if (!isCjsJsFile(sf, lowerer.program)) return null;
|
||||
const assigned = exportsAssignments(lowerer, sf);
|
||||
if (assigned.exports || assigned.moduleExports) return null;
|
||||
return cjsInFileExportGlobalOf(lowerer, target);
|
||||
}
|
||||
|
||||
/** Whether a function body reads `this` (arrow functions inherit it; other
|
||||
* nested functions bind their own). */
|
||||
function functionUsesThis(fn: ts.FunctionLikeDeclaration): boolean {
|
||||
if (fn.body === undefined) return false;
|
||||
let found = false;
|
||||
ts.walkPreorder(fn.body, (node) => {
|
||||
if (node !== fn.body && ts.isFunctionLike(node) && !ts.isArrowFunction(node)) return "skip";
|
||||
if (node.kind === ts.SyntaxKind.ThisKeyword) {
|
||||
found = true;
|
||||
return "stop";
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
/** A function declaration, or a variable initialized with a function or
|
||||
* arrow expression, that never reads `this`. */
|
||||
function symbolIsThisFreeFunction(lowerer: Lowerer, sym: ts.Symbol | undefined): boolean {
|
||||
const decls = sym ? lowerer.checker.declarationsOf(sym) : [];
|
||||
const fn = decls.find(ts.isFunctionDeclaration);
|
||||
if (fn !== undefined) return !functionUsesThis(fn);
|
||||
const v = decls.find(ts.isVariableDeclaration);
|
||||
let init = v?.initializer;
|
||||
while (init !== undefined && ts.isParenthesizedExpression(init)) init = init.expression;
|
||||
return (
|
||||
init !== undefined &&
|
||||
(ts.isFunctionExpression(init) || ts.isArrowFunction(init)) &&
|
||||
!functionUsesThis(init)
|
||||
);
|
||||
}
|
||||
|
||||
/** True when every value the module assigns to its export `name` is a
|
||||
* function that never reads `this`. A call through the exports object
|
||||
* (`exports.f(x)`) binds `this` to that object, which the static export
|
||||
* table cannot supply, so only these calls lower; the others keep a fence. */
|
||||
export function cjsExportCallIgnoresThis(
|
||||
lowerer: Lowerer,
|
||||
sf: ts.SourceFile,
|
||||
name: string,
|
||||
depth = 0,
|
||||
): boolean {
|
||||
if (depth > 16) return false;
|
||||
const values: ts.Expression[] = [];
|
||||
ts.walkPreorder(sf, (node) => {
|
||||
if (
|
||||
ts.isBinaryExpression(node) &&
|
||||
node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
||||
ts.isPropertyAccessExpression(node.left) &&
|
||||
node.left.name.text === name &&
|
||||
(isCjsExportsIdentifier(lowerer, node.left.expression) ||
|
||||
isCjsModuleExports(lowerer, node.left.expression))
|
||||
) {
|
||||
values.push(node.right);
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
// Entries of a `module.exports = { ... }` table (direct, or a const
|
||||
// literal the statement names).
|
||||
let methodEntry: ts.MethodDeclaration | null = null;
|
||||
let shorthandOnly = false;
|
||||
for (const stmt of sf.statements) {
|
||||
const cjs = cjsExportAssignmentOf(stmt);
|
||||
if (cjs?.kind !== "table") continue;
|
||||
const table = cjs.obj ?? cjsExportTargetLiteral(cjs.expr.right, sf);
|
||||
for (const prop of table?.properties ?? []) {
|
||||
if (prop.name === undefined || !ts.isIdentifier(prop.name) || prop.name.text !== name)
|
||||
continue;
|
||||
if (ts.isPropertyAssignment(prop)) values.push(prop.initializer);
|
||||
else if (ts.isShorthandPropertyAssignment(prop)) {
|
||||
const value = lowerer.checker.getShorthandAssignmentValueSymbol(prop);
|
||||
if (!symbolIsThisFreeFunction(lowerer, value)) return false;
|
||||
shorthandOnly = true;
|
||||
} else if (ts.isMethodDeclaration(prop)) methodEntry = prop;
|
||||
}
|
||||
}
|
||||
if (methodEntry !== null) return values.length === 0 && !functionUsesThis(methodEntry);
|
||||
if (values.length === 0) return shorthandOnly;
|
||||
return values.every((v) => {
|
||||
let e = v;
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
if (ts.isFunctionExpression(e) || ts.isArrowFunction(e)) return !functionUsesThis(e);
|
||||
if (ts.isIdentifier(e)) {
|
||||
return symbolIsThisFreeFunction(lowerer, lowerer.checker.getSymbolAtLocation(e));
|
||||
}
|
||||
// Another module's member (`exports.f = require("./lib").f`, or through
|
||||
// a module binding): that module's own export values decide.
|
||||
if (ts.isPropertyAccessExpression(e) && ts.isIdentifier(e.name)) {
|
||||
const dep = ts.isIdentifier(e.expression)
|
||||
? lowerer.cjsLocalModuleBindingOf(e.expression)
|
||||
? lowerer.cjsModuleBindingDepOf(e.expression)
|
||||
: null
|
||||
: lowerer.cjsModuleRefDepOf(e.expression);
|
||||
return dep !== null && cjsExportCallIgnoresThis(lowerer, dep, e.name.text, depth + 1);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
/* Inline CommonJS `require("./x")` of a program module in expression
|
||||
* position. A top-level `const m = require("./x")` binding is alias
|
||||
* plumbing: member reads resolve to the exporter's declarations, and the
|
||||
* statement lowers to the dependency's run-once %init call. An inline
|
||||
* require is the same thing without the binding: the %init call runs at
|
||||
* exactly the call's position (Node evaluates the module there, or
|
||||
* answers from its cache), and the value is then read statically:
|
||||
*
|
||||
* - `require("./x").f` and `require("./x").f(...)` over a module whose
|
||||
* exports are a table (member exports or `module.exports = {...}`)
|
||||
* read the member like a binding would;
|
||||
* - `require("./x")` as a value over a module whose whole export is one
|
||||
* value (`module.exports = fn` / `= class {}` / `= make()` / `= 42` /
|
||||
* `= require("./y")`) answers that value.
|
||||
*
|
||||
* A table module's exports object as a value has no static
|
||||
* representation; it keeps a named refusal (a first-class CommonJS module
|
||||
* object is the two-tier design's S9 stage). */
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import {
|
||||
canonicalBuiltinModule,
|
||||
cjsExportAssignmentOf,
|
||||
cjsExportDiscardReason,
|
||||
cjsExportTargetLiteral,
|
||||
isCjsJsFile,
|
||||
isJsSourceFile,
|
||||
isModuleExportsAccess,
|
||||
locOf,
|
||||
npmStaticDepSf7,
|
||||
requireSpecOf,
|
||||
resolveImport,
|
||||
} from "../program.js";
|
||||
import { createRequireCalleeFileOf } from "./builtins/module-bindings.js";
|
||||
import { isCjsRequireGlobal } from "./lower-node-module.js";
|
||||
import { DYN, type IrExpr } from "../../ir/ir.js";
|
||||
import { nodeThrowExpr } from "../../ir/build.js";
|
||||
import { probeNodeRequireRefusal } from "../npm.js";
|
||||
|
||||
export interface InlineRequire {
|
||||
call: ts.CallExpression;
|
||||
spec: string;
|
||||
dep: ts.SourceFile;
|
||||
}
|
||||
|
||||
/** A `require("<literal>")` call of the CommonJS global in a CommonJS JS
|
||||
* file whose target is a compiled program module (relative, aliased, or an
|
||||
* opted-in --npm-static package). Builtins, JSON documents and
|
||||
* createRequire bindings answer null: their own lowerings own them. */
|
||||
export function inlineProgramRequireOf(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.Expression,
|
||||
): InlineRequire | null {
|
||||
let e = expr;
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
if (!ts.isCallExpression(e) || !ts.isIdentifier(e.expression)) return null;
|
||||
const spec = requireSpecOf(e);
|
||||
if (spec === null || canonicalBuiltinModule(spec) !== null) return null;
|
||||
const sf = e.getSourceFile();
|
||||
if (!isCjsJsFile(sf, lowerer.program)) return null;
|
||||
if (!isCjsRequireGlobal(lowerer, e.expression)) return null;
|
||||
if (createRequireCalleeFileOf(lowerer, e.expression) !== null) return null;
|
||||
const dep =
|
||||
resolveImport(lowerer.program, sf, spec, "require") ??
|
||||
npmStaticDepSf7(lowerer.program, sf, spec, "require");
|
||||
if (!dep || dep.isDeclarationFile || dep.fileName.endsWith(".json")) return null;
|
||||
return { call: e, spec, dep };
|
||||
}
|
||||
|
||||
/** The kept (last, not discarded) top-level `module.exports = <value>` of a
|
||||
* CommonJS module whose whole export is a single value, or null when its
|
||||
* exports are a table: member exports only, or a literal / const-literal /
|
||||
* Proxy-over-literal table. */
|
||||
export function cjsWholeExportValueOf(
|
||||
sf: ts.SourceFile,
|
||||
): { assign: ts.BinaryExpression; rhs: ts.Expression } | null {
|
||||
let kept: ts.BinaryExpression | null = null;
|
||||
for (const stmt of sf.statements) {
|
||||
const cjs = cjsExportAssignmentOf(stmt);
|
||||
if (cjs?.kind === "table" && cjsExportDiscardReason(stmt) === null) kept = cjs.expr;
|
||||
}
|
||||
if (kept === null) return null;
|
||||
let rhs: ts.Expression = kept.right;
|
||||
while (ts.isParenthesizedExpression(rhs)) rhs = rhs.expression;
|
||||
if (ts.isObjectLiteralExpression(rhs) || cjsExportTargetLiteral(rhs, sf) !== null) return null;
|
||||
return { assign: kept, rhs };
|
||||
}
|
||||
|
||||
/** True when `dep`'s exports, through `module.exports = require("./y")`
|
||||
* re-export chains, are a table that member reads resolve against. */
|
||||
export function cjsNamespaceModule(lowerer: Lowerer, dep: ts.SourceFile, depth = 0): boolean {
|
||||
const whole = cjsWholeExportValueOf(dep);
|
||||
if (whole === null) return true;
|
||||
const inner = inlineProgramRequireOf(lowerer, whole.rhs);
|
||||
return inner !== null && depth < 32 && cjsNamespaceModule(lowerer, inner.dep, depth + 1);
|
||||
}
|
||||
|
||||
/** An inline program-module require over a table module (member reads and
|
||||
* member calls resolve through it), or null. */
|
||||
export function inlineNamespaceRequireOf(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.Expression,
|
||||
): InlineRequire | null {
|
||||
const req = inlineProgramRequireOf(lowerer, expr);
|
||||
return req !== null && cjsNamespaceModule(lowerer, req.dep) ? req : null;
|
||||
}
|
||||
|
||||
/** `value` evaluated after the required module's run-once %init (and its
|
||||
* module-registry link) at the require's own position. */
|
||||
export function withRequireInit(lowerer: Lowerer, req: InlineRequire, value: IrExpr): IrExpr {
|
||||
if (lowerer.inlineRequireCycleCalls.has(req.call)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
req.call,
|
||||
`require() of '${req.spec}' while it is still evaluating (a require cycle: Node answers its partial exports, which the static export table does not model)`,
|
||||
);
|
||||
}
|
||||
// Every inline program require is a preflight module edge, so its module
|
||||
// has an initializer; never read its exports without evaluating it.
|
||||
if (!lowerer.initNameOf.has(req.dep)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
req.call,
|
||||
`require() of '${req.spec}' here (the module is not part of the compiled module order)`,
|
||||
);
|
||||
}
|
||||
const init = lowerer.requireInitStmt(req.spec, req.call);
|
||||
if (init === null) return value;
|
||||
return { kind: "seqExpr", stmts: [init], result: value, type: value.type, loc: value.loc };
|
||||
}
|
||||
|
||||
/** The whole-export value of `dep` read from the requirer, following
|
||||
* `module.exports = require("./y")` re-exports (the inner module already
|
||||
* evaluated inside dep's own %init). Null for table modules and for
|
||||
* whole-export shapes with no represented value. */
|
||||
export function wholeExportValue(
|
||||
lowerer: Lowerer,
|
||||
dep: ts.SourceFile,
|
||||
blame: ts.Node,
|
||||
depth = 0,
|
||||
): IrExpr | null {
|
||||
const whole = cjsWholeExportValueOf(dep);
|
||||
if (whole === null || depth > 32) return null;
|
||||
const inner = inlineProgramRequireOf(lowerer, whole.rhs);
|
||||
if (inner !== null) return wholeExportValue(lowerer, inner.dep, blame, depth + 1);
|
||||
// An identifier names a declaration of the exporting module (or a
|
||||
// builtin global): reading it IS the export, exactly what requirer
|
||||
// bindings alias to.
|
||||
if (ts.isIdentifier(whole.rhs)) return lowerer.lowerExpr(whole.rhs);
|
||||
if (ts.isClassExpression(whole.rhs)) {
|
||||
return lowerer.classValueRef(lowerer.lowerClassExpressionInfo(whole.rhs), blame);
|
||||
}
|
||||
const g = lowerer.globalsByDeclNode.get(whole.assign);
|
||||
if (g === undefined) return null;
|
||||
return { kind: "varRef", localId: g.id, type: g.type, loc: locOf(blame) };
|
||||
}
|
||||
|
||||
/** `require("./x")` as a value (`require("debug")("ns")`, `new
|
||||
* (require("./C"))()`, `{ verify: require("./verify") }`): the module's
|
||||
* %init at this position, then its whole-export value. Null when `call`
|
||||
* is not an inline program-module require. */
|
||||
export function lowerInlineRequireValue(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null {
|
||||
const req = inlineProgramRequireOf(lowerer, call);
|
||||
if (req === null) return null;
|
||||
const value = wholeExportValue(lowerer, req.dep, call, 0);
|
||||
if (value === null) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
call,
|
||||
cjsNamespaceModule(lowerer, req.dep)
|
||||
? `the exports object of '${req.spec}' as a value (its exports compile to a static table: read require(${JSON.stringify(req.spec)}).member directly, or bind const m = require(${JSON.stringify(req.spec)}) at the module's top level)`
|
||||
: `the whole export of '${req.spec}' as a value (its 'module.exports =' value has no static representation)`,
|
||||
);
|
||||
}
|
||||
return withRequireInit(lowerer, req, value);
|
||||
}
|
||||
|
||||
/** Lowers `inner` (a member read or member call whose receiver is the
|
||||
* inline require) with that require marked as handled, so the receiver
|
||||
* resolves as a module binding, then sequences the module's %init first. */
|
||||
export function lowerWithInlineRequireInit(
|
||||
lowerer: Lowerer,
|
||||
req: InlineRequire,
|
||||
inner: () => IrExpr,
|
||||
): IrExpr {
|
||||
lowerer.inlineRequireInitDone.add(req.call);
|
||||
try {
|
||||
return withRequireInit(lowerer, req, inner());
|
||||
} finally {
|
||||
lowerer.inlineRequireInitDone.delete(req.call);
|
||||
}
|
||||
}
|
||||
|
||||
/** A `require(...)` call of the CommonJS global in a CommonJS file that no
|
||||
* other lowering claimed (not a builtin, JSON document, program module or
|
||||
* createRequire binding): a package nothing installed resolves throws
|
||||
* Node's catchable MODULE_NOT_FOUND at the call, and every other form is
|
||||
* refused by name instead of as an unlowerable `require` value. */
|
||||
export function lowerUncompiledRequireCall(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
): IrExpr | null {
|
||||
if (!ts.isIdentifier(call.expression) || call.expression.text !== "require") return null;
|
||||
const sf = call.getSourceFile();
|
||||
if (!isCjsJsFile(sf, lowerer.program) || !isCjsRequireGlobal(lowerer, call.expression))
|
||||
return null;
|
||||
if (createRequireCalleeFileOf(lowerer, call.expression) !== null) return null;
|
||||
const spec = requireSpecOf(call);
|
||||
if (spec === null) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
call,
|
||||
'require() with a computed specifier (the compiled module graph is fixed at build time: require("<string literal>") lowers)',
|
||||
);
|
||||
}
|
||||
if (canonicalBuiltinModule(spec) !== null) return null;
|
||||
if (!spec.startsWith(".") && !spec.startsWith("/") && !spec.startsWith("#")) {
|
||||
const refusal = probeNodeRequireRefusal(sf.fileName, spec);
|
||||
if (refusal !== null) {
|
||||
return nodeThrowExpr(0, "MODULE_NOT_FOUND", refusal.message, DYN, locOf(call));
|
||||
}
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
call,
|
||||
`require() of the package '${spec}' in an expression (the package is not compiled into this program)`,
|
||||
);
|
||||
}
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
call,
|
||||
`require() of '${spec}' in an expression (it does not resolve to a compiled program module)`,
|
||||
);
|
||||
}
|
||||
|
||||
/** The elements of a top-level `const { key: { ... } } = require("./x")`
|
||||
* declaration over a program module whose nested pattern destructures the
|
||||
* named export's value (preflight admits exactly these). Plain elements
|
||||
* stay alias plumbing. */
|
||||
export function requireNestedPatternsOf(decl: ts.VariableDeclaration): ts.BindingElement[] {
|
||||
if (!ts.isObjectBindingPattern(decl.name) || decl.initializer === undefined) return [];
|
||||
const spec = requireSpecOf(decl.initializer);
|
||||
if (spec === null || canonicalBuiltinModule(spec) !== null) return [];
|
||||
if (!isJsSourceFile(decl.getSourceFile())) return [];
|
||||
return decl.name.elements.filter(
|
||||
(e) =>
|
||||
e.name !== undefined &&
|
||||
ts.isObjectBindingPattern(e.name) &&
|
||||
e.propertyName !== undefined &&
|
||||
ts.isIdentifier(e.propertyName) &&
|
||||
e.initializer === undefined &&
|
||||
e.dotDotDotToken === undefined,
|
||||
);
|
||||
}
|
||||
|
||||
/** A plain method entry of a `module.exports = { ... }` table: identifier
|
||||
* name, a body, not a generator. Its value is the method's function. */
|
||||
export function cjsTableMethodEntry(
|
||||
prop: ts.ObjectLiteralElementLike,
|
||||
): (ts.MethodDeclaration & { name: ts.Identifier; body: ts.Block }) | null {
|
||||
if (!ts.isMethodDeclaration(prop) || !ts.isIdentifier(prop.name) || prop.body === undefined)
|
||||
return null;
|
||||
if (prop.asteriskToken !== undefined) return null;
|
||||
return prop as ts.MethodDeclaration & { name: ts.Identifier; body: ts.Block };
|
||||
}
|
||||
|
||||
/** `exports.<name> = value` / `module.exports.<name> = value` assignments in
|
||||
* VALUE position inside a top-level statement (`const src = exports.src =
|
||||
* []`), outside function bodies and outside the statement's own export
|
||||
* chain (`statementExport`, which the statement-level machinery owns). */
|
||||
export function nestedCjsExportAssignmentsOf(
|
||||
stmt: ts.Statement,
|
||||
statementExport: ts.BinaryExpression | undefined,
|
||||
): { name: ts.Identifier; value: ts.Expression }[] {
|
||||
if (!ts.isVariableStatement(stmt) && !ts.isExpressionStatement(stmt)) return [];
|
||||
const owned = new Set<ts.Node>();
|
||||
for (let e: ts.Expression | undefined = statementExport; e !== undefined;) {
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
if (!ts.isBinaryExpression(e) || e.operatorToken.kind !== ts.SyntaxKind.EqualsToken) break;
|
||||
owned.add(e);
|
||||
e = e.right;
|
||||
}
|
||||
const out: { name: ts.Identifier; value: ts.Expression }[] = [];
|
||||
ts.walkPreorder(stmt, (node) => {
|
||||
if (
|
||||
node !== stmt &&
|
||||
(ts.isFunctionLike(node) || ts.isClassDeclaration(node) || ts.isClassExpression(node))
|
||||
)
|
||||
return "skip";
|
||||
if (
|
||||
ts.isBinaryExpression(node) &&
|
||||
node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
||||
!owned.has(node) &&
|
||||
ts.isPropertyAccessExpression(node.left) &&
|
||||
node.left.questionDotToken === undefined &&
|
||||
ts.isIdentifier(node.left.name) &&
|
||||
((ts.isIdentifier(node.left.expression) && node.left.expression.text === "exports") ||
|
||||
isModuleExportsAccess(node.left.expression))
|
||||
) {
|
||||
out.push({ name: node.left.name, value: node.right });
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
/** `const { A, B: b } = ns` at a module's top level, where `ns` is a
|
||||
* CommonJS module binding over a table module (picomatch's `const {
|
||||
* MAX_LENGTH, ... } = constants`): each name takes a snapshot of the named
|
||||
* export's value at this statement, exactly like destructuring the exports
|
||||
* object would. Null for every other declaration; the plain elements and
|
||||
* their key names otherwise. */
|
||||
export function cjsNamespaceDestructureOf(
|
||||
lowerer: Lowerer,
|
||||
decl: ts.VariableDeclaration,
|
||||
): { dep: ts.SourceFile; elements: { name: ts.Identifier; key: string }[] } | null {
|
||||
if (!ts.isObjectBindingPattern(decl.name) || decl.initializer === undefined) return null;
|
||||
const stmt = decl.parent?.parent;
|
||||
if (stmt === undefined || !ts.isVariableStatement(stmt) || !ts.isSourceFile(stmt.parent))
|
||||
return null;
|
||||
if (!isJsSourceFile(stmt.parent)) return null;
|
||||
let init: ts.Expression = decl.initializer;
|
||||
while (ts.isParenthesizedExpression(init)) init = init.expression;
|
||||
if (!ts.isIdentifier(init) || !lowerer.cjsLocalModuleBindingOf(init)) return null;
|
||||
const dep = lowerer.cjsModuleBindingDepOf(init);
|
||||
if (dep === null) return null;
|
||||
const elements: { name: ts.Identifier; key: string }[] = [];
|
||||
for (const e of decl.name.elements) {
|
||||
if (e.initializer !== undefined || e.dotDotDotToken !== undefined) return null;
|
||||
if (e.name === undefined || !ts.isIdentifier(e.name)) return null;
|
||||
const key =
|
||||
e.propertyName === undefined
|
||||
? e.name.text
|
||||
: ts.isIdentifier(e.propertyName) || ts.isStringLiteral(e.propertyName)
|
||||
? e.propertyName.text
|
||||
: null;
|
||||
if (key === null) return null;
|
||||
elements.push({ name: e.name, key });
|
||||
}
|
||||
return { dep, elements };
|
||||
}
|
||||
|
||||
/** The value of `dep`'s export `key` read from another module: its export
|
||||
* global, or the function/class declaration it aliases. Null when it has no
|
||||
* static value. */
|
||||
export function cjsExportValueByName(
|
||||
lowerer: Lowerer,
|
||||
dep: ts.SourceFile,
|
||||
key: string,
|
||||
blame: ts.Node,
|
||||
): IrExpr | null {
|
||||
let sym = lowerer.cjsModuleExportSymbol(dep, key);
|
||||
if (sym === undefined) return null;
|
||||
if (sym.flags & ts.SymbolFlags.Alias) sym = lowerer.checker.getAliasedSymbol(sym);
|
||||
const direct = lowerer.globalsBySymbol.get(sym);
|
||||
if (direct) return { kind: "varRef", localId: direct.id, type: direct.type, loc: locOf(blame) };
|
||||
for (const d of lowerer.checker.declarationsOf(sym)) {
|
||||
const byDecl = lowerer.globalsByDeclNode.get(d);
|
||||
if (byDecl) return { kind: "varRef", localId: byDecl.id, type: byDecl.type, loc: locOf(blame) };
|
||||
// Alias plumbing: a shorthand/identifier table entry or a member
|
||||
// export naming a declaration. Reading that identifier IS the value.
|
||||
if (ts.isShorthandPropertyAssignment(d)) return lowerer.lowerShorthandValue(d);
|
||||
if (ts.isPropertyAssignment(d) && ts.isIdentifier(d.initializer)) {
|
||||
return lowerer.lowerExpr(d.initializer);
|
||||
}
|
||||
if ((ts.isFunctionDeclaration(d) || ts.isClassDeclaration(d)) && d.name !== undefined) {
|
||||
return lowerer.lowerExpr(d.name);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
/* tsc's CommonJS import interop: `const x_1 = __importDefault(require("./m"))`
|
||||
* and `const x_1 = __importStar(require("./m"))` at a module's top level,
|
||||
* with the helpers tsc emits at the top of the file:
|
||||
*
|
||||
* var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
* return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
* };
|
||||
*
|
||||
* For a CommonJS dependency the helper's answer is fixed by the
|
||||
* dependency's source: stamped with `__esModule` (tsc output), the binding
|
||||
* IS the module's exports; otherwise `x_1.default` is the module's exports
|
||||
* (and __importStar also copies every export onto x_1). Both are the
|
||||
* static alias model a plain `const x_1 = require("./m")` already has, so
|
||||
* the binding lowers as a require binding: the statement is the module's
|
||||
* %init, `x_1.f` / `x_1.default.f` read the exports statically, and
|
||||
* `x_1.default` over a single-value module is that value. The helper
|
||||
* declarations themselves are dead once every call is such a binding (the
|
||||
* self-probe `this.__importDefault` reads the pristine exports object,
|
||||
* which never has the name), so they lower to nothing. */
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import {
|
||||
isCjsJsFile,
|
||||
isEsModuleStamp,
|
||||
npmStaticDepSf7,
|
||||
requireSpecOf,
|
||||
resolveImport,
|
||||
} from "../program.js";
|
||||
import { cjsNamespaceModule } from "./cjs-inline-require.js";
|
||||
|
||||
const HELPERS = new Set([
|
||||
"__importDefault",
|
||||
"__importStar",
|
||||
"__createBinding",
|
||||
"__setModuleDefault",
|
||||
]);
|
||||
|
||||
export interface TscInteropBinding {
|
||||
helper: "__importDefault" | "__importStar";
|
||||
spec: string;
|
||||
dep: ts.SourceFile;
|
||||
/** The dependency stamps `__esModule`: the binding IS its exports. */
|
||||
stamped: boolean;
|
||||
}
|
||||
|
||||
/** `var <helper> = (this && this.<helper>) || <function>` at a module's top
|
||||
* level: the tsc helper declaration shape. */
|
||||
function tscHelperDeclName(stmt: ts.Statement): string | null {
|
||||
if (!ts.isVariableStatement(stmt)) return null;
|
||||
const decls = stmt.declarationList.declarations;
|
||||
if (decls.length !== 1) return null;
|
||||
const d = decls[0]!;
|
||||
if (!ts.isIdentifier(d.name) || !HELPERS.has(d.name.text) || d.initializer === undefined)
|
||||
return null;
|
||||
let init: ts.Expression = d.initializer;
|
||||
while (ts.isParenthesizedExpression(init)) init = init.expression;
|
||||
if (!ts.isBinaryExpression(init) || init.operatorToken.kind !== ts.SyntaxKind.BarBarToken)
|
||||
return null;
|
||||
let probe: ts.Expression = init.left;
|
||||
while (ts.isParenthesizedExpression(probe)) probe = probe.expression;
|
||||
if (
|
||||
!ts.isBinaryExpression(probe) ||
|
||||
probe.operatorToken.kind !== ts.SyntaxKind.AmpersandAmpersandToken ||
|
||||
probe.left.kind !== ts.SyntaxKind.ThisKeyword ||
|
||||
!ts.isPropertyAccessExpression(probe.right) ||
|
||||
probe.right.expression.kind !== ts.SyntaxKind.ThisKeyword ||
|
||||
probe.right.name.text !== d.name.text
|
||||
)
|
||||
return null;
|
||||
// The fallback is the helper body: a function, or (newer tsc output) a
|
||||
// conditional or IIFE producing one. It never runs when the helper is dead.
|
||||
return d.name.text;
|
||||
}
|
||||
|
||||
/** True when the dependency stamps itself as an ES module translation. */
|
||||
function stampsEsModule(dep: ts.SourceFile): boolean {
|
||||
return dep.statements.some((s) => {
|
||||
if (!ts.isExpressionStatement(s)) return false;
|
||||
if (isEsModuleStamp(s.expression)) return true;
|
||||
const e = s.expression;
|
||||
return (
|
||||
ts.isBinaryExpression(e) &&
|
||||
e.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
||||
ts.isPropertyAccessExpression(e.left) &&
|
||||
ts.isIdentifier(e.left.expression) &&
|
||||
e.left.expression.text === "exports" &&
|
||||
e.left.name.text === "__esModule" &&
|
||||
e.right.kind === ts.SyntaxKind.TrueKeyword
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/** The interop binding a top-level `const x = __importDefault(require(s))` /
|
||||
* `__importStar(require(s))` declaration makes, or null. The helper must be
|
||||
* the module's own tsc helper declaration and the dependency a compiled
|
||||
* CommonJS program module. */
|
||||
export function tscInteropBindingOfDecl(
|
||||
lowerer: Lowerer,
|
||||
decl: ts.VariableDeclaration,
|
||||
): TscInteropBinding | null {
|
||||
if (!ts.isIdentifier(decl.name) || decl.initializer === undefined) return null;
|
||||
const stmt = decl.parent?.parent;
|
||||
if (stmt === undefined || !ts.isVariableStatement(stmt) || !ts.isSourceFile(stmt.parent))
|
||||
return null;
|
||||
const sf = stmt.parent;
|
||||
if (!isCjsJsFile(sf, lowerer.program)) return null;
|
||||
let init: ts.Expression = decl.initializer;
|
||||
while (ts.isParenthesizedExpression(init)) init = init.expression;
|
||||
if (!ts.isCallExpression(init) || init.arguments.length !== 1) return null;
|
||||
const callee = init.expression;
|
||||
if (!ts.isIdentifier(callee)) return null;
|
||||
if (callee.text !== "__importDefault" && callee.text !== "__importStar") return null;
|
||||
if (!sf.statements.some((s) => tscHelperDeclName(s) === callee.text)) return null;
|
||||
const spec = requireSpecOf(init.arguments[0]!);
|
||||
if (spec === null) return null;
|
||||
const dep =
|
||||
resolveImport(lowerer.program, sf, spec, "require") ??
|
||||
npmStaticDepSf7(lowerer.program, sf, spec, "require");
|
||||
if (!dep || dep.isDeclarationFile || !isCjsJsFile(dep, lowerer.program)) return null;
|
||||
return {
|
||||
helper: callee.text as TscInteropBinding["helper"],
|
||||
spec,
|
||||
dep,
|
||||
stamped: stampsEsModule(dep),
|
||||
};
|
||||
}
|
||||
|
||||
/** The interop binding an identifier reads, or null. */
|
||||
export function tscInteropBindingOf(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.Expression,
|
||||
): TscInteropBinding | null {
|
||||
if (!ts.isIdentifier(expr)) return null;
|
||||
const sym = lowerer.checker.getSymbolAtLocation(expr);
|
||||
const decl = sym ? lowerer.checker.declarationsOf(sym).find(ts.isVariableDeclaration) : undefined;
|
||||
return decl ? tscInteropBindingOfDecl(lowerer, decl) : null;
|
||||
}
|
||||
|
||||
/** The module whose exports `expr` denotes through an interop binding:
|
||||
* the binding itself when the dependency is stamped (or for __importStar,
|
||||
* whose copy carries every export), and `binding.default` when it is not.
|
||||
* Null for anything else. */
|
||||
export function tscInteropModuleOf(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.Expression,
|
||||
): TscInteropBinding | null {
|
||||
let e = expr;
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
const direct = tscInteropBindingOf(lowerer, e);
|
||||
if (direct !== null) {
|
||||
return direct.stamped || direct.helper === "__importStar" ? direct : null;
|
||||
}
|
||||
if (ts.isPropertyAccessExpression(e) && !e.questionDotToken && e.name.text === "default") {
|
||||
const via = tscInteropBindingOf(lowerer, e.expression);
|
||||
if (via !== null && !via.stamped) return via;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** `binding.default` over an unstamped single-value dependency: that value
|
||||
* (the caller lowers the dependency's whole export). */
|
||||
export function tscInteropDefaultValueOf(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.PropertyAccessExpression,
|
||||
): TscInteropBinding | null {
|
||||
if (expr.questionDotToken || expr.name.text !== "default") return null;
|
||||
const via = tscInteropBindingOf(lowerer, expr.expression);
|
||||
if (via === null || via.stamped) return null;
|
||||
return cjsNamespaceModule(lowerer, via.dep) ? null : via;
|
||||
}
|
||||
|
||||
/** True for a tsc helper declaration every reference to which is a
|
||||
* recognized interop binding's callee, its own self-probe, or a reference
|
||||
* from another such dead helper: the statement lowers to nothing. */
|
||||
export function isDeadTscHelperDecl(lowerer: Lowerer, stmt: ts.Statement): boolean {
|
||||
const name = tscHelperDeclName(stmt);
|
||||
if (name === null) return false;
|
||||
const sf = stmt.getSourceFile();
|
||||
const helperSpans = new Map<string, ts.Statement>();
|
||||
for (const s of sf.statements) {
|
||||
const n = tscHelperDeclName(s);
|
||||
if (n !== null) helperSpans.set(n, s);
|
||||
}
|
||||
const dead = new Set<string>();
|
||||
for (let changed = true; changed;) {
|
||||
changed = false;
|
||||
for (const [helper, decl] of helperSpans) {
|
||||
if (dead.has(helper)) continue;
|
||||
let live = false;
|
||||
ts.walkPreorder(sf, (node) => {
|
||||
if (!ts.isIdentifier(node) || node.text !== helper) return undefined;
|
||||
const parent = node.parent;
|
||||
if (parent !== undefined && ts.isVariableDeclaration(parent) && parent.name === node)
|
||||
return undefined;
|
||||
const at = node.getStart(sf);
|
||||
if (at >= decl.getStart(sf) && at < decl.getEnd()) return undefined;
|
||||
for (const [other, otherDecl] of helperSpans) {
|
||||
if (other !== helper && dead.has(other)) {
|
||||
if (at >= otherDecl.getStart(sf) && at < otherDecl.getEnd()) return undefined;
|
||||
}
|
||||
}
|
||||
const call = parent;
|
||||
const binding = call?.parent;
|
||||
if (
|
||||
call !== undefined &&
|
||||
ts.isCallExpression(call) &&
|
||||
call.expression === node &&
|
||||
binding !== undefined &&
|
||||
ts.isVariableDeclaration(binding) &&
|
||||
tscInteropBindingOfDecl(lowerer, binding) !== null
|
||||
)
|
||||
return undefined;
|
||||
live = true;
|
||||
return "stop";
|
||||
});
|
||||
if (!live) {
|
||||
dead.add(helper);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return dead.has(name);
|
||||
}
|
||||
@@ -747,7 +747,7 @@ function lowerForOfMapOrSet(
|
||||
* Recurses through every nested statement list; nested functions are
|
||||
* lifted elsewhere and never appear here, so every return found exits
|
||||
* THIS function through the active iteration. */
|
||||
function exitBeforeReturns(stmts: IrStmt[], make: () => IrStmt): IrStmt[] {
|
||||
export function exitBeforeReturns(stmts: IrStmt[], make: () => IrStmt): IrStmt[] {
|
||||
const walk = (list: IrStmt[]): IrStmt[] =>
|
||||
list.flatMap((s): IrStmt[] => {
|
||||
switch (s.kind) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -315,7 +315,13 @@ export function refreshErrorPropertyDispatch(
|
||||
info.def.name,
|
||||
member,
|
||||
value,
|
||||
[varRef("value", STRING, loc)],
|
||||
[
|
||||
// A JavaScript setter takes its value untyped.
|
||||
lowerer.coerceToExpected(
|
||||
varRef("value", STRING, loc),
|
||||
accessor.sig.params[0]?.type ?? STRING,
|
||||
),
|
||||
],
|
||||
VOID,
|
||||
loc,
|
||||
),
|
||||
|
||||
@@ -26,7 +26,8 @@ import type { Lowerer } from "../lowerer.js";
|
||||
* deeper — `x?.trim().toLowerCase()` reads nothing past the guard when x
|
||||
* is nullish, so the whole tail must lower inside it. Walks
|
||||
* property/element accesses and call steps only (parens and `!` break
|
||||
* the chain per the grammar: `(a?.b).c` throws on undefined in JS);
|
||||
* the chain per the grammar: `(a?.b).c` throws on undefined in JS, while
|
||||
* an unparenthesized `a?.b!.c` stays one chain, as TypeScript emits it);
|
||||
* the node's OWN token — and a call's immediate callee token — are the
|
||||
* plain entries' cases, not tails, and handled markers make the chain
|
||||
* lowering's re-dispatch walk past its own step. `f?.()` steps stay out
|
||||
@@ -41,6 +42,10 @@ function chainTailDot(
|
||||
// An active chain's bound receiver read is a LEAF — its own token
|
||||
// was consumed by the chain that bound it.
|
||||
if (lowerer.chainRecvByNode.has(cur)) return null;
|
||||
if (ts.isNonNullExpression(cur)) {
|
||||
cur = cur.expression;
|
||||
continue;
|
||||
}
|
||||
if (ts.isCallExpression(cur)) {
|
||||
// `f?.()` steps route through their own entry (or are being
|
||||
// handled); a deeper token under one is that chain's business.
|
||||
@@ -91,20 +96,29 @@ export function isOptionalChainTail(lowerer: Lowerer, expr: ts.Expression): bool
|
||||
if (!tail) return false;
|
||||
const recvT = lowerer.mapTypeOf(lowerer.typeOf(tail.expression));
|
||||
if (recvT?.kind === "dyn") return true;
|
||||
if (recvT?.kind !== "union") return false;
|
||||
const def = lowerer.unions.get(recvT.unionId);
|
||||
return !!def && def.arms.some(isUnitType);
|
||||
if (recvT?.kind === "union") {
|
||||
const def = lowerer.unions.get(recvT.unionId);
|
||||
if (def?.arms.some(isUnitType)) return true;
|
||||
}
|
||||
// The checker calls the receiver present, but an unchecked read (`a[0]`
|
||||
// past the end, a missing index-signature key) can still lower to an
|
||||
// undefined-armed value, and `a[0]?.b.c` must then short-circuit `.c`
|
||||
// too. The chain lowering inspects the lowered receiver: a present value
|
||||
// re-dispatches the plain lowering, so only genuinely optional receivers
|
||||
// change shape.
|
||||
return lowerer.isRuntimeOptionalOrigin(tail.expression);
|
||||
}
|
||||
|
||||
/** True when an EARLIER step of this access chain carries `?.` — JS
|
||||
* short-circuits the whole tail (`a?.b.c` reads nothing when a is
|
||||
* nullish), so a dyn tail read must answer undefined instead of
|
||||
* throwing. Walks the receiver spine only; the argument of the access
|
||||
* itself is not part of the guard. */
|
||||
* itself is not part of the guard. Parentheses end the chain
|
||||
* (`(a?.b).c` reads `.c` from undefined); `!` does not. */
|
||||
export function hasOptionalChainGuard(expr: ts.Expression): boolean {
|
||||
let cur: ts.Expression = expr;
|
||||
for (;;) {
|
||||
if (ts.isParenthesizedExpression(cur) || ts.isNonNullExpression(cur)) {
|
||||
if (ts.isNonNullExpression(cur)) {
|
||||
cur = cur.expression;
|
||||
continue;
|
||||
}
|
||||
@@ -393,7 +407,8 @@ export function lowerOptionalChain(
|
||||
for (let cur: ts.Expression = expr; cur !== dotNode;) {
|
||||
const next: ts.Expression = ts.isCallExpression(cur)
|
||||
? cur.expression
|
||||
: (cur as ts.PropertyAccessExpression | ts.ElementAccessExpression).expression;
|
||||
: (cur as ts.PropertyAccessExpression | ts.ElementAccessExpression | ts.NonNullExpression)
|
||||
.expression;
|
||||
if (next === dotNode) break;
|
||||
if (!lowerer.chainNarrowedType.has(next)) {
|
||||
lowerer.chainNarrowedType.set(
|
||||
|
||||
@@ -15,7 +15,7 @@ import { isJsSourceFile, locOf } from "../program.js";
|
||||
import { typeKey } from "../type-mapper.js";
|
||||
import { PoisonError, dynFallbackType, neverTaintedJsType, newFnCtx } from "./lowerer.js";
|
||||
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
|
||||
import { findGenericMethodOn, type ClassInfo } from "./lower-classes.js";
|
||||
import { findGenericMethodOn, storedClassValueType, type ClassInfo } from "./lower-classes.js";
|
||||
import { nsPathPrefix } from "./lower-namespaces.js";
|
||||
import { declSymbolOf } from "./lower-modules.js";
|
||||
import { npmStaticPackageOfPath } from "../npm-static.js";
|
||||
@@ -292,6 +292,8 @@ export function genericCallInstance(
|
||||
(_param, i) => expr.arguments[i] ?? expr,
|
||||
);
|
||||
const retTs = lowerer.checker.getReturnTypeOfSignature(rsig);
|
||||
const local = localClassResultInstance(lowerer, expr, info, rsig, params, retTs);
|
||||
if (local) return local;
|
||||
const returnType = lowerer.mapTypeOf(retTs);
|
||||
if (!returnType) {
|
||||
fenceGenericSignatureResult(lowerer, expr, retTs);
|
||||
@@ -331,6 +333,150 @@ export function genericCallInstance(
|
||||
);
|
||||
}
|
||||
|
||||
/** A generic factory whose result is one of its own local classes
|
||||
* (`function base<T>(v: T) { return class { value: T = v }; }`). Each
|
||||
* instance has its own class, so the instance is keyed on its bindings and
|
||||
* the result maps inside it, where the local class takes the instance's
|
||||
* name and bindings. */
|
||||
function localClassResultInstance(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.CallExpression,
|
||||
info: GenericFnInfo,
|
||||
rsig: ts.Signature,
|
||||
params: ParamShape[],
|
||||
retTs: ts.Type,
|
||||
): GenericInstance | null {
|
||||
if (!localClassOfResult(lowerer, info, retTs)) return null;
|
||||
const tsBindings = new Map<ts.Symbol, ts.Type>();
|
||||
const bindings = lowerer.inferTypeParamBindings(expr, info, rsig, tsBindings);
|
||||
const bindingKey = info.typeParams
|
||||
.map((tp) => {
|
||||
const bound = bindings.get(tp);
|
||||
return bound ? typeKey(bound) : "?";
|
||||
})
|
||||
.join(",");
|
||||
const pending: IrType = { kind: "void" };
|
||||
const inst = internGenericInstance(lowerer, expr, info, params, pending, () => bindings, {
|
||||
extraKey: `@class<${bindingKey}>`,
|
||||
tsBindings,
|
||||
});
|
||||
if (inst.returnType !== pending) return inst;
|
||||
const returnType = withInstanceScope(lowerer, info, inst, () => lowerer.mapTypeOf(retTs));
|
||||
if (!returnType) {
|
||||
fenceGenericSignatureResult(lowerer, expr, retTs);
|
||||
lowerer.badType(expr, retTs);
|
||||
}
|
||||
inst.returnType = returnType;
|
||||
// Values of the class outside the factory (`const b = new B()`, `b.read()`)
|
||||
// carry the instantiated checker type; record which class it names. A
|
||||
// call inside another generic instance can have a type that depends on
|
||||
// that instance's parameters, which no single class answers.
|
||||
if (lowerer.typeParamBindings !== null) return inst;
|
||||
const instance =
|
||||
returnType.kind === "classval"
|
||||
? lowerer.checker
|
||||
.getConstructSignatures(retTs)
|
||||
.map((sig) => lowerer.checker.getReturnTypeOfSignature(sig))
|
||||
: [retTs];
|
||||
const className =
|
||||
returnType.kind === "classval" || returnType.kind === "object" ? returnType.className : null;
|
||||
if (className !== null)
|
||||
for (const type of instance) lowerer.localClassInstanceTypes.set(type.id, className);
|
||||
return inst;
|
||||
}
|
||||
|
||||
/** The class value a call of a generic factory returns when the result is
|
||||
* one of the factory's local classes (`class C extends base(42) {}`), or
|
||||
* null for any other call. */
|
||||
export function genericLocalClassResult(lowerer: Lowerer, expr: ts.CallExpression): IrType | null {
|
||||
if (!ts.isIdentifier(expr.expression)) return null;
|
||||
const info = lowerer.genericFnOf(expr.expression);
|
||||
if (!info || info.implicitParams) return null;
|
||||
const rsig = lowerer.checker.getResolvedSignature(expr);
|
||||
if (!rsig || !localClassOfResult(lowerer, info, lowerer.checker.getReturnTypeOfSignature(rsig)))
|
||||
return null;
|
||||
return genericCallInstance(lowerer, expr, info).returnType;
|
||||
}
|
||||
|
||||
/** The type of a `const` binding whose initializer calls a generic factory
|
||||
* returning one of its local classes (`const B = base(7)`), or constructs
|
||||
* such a class (`const b = new B()`); null for any other binding. */
|
||||
export function genericLocalClassBinding(lowerer: Lowerer, node: ts.Node): IrType | null {
|
||||
const decl = node.parent;
|
||||
if (
|
||||
!ts.isIdentifier(node) ||
|
||||
!decl ||
|
||||
!ts.isVariableDeclaration(decl) ||
|
||||
decl.name !== node ||
|
||||
!decl.initializer ||
|
||||
(ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) === 0
|
||||
)
|
||||
return null;
|
||||
// An `any` binding keeps the checked-dynamic representation.
|
||||
if ((lowerer.typeOf(node).flags & ts.TypeFlags.Any) !== 0) return null;
|
||||
let init = decl.initializer;
|
||||
while (ts.isParenthesizedExpression(init)) init = init.expression;
|
||||
if (ts.isCallExpression(init)) return genericLocalClassResult(lowerer, init);
|
||||
if (!ts.isNewExpression(init)) return null;
|
||||
const constructed = storedClassValueType(lowerer, init.expression);
|
||||
if (constructed?.kind !== "classval") return null;
|
||||
const classDecl = lowerer.classes.get(constructed.className)?.decl;
|
||||
return classDecl && insideGenericFunction(classDecl)
|
||||
? { kind: "object", className: constructed.className }
|
||||
: null;
|
||||
}
|
||||
|
||||
/** Whether `node` is declared inside a function with type parameters. */
|
||||
function insideGenericFunction(node: ts.Node): boolean {
|
||||
for (let parent = node.parent; parent && !ts.isSourceFile(parent); parent = parent.parent)
|
||||
if (ts.isFunctionLike(parent) && (parent.typeParameters?.length ?? 0) > 0) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/** The class declared inside a generic function that `type` is the
|
||||
* constructor or instance type of, or null. */
|
||||
function localClassOfResult(
|
||||
lowerer: Lowerer,
|
||||
info: GenericFnInfo,
|
||||
type: ts.Type,
|
||||
): ts.ClassLikeDeclaration | null {
|
||||
const symbol = type.getSymbol();
|
||||
if (!symbol || !(symbol.flags & ts.SymbolFlags.Class)) return null;
|
||||
const decl = lowerer.checker.valueDeclarationOf(symbol);
|
||||
if (!decl || !(ts.isClassExpression(decl) || ts.isClassDeclaration(decl))) return null;
|
||||
for (let parent = decl.parent; parent && !ts.isSourceFile(parent); parent = parent.parent)
|
||||
if (parent === info.decl) return decl;
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Runs `fn` under an instance's type bindings and local-class names, as
|
||||
* its body lowers. */
|
||||
function withInstanceScope<T>(
|
||||
lowerer: Lowerer,
|
||||
info: GenericFnInfo,
|
||||
inst: GenericInstance,
|
||||
fn: () => T,
|
||||
): T {
|
||||
const prevBindings = lowerer.typeParamBindings;
|
||||
const prevTsBindings = lowerer.typeParamTsBindings;
|
||||
const previousPath = lowerer.genericInstantiationPath;
|
||||
const prevContext = lowerer.instantiationContext;
|
||||
lowerer.typeParamBindings = inst.bindings;
|
||||
lowerer.typeParamTsBindings = inst.tsBindings ?? null;
|
||||
lowerer.genericInstantiationPath = inst.path;
|
||||
lowerer.instantiationContext = `instantiating '${info.baseName}' with ${inst.typeArgsText}`;
|
||||
lowerer.localClassInstantiations.push({ owner: info.decl, name: inst.name });
|
||||
try {
|
||||
return fn();
|
||||
} finally {
|
||||
lowerer.localClassInstantiations.pop();
|
||||
lowerer.typeParamBindings = prevBindings;
|
||||
lowerer.typeParamTsBindings = prevTsBindings;
|
||||
lowerer.genericInstantiationPath = previousPath;
|
||||
lowerer.instantiationContext = prevContext;
|
||||
}
|
||||
}
|
||||
|
||||
/** The one instance table both instantiation routes share: key identity IS
|
||||
* signature identity, so a call (`identity(1)`) and a pinned VALUE
|
||||
* (`const f: (x: number) => number = identity`) reuse one compiled
|
||||
|
||||
@@ -24,6 +24,13 @@ import {
|
||||
lowerZlibStreamMethodCall,
|
||||
} from "./builtins/compression.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import {
|
||||
inlineNamespaceRequireOf,
|
||||
lowerInlineRequireValue,
|
||||
lowerUncompiledRequireCall,
|
||||
lowerWithInlineRequireInit,
|
||||
} from "./cjs-inline-require.js";
|
||||
import { cjsExportCallIgnoresThis, cjsInFileExportGlobalOf } from "./cjs-exports-object.js";
|
||||
import { lowerGenMethodCall } from "./lower-generators.js";
|
||||
import { lowerClassSymbolMethodCall } from "./symbol-methods.js";
|
||||
import { errorToStringCall } from "./error-methods.js";
|
||||
@@ -61,7 +68,11 @@ import { lowerProtocolMethodCall } from "./class-protocol-dispatch.js";
|
||||
import { isJsSourceFile, locOf, requireSpecOf } from "../program.js";
|
||||
import { lowerJsonRequireCall } from "./json-require.js";
|
||||
import { lowerAssertInstanceCall } from "./assert-instances.js";
|
||||
import { isGenericCallableMemberType, typeKey } from "../type-mapper.js";
|
||||
import {
|
||||
isGenericCallableMemberType,
|
||||
isOverloadedCallableMemberType,
|
||||
typeKey,
|
||||
} from "../type-mapper.js";
|
||||
import { dynFallbackType, jsFuncNameOf, newFnCtx, staticImportNamespaceType } from "./lowerer.js";
|
||||
import {
|
||||
NARROW_FIRST,
|
||||
@@ -79,7 +90,10 @@ import {
|
||||
import { lowerArrayFromAsyncCall } from "./containers/array-from-async.js";
|
||||
import { lowerDynArrayFilterCall, lowerDynArrayFlatMapCall } from "./containers/array-callbacks.js";
|
||||
import { lowerGroupByStaticCall } from "./containers/collection-grouping.js";
|
||||
import { lowerIteratorHelperCall } from "./containers/iterator-helpers.js";
|
||||
import {
|
||||
lowerGeneratorHelperCall,
|
||||
lowerIteratorHelperCall,
|
||||
} from "./containers/iterator-helpers.js";
|
||||
import {
|
||||
lowerObjectAssignIndexShape,
|
||||
lowerObjectConstructor,
|
||||
@@ -235,6 +249,32 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
const json = lowerJsonRequireCall(lowerer, expr);
|
||||
if (json) return json;
|
||||
|
||||
// Inline require() of a program module (cjs-inline-require.ts): a
|
||||
// member call over its export table resolves like a binding's member
|
||||
// call after the module's %init at this position; the require itself
|
||||
// as a value answers the module's whole-export value.
|
||||
if (ts.isPropertyAccessExpression(expr.expression) && !expr.expression.questionDotToken) {
|
||||
const req = inlineNamespaceRequireOf(lowerer, expr.expression.expression);
|
||||
if (req !== null && !lowerer.inlineRequireInitDone.has(req.call)) {
|
||||
// Node calls the member with `this` bound to the module's exports.
|
||||
if (
|
||||
ts.isIdentifier(expr.expression.name) &&
|
||||
!cjsExportCallIgnoresThis(lowerer, req.dep, expr.expression.name.text)
|
||||
) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
`calling '${expr.expression.name.text}' through require(${JSON.stringify(req.spec)}) when the function may read 'this' (Node binds it to the module's exports object, which compiles to a static table — bind the module and call a local function instead)`,
|
||||
);
|
||||
}
|
||||
return lowerWithInlineRequireInit(lowerer, req, () => lowerCall(lowerer, expr));
|
||||
}
|
||||
}
|
||||
{
|
||||
const inlineValue = lowerInlineRequireValue(lowerer, expr);
|
||||
if (inlineValue) return inlineValue;
|
||||
}
|
||||
|
||||
// A call whose chain ROOTS at an ambient-undefined name (`declare
|
||||
// const value: Y | undefined; value?.foo("a")`, `declare function
|
||||
// chain...; chain(o).mapValues(f).value()`, a trap binding's read):
|
||||
@@ -311,6 +351,12 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
lowerImportMetaResolveCall(lowerer, expr) ?? lowerRequireResolveCall(lowerer, expr);
|
||||
if (resolved) return resolved;
|
||||
}
|
||||
// Any other require() call of the CommonJS global: MODULE_NOT_FOUND for
|
||||
// packages nothing installed resolves, a named refusal otherwise.
|
||||
{
|
||||
const uncompiled = lowerUncompiledRequireCall(lowerer, expr);
|
||||
if (uncompiled) return uncompiled;
|
||||
}
|
||||
|
||||
// `process.getuid?.()` — intercepted BEFORE the optional-chain
|
||||
// machinery (the member always exists on a POSIX target, so the
|
||||
@@ -1275,6 +1321,32 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
const args = completeFuncValueArgs(lowerer, expr, callee.type, loc);
|
||||
return { kind: "callValue", callee, args, type: callee.type.ret, loc };
|
||||
}
|
||||
// `exports.f(x)` / `module.exports.f(x)` inside the module: a call of
|
||||
// the member's export global (cjs-exports-object.ts), the value every
|
||||
// `exports.f =` statement assigns.
|
||||
if (ts.isPropertyAccessExpression(expr.expression) && !expr.questionDotToken) {
|
||||
const g = cjsInFileExportGlobalOf(lowerer, expr.expression);
|
||||
if (
|
||||
g &&
|
||||
(g.type.kind === "func" || g.type.kind === "dyn") &&
|
||||
!cjsExportCallIgnoresThis(lowerer, expr.getSourceFile(), expr.expression.name.text)
|
||||
) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
`calling '${expr.expression.getText()}' through the module's exports object when the function may read 'this' (Node binds it to the exports object, which compiles to a static table — call a local function instead)`,
|
||||
);
|
||||
}
|
||||
if (g && (g.type.kind === "func" || g.type.kind === "dyn")) {
|
||||
const callee: IrExpr = { kind: "varRef", localId: g.id, type: g.type, loc };
|
||||
const checked = lowerCheckedCallableValue(lowerer, expr, callee, loc);
|
||||
if (checked) return checked;
|
||||
if (callee.type.kind === "func") {
|
||||
const args = completeFuncValueArgs(lowerer, expr, callee.type, loc);
|
||||
return { kind: "callValue", callee, args, type: callee.type.ret, loc };
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ts.isPropertyAccessExpression(expr.expression)) {
|
||||
const intrinsic =
|
||||
fenceNodeModuleMutationCall(lowerer, expr, expr.expression) ??
|
||||
@@ -1422,6 +1494,8 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
// array method paths (the terminal names collide with array
|
||||
// methods, but only iterator-typed receivers reach this).
|
||||
lowerIteratorHelperCall(lowerer, expr, expr.expression) ??
|
||||
// Helper calls on generators and other iterator objects.
|
||||
lowerGeneratorHelperCall(lowerer, expr, expr.expression) ??
|
||||
lowerIteratorStaticFence(lowerer, expr, expr.expression) ??
|
||||
lowerObjectStaticCall(lowerer, expr, expr.expression) ??
|
||||
lowerObjectFromEntriesCall(lowerer, expr, expr.expression) ??
|
||||
@@ -3075,8 +3149,9 @@ function lowerOptionalStringNumber(
|
||||
return { kind: "call", callee: helper, args: [arg], type: F64, loc };
|
||||
}
|
||||
|
||||
/** Iterator types whose helper calls have no lowering on any path (the
|
||||
* array iterator's chains lower in lowerIteratorHelperCall). */
|
||||
/** Iterator types with checked runtime receivers: Map/Set iterators'
|
||||
* helper calls lower in lowerGeneratorHelperCall; the string iterator's
|
||||
* have no lowering. */
|
||||
const UNLOWERED_ITERATOR_TYPES = new Set(["MapIterator", "SetIterator", "StringIterator"]);
|
||||
const ITERATOR_HELPER_METHODS = new Set([
|
||||
"map",
|
||||
@@ -3143,6 +3218,8 @@ function lowerDynReceiverMethodCall(
|
||||
lowerer.isStdlibMember(access)
|
||||
) {
|
||||
if (recvSymbol.name === "MapIterator" || recvSymbol.name === "SetIterator") {
|
||||
const helper = lowerGeneratorHelperCall(lowerer, call, access);
|
||||
if (helper) return helper;
|
||||
lowerer.noLowering(
|
||||
`collection iterator .${access.name.text}()`,
|
||||
call,
|
||||
@@ -3153,7 +3230,7 @@ function lowerDynReceiverMethodCall(
|
||||
lowerer.noLowering(
|
||||
`${recvSymbol.name}.${access.name.text}`,
|
||||
call,
|
||||
"iterator helpers lower on chains from arr.values() or arr.entries() that end in a terminal (toArray, forEach, reduce, some, every, find) — spread the string first: [...s].values()",
|
||||
"iterator helpers lower on generators and on array, Map and Set iterators — spread the string first: [...s].values()",
|
||||
);
|
||||
}
|
||||
const arrayReceiver = lowerer.checker.isArrayType(recvTs);
|
||||
@@ -4171,10 +4248,32 @@ function lowerNumberToStringCall(
|
||||
}
|
||||
if (call.arguments.length === 1) {
|
||||
if (operand.type.kind !== "f64") return null;
|
||||
const arg = call.arguments[0]!;
|
||||
// A number radix needs no box: ToIntegerOrInfinity of a number is a
|
||||
// truncation, which the typed entry point applies itself.
|
||||
if (!ts.isSpreadElement(arg) && lowerer.mapTypeOf(lowerer.typeOf(arg))?.kind === "f64") {
|
||||
const radix = lowerer.lowerExpr(arg);
|
||||
if (radix.type.kind === "f64") {
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "num.toStringRadixF64",
|
||||
args: [operand, radix],
|
||||
type: STRING,
|
||||
loc: locOf(call),
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "num.toStringRadix",
|
||||
args: [operand, lowerer.coerceToExpected(radix, DYN)],
|
||||
type: STRING,
|
||||
loc: locOf(call),
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "num.toStringRadix",
|
||||
args: [operand, lowerer.lowerExprExpecting(call.arguments[0]!, DYN)],
|
||||
args: [operand, lowerer.lowerExprExpecting(arg, DYN)],
|
||||
type: STRING,
|
||||
loc: locOf(call),
|
||||
};
|
||||
@@ -6526,9 +6625,9 @@ function recordAssignHelper(
|
||||
}
|
||||
|
||||
/** The `Iterator` global's statics (ES2025 — Iterator.from, and the
|
||||
* abstract constructor as a value): no first-class iterator objects
|
||||
* exist here, so every member fences with the working spelling named
|
||||
* instead of the generic-method fence's monomorphization wording. */
|
||||
* abstract constructor as a value) have no lowering: every member fences
|
||||
* with the working spelling named instead of the generic-method fence's
|
||||
* monomorphization wording. */
|
||||
function lowerIteratorStaticFence(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
@@ -6539,8 +6638,8 @@ function lowerIteratorStaticFence(
|
||||
lowerer.noLowering(
|
||||
`Iterator.${access.name.text}`,
|
||||
call,
|
||||
"first-class iterator objects have no lowering — iterator helpers compile as one chain on an " +
|
||||
"array iterator, consumed in place: arr.values().map(f).take(n).toArray()",
|
||||
"iterator helpers apply directly to generators and to array, Map and Set iterators: " +
|
||||
"arr.values().map(f).take(n).toArray()",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8191,6 +8290,30 @@ function lowerObjectAssignHybrid(lowerer: Lowerer, call: ts.CallExpression): IrE
|
||||
return { kind: "recordLit", fields, type: mapped, loc };
|
||||
}
|
||||
|
||||
/** The accessed member is an overload set (isOverloadedCallableMemberType),
|
||||
* also under an optional chain whose access still spells `| undefined`. */
|
||||
function isOverloadedMemberAccess(lowerer: Lowerer, access: ts.PropertyAccessExpression): boolean {
|
||||
return isOverloadedCallableMemberType(
|
||||
lowerer.checker.getNonNullableType(lowerer.typeOf(access)),
|
||||
lowerer.checker,
|
||||
);
|
||||
}
|
||||
|
||||
/** The result type of the overload a call through an overloaded member
|
||||
* resolved to: the erased slot's result converts to it. A non-generic
|
||||
* overload answers through its declaration, because the resolved signature
|
||||
* of an optional-chain call (`api?.m(x)`) also carries the chain's
|
||||
* undefined; a generic one answers through its instantiation. */
|
||||
function resolvedOverloadResultType(lowerer: Lowerer, call: ts.CallExpression): IrType {
|
||||
const { checker } = lowerer;
|
||||
const signature = checker.getResolvedSignature(call);
|
||||
const decl = signature ? checker.signatureDeclaration(signature) : undefined;
|
||||
const declared = decl ? checker.getSignatureFromDeclaration(decl) : undefined;
|
||||
const source = declared && declared.getTypeParameters().length === 0 ? declared : signature;
|
||||
const result = source ? lowerer.mapTypeOf(checker.getReturnTypeOfSignature(source)) : null;
|
||||
return result ?? lowerer.irTypeOf(call);
|
||||
}
|
||||
|
||||
export function lowerRecordFieldCall(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
@@ -8222,7 +8345,18 @@ export function lowerRecordFieldCall(
|
||||
const callee = lowerer.lowerUnionProperty(access);
|
||||
if (callee?.type.kind !== "func") return null;
|
||||
const args = completeFuncValueArgs(lowerer, call, callee.type, locOf(call));
|
||||
return { kind: "callValue", callee, args, type: callee.type.ret, loc: locOf(call) };
|
||||
const value: IrExpr = {
|
||||
kind: "callValue",
|
||||
callee,
|
||||
args,
|
||||
type: callee.type.ret,
|
||||
loc: locOf(call),
|
||||
};
|
||||
// An overloaded member's erased result converts to the resolved
|
||||
// overload's type, as on a single record receiver below.
|
||||
return isOverloadedMemberAccess(lowerer, access)
|
||||
? lowerer.coerceInto(call, value, resolvedOverloadResultType(lowerer, call))
|
||||
: value;
|
||||
}
|
||||
const target = lowerer.fieldTarget(access);
|
||||
const loc = locOf(call);
|
||||
@@ -8297,10 +8431,14 @@ export function lowerRecordFieldCall(
|
||||
type: callee.type.ret,
|
||||
loc,
|
||||
};
|
||||
// A generic member's field holds its erased instance: the result
|
||||
// converts to this call's instantiated return type at the call.
|
||||
if (isGenericCallableMemberType(lowerer.typeOf(access), lowerer.checker))
|
||||
// A generic member's field holds its erased instance, and an overloaded
|
||||
// member's field its erased function (the overloads joined): the result
|
||||
// converts to this call's instantiated or resolved return type.
|
||||
const memberType = lowerer.typeOf(access);
|
||||
if (isGenericCallableMemberType(memberType, lowerer.checker))
|
||||
result = lowerer.coerceInto(call, result, lowerer.irTypeOf(call));
|
||||
else if (isOverloadedMemberAccess(lowerer, access))
|
||||
result = lowerer.coerceInto(call, result, resolvedOverloadResultType(lowerer, call));
|
||||
return init ? { kind: "seqExpr", stmts: [init], result, type: result.type, loc } : result;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
builtinClassRefOf,
|
||||
} from "./builtins/class-values.js";
|
||||
import type { FnCtx, Lowerer } from "./lowerer.js";
|
||||
import { inlineNamespaceRequireOf, lowerWithInlineRequireInit } from "./cjs-inline-require.js";
|
||||
import {
|
||||
BOOL,
|
||||
DATE_T,
|
||||
@@ -50,6 +51,7 @@ import {
|
||||
implicitAnyParamSymbolsOf,
|
||||
implicitCallInstance,
|
||||
implicitMonoFile,
|
||||
genericLocalClassResult,
|
||||
type GenericFnInfo,
|
||||
} from "./generic-functions.js";
|
||||
import {
|
||||
@@ -228,6 +230,11 @@ export function storedClassValueType(
|
||||
if (value?.kind === "classval") return value;
|
||||
}
|
||||
}
|
||||
// A generic factory's own local class has one class per instance.
|
||||
if (ts.isCallExpression(expression)) {
|
||||
const local = genericLocalClassResult(lowerer, expression);
|
||||
if (local?.kind === "classval") return local;
|
||||
}
|
||||
const type = lowerer.typeOf(expression);
|
||||
const mapped = lowerer.mapTypeOf(type);
|
||||
if (mapped?.kind === "classval" && !lowerer.classes.has(mapped.className)) {
|
||||
@@ -8475,6 +8482,14 @@ function isCollectionOrErrorClass(ref: BuiltinClassRef | null): boolean {
|
||||
|
||||
export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
|
||||
const loc = locOf(expr);
|
||||
// `new (require("./x").C)()`: the module's %init at the require's
|
||||
// position, then construction through the member as for a binding.
|
||||
if (ts.isPropertyAccessExpression(expr.expression) && !expr.expression.questionDotToken) {
|
||||
const req = inlineNamespaceRequireOf(lowerer, expr.expression.expression);
|
||||
if (req !== null && !lowerer.inlineRequireInitDone.has(req.call)) {
|
||||
return lowerWithInlineRequireInit(lowerer, req, () => lowerNew(lowerer, expr));
|
||||
}
|
||||
}
|
||||
const worker = lowerWorkerNew(lowerer, expr);
|
||||
if (worker) return worker;
|
||||
const readlineUi = lowerReadlineUiNew(lowerer, expr);
|
||||
|
||||
@@ -11,6 +11,7 @@ import { lowerWorkerMetadata } from "./builtins/workers.js";
|
||||
import { esmImportBindingKind } from "./builtins/module-bindings.js";
|
||||
import { lowerReadlineProperty } from "./builtins/readline.js";
|
||||
import { lowerBlobInstanceof } from "./builtins/blob.js";
|
||||
import { lowerWebAssemblyTypeof, refuseWebAssemblyUse } from "./builtins/webassembly.js";
|
||||
import { lowerKeyObjectInstanceof } from "./builtins/key-object.js";
|
||||
import { lowerNetAddressInstanceof, lowerNetAddressStaticRead } from "./builtins/net-address.js";
|
||||
import { isExecExceptionType, refuseExecExceptionCode } from "./builtins/errors.js";
|
||||
@@ -45,6 +46,18 @@ import { constituentTypes } from "../ts7/checker.js";
|
||||
import { dirname } from "node:path";
|
||||
import * as posix from "node:path/posix";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import {
|
||||
inlineNamespaceRequireOf,
|
||||
lowerWithInlineRequireInit,
|
||||
wholeExportValue,
|
||||
} from "./cjs-inline-require.js";
|
||||
import { tscInteropDefaultValueOf } from "./cjs-tsc-interop.js";
|
||||
import {
|
||||
cjsExportMemberWriteGlobalOf,
|
||||
cjsInFileExportGlobalOf,
|
||||
isCjsExportsIdentifier,
|
||||
lowerCjsExportsObjectValue,
|
||||
} from "./cjs-exports-object.js";
|
||||
import {
|
||||
lowerMaybePromiseAwait,
|
||||
maybePromiseInstanceOf,
|
||||
@@ -212,6 +225,7 @@ import {
|
||||
isNodeModuleValue,
|
||||
lowerCjsTopLevelThis,
|
||||
lowerNodeModuleIdentifier,
|
||||
isCjsModuleGlobal,
|
||||
lowerNodeModuleProperty,
|
||||
lowerRequireCacheElement,
|
||||
lowerRequireCacheHas,
|
||||
@@ -450,6 +464,8 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
if (externalTypeSpecifier !== null) {
|
||||
lowerer.externalHostFence(externalTypeSpecifier, expr);
|
||||
}
|
||||
// Every reached use of the WebAssembly API fences at its expression.
|
||||
refuseWebAssemblyUse(lowerer, expr);
|
||||
|
||||
if (ts.isNumericLiteral(expr)) {
|
||||
const value = Number(expr.text.replace(/_/g, ""));
|
||||
@@ -615,6 +631,8 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
);
|
||||
}
|
||||
if (ts.isTypeOfExpression(expr)) {
|
||||
const webAssembly = lowerWebAssemblyTypeof(lowerer, expr.expression);
|
||||
if (webAssembly) return webAssembly;
|
||||
if (lowerer.isStdlibGlobal(expr.expression, "process"))
|
||||
return { kind: "strLit", value: "object", type: STRING, loc };
|
||||
if (lowerer.isStdlibGlobal(expr.expression, "fetch"))
|
||||
@@ -867,6 +885,10 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
|
||||
const moduleValue = lowerNodeModuleIdentifier(lowerer, expr);
|
||||
if (moduleValue) return moduleValue;
|
||||
if (expr.text === "exports") {
|
||||
const exportsValue = lowerCjsExportsObjectValue(lowerer, expr);
|
||||
if (exportsValue) return exportsValue;
|
||||
}
|
||||
if (lowerer.isSelfReference(expr)) {
|
||||
return { kind: "selfRef", type: lowerer.ctx.selfType!, loc };
|
||||
}
|
||||
@@ -1860,6 +1882,28 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
if (ts.isConditionalExpression(expr)) return lowerTernary(lowerer, expr);
|
||||
|
||||
if (ts.isPropertyAccessExpression(expr)) {
|
||||
// `require("./x").member` over a program module's export table: the
|
||||
// module's %init at this position, then the member read exactly like
|
||||
// a `const m = require("./x")` binding's (cjs-inline-require.ts).
|
||||
if (!expr.questionDotToken) {
|
||||
const req = inlineNamespaceRequireOf(lowerer, expr.expression);
|
||||
if (req !== null && !lowerer.inlineRequireInitDone.has(req.call)) {
|
||||
return lowerWithInlineRequireInit(lowerer, req, () => lowerExpr(lowerer, expr));
|
||||
}
|
||||
// `module.exports` compared by identity inside its own module, or
|
||||
// refused by name as any other value (cjs-exports-object.ts).
|
||||
if (expr.name.text === "exports") {
|
||||
const exportsValue = lowerCjsExportsObjectValue(lowerer, expr);
|
||||
if (exportsValue) return exportsValue;
|
||||
}
|
||||
// tsc's `x_1.default` over a dependency whose whole export is one
|
||||
// value: that value (cjs-tsc-interop.ts).
|
||||
const interop = tscInteropDefaultValueOf(lowerer, expr);
|
||||
if (interop !== null) {
|
||||
const value = wholeExportValue(lowerer, interop.dep, expr);
|
||||
if (value !== null) return value;
|
||||
}
|
||||
}
|
||||
{
|
||||
const member = lifetimeRegionMemberOf(lowerer.program, expr, lowerer.experimentalFiles);
|
||||
if (member !== null)
|
||||
@@ -2206,35 +2250,12 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
// global (pure alias plumbing) and fall through unchanged; write
|
||||
// positions belong to the export-assignment machinery.
|
||||
{
|
||||
const sf = expr.getSourceFile();
|
||||
const cjsMemberRecv =
|
||||
isModuleExportsAccess(expr.expression) ||
|
||||
(ts.isIdentifier(expr.expression) &&
|
||||
expr.expression.text === "exports" &&
|
||||
!lowerer.resolveLocal(expr.expression) &&
|
||||
!lowerer.globalOf(expr.expression));
|
||||
const writePos =
|
||||
ts.isBinaryExpression(expr.parent) &&
|
||||
expr.parent.left === expr &&
|
||||
expr.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken;
|
||||
if (
|
||||
!expr.questionDotToken &&
|
||||
cjsMemberRecv &&
|
||||
!writePos &&
|
||||
isCjsJsFile(sf, lowerer.program)
|
||||
) {
|
||||
const sym =
|
||||
lowerer.checker.getSymbolAtLocation(expr.name) ??
|
||||
lowerer.cjsModuleExportSymbol(sf, expr.name.text);
|
||||
const resolved =
|
||||
sym !== undefined && sym.flags & ts.SymbolFlags.Alias
|
||||
? lowerer.checker.getAliasedSymbol(sym)
|
||||
: sym;
|
||||
const g =
|
||||
(sym ? lowerer.globalsBySymbol.get(sym) : undefined) ??
|
||||
(resolved ? lowerer.globalsBySymbol.get(resolved) : undefined);
|
||||
if (g) return { kind: "varRef", localId: g.id, type: g.type, loc };
|
||||
}
|
||||
const g = cjsInFileExportGlobalOf(lowerer, expr);
|
||||
if (g && !writePos) return { kind: "varRef", localId: g.id, type: g.type, loc };
|
||||
}
|
||||
// Namespace-qualified reads (`N.x`, `A.B.C.f`, import= alias
|
||||
// chains): the member is a compile-time-known declaration of a
|
||||
@@ -2830,6 +2851,23 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
);
|
||||
}
|
||||
}
|
||||
// A member a JavaScript module attached to one of its functions
|
||||
// (`status.message = codes; module.exports = status`), read from
|
||||
// TypeScript: JavaScript stores such members on the function's own
|
||||
// property table (`fn.member = v` writes through the function's
|
||||
// checked-dynamic face), so the read takes the same path and enters
|
||||
// the static code at the type inference gave the member.
|
||||
if (recvLowered.type.kind === "func" && jsFunctionExpandoMember(lowerer, expr)) {
|
||||
const read: IrExpr = {
|
||||
kind: "dynKeyGet",
|
||||
value: lowerer.coerceToExpected(recvLowered, DYN),
|
||||
key: { kind: "strLit", value: expr.name.text, type: STRING, loc: locOf(expr.name) },
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
const mapped = lowerer.mapTypeOf(lowerer.typeOf(expr));
|
||||
return mapped && mapped.kind !== "void" ? lowerer.coerceInto(expr, read, mapped) : read;
|
||||
}
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
@@ -10230,6 +10268,35 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
// RHS value is the expression's value — its OWN type, so the
|
||||
// consumer sees exactly what the checker typed.
|
||||
if (ts.isPropertyAccessExpression(expr.left) && !expr.left.questionDotToken) {
|
||||
// `const src = exports.src = []`: an export member assigned in
|
||||
// value position writes its export global and yields the value.
|
||||
{
|
||||
let inFunction = false;
|
||||
for (let p: ts.Node | undefined = expr.parent; p !== undefined; p = p.parent) {
|
||||
if (ts.isFunctionLike(p) || ts.isClassDeclaration(p) || ts.isClassExpression(p)) {
|
||||
inFunction = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const g = inFunction
|
||||
? cjsExportMemberWriteGlobalOf(lowerer, expr.left)
|
||||
: cjsInFileExportGlobalOf(lowerer, expr.left);
|
||||
if (g) {
|
||||
const value = lowerer.lowerExprExpecting(expr.right, g.type);
|
||||
const tmp = lowerer.declareHiddenLocal("%exportValue", value.type);
|
||||
const result = varRef(tmp.id, value.type, loc);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
{ kind: "varDecl", localId: tmp.id, init: value, loc },
|
||||
{ kind: "assign", localId: g.id, value: result, loc },
|
||||
],
|
||||
result,
|
||||
type: value.type,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
}
|
||||
const recv = tryLowerExpression(lowerer, expr.left.expression);
|
||||
if (recv && recv.type.kind === "dyn") return lowerDynMemberAssignment(lowerer, expr, recv);
|
||||
const unionWrite = lowerUnionFieldWrite(lowerer, expr.left, expr.right);
|
||||
@@ -10339,6 +10406,7 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
op === ts.SyntaxKind.ExclamationEqualsEqualsToken
|
||||
) {
|
||||
const test =
|
||||
lowerWebAssemblyTypeofTest(lowerer, expr, loc) ??
|
||||
lowerErrorCodeTypeofTest(lowerer, expr, loc) ??
|
||||
lowerer.lowerCaughtTypeofTest(expr, loc) ??
|
||||
lowerDynTypeofTest(lowerer, expr, loc) ??
|
||||
@@ -11731,6 +11799,24 @@ function errorRootedObjectOf(lowerer: Lowerer, t: ts.Type): IrType | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** `typeof WebAssembly === "object"` and `typeof WebAssembly.<member> ===
|
||||
* "function"`: the operand needs no value, so the test folds as bare typeof
|
||||
* does (builtins/webassembly.ts) before any dyn-typed operand lowers. */
|
||||
function lowerWebAssemblyTypeofTest(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.BinaryExpression,
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
const negated = expr.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsEqualsToken;
|
||||
for (const [a, b] of operandPairs(expr)) {
|
||||
if (!ts.isTypeOfExpression(a) || !ts.isStringLiteral(b)) continue;
|
||||
const folded = lowerWebAssemblyTypeof(lowerer, a.expression);
|
||||
if (folded?.kind !== "strLit") continue;
|
||||
return { kind: "boolLit", value: (folded.value === b.text) !== negated, type: BOOL, loc };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** `typeof e.code === "string"` / `"undefined"` (and the `!==` forms)
|
||||
* where e — through parens and as-casts — is a catch binding or a value
|
||||
* of %Error-rooted type: the runtime error's code slot is a string
|
||||
@@ -12428,6 +12514,7 @@ export function caughtLocalOf(lowerer: Lowerer, node: ts.Expression): IrLocal |
|
||||
* Folding is limited to side-effect-free operands — dropping a computed
|
||||
* operand would skip its effects, so those are rejected instead. */
|
||||
export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc: SrcLoc): IrExpr {
|
||||
refuseWebAssemblyUse(lowerer, expr.right);
|
||||
// `x instanceof net.Socket` over a union with a netSocket arm — the
|
||||
// h2 compat 'connect' narrowing (lower-server.ts): a union tag test.
|
||||
const sockTest = lowerSocketInstanceOf(lowerer, expr, loc);
|
||||
@@ -15089,6 +15176,9 @@ export function fieldTarget(
|
||||
access: ts.PropertyAccessExpression,
|
||||
): FieldTarget | null {
|
||||
if (lowerer.cjsLocalModuleBindingOf(access.expression)) return null;
|
||||
// `module.exports` / `module.id`: CommonJS module surface, never a field.
|
||||
if (isCjsModuleGlobal(lowerer, access.expression)) return null;
|
||||
if (isCjsExportsIdentifier(lowerer, access.expression)) return null;
|
||||
if (lowerer.chainBlocked(access)) return null;
|
||||
if (
|
||||
access.name.text === "stackTraceLimit" &&
|
||||
@@ -16379,3 +16469,28 @@ function isBuiltinModuleValueDecl(decl: ts.Node): boolean {
|
||||
ts.isVariableDeclaration(decl))
|
||||
);
|
||||
}
|
||||
|
||||
/** True when `access` reads a member that JavaScript sources attach to a
|
||||
* function by assignment (`fn.member = value`): the receiver's type is
|
||||
* callable and every declaration of the member is such an assignment in a
|
||||
* JS file. */
|
||||
function jsFunctionExpandoMember(lowerer: Lowerer, access: ts.PropertyAccessExpression): boolean {
|
||||
if (access.questionDotToken || !ts.isIdentifier(access.name)) return false;
|
||||
const recvType = lowerer.typeOf(access.expression);
|
||||
if (lowerer.checker.getCallSignatures(recvType).length === 0) return false;
|
||||
const member = lowerer.checker.getPropertyOfType(recvType, access.name.text);
|
||||
const decls = member ? lowerer.checker.declarationsOf(member) : [];
|
||||
return (
|
||||
decls.length > 0 &&
|
||||
decls.every((d) => {
|
||||
if (!isJsSourceFile(d.getSourceFile())) return false;
|
||||
const assign = ts.isBinaryExpression(d) ? d : d.parent;
|
||||
return (
|
||||
assign !== undefined &&
|
||||
ts.isBinaryExpression(assign) &&
|
||||
assign.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
||||
ts.isPropertyAccessExpression(assign.left)
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -115,6 +115,16 @@ export function lowerGenMethodCall(
|
||||
if (call.arguments.some(ts.isSpreadElement)) {
|
||||
lowerer.unsupported("SC1090", call, "spread arguments");
|
||||
}
|
||||
// Iterator helpers and the built-in iterators have no throw method; an
|
||||
// iterator-typed value may hold one, so only a generator's own member
|
||||
// lowers.
|
||||
if (name === "throw" && !isGeneratorMember(lowerer, access)) {
|
||||
lowerer.noLowering(
|
||||
`${lowerer.checker.typeToString(lowerer.typeOf(access.expression))}.throw`,
|
||||
call,
|
||||
"throw() lowers on values typed as generators (iterator helpers and built-in iterators have no throw method)",
|
||||
);
|
||||
}
|
||||
const loc = locOf(call);
|
||||
const gen = lowerer.lowerExpr(access.expression);
|
||||
if (gen.type.kind !== "generator") return null;
|
||||
@@ -187,6 +197,23 @@ export function lowerGenMethodCall(
|
||||
return { kind: "genResume", mode: name, gen, arg, type: resultT, loc };
|
||||
}
|
||||
|
||||
/** Whether `access` names a member the lib declares on Generator or
|
||||
* AsyncGenerator itself. */
|
||||
function isGeneratorMember(lowerer: Lowerer, access: ts.PropertyAccessExpression): boolean {
|
||||
const member = lowerer.checker.getSymbolAtLocation(access.name);
|
||||
return (
|
||||
member !== undefined &&
|
||||
lowerer.checker
|
||||
.declarationsOf(member)
|
||||
.some(
|
||||
(d) =>
|
||||
ts.isInterfaceDeclaration(d.parent) &&
|
||||
(d.parent.name.text === "Generator" || d.parent.name.text === "AsyncGenerator") &&
|
||||
lowerer.isStdlibFile(d.getSourceFile()),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/** Extraction of a suspended resume's yield value out of the result
|
||||
* record's V slot, typed back to the yield channel: V IS the channel for
|
||||
* dyn (and for a union channel already equal to V); a non-union channel
|
||||
@@ -319,7 +346,7 @@ export function genValue(gen: IrExpr & { type: GenType }, loc: SrcLoc): IrExpr {
|
||||
return { kind: "genResume", mode: "value", gen, arg: null, type: gen.type.yieldT, loc };
|
||||
}
|
||||
|
||||
function generatorLoopBinding(
|
||||
export function generatorLoopBinding(
|
||||
lowerer: Lowerer,
|
||||
stmt: ts.ForOfStatement,
|
||||
value: IrExpr,
|
||||
@@ -572,7 +599,9 @@ export function lowerForOfGenerator(
|
||||
catchBody: null,
|
||||
catchLocalId: null,
|
||||
// IteratorClose: every exit that leaves the generator unfinished
|
||||
// closes it — finallys run; the .return() result is dropped.
|
||||
// closes it — finallys run; the .return() result is dropped, and
|
||||
// after a throw from the body an error from the close is too.
|
||||
dropFinallyErrors: true,
|
||||
finallyBody: [
|
||||
{
|
||||
kind: "if",
|
||||
|
||||
@@ -18,7 +18,9 @@ import { isRelativeSpecifier } from "../workspace-registry.js";
|
||||
import {
|
||||
canonicalBuiltinModule,
|
||||
cjsExportAssignmentOf,
|
||||
cjsExportChainOf,
|
||||
cjsExportDiscardReason,
|
||||
cjsExportTargetLiteral,
|
||||
entryPackageFilePredicate,
|
||||
isCjsJsFile,
|
||||
isJsSourceFile,
|
||||
@@ -26,6 +28,7 @@ import {
|
||||
locOf,
|
||||
makeCycleAdmission,
|
||||
orderedImportsOf,
|
||||
requireSpecOf,
|
||||
resolveImport,
|
||||
resolveNpmImport,
|
||||
} from "../program.js";
|
||||
@@ -36,6 +39,13 @@ import {
|
||||
withCycleInitFlags,
|
||||
} from "./cycle-initialization.js";
|
||||
import { markRequireEarlyBindings } from "./require-initialization.js";
|
||||
import {
|
||||
cjsTableMethodEntry,
|
||||
inlineProgramRequireOf,
|
||||
nestedCjsExportAssignmentsOf,
|
||||
requireNestedPatternsOf,
|
||||
} from "./cjs-inline-require.js";
|
||||
import { isDeadTscHelperDecl, tscInteropBindingOfDecl } from "./cjs-tsc-interop.js";
|
||||
import { collectJsonRequires, isJsonRequireDecl } from "./json-require.js";
|
||||
import {
|
||||
invalidJsonModuleDiag,
|
||||
@@ -1391,6 +1401,11 @@ export function collectGlobals(
|
||||
deferJsCollectionDiags(lowerer, sf, diagsBefore);
|
||||
}
|
||||
};
|
||||
// Export members assigned in VALUE position at the module's top
|
||||
// level (`const src = exports.src = []`) are exports too.
|
||||
for (const nested of nestedCjsExportAssignmentsOf(stmt, cjs?.expr)) {
|
||||
registerExport(nested.name, nested.name.text, nested.value);
|
||||
}
|
||||
if (cjs?.kind === "member" && ts.isIdentifier(cjs.name)) {
|
||||
// `exports.C = C` naming a CLASS declaration: alias plumbing —
|
||||
// no storage (a class VALUE global would fence for
|
||||
@@ -1398,6 +1413,13 @@ export function collectGlobals(
|
||||
// symbol (cjsMemberExportClassSymbol) and the class registry
|
||||
// applies unchanged.
|
||||
if (lowerer.cjsMemberExportClassSymbol(cjs.expr) !== null) continue;
|
||||
// A chain `exports.a = exports.b = value`: every member is an
|
||||
// export holding the chain's one value.
|
||||
const chain = cjsExportChainOf(cjs.expr);
|
||||
if (chain !== null) {
|
||||
for (const nameNode of chain.names) registerExport(nameNode, nameNode.text, chain.value);
|
||||
continue;
|
||||
}
|
||||
registerExport(cjs.name, cjs.name.text, cjs.value);
|
||||
continue;
|
||||
}
|
||||
@@ -1429,7 +1451,22 @@ export function collectGlobals(
|
||||
// statement lowering evaluates the call exactly once at its
|
||||
// source position and assigns that slot. Unmappable calls retain
|
||||
// the existing statement-level fence.
|
||||
const factoryValued = ts.isCallExpression(rhs);
|
||||
// Any other computed value (`os.homedir || function () {…}`,
|
||||
// `cond ? a : b`, `new Registry()`, `ns.member`) is the same single
|
||||
// snapshot: Node stores whatever the expression produced. A
|
||||
// require() of a program module is not: the exporter re-exports
|
||||
// that module's exports, which requirers resolve through the
|
||||
// checker's alias (the statement lowers to the dep's %init).
|
||||
const factoryValued =
|
||||
!ts.isIdentifier(rhs) &&
|
||||
!ts.isClassExpression(rhs) &&
|
||||
!ts.isObjectLiteralExpression(rhs) &&
|
||||
!(
|
||||
requireSpecOf(rhs) !== null &&
|
||||
inlineProgramRequireOf(lowerer, rhs) !== null &&
|
||||
!sf.statements.some((s) => cjsExportAssignmentOf(s)?.kind === "member")
|
||||
) &&
|
||||
cjsExportTargetLiteral(rhs, sf) === null;
|
||||
if (cjsScalarLiteral(rhs) || fnValued || factoryValued) {
|
||||
const diagsBefore = lowerer.diags.length;
|
||||
try {
|
||||
@@ -1464,6 +1501,10 @@ export function collectGlobals(
|
||||
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
|
||||
registerExport(prop.name, prop.name.text, prop.initializer);
|
||||
}
|
||||
// A METHOD entry (`module.exports = { f(x) { … } }`) is an
|
||||
// expression-valued export whose value is the method's function.
|
||||
const method = cjsTableMethodEntry(prop);
|
||||
if (method !== null) registerExport(method.name, method.name.text, method.name);
|
||||
// other property forms fence in the statement lowering
|
||||
}
|
||||
continue;
|
||||
@@ -1527,6 +1568,16 @@ export function collectGlobals(
|
||||
collectNestedVarGlobals(lowerer, sf, stmt, tag);
|
||||
continue;
|
||||
}
|
||||
// tsc's interop bindings and its then-dead helper declarations own no
|
||||
// storage either (cjs-tsc-interop.ts).
|
||||
if (
|
||||
isJsSourceFile(sf) &&
|
||||
(isDeadTscHelperDecl(lowerer, stmt) ||
|
||||
(stmt.declarationList.declarations.length === 1 &&
|
||||
tscInteropBindingOfDecl(lowerer, stmt.declarationList.declarations[0]!) !== null))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
// CommonJS require declarations are alias plumbing (preflight owns
|
||||
// the module edges; resolveValueSymbol routes reads through to the
|
||||
// exporter's declarations): no storage, and the statement lowers to
|
||||
@@ -1549,6 +1600,38 @@ export function collectGlobals(
|
||||
stdlibGlobalAliasDecl(lowerer, decl.name, decl.initializer);
|
||||
}
|
||||
}
|
||||
// `const { codes: { ERR_X } } = require("./errors")`: the outer
|
||||
// keys stay alias plumbing, but a nested pattern destructures the
|
||||
// export's value at the require, so its bindings own storage like
|
||||
// any destructured declaration (lowerVarDeclarator assigns them).
|
||||
if (ts.isVariableStatement(stmt)) {
|
||||
const isLetList =
|
||||
(stmt.declarationList.flags & ts.NodeFlags.Let) !== 0 ||
|
||||
(stmt.declarationList.flags & ts.NodeFlags.BlockScoped) === 0;
|
||||
for (const decl of stmt.declarationList.declarations) {
|
||||
for (const element of requireNestedPatternsOf(decl)) {
|
||||
for (const nameNode of boundIdentifiersOf(element.name!)) {
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(nameNode);
|
||||
if (!symbol || lowerer.globalsBySymbol.has(symbol)) continue;
|
||||
const mapped = lowerer.mapTypeOf(lowerer.typeOf(nameNode));
|
||||
const type = mapped && mapped.kind !== "void" ? mapped : DYN;
|
||||
const g: IrGlobal = {
|
||||
id: `%g.${tag}${nameNode.text}`,
|
||||
name: nameNode.text,
|
||||
type,
|
||||
mutable: isLetList,
|
||||
source: bindingSource(nameNode),
|
||||
...((type.kind === "func" || type.kind === "dyn") && !isVarDeclared(decl)
|
||||
? { tdz: true as const }
|
||||
: {}),
|
||||
};
|
||||
lowerer.globalsBySymbol.set(symbol, g);
|
||||
lowerer.globalsList.push(g);
|
||||
if (isVarDeclared(decl)) noteVarGlobalEntryInit(lowerer, sf, g);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// `declare const __VERSION__: string` — AMBIENT: no storage exists
|
||||
|
||||
@@ -8,6 +8,7 @@ import * as win32 from "node:path/win32";
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import type { FileParts } from "./lower-modules.js";
|
||||
import { lowerCjsExportsObjectValue } from "./cjs-exports-object.js";
|
||||
import { isCjsJsFile, isNodeEsmFile, locOf } from "../program.js";
|
||||
import { runtimePathForTarget } from "../runtime-resolve.js";
|
||||
import {
|
||||
@@ -177,6 +178,12 @@ export function cjsModuleRef(lowerer: Lowerer, node: ts.Node): IrExpr | null {
|
||||
return id === undefined ? null : numLit(id, locOf(node));
|
||||
}
|
||||
|
||||
/** True when `node` is the CommonJS `require` global (not shadowed by a
|
||||
* source binding, and not in an ES module). */
|
||||
export function isCjsRequireGlobal(lowerer: Lowerer, node: ts.Expression): boolean {
|
||||
return cjsGlobal(lowerer, node, "require");
|
||||
}
|
||||
|
||||
export function isCjsModuleGlobal(lowerer: Lowerer, node: ts.Expression): boolean {
|
||||
return cjsGlobal(lowerer, node, "module");
|
||||
}
|
||||
@@ -428,6 +435,13 @@ export function lowerNodeModuleIdentifier(lowerer: Lowerer, expr: ts.Identifier)
|
||||
if (!isCjsModuleGlobal(lowerer, expr)) return null;
|
||||
const ref = cjsModuleRef(lowerer, expr);
|
||||
if (ref) return ref;
|
||||
if (isCjsJsFile(expr.getSourceFile(), lowerer.program)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
"the CommonJS 'module' object in this position (module.exports members, the whole module.exports in comparisons, and the module metadata fields lower)",
|
||||
);
|
||||
}
|
||||
lowerer.unsupported("SC1090", expr, "the CommonJS 'module' object in an ES module");
|
||||
}
|
||||
|
||||
@@ -441,6 +455,10 @@ export function lowerNodeModuleProperty(
|
||||
const member = expr.name.text;
|
||||
if (member === "exports" && direct && moduleExportsAlwaysTruthy(lowerer, expr))
|
||||
return boolLit(true, locOf(expr));
|
||||
if (member === "exports" && direct) {
|
||||
const value = lowerCjsExportsObjectValue(lowerer, expr);
|
||||
if (value) return value;
|
||||
}
|
||||
if (member === "exports" || member === "require") return null;
|
||||
let moduleRef = direct
|
||||
? cjsModuleRef(lowerer, expr.expression)
|
||||
|
||||
@@ -15,6 +15,19 @@ import { requireInitFlagStmt } from "./require-initialization.js";
|
||||
import { isJsonRequireDecl } from "./json-require.js";
|
||||
import { bindingSource } from "../binding-source.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import {
|
||||
cjsExportValueByName,
|
||||
cjsNamespaceDestructureOf,
|
||||
cjsTableMethodEntry,
|
||||
inlineProgramRequireOf,
|
||||
requireNestedPatternsOf,
|
||||
} from "./cjs-inline-require.js";
|
||||
import {
|
||||
cjsExportMemberWriteGlobalOf,
|
||||
isCjsExportsIdentifier,
|
||||
isFreshExportsReset,
|
||||
} from "./cjs-exports-object.js";
|
||||
import { isDeadTscHelperDecl, tscInteropBindingOfDecl } from "./cjs-tsc-interop.js";
|
||||
import {
|
||||
arrayValueRead,
|
||||
arrayValueStore,
|
||||
@@ -60,6 +73,7 @@ import {
|
||||
import { enforceLibBoundary, lowerSurplusCalls } from "./lib-boundary.js";
|
||||
import {
|
||||
cjsExportAssignmentOf,
|
||||
cjsExportChainOf,
|
||||
cjsExportDiscardReason,
|
||||
cjsExportTargetLiteral,
|
||||
isCjsJsFile,
|
||||
@@ -97,6 +111,7 @@ import {
|
||||
lowerForOfSearchParams,
|
||||
lowerForOfSet,
|
||||
} from "./containers/for-of.js";
|
||||
import { lowerForOfHelperChain } from "./containers/iterator-helpers.js";
|
||||
import { objectIterOverIndexShape } from "./containers/indexed-objects.js";
|
||||
import { strCharsCall } from "./containers/array-construction.js";
|
||||
import {
|
||||
@@ -107,6 +122,7 @@ import {
|
||||
implicitLocalFnInfoOf,
|
||||
implicitLocalFnNodeOf,
|
||||
implicitMethodCallInfersReturn,
|
||||
genericLocalClassBinding,
|
||||
} from "./generic-functions.js";
|
||||
import {
|
||||
bindingNeverReassigned,
|
||||
@@ -663,10 +679,14 @@ export function lowerStmts(lowerer: Lowerer, stmts: readonly ts.Statement[]): Ir
|
||||
out,
|
||||
};
|
||||
lowerer.activeStmtLists.push(entry);
|
||||
const inFunction = stmts.length > 0 && enclosedByFunction(stmts[0]!);
|
||||
let unreachable = false;
|
||||
try {
|
||||
for (let i = 0; i < stmts.length; i++) {
|
||||
const stmt = stmts[i]!;
|
||||
entry.index = i;
|
||||
if (unreachable && skipsWhenUnreachable(stmt)) continue;
|
||||
if (inFunction && isAbruptStatement(stmt)) unreachable = true;
|
||||
if (!lowerer.suppressStats) {
|
||||
lowerer.stats.statementsTotal++;
|
||||
if (sf !== undefined) lowerer.bumpFileStat(sf.fileName, "total");
|
||||
@@ -788,6 +808,54 @@ export function lowerStmts(lowerer: Lowerer, stmts: readonly ts.Statement[]): Ir
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Whether a statement list belongs to a function body (at any block
|
||||
* depth) rather than to a module's top level. */
|
||||
function enclosedByFunction(stmt: ts.Statement): boolean {
|
||||
let owner: ts.Node | undefined = stmt.parent;
|
||||
while (owner !== undefined && !ts.isFunctionLike(owner) && !ts.isSourceFile(owner))
|
||||
owner = owner.parent;
|
||||
return owner !== undefined && ts.isFunctionLike(owner);
|
||||
}
|
||||
|
||||
/** `return`, `throw`, `break` and `continue`: the rest of the enclosing
|
||||
* statement list never runs. */
|
||||
function isAbruptStatement(stmt: ts.Statement): boolean {
|
||||
return (
|
||||
ts.isReturnStatement(stmt) ||
|
||||
ts.isThrowStatement(stmt) ||
|
||||
ts.isBreakStatement(stmt) ||
|
||||
ts.isContinueStatement(stmt)
|
||||
);
|
||||
}
|
||||
|
||||
/** Statements after an abrupt statement in a function body that lowering
|
||||
* skips: they never execute, so neither their unsupported constructs nor the
|
||||
* functions they reference become part of the program (production builds of
|
||||
* libraries keep development-only code behind `throw` stubs this way).
|
||||
* Hoisting keeps its meaning: a reference above the abrupt statement to a
|
||||
* later function declaration lowers that declaration eagerly
|
||||
* (predeclareForwardFnDecl), and references to a later `var`, `let` or
|
||||
* `const` binding resolve through the forward-hoisting machinery to its
|
||||
* undefined or TDZ state, which skipping its initializer preserves. Class,
|
||||
* enum, module and import/export declarations and generic function
|
||||
* declarations still lower. */
|
||||
function skipsWhenUnreachable(stmt: ts.Statement): boolean {
|
||||
if (ts.isFunctionDeclaration(stmt)) return stmt.typeParameters === undefined;
|
||||
if (ts.isVariableStatement(stmt))
|
||||
return !(stmt.modifiers ?? []).some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
|
||||
return !(
|
||||
ts.isClassDeclaration(stmt) ||
|
||||
ts.isEnumDeclaration(stmt) ||
|
||||
ts.isModuleDeclaration(stmt) ||
|
||||
ts.isImportDeclaration(stmt) ||
|
||||
ts.isImportEqualsDeclaration(stmt) ||
|
||||
ts.isExportDeclaration(stmt) ||
|
||||
ts.isExportAssignment(stmt) ||
|
||||
ts.isInterfaceDeclaration(stmt) ||
|
||||
ts.isTypeAliasDeclaration(stmt)
|
||||
);
|
||||
}
|
||||
|
||||
const TDZ_KINDS = new Set<IrType["kind"]>([
|
||||
"func",
|
||||
"string",
|
||||
@@ -1556,6 +1624,16 @@ function lowerVarDeclarator(
|
||||
const init = lowerer.requireInitStmt(target.spec, decl);
|
||||
return init ? [init] : [];
|
||||
}
|
||||
// tsc's interop bindings (`const x_1 = __importDefault(require("./m"))`)
|
||||
// are require bindings (cjs-tsc-interop.ts): the module's %init here.
|
||||
if (ts.isSourceFile(stmt.parent)) {
|
||||
const interop = tscInteropBindingOfDecl(lowerer, decl);
|
||||
if (interop !== null) {
|
||||
const init = lowerer.requireInitStmt(interop.spec, decl);
|
||||
return init ? [init] : [];
|
||||
}
|
||||
if (isDeadTscHelperDecl(lowerer, stmt)) return [];
|
||||
}
|
||||
// CommonJS require declarations (JS files): at the module's top
|
||||
// level the BINDINGS are alias plumbing (no storage;
|
||||
// resolveValueSymbol routes the reads), but the require itself is
|
||||
@@ -1576,7 +1654,21 @@ function lowerVarDeclarator(
|
||||
if (lowerer.externalTypes.has(spec)) lowerer.externalHostFence(spec, decl, false);
|
||||
if (ts.isSourceFile(stmt.parent)) {
|
||||
const init = lowerer.requireInitStmt(spec, decl);
|
||||
return init ? [init] : [];
|
||||
const out: IrStmt[] = init ? [init] : [];
|
||||
// Nested patterns destructure the named export's value right after
|
||||
// the module evaluated (`const { codes: { ERR_X } } = require(...)`).
|
||||
for (const element of requireNestedPatternsOf(decl)) {
|
||||
const key = element.propertyName as ts.Identifier;
|
||||
const value = lowerer.lowerExpr(key);
|
||||
lowerer.lowerBindingPattern(
|
||||
element.name as ts.ObjectBindingPattern,
|
||||
() => lowerer.lowerExpr(key),
|
||||
value.type,
|
||||
isLet,
|
||||
out,
|
||||
);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
// A nested BUILTIN require (`get inFreeBSDJail() { const { execSync }
|
||||
// = require('child_process'); ... }` — test/common's lazy-getter
|
||||
@@ -1605,6 +1697,35 @@ function lowerVarDeclarator(
|
||||
// `const { createSign } = crypto` over a builtin NAMESPACE binding:
|
||||
// alias plumbing (builtinImportOf routes the reads) — no statement.
|
||||
if (builtinNamespaceDestructureModuleOf(lowerer, decl) !== null) return [];
|
||||
// `const { A, B } = ns` over a CommonJS module binding: each name
|
||||
// snapshots the named export's value here (cjs-inline-require.ts).
|
||||
{
|
||||
const nsDestructure = cjsNamespaceDestructureOf(lowerer, decl);
|
||||
if (nsDestructure !== null) {
|
||||
const out: IrStmt[] = [];
|
||||
for (const element of nsDestructure.elements) {
|
||||
const target = lowerer.globalOf(element.name);
|
||||
const value = cjsExportValueByName(lowerer, nsDestructure.dep, element.key, element.name);
|
||||
if (target === null || value === null) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
element.name,
|
||||
`destructuring the export '${element.key}' of a CommonJS module binding (it has no static value)`,
|
||||
);
|
||||
}
|
||||
out.push({
|
||||
kind: "assign",
|
||||
localId: target.id,
|
||||
value: typeEquals(value.type, target.type)
|
||||
? value
|
||||
: lowerer.coerceInto(element.name, value, target.type),
|
||||
loc: locOf(element.name),
|
||||
...("tdz" in target && target.tdz ? { initializes: true as const } : {}),
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
return lowerer.lowerDestructuringDecl(decl, isLet);
|
||||
}
|
||||
// STORED protocol values with statically closed representations:
|
||||
@@ -4913,6 +5034,9 @@ export function lowerVarDecl(
|
||||
) {
|
||||
type = init.type;
|
||||
}
|
||||
// `const B = factory(1)` inside a generic instance: the factory's local
|
||||
// class for the instance the call names.
|
||||
if (!type && !isLet) type = genericLocalClassBinding(lowerer, decl.name);
|
||||
if (!type) {
|
||||
// The JS declaration fallback (irTypeOf's story): the binding holds
|
||||
// the checked-dynamic kind when even the initializer's type has no
|
||||
@@ -6054,6 +6178,49 @@ function lowerCjsExportStatement(
|
||||
if (whole?.kind !== "table" || cjsExportDiscardReason(stmt) !== null) continue;
|
||||
root = lowerer.globalsByDeclNode.get(whole.expr);
|
||||
}
|
||||
// A CHAIN `exports.a = exports.b = value`: Node evaluates the value
|
||||
// once, assigns the innermost member first, and every member holds
|
||||
// the same value. Each member is its own export global
|
||||
// (collectGlobals registers the inner ones too).
|
||||
{
|
||||
const chain = cjsExportChainOf(cjs.expr);
|
||||
if (chain !== null && root?.type.kind !== "dyn") {
|
||||
const globalOfName = (nameNode: ts.Identifier): IrGlobal => {
|
||||
const symbol =
|
||||
lowerer.checker.getSymbolAtLocation(nameNode) ??
|
||||
lowerer.cjsModuleExportSymbol(sf, nameNode.text);
|
||||
const g = symbol ? lowerer.globalsBySymbol.get(symbol) : undefined;
|
||||
if (!g) {
|
||||
lowerer.lowerExpr(chain.value);
|
||||
lowerer.badType(chain.value, lowerer.typeOf(chain.value));
|
||||
}
|
||||
return g;
|
||||
};
|
||||
const inner = chain.names[chain.names.length - 1]!;
|
||||
const innerG = globalOfName(inner);
|
||||
const body: IrStmt[] = [
|
||||
{
|
||||
kind: "assign",
|
||||
localId: innerG.id,
|
||||
value: lowerer.lowerExprExpecting(chain.value, innerG.type),
|
||||
loc: locOf(chain.value),
|
||||
},
|
||||
];
|
||||
for (let i = chain.names.length - 2; i >= 0; i--) {
|
||||
const g = globalOfName(chain.names[i]!);
|
||||
const read: IrExpr = { kind: "varRef", localId: innerG.id, type: innerG.type, loc };
|
||||
body.push({
|
||||
kind: "assign",
|
||||
localId: g.id,
|
||||
value: typeEquals(g.type, innerG.type)
|
||||
? read
|
||||
: lowerer.coerceInto(chain.value, read, g.type),
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return { kind: "block", body, loc };
|
||||
}
|
||||
}
|
||||
return assignExport(cjs.name, cjs.value, root);
|
||||
}
|
||||
let tableObj = cjs.obj;
|
||||
@@ -6119,6 +6286,30 @@ function lowerCjsExportStatement(
|
||||
body.push(assignExport(prop.name, prop.initializer));
|
||||
continue;
|
||||
}
|
||||
// METHOD entries: the export global holds the method's function,
|
||||
// created at this statement like a function-expression entry. Node
|
||||
// calls it with `this` bound to the table, which the static export
|
||||
// table cannot represent, so a body that uses `this` keeps a fence.
|
||||
{
|
||||
const method = cjsTableMethodEntry(prop);
|
||||
if (method !== null && !resolvedTarget) {
|
||||
lowerer.rejectThisInObjectMethod(method.body);
|
||||
const symbol =
|
||||
lowerer.checker.getSymbolAtLocation(method.name) ??
|
||||
lowerer.cjsModuleExportSymbol(cjs.expr.getSourceFile(), method.name.text);
|
||||
const g = symbol ? lowerer.globalsBySymbol.get(symbol) : undefined;
|
||||
if (g) {
|
||||
const fn = lowerer.lowerLambda(method);
|
||||
body.push({
|
||||
kind: "assign",
|
||||
localId: g.id,
|
||||
value: typeEquals(fn.type, g.type) ? fn : lowerer.coerceInto(method, fn, g.type),
|
||||
loc: locOf(method),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
// ACCESSOR entries: no storage and no code here — reads lift the
|
||||
// getter as a module-level function and call it per read
|
||||
// (cjsExportAccessorRead); writes through the setter keep their
|
||||
@@ -6134,7 +6325,15 @@ function lowerCjsExportStatement(
|
||||
if (ts.isSpreadAssignment(prop)) {
|
||||
let inner: ts.Expression = prop.expression;
|
||||
while (ts.isParenthesizedExpression(inner)) inner = inner.expression;
|
||||
if (requireSpecOf(inner) !== null) continue;
|
||||
if (requireSpecOf(inner) !== null) {
|
||||
// The spread still EVALUATES the module, at its place among the
|
||||
// table's entries (a program module joins the graph through this
|
||||
// inline require; cjs-inline-require.ts).
|
||||
const req = inlineProgramRequireOf(lowerer, inner);
|
||||
const init = req !== null ? lowerer.requireInitStmt(req.spec, req.call) : null;
|
||||
if (init !== null && !resolvedTarget) body.push(init);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
@@ -6167,6 +6366,16 @@ function lowerCjsSingleValueExport(
|
||||
): IrStmt | null {
|
||||
let rhs: ts.Expression = expr.right;
|
||||
while (ts.isParenthesizedExpression(rhs)) rhs = rhs.expression;
|
||||
// `module.exports = require("./x")`: the module re-exports x's exports.
|
||||
// Requirers resolve through the checker's alias to x's own export
|
||||
// machinery; this statement is x's evaluation at this position.
|
||||
{
|
||||
const req = inlineProgramRequireOf(lowerer, rhs);
|
||||
// With later member exports (`module.exports.extra = …`) the module
|
||||
// extends that object, so it keeps the factory root registration.
|
||||
if (req !== null && !lowerer.globalsByDeclNode.has(expr))
|
||||
return lowerer.requireInitStmt(req.spec, req.call) ?? { kind: "block", body: [], loc };
|
||||
}
|
||||
if (ts.isIdentifier(rhs)) {
|
||||
const sym = lowerer.resolveValueSymbol(rhs);
|
||||
if (!sym) return null;
|
||||
@@ -6177,6 +6386,17 @@ function lowerCjsSingleValueExport(
|
||||
) {
|
||||
return { kind: "block", body: [], loc };
|
||||
}
|
||||
// A require binding (`const x = require("./x"); module.exports = x`)
|
||||
// re-exports that module exactly like the inline form (the binding's
|
||||
// own statement already evaluated it), and a builtin global
|
||||
// (`module.exports = TypeError`) is the global itself: requirer
|
||||
// bindings alias straight through to it, so nothing is stored.
|
||||
if (
|
||||
lowerer.checker.declarationsOf(sym).some((d) => ts.isSourceFile(d)) ||
|
||||
lowerer.isStdlibSymbol(sym)
|
||||
) {
|
||||
return { kind: "block", body: [], loc };
|
||||
}
|
||||
const g = lowerer.globalsBySymbol.get(sym);
|
||||
if (g) {
|
||||
if (g.mutable) {
|
||||
@@ -6398,6 +6618,21 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm
|
||||
);
|
||||
}
|
||||
if (topLevelJs && ts.isExpressionStatement(stmtNode)) {
|
||||
if (isFreshExportsReset(lowerer, stmtNode))
|
||||
return { kind: "block", body: [], loc: locOf(expr) };
|
||||
// Any other `exports = …` rebinds the wrapper's exports binding: the
|
||||
// members later attached through it belong to the new value.
|
||||
if (
|
||||
ts.isBinaryExpression(expr) &&
|
||||
expr.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
||||
isCjsExportsIdentifier(lowerer, expr.left)
|
||||
) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr.left,
|
||||
"rebinding 'exports' (exports = module.exports = value): members attached through 'exports' afterwards are properties of that value, which the static export table does not model — write module.exports.<name> = … and module.exports = value",
|
||||
);
|
||||
}
|
||||
const cjs = cjsExportAssignmentOf(stmtNode);
|
||||
if (cjs) return lowerCjsExportStatement(lowerer, cjs);
|
||||
// The tsc CJS interop stamp — `Object.defineProperty(exports,
|
||||
@@ -6939,6 +7174,19 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm
|
||||
const parts = lowerDestructuringAssignParts(lowerer, expr.left, expr.right, locOf(expr));
|
||||
return { kind: "block", body: parts.stmts, loc: locOf(expr) };
|
||||
}
|
||||
// `exports.x = v` inside a function updates the member's export
|
||||
// global (cjs-exports-object.ts).
|
||||
if (ts.isPropertyAccessExpression(expr.left)) {
|
||||
const g = cjsExportMemberWriteGlobalOf(lowerer, expr.left);
|
||||
if (g) {
|
||||
return {
|
||||
kind: "assign",
|
||||
localId: g.id,
|
||||
value: lowerer.lowerExprExpecting(expr.right, g.type),
|
||||
loc: locOf(expr),
|
||||
};
|
||||
}
|
||||
}
|
||||
if (!ts.isIdentifier(expr.left)) {
|
||||
lowerer.unsupported("SC1090", expr.left, "assignment to non-variables");
|
||||
}
|
||||
@@ -8299,6 +8547,12 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt {
|
||||
}
|
||||
}
|
||||
}
|
||||
// A head that is a chain of iterator-helper stages runs as one loop
|
||||
// over the chain's source.
|
||||
{
|
||||
const fused = lowerForOfHelperChain(lowerer, stmt, labels);
|
||||
if (fused) return fused;
|
||||
}
|
||||
// `for (const k of m.keys())` / `.values()` / `.entries()`: the
|
||||
// iterator methods consumed DIRECTLY by a for-of head ride the
|
||||
// container's live walk — exactly `for..of m` with the projection
|
||||
|
||||
@@ -26,6 +26,9 @@ import { isSettledIteratorValueRead } from "./lower-generators.js";
|
||||
import { concreteCollectionNarrow } from "./collection-narrowing.js";
|
||||
import { lowerWorkerMetadata } from "./builtins/workers.js";
|
||||
import { fenceStaticBlobMember, lowerBlobNew, lowerStaticBlobCall } from "./builtins/blob.js";
|
||||
import { refuseWebAssemblyUse } from "./builtins/webassembly.js";
|
||||
import { discoverWasmModules, type WasmModuleReport } from "../../wasm/discover.js";
|
||||
import { resolveRequireRuntime } from "../runtime-resolve.js";
|
||||
import { lowerKeyObjectCall } from "./builtins/key-object.js";
|
||||
import { hashStreamOf } from "./builtins/hash-stream.js";
|
||||
import { lowerNetAddressCall, lowerNetAddressNew } from "./builtins/net-address.js";
|
||||
@@ -231,9 +234,11 @@ import {
|
||||
containsUnion,
|
||||
describeComponentBlocker,
|
||||
describeRecordMemberBlocker,
|
||||
erasedOverloadMemberType,
|
||||
formatIrType,
|
||||
FETCH_AMBIENT_TYPES,
|
||||
isOsConstantsSymbol,
|
||||
isOverloadedCallableMemberType,
|
||||
isUnitOnlyTsType,
|
||||
isWorkerHandleType,
|
||||
jsOpenObjectType,
|
||||
@@ -281,6 +286,13 @@ import {
|
||||
appendWorkerModules,
|
||||
} from "./lower-modules.js";
|
||||
import { prepareCjsModuleGraph, usesCjsTopLevelThis } from "./lower-node-module.js";
|
||||
import {
|
||||
cjsNamespaceModule,
|
||||
inlineNamespaceRequireOf,
|
||||
inlineProgramRequireOf,
|
||||
} from "./cjs-inline-require.js";
|
||||
import { tscInteropModuleOf } from "./cjs-tsc-interop.js";
|
||||
import { usesCjsExportsIdentity } from "./cjs-exports-object.js";
|
||||
import { registerBuiltinEventTargetClasses } from "./lower-event-target.js";
|
||||
import {
|
||||
type ClassInfo,
|
||||
@@ -367,6 +379,7 @@ import {
|
||||
lowerGenericFnValue,
|
||||
inferTypeParamBindings,
|
||||
lowerGenericInstance,
|
||||
genericLocalClassBinding,
|
||||
} from "./generic-functions.js";
|
||||
import { bindingNeverReassigned } from "./binding-analysis.js";
|
||||
import type { InstantiationPath } from "./instantiation-path.js";
|
||||
@@ -719,6 +732,8 @@ export interface LowerResult {
|
||||
/** 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; runtimeFences?: ScrDiagnostic[] };
|
||||
/** Coverage only: the WebAssembly modules the build can see (wasm/discover.ts). */
|
||||
wasmModules?: WasmModuleReport[];
|
||||
/** Compile-time-detectable behavior differences from Node (SC6xxx
|
||||
* divergence warnings) in the program's own modules. Never failures. */
|
||||
divergences?: ScrDiagnostic[];
|
||||
@@ -739,6 +754,8 @@ export interface LowerOptions {
|
||||
* diagnostics and stats under `unreached` — the whole-program analysis
|
||||
* builds deliberately gave up. */
|
||||
coverage?: boolean;
|
||||
/** Coverage only: module files named by --wasm (absolute). */
|
||||
wasmFiles?: readonly string[];
|
||||
/** The platform the build TARGETS ("win32" under a windows cross triple,
|
||||
* the host platform otherwise — see buildTargetPlatform in index.ts).
|
||||
* The whole program compiles for one platform, so the platform-keyed
|
||||
@@ -751,6 +768,8 @@ export interface LowerOptions {
|
||||
* resolution walk and named-import link checks): the program
|
||||
* compiles to that startup crash. */
|
||||
startupCrash?: StartupCrash | null;
|
||||
/** LoadResult.inlineRequireCycleCalls. */
|
||||
inlineRequireCycleCalls?: ReadonlySet<ts.Node> | undefined;
|
||||
/** LIBRARY mode's reachability roots: the profile-mapped exports of the
|
||||
* entry module. Executable builds root at the entry's top level alone
|
||||
* (an unreferenced export dead-strips); a library's exports are called from
|
||||
@@ -843,6 +862,8 @@ export interface LowererMode {
|
||||
* module init — Node refuses the whole graph before anything evaluates,
|
||||
* so nothing runs. */
|
||||
startupCrash?: StartupCrash | null;
|
||||
/** LoadResult.inlineRequireCycleCalls. */
|
||||
inlineRequireCycleCalls?: ReadonlySet<ts.Node> | undefined;
|
||||
/** The build's outbound native FFI declarations. */
|
||||
ffiImports?: readonly IrFfiImport[];
|
||||
/** LowerOptions.libraryCallbacks (see there). */
|
||||
@@ -956,6 +977,7 @@ export function lowerToIr(
|
||||
projectCheckJs: options.projectCheckJs ?? false,
|
||||
targetPlatform,
|
||||
startupCrash,
|
||||
inlineRequireCycleCalls: options.inlineRequireCycleCalls,
|
||||
ffiImports,
|
||||
libraryCallbacks,
|
||||
forkTargets,
|
||||
@@ -979,6 +1001,7 @@ export function lowerToIr(
|
||||
projectCheckJs: options.projectCheckJs ?? false,
|
||||
targetPlatform,
|
||||
startupCrash,
|
||||
inlineRequireCycleCalls: options.inlineRequireCycleCalls,
|
||||
ffiImports,
|
||||
libraryCallbacks,
|
||||
forkTargets,
|
||||
@@ -1009,6 +1032,7 @@ export function lowerToIr(
|
||||
reachable,
|
||||
targetPlatform,
|
||||
startupCrash,
|
||||
inlineRequireCycleCalls: options.inlineRequireCycleCalls,
|
||||
ffiImports,
|
||||
libraryCallbacks,
|
||||
forkTargets,
|
||||
@@ -1067,6 +1091,7 @@ export function lowerToIr(
|
||||
reachable,
|
||||
remainder: true,
|
||||
alreadyFlushed: resultLowerer.flushedSymbols,
|
||||
inlineRequireCycleCalls: options.inlineRequireCycleCalls,
|
||||
targetPlatform,
|
||||
ffiImports,
|
||||
libraryCallbacks,
|
||||
@@ -1112,6 +1137,23 @@ export function lowerToIr(
|
||||
},
|
||||
};
|
||||
if (tiers !== undefined) covered.tiers = tiers;
|
||||
const wasmModules = discoverWasmModules({
|
||||
files: moduleOrder.includes(entry) ? moduleOrder : [...moduleOrder, entry],
|
||||
entry: entry.fileName,
|
||||
configured: options.wasmFiles ?? [],
|
||||
resolveRequest: (fromFile, specifier) => {
|
||||
const resolved = resolveRequireRuntime(
|
||||
fromFile,
|
||||
specifier,
|
||||
targetPlatform,
|
||||
undefined,
|
||||
options.frontendServices,
|
||||
);
|
||||
return resolved.ok ? resolved.value : null;
|
||||
},
|
||||
});
|
||||
timing("wasm-discovery", { modules: wasmModules.length });
|
||||
if (wasmModules.length > 0) covered.wasmModules = wasmModules;
|
||||
return covered;
|
||||
}
|
||||
|
||||
@@ -1629,6 +1671,15 @@ export class Lowerer {
|
||||
readonly checker: ts.TypeChecker;
|
||||
readonly diags: ScrDiagnostic[] = [];
|
||||
readonly fnSigsBySymbol = new Map<ts.Symbol, FnSig>();
|
||||
/** Per file: whether it assigns `exports` and `module.exports`
|
||||
* (cjs-exports-object.ts). */
|
||||
readonly cjsExportsAssignments = new Map<
|
||||
ts.SourceFile,
|
||||
{ exports: boolean; moduleExports: boolean }
|
||||
>();
|
||||
/** Inline require() calls whose module edge closes a require cycle
|
||||
* (LoadResult.inlineRequireCycleCalls). */
|
||||
readonly inlineRequireCycleCalls: ReadonlySet<ts.Node>;
|
||||
/** Immutable callable aliases and compiler-generated adapters. The map
|
||||
* is symbol-keyed, so shadowing and cross-module imports keep the same
|
||||
* exact binding discipline as globals/functions. */
|
||||
@@ -1855,6 +1906,9 @@ export class Lowerer {
|
||||
* and erased instances). */
|
||||
erasingDepth = 0;
|
||||
localClassInstantiations: { owner: ts.FunctionLikeDeclaration; name: string }[] = [];
|
||||
/** Instance types (by checker type id) of the local classes generic
|
||||
* factory calls return, and the per-instance class each names. */
|
||||
readonly localClassInstanceTypes = new Map<number, string>();
|
||||
/** The ts-level twin of typeParamBindings, non-null while a CALL-keyed
|
||||
* instance body lowers: type-parameter symbol → the bound CHECKER type,
|
||||
* consulted where the mapped IrType has already widened away information
|
||||
@@ -1925,6 +1979,9 @@ export class Lowerer {
|
||||
/** Per CommonJS file using top-level `this`: the global holding the
|
||||
* object that stands for its original exports (lowerCjsTopLevelThis). */
|
||||
readonly cjsExportsThis = new Map<ts.SourceFile, IrGlobal>();
|
||||
/** Inline program-module require calls whose %init the enclosing member
|
||||
* read or member call is already sequencing (cjs-inline-require.ts). */
|
||||
readonly inlineRequireInitDone = new Set<ts.CallExpression>();
|
||||
/** Bindings whose unchecked string arithmetic can produce either a number
|
||||
* (NaN for an absent read) or a string. Preserve that result union at bare
|
||||
* reads instead of applying the checker-bare string narrowing adapter. */
|
||||
@@ -2081,6 +2138,42 @@ export class Lowerer {
|
||||
};
|
||||
}
|
||||
|
||||
/** True when `node`'s lowered value can hold a hidden `undefined` the
|
||||
* checker type does not admit: an unchecked element or member read, a call
|
||||
* whose lowered return admits undefined, or a binding whose storage the
|
||||
* optional-read analysis marked. Whether the value actually carries the
|
||||
* undefined arm is only known once it is lowered
|
||||
* (runtimeOptionalSourceValue); this is the syntactic precondition. */
|
||||
isRuntimeOptionalOrigin(node: ts.Expression): boolean {
|
||||
let origin: ts.Expression = node;
|
||||
while (ts.isParenthesizedExpression(origin)) origin = origin.expression;
|
||||
if (ts.isElementAccessExpression(origin) || ts.isPropertyAccessExpression(origin)) return true;
|
||||
if (ts.isCallExpression(origin)) {
|
||||
const name = ts.isPropertyAccessExpression(origin.expression)
|
||||
? origin.expression.name
|
||||
: origin.expression;
|
||||
const symbol = ts.isIdentifier(name) ? this.resolveValueSymbol(name) : undefined;
|
||||
const signature = symbol ? this.fnSigsBySymbol.get(symbol) : undefined;
|
||||
return (
|
||||
this.runtimeOptionalFunctionReturns.has(origin) ||
|
||||
(signature?.returnType.kind === "union" &&
|
||||
this.armTag(signature.returnType.unionId, UNDEFINED_T) >= 0)
|
||||
);
|
||||
}
|
||||
if (ts.isIdentifier(origin)) {
|
||||
const local = this.resolveLocal(origin);
|
||||
const root = local ? this.runtimeOptionalRootOf(local) : null;
|
||||
const global = this.globalOf(origin);
|
||||
return (
|
||||
(!!root && this.runtimeOptionalStorageLocals.has(root)) ||
|
||||
(!!global &&
|
||||
(this.isRuntimeOptionalGlobal(global) ||
|
||||
(global.type.kind === "union" && this.armTag(global.type.unionId, UNDEFINED_T) >= 0)))
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Recover the original optional union when a checked single-arm helper
|
||||
* was installed before a strict property consumer could name its member.
|
||||
* Recovery covers member reads and storage roots marked by the
|
||||
@@ -2089,32 +2182,7 @@ export class Lowerer {
|
||||
runtimeOptionalSourceValue(node: ts.Expression, value: IrExpr): IrExpr | null {
|
||||
const slot = this.deferredFieldSlot(value);
|
||||
if (slot) return slot;
|
||||
let origin: ts.Expression = node;
|
||||
while (ts.isParenthesizedExpression(origin)) origin = origin.expression;
|
||||
let optionalOrigin =
|
||||
ts.isElementAccessExpression(origin) || ts.isPropertyAccessExpression(origin);
|
||||
if (ts.isCallExpression(origin)) {
|
||||
const name = ts.isPropertyAccessExpression(origin.expression)
|
||||
? origin.expression.name
|
||||
: origin.expression;
|
||||
const symbol = ts.isIdentifier(name) ? this.resolveValueSymbol(name) : undefined;
|
||||
const signature = symbol ? this.fnSigsBySymbol.get(symbol) : undefined;
|
||||
optionalOrigin =
|
||||
this.runtimeOptionalFunctionReturns.has(origin) ||
|
||||
(signature?.returnType.kind === "union" &&
|
||||
this.armTag(signature.returnType.unionId, UNDEFINED_T) >= 0);
|
||||
}
|
||||
if (ts.isIdentifier(origin)) {
|
||||
const local = this.resolveLocal(origin);
|
||||
const root = local ? this.runtimeOptionalRootOf(local) : null;
|
||||
const global = this.globalOf(origin);
|
||||
optionalOrigin =
|
||||
(!!root && this.runtimeOptionalStorageLocals.has(root)) ||
|
||||
(!!global &&
|
||||
(this.isRuntimeOptionalGlobal(global) ||
|
||||
(global.type.kind === "union" && this.armTag(global.type.unionId, UNDEFINED_T) >= 0)));
|
||||
}
|
||||
if (!optionalOrigin) return null;
|
||||
if (!this.isRuntimeOptionalOrigin(node)) return null;
|
||||
|
||||
// A checked JavaScript indexed read can already be boxed. The checker
|
||||
// calls the element present, but that inference is not a runtime guard.
|
||||
@@ -2857,6 +2925,7 @@ export class Lowerer {
|
||||
this.alreadyFlushed = mode.alreadyFlushed ?? new Set();
|
||||
this.targetPlatform = mode.targetPlatform ?? process.platform;
|
||||
this.startupCrash = mode.startupCrash ?? null;
|
||||
this.inlineRequireCycleCalls = mode.inlineRequireCycleCalls ?? new Set();
|
||||
this.projectCheckJs = mode.projectCheckJs ?? false;
|
||||
this.ffiImports = mode.ffiImports ?? [];
|
||||
this.libraryCallbacks = mode.libraryCallbacks ?? false;
|
||||
@@ -2888,7 +2957,11 @@ export class Lowerer {
|
||||
this.mixinTypeContext && this.mixinTypeContext.classNode === decl
|
||||
? { kind: "object", className: this.mixinTypeContext.className }
|
||||
: null,
|
||||
localClassInstance: (decl) => {
|
||||
localClassInstance: (decl, instanceType) => {
|
||||
// A generic factory's local class, by the instance type a call of
|
||||
// the factory produced (localClassResultInstance).
|
||||
const instanceClass = instanceType && this.localClassInstanceTypes.get(instanceType.id);
|
||||
if (instanceClass) return { kind: "object", className: instanceClass };
|
||||
// Computed heritage can close over parameters of its factory.
|
||||
// A speculative checker query outside that factory must not lower
|
||||
// the extends expression in the caller's lexical scope.
|
||||
@@ -3168,8 +3241,10 @@ export class Lowerer {
|
||||
ident.parent.name === ident
|
||||
) {
|
||||
const recv = ident.parent.expression;
|
||||
if (ts.isIdentifier(recv) && this.cjsLocalModuleBindingOf(recv)) {
|
||||
const dep = this.localModuleBindingDepOf(recv);
|
||||
if (this.cjsLocalModuleBindingOf(recv)) {
|
||||
const dep = ts.isIdentifier(recv)
|
||||
? this.localModuleBindingDepOf(recv)
|
||||
: this.cjsModuleRefDepOf(recv);
|
||||
const exported = dep ? this.cjsModuleExportSymbol(dep, ident.text) : undefined;
|
||||
// A createRequire call is typed as `any`, so tsgo can manufacture a
|
||||
// transient property symbol with no relation to the target module.
|
||||
@@ -3561,6 +3636,21 @@ export class Lowerer {
|
||||
return dep;
|
||||
}
|
||||
|
||||
/** The table module a non-identifier module reference names: an inline
|
||||
* `require("./x")` (cjs-inline-require.ts) or tsc's unstamped
|
||||
* `x_1.default` interop read (cjs-tsc-interop.ts). */
|
||||
cjsModuleRefDepOf(expr: ts.Expression): ts.SourceFile | null {
|
||||
const inline = inlineNamespaceRequireOf(this, expr);
|
||||
if (inline !== null) return inline.dep;
|
||||
const interop = tscInteropModuleOf(this, expr);
|
||||
return interop !== null && cjsNamespaceModule(this, interop.dep) ? interop.dep : null;
|
||||
}
|
||||
|
||||
/** The table module a CommonJS module binding identifier names. */
|
||||
cjsModuleBindingDepOf(expr: ts.Identifier): ts.SourceFile | null {
|
||||
return this.localModuleBindingDepOf(expr);
|
||||
}
|
||||
|
||||
/** The compiled module behind a require-like namespace binding: an
|
||||
* ordinary CommonJS require declaration, a canonical createRequire
|
||||
* declaration, or an ESM default import of CommonJS. One resolver keeps
|
||||
@@ -3573,6 +3663,10 @@ export class Lowerer {
|
||||
if (ts.isImportClause(decl)) return this.cjsDefaultImportDepOf(decl);
|
||||
if (!ts.isVariableDeclaration(decl) || !ts.isIdentifier(decl.name) || !decl.initializer)
|
||||
return null;
|
||||
{
|
||||
const interop = tscInteropModuleOf(this, expr);
|
||||
if (interop !== null) return cjsNamespaceModule(this, interop.dep) ? interop.dep : null;
|
||||
}
|
||||
const directSpec = requireSpecOf(decl.initializer);
|
||||
if (directSpec !== null) {
|
||||
if (isJsonRequireDecl(this, decl)) return null;
|
||||
@@ -3619,6 +3713,10 @@ export class Lowerer {
|
||||
if (wholeAssignment === undefined) return null;
|
||||
let rhs: ts.Expression = wholeAssignment.right;
|
||||
while (ts.isParenthesizedExpression(rhs)) rhs = rhs.expression;
|
||||
// A pure re-export (`module.exports = require("./core")`) is the inner
|
||||
// module's export table, read like any namespace binding.
|
||||
if (inlineProgramRequireOf(this, rhs) !== null && !this.globalsByDeclNode.has(wholeAssignment))
|
||||
return null;
|
||||
if (ts.isIdentifier(rhs)) {
|
||||
const value = this.checker.getSymbolAtLocation(rhs);
|
||||
if (value) {
|
||||
@@ -3671,7 +3769,9 @@ export class Lowerer {
|
||||
* symbols → resolveValueSymbol); the bare value keeps the
|
||||
* namespace-object fence, like ESM namespace imports of builtins. */
|
||||
cjsLocalModuleBindingOf(expr: ts.Expression): boolean {
|
||||
if (!ts.isIdentifier(expr)) return false;
|
||||
// An inline `require("./x")` over a table module reads its members like
|
||||
// a binding (cjs-inline-require.ts sequences the module's %init).
|
||||
if (!ts.isIdentifier(expr)) return this.cjsModuleRefDepOf(expr) !== null;
|
||||
const sym = this.checker.getSymbolAtLocation(expr);
|
||||
if (this.localModuleBindingDepOf(expr) === null) return false;
|
||||
if (this.createRequireWholeExportSymbolOf(expr) !== null) return false;
|
||||
@@ -3684,6 +3784,8 @@ export class Lowerer {
|
||||
// the object literal) keep the namespace reading — member accesses
|
||||
// resolve through the table's property symbols.
|
||||
if (sym && sym.flags & ts.SymbolFlags.Alias) {
|
||||
// `module.exports = TypeError`: the binding IS the builtin global.
|
||||
if (this.isStdlibSymbol(this.checker.getAliasedSymbol(sym))) return false;
|
||||
const d = this.checker.declarationsOf(this.checker.getAliasedSymbol(sym))[0];
|
||||
if (d && (ts.isClassDeclaration(d) || ts.isFunctionDeclaration(d))) return false;
|
||||
if (d && ts.isVariableDeclaration(d)) {
|
||||
@@ -3781,7 +3883,7 @@ export class Lowerer {
|
||||
const id = `%g.${tag}%loaded`;
|
||||
this.moduleGuardOf.set(fp.sf, id);
|
||||
this.globalsList.push({ id, name: "%loaded", type: BOOL, mutable: true });
|
||||
if (usesCjsTopLevelThis(this, fp.sf)) {
|
||||
if (usesCjsTopLevelThis(this, fp.sf) || usesCjsExportsIdentity(this, fp.sf)) {
|
||||
const exportsThis: IrGlobal = {
|
||||
id: `%g.${tag}%exportsThis`,
|
||||
name: "%exportsThis",
|
||||
@@ -7786,6 +7888,15 @@ export class Lowerer {
|
||||
: mapped;
|
||||
}
|
||||
|
||||
/** The runtime type of a record member slot typed `t`: an overloaded
|
||||
* member's erased function (erasedOverloadMemberType — the type a record
|
||||
* shape stores for it), otherwise mapTypeOf. */
|
||||
memberSlotTypeOf(t: ts.Type): IrType | null {
|
||||
if (isOverloadedCallableMemberType(t, this.checker))
|
||||
return erasedOverloadMemberType(t, this.typeCtx) ?? null;
|
||||
return this.mapTypeOf(t);
|
||||
}
|
||||
|
||||
/** The one position where a contextual UNION must not be adopted over the
|
||||
* expression's own: the LEFT operand of `&&`, `||`, or `??`. Adopting a
|
||||
* contextual union is normally safe because tsc proved the value
|
||||
@@ -7959,6 +8070,8 @@ export class Lowerer {
|
||||
if (!mapped) {
|
||||
const argumentsType = jsArgumentsFunctionType(this, t);
|
||||
if (argumentsType !== null) return argumentsType;
|
||||
const localClass = genericLocalClassBinding(this, node);
|
||||
if (localClass !== null) return localClass;
|
||||
// The checked-dynamic declaration fallback (dynFallbackType): a
|
||||
// JAVASCRIPT binding of any inference residue, or a TypeScript
|
||||
// binding of genuine checker-`any`, becomes the checked-dynamic
|
||||
@@ -11396,6 +11509,7 @@ export class Lowerer {
|
||||
// dispatch. The general identifier-call paths have no lowering for a
|
||||
// promise-returning ambient global, and `import` is a keyword callee no
|
||||
// identifier path matches.
|
||||
refuseWebAssemblyUse(this, expr.expression);
|
||||
return (
|
||||
lowerLifetimeRegionCall(this, expr) ??
|
||||
lowerInternTableCall(this, expr) ??
|
||||
@@ -11621,6 +11735,7 @@ export class Lowerer {
|
||||
}
|
||||
|
||||
lowerNew(expr: ts.NewExpression): IrExpr {
|
||||
refuseWebAssemblyUse(this, expr.expression, "new ");
|
||||
const builtin =
|
||||
lowerLifetimeRegionNew(this, expr) ??
|
||||
lowerInternTableNew(this, expr) ??
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { npmStaticIneligibleReason } from "./npm-static.js";
|
||||
import { npmStaticIneligibleReason, npmStaticTransformPkgJson } from "./npm-static.js";
|
||||
|
||||
test("an unreadable runtime entry is an eligibility refusal", async () => {
|
||||
const dir = await mkdtemp("/tmp/scriptc-npm-static-unreadable-");
|
||||
@@ -14,3 +14,25 @@ test("an unreadable runtime entry is an eligibility refusal", async () => {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("a hoisted node condition leaves no types claim behind", () => {
|
||||
// uuid 14: the node object carries its own "types" key. Left in place,
|
||||
// tsgo maps the hidden declaration to its same-stem browser build.
|
||||
const pkg: Record<string, unknown> = {
|
||||
types: "./dist/index.d.ts",
|
||||
exports: {
|
||||
".": {
|
||||
node: { types: "./dist/index.d.ts", default: "./dist-node/index.js" },
|
||||
default: "./dist/index.js",
|
||||
},
|
||||
"./package.json": "./package.json",
|
||||
},
|
||||
};
|
||||
npmStaticTransformPkgJson(pkg);
|
||||
expect(pkg).toEqual({
|
||||
exports: {
|
||||
".": { default: "./dist-node/index.js" },
|
||||
"./package.json": "./package.json",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -157,8 +157,6 @@ export function npmStaticTransformPkgJson(pkg: Record<string, unknown>): void {
|
||||
return;
|
||||
}
|
||||
const obj = v as Record<string, unknown>;
|
||||
delete obj["types"];
|
||||
delete obj["typings"];
|
||||
// 0. The "node" condition HOISTS: Node's runtime resolution matches it
|
||||
// (yaml's `{ types, node: "./dist/index.js", default: "./browser/
|
||||
// index.js" }` runs the node dist; the browser dist is a DIFFERENT
|
||||
@@ -187,6 +185,14 @@ export function npmStaticTransformPkgJson(pkg: Record<string, unknown>): void {
|
||||
}
|
||||
}
|
||||
}
|
||||
// 1. The types claims go AFTER the hoist: a node object's own
|
||||
// "types" key (uuid's `node: { types: "./dist/index.d.ts", default:
|
||||
// "./dist-node/index.js" }`) splices in with its siblings, and left
|
||||
// in place tsgo follows it, maps the hidden declaration to its
|
||||
// same-stem JS (dist/index.js, the browser build) and disagrees with
|
||||
// this resolver's runtime answer.
|
||||
delete obj["types"];
|
||||
delete obj["typings"];
|
||||
for (const value of Object.values(obj)) transform(value);
|
||||
};
|
||||
if (pkg["exports"] !== undefined) transform(pkg["exports"]);
|
||||
@@ -208,6 +214,13 @@ function npmStaticTransformPkgJsonText(text: string): string {
|
||||
}
|
||||
if (doc === null || typeof doc !== "object" || Array.isArray(doc)) return text;
|
||||
npmStaticTransformPkgJson(doc as Record<string, unknown>);
|
||||
// tsgo deduplicates installs by package id (name + version): a second
|
||||
// copy of the same name@version (express/node_modules/fresh beside
|
||||
// send/node_modules/fresh) becomes a redirect to the first copy loaded,
|
||||
// so its own path never joins the program. Node loads every installed
|
||||
// copy as its own module, and resolve.ts answers each importer's own
|
||||
// copy; without a version the checker keeps the copies apart too.
|
||||
delete (doc as Record<string, unknown>)["version"];
|
||||
return JSON.stringify(doc);
|
||||
}
|
||||
|
||||
@@ -275,6 +288,20 @@ function packageIsUntyped(path: string): boolean {
|
||||
return untyped;
|
||||
}
|
||||
|
||||
/** An installed TypeScript source with no runtime JavaScript at its stem
|
||||
* (tldts ships `index.ts` beside `main: "dist/cjs/index.js"`). Node never
|
||||
* executes TypeScript under node_modules and resolve.ts answers only
|
||||
* runtime JS for opted-in packages, yet tsgo's TypeScript-first lookup
|
||||
* would take the package-root `index.ts` over `main`. A same-stem JS twin
|
||||
* keeps the source visible: that pair is the shape npmStaticProgramDep
|
||||
* accepts in the JavaScript's place. */
|
||||
function isUnloadableInstalledTsSource(path: string): boolean {
|
||||
if (!/\.(ts|tsx|mts|cts)$/.test(path) || isDeclarationFileName(path)) return false;
|
||||
if (!isNodeModulesPathNorm(path)) return false;
|
||||
const stem = path.replace(/\.(ts|tsx|mts|cts)$/, "");
|
||||
return ![".js", ".jsx", ".mjs", ".cjs"].some((ext) => trackedExists(stem + ext));
|
||||
}
|
||||
|
||||
export interface NpmStaticFsShadow {
|
||||
/** Shadowed CONTENT for a real path (the types-stripped package.json),
|
||||
* or undefined (no shadow — fall through). */
|
||||
@@ -383,7 +410,7 @@ export function npmStaticFsShadow(services: FrontendServices): NpmStaticFsShadow
|
||||
const target = shadowTargetOf(path);
|
||||
if (!target) return false;
|
||||
if (target.viaTypes) return true; // the @types twin hides whole
|
||||
return isDeclarationFileName(path);
|
||||
return isDeclarationFileName(path) || isUnloadableInstalledTsSource(path);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -39,6 +39,21 @@ import { THREADS_MODULE } from "./threads-import.js";
|
||||
import { erasedTypeOnlyImport } from "./type-only-imports.js";
|
||||
import { EXPERIMENTAL_MODULE } from "./experimental-import.js";
|
||||
|
||||
/** Where a package-anchored preflight diagnostic sits, as
|
||||
* `<package path>:<line>` — the SC2013 refusal that drops the package
|
||||
* reports only at the consumer's import, so its reason carries the inner
|
||||
* location. */
|
||||
function packageDiagWhere(load: LoadResult, loc: SrcLoc): string {
|
||||
const file = loc.file.split("\\").join("/");
|
||||
const at = file.lastIndexOf("/node_modules/");
|
||||
const shown = at >= 0 ? file.slice(at + "/node_modules/".length) : file;
|
||||
const text = load.program.getSourceFile(loc.file)?.text;
|
||||
if (text === undefined) return shown;
|
||||
let line = 1;
|
||||
for (let i = 0; i < loc.start && i < text.length; i++) if (text.charCodeAt(i) === 10) line++;
|
||||
return `${shown}:${line}`;
|
||||
}
|
||||
|
||||
export interface ProgramLoadOptions {
|
||||
npmStatic?: readonly string[];
|
||||
externalTypes?: Readonly<Record<string, string>> | undefined;
|
||||
@@ -445,7 +460,8 @@ function loadFrontend(
|
||||
const reasons = new Map<string, string>(npmStaticOffenders());
|
||||
for (const d of preflight) {
|
||||
const pkg = npmStaticPackageOfPath(d.loc.file);
|
||||
if (pkg !== null && !reasons.has(pkg)) reasons.set(pkg, `${d.code}: ${d.message}`);
|
||||
if (pkg !== null && !reasons.has(pkg))
|
||||
reasons.set(pkg, `${d.code} at ${packageDiagWhere(load, d.loc)}: ${d.message}`);
|
||||
}
|
||||
if (![...reasons.keys()].some((p) => effective.has(p))) {
|
||||
const named = new Map<string, number>();
|
||||
@@ -577,6 +593,7 @@ function loadFrontend(
|
||||
...opts,
|
||||
frontendServices: finalLoad.services,
|
||||
startupCrash: finalLoad.startupCrash ?? null,
|
||||
inlineRequireCycleCalls: finalLoad.inlineRequireCycleCalls,
|
||||
projectCheckJs: finalLoad.typeWorld?.projectCheckJs ?? false,
|
||||
externalTypes: finalLoad.externalTypes,
|
||||
externalTypeSpecifiersByFile: finalLoad.externalTypeSpecifiersByFile,
|
||||
|
||||
@@ -338,6 +338,12 @@ export interface LoadResult {
|
||||
* last. Empty until checkPreflight runs (and empty when preflight fails
|
||||
* structurally). */
|
||||
moduleOrder: ts.SourceFile[];
|
||||
/** Inline require() calls whose edge closes a require cycle. Node answers
|
||||
* them with the still-evaluating module's partial exports, which the
|
||||
* static export globals do not model; their lowering refuses them by name
|
||||
* at run time instead of refusing the whole program. Filled by
|
||||
* checkPreflight. */
|
||||
inlineRequireCycleCalls?: ReadonlySet<ts.Node>;
|
||||
/** Node's startup refusal, when the graph carries an import edge Node
|
||||
* rejects before ANY module evaluates — a RESOLUTION refusal (a bare
|
||||
* specifier nothing installed resolves, an exports/imports map that
|
||||
@@ -640,9 +646,14 @@ export function entryPackageFilePredicate(entryPath: string): (file: string) =>
|
||||
}
|
||||
|
||||
/** maxNodeModuleJsDepth can omit relative imports within an admitted
|
||||
* package. Discover its runtime sources to a fixpoint so the checker and
|
||||
* module resolver see the same graph regardless of import traversal order. */
|
||||
function entryPackageProgramRoots7(program: ts.Program, entryPath: string): string[] {
|
||||
* package, and opted-in packages deep in the dependency graph. Discover
|
||||
* their runtime sources to a fixpoint so the checker and module resolver
|
||||
* see the same graph regardless of import traversal order or depth. */
|
||||
function entryPackageProgramRoots7(
|
||||
program: ts.Program,
|
||||
entryPath: string,
|
||||
rootNpmStaticEdges: boolean,
|
||||
): string[] {
|
||||
const belongsToEntry = entryPackageFilePredicate(entryPath);
|
||||
const roots = new Set<string>();
|
||||
for (const sf of program.getImplementationSourceFiles()) {
|
||||
@@ -661,15 +672,63 @@ function entryPackageProgramRoots7(program: ts.Program, entryPath: string): stri
|
||||
program.getSourceFile(target) === undefined
|
||||
)
|
||||
roots.add(target);
|
||||
// An opted-in package's bare edge counts against the same depth
|
||||
// budget: tsgo charges every node_modules JS hop, so express's
|
||||
// body-parser → qs → side-channel → get-intrinsic chain runs out of
|
||||
// depth before its leaves load. Root the runtime file this edge
|
||||
// lowers to (preflight's own resolution and miss rule) so the leaf
|
||||
// joins like a shallow one; complete programs add nothing. Only
|
||||
// the shadowed lowering world does this (the authored world checks
|
||||
// user code against declarations), and only from package files: a
|
||||
// program file's own edges are never deeper than one hop.
|
||||
// (resolveBareModule directly: resolveNpmImport7's workspace
|
||||
// registration stays with preflight's own walk.)
|
||||
if (
|
||||
target === null &&
|
||||
rootNpmStaticEdges &&
|
||||
npmStaticPackageOfPath(sf.fileName) !== null &&
|
||||
!isRelativeSpecifier(specifier.text)
|
||||
) {
|
||||
const spec = specifier.text;
|
||||
if (spec.startsWith("#") || canonicalBuiltinModule(spec) !== null) continue;
|
||||
const npm = resolveBareModule(
|
||||
sf.fileName,
|
||||
spec,
|
||||
undefined,
|
||||
moduleSpecifierKind7(specifier),
|
||||
);
|
||||
if (
|
||||
npm !== null &&
|
||||
isNpmStaticPackage(npm.packageName) &&
|
||||
(isNodeModulesPath(npm.typesFile) || npm.workspaceDir !== undefined) &&
|
||||
isJsSourceFileName(npm.typesFile) &&
|
||||
npmStaticProgramFile(program, npm.typesFile) === null
|
||||
)
|
||||
roots.add(tsgoPath(resolve(npm.typesFile)));
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...roots].sort();
|
||||
}
|
||||
|
||||
/** "require" for a CommonJS edge (`require("x")`, `import x = require("x")`),
|
||||
* "import" for every ESM form — the resolution kind preflight uses. */
|
||||
function moduleSpecifierKind7(specifier: ts.StringLiteralLike): "import" | "require" {
|
||||
const parent = specifier.parent;
|
||||
if (ts.isExternalModuleReference(parent)) return "require";
|
||||
return ts.isCallExpression(parent) &&
|
||||
ts.isIdentifier(parent.expression) &&
|
||||
parent.expression.text === "require"
|
||||
? "require"
|
||||
: "import";
|
||||
}
|
||||
|
||||
function loadProgram7(
|
||||
host: ts.Ts7Host,
|
||||
entryPath: string,
|
||||
externalTypes: ReadonlyMap<string, string> = new Map(),
|
||||
/** The --npm-static lowering world: root opted-in packages' deep edges. */
|
||||
rootNpmStaticEdges = false,
|
||||
): LoadResult & { disposeAll: () => void } {
|
||||
const config = adoptProjectConfig7(host, entryPath);
|
||||
const configuredPaths = config.options["paths"];
|
||||
@@ -717,7 +776,7 @@ function loadProgram7(
|
||||
try {
|
||||
for (let pass = 0; pass < 32; pass++) {
|
||||
const candidates = [
|
||||
...entryPackageProgramRoots7(program, entryPath),
|
||||
...entryPackageProgramRoots7(program, entryPath, rootNpmStaticEdges),
|
||||
...createRequireProgramRoots7(program),
|
||||
...forkTargetPaths(program, program.getImplementationSourceFiles()),
|
||||
...workerTargetPaths(program, program.getImplementationSourceFiles()),
|
||||
@@ -931,7 +990,7 @@ export function loadProgram(
|
||||
};
|
||||
const host = services.createProgramHost({ cwd: dirname(entryPath), fsShadow });
|
||||
try {
|
||||
const load = loadProgram7(host, entryPath, externalTypes);
|
||||
const load = loadProgram7(host, entryPath, externalTypes, npmStaticPackages.length > 0);
|
||||
let authoredHost: ts.Ts7Host | null = null;
|
||||
let authoredLoad: ReturnType<typeof loadProgram7> | null = null;
|
||||
return {
|
||||
@@ -975,9 +1034,10 @@ export function loadProgram(
|
||||
* preflight diagnostics. Preserve the caller's complete load shape when
|
||||
* mutating it, including its owned services and disposal callback. */
|
||||
export function checkPreflight<T extends LoadResult>(load: T): ScrDiagnostic[] {
|
||||
const { diags, moduleOrder, startupCrash } = preflight7(load);
|
||||
const { diags, moduleOrder, startupCrash, inlineRequireCycleCalls } = preflight7(load);
|
||||
load.moduleOrder = moduleOrder;
|
||||
load.startupCrash = startupCrash;
|
||||
load.inlineRequireCycleCalls = inlineRequireCycleCalls;
|
||||
return diags;
|
||||
}
|
||||
|
||||
@@ -1842,6 +1902,36 @@ function nestedBareRequiresOf7(sf: ts.SourceFile): ts.CallExpression[] {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** INLINE `require("...")` calls in expression position anywhere in a file
|
||||
* (`require("./x").f`, `require("debug")("ns")`, `module.exports =
|
||||
* require("./y")`, `{ v: require("./v") }`): each evaluates its module at
|
||||
* that position, exactly like a bare require statement. Top-level require
|
||||
* statements and bare require statements are collected by their own
|
||||
* scans; a require that initializes a nested declaration keeps its
|
||||
* lowering fence and is not an edge. */
|
||||
function inlineRequiresOf7(sf: ts.SourceFile): ts.CallExpression[] {
|
||||
const out: ts.CallExpression[] = [];
|
||||
ts.walkPreorder(sf, (n) => {
|
||||
if (!ts.isCallExpression(n) || requireSpecOf7(n) === null) return;
|
||||
let outer: ts.Node = n;
|
||||
while (
|
||||
outer.parent !== undefined &&
|
||||
(ts.isParenthesizedExpression(outer.parent) ||
|
||||
ts.isAsExpression(outer.parent) ||
|
||||
ts.isTypeAssertion(outer.parent) ||
|
||||
ts.isNonNullExpression(outer.parent))
|
||||
) {
|
||||
outer = outer.parent;
|
||||
}
|
||||
const parent = outer.parent;
|
||||
if (parent !== undefined && ts.isExpressionStatement(parent)) return;
|
||||
if (parent !== undefined && ts.isVariableDeclaration(parent) && parent.initializer === outer)
|
||||
return;
|
||||
out.push(n);
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
type RequireCallBindingKind7 = "ambient" | "createRequire" | "source";
|
||||
|
||||
function stripRequireCasts7(expr: ts.Expression): ts.Expression {
|
||||
@@ -2408,6 +2498,13 @@ function nsBindingUsesAreBareStatements7(
|
||||
export interface CycleEdge {
|
||||
dep: ts.SourceFile;
|
||||
stmt?: ts.ImportDeclaration | ts.ExportDeclaration;
|
||||
/** An inline require() in a deferred position (a function body): Node
|
||||
* evaluates the dependency when that code runs, after this module's own
|
||||
* top level, so the edge orders the dependency after the module and
|
||||
* never closes a cycle through the module's initialization. */
|
||||
lazy?: true;
|
||||
/** The inline require() call behind the edge (inline edges only). */
|
||||
call?: ts.CallExpression;
|
||||
}
|
||||
|
||||
/** True when the reference at `node` can only evaluate AFTER the module
|
||||
@@ -4006,6 +4103,24 @@ function locOf7(node: ts.Node): { file: string; start: number; end: number } {
|
||||
return { file: sf.fileName, start: node.getStart(sf), end: node.getEnd() };
|
||||
}
|
||||
|
||||
/** The program file holding an opted-in package's resolved runtime JS:
|
||||
* the file itself, or its same-stem TypeScript twin. */
|
||||
function npmStaticProgramFile(program: ts.Program, resolvedFile: string): ts.SourceFile | null {
|
||||
let dep = program.getSourceFile(resolvedFile) ?? null;
|
||||
if (dep === null) {
|
||||
// A workspace-linked package shipping its .ts SOURCE beside the
|
||||
// compiled .js: the checker's resolution preferred the source (it
|
||||
// outranks the sibling .js under bundler resolution), so the program
|
||||
// holds the .ts twin at the same stem — the same module, in its
|
||||
// richer spelling.
|
||||
for (const ext of [".ts", ".tsx", ".mts", ".cts"]) {
|
||||
dep = program.getSourceFile(resolvedFile.replace(/\.(js|mjs|cjs)$/, ext)) ?? null;
|
||||
if (dep !== null) break;
|
||||
}
|
||||
}
|
||||
return dep;
|
||||
}
|
||||
|
||||
/** The PROGRAM source file an opted-in --npm-static package's resolved
|
||||
* entry maps to, or null with the package marked an offender (resolution
|
||||
* still answering declarations, or the file missing from the type-checked
|
||||
@@ -4022,18 +4137,7 @@ function npmStaticProgramDep(
|
||||
);
|
||||
return null;
|
||||
}
|
||||
let dep = program.getSourceFile(resolvedFile) ?? null;
|
||||
if (dep === null) {
|
||||
// A workspace-linked package shipping its .ts SOURCE beside the
|
||||
// compiled .js: the checker's resolution preferred the source (it
|
||||
// outranks the sibling .js under bundler resolution), so the program
|
||||
// holds the .ts twin at the same stem — the same module, in its
|
||||
// richer spelling.
|
||||
for (const ext of [".ts", ".tsx", ".mts", ".cts"]) {
|
||||
dep = program.getSourceFile(resolvedFile.replace(/\.(js|mjs|cjs)$/, ext)) ?? null;
|
||||
if (dep !== null) break;
|
||||
}
|
||||
}
|
||||
const dep = npmStaticProgramFile(program, resolvedFile);
|
||||
if (dep === null) {
|
||||
reportNpmStaticOffender(
|
||||
packageName,
|
||||
@@ -4085,6 +4189,7 @@ function preflight7(load: LoadResult): {
|
||||
diags: ScrDiagnostic[];
|
||||
moduleOrder: ts.SourceFile[];
|
||||
startupCrash: StartupCrash | null;
|
||||
inlineRequireCycleCalls: ReadonlySet<ts.Node>;
|
||||
} {
|
||||
const { program, entry } = load;
|
||||
const diags: ScrDiagnostic[] = [...load.configDiags];
|
||||
@@ -4885,6 +4990,21 @@ function preflight7(load: LoadResult): {
|
||||
pattern.elements.every((e) => {
|
||||
if (e.initializer !== undefined || e.dotDotDotToken !== undefined) return false;
|
||||
if (e.name !== undefined && ts.isIdentifier(e.name)) return true;
|
||||
// A nested pattern under a named export of a program module
|
||||
// destructures that export's VALUE at the require
|
||||
// (`const { codes: { ERR_X } } = require("./errors")`): the
|
||||
// inner bindings are ordinary destructured storage.
|
||||
if (
|
||||
module === null &&
|
||||
builtin === null &&
|
||||
bindingKind !== "createRequire" &&
|
||||
pattern === req.decl!.name &&
|
||||
e.name !== undefined &&
|
||||
ts.isObjectBindingPattern(e.name) &&
|
||||
e.propertyName !== undefined &&
|
||||
ts.isIdentifier(e.propertyName)
|
||||
)
|
||||
return true;
|
||||
if (module === null || e.name === undefined || !ts.isObjectBindingPattern(e.name))
|
||||
return false;
|
||||
const key = e.propertyName;
|
||||
@@ -4998,6 +5118,32 @@ function preflight7(load: LoadResult): {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Inline requires of program modules are module edges like the bare
|
||||
// statements above (cjs-inline-require.ts lowers each to the module's
|
||||
// %init at its position). Only the CommonJS global in a CommonJS file
|
||||
// counts, and targets outside the compiled graph add nothing here:
|
||||
// their use sites keep their existing lowering fences.
|
||||
if (jsRequireFile && !isNodeEsmFile7(sf, program)) {
|
||||
for (const call of inlineRequiresOf7(sf)) {
|
||||
if (requireCallBindingKind7(program, call) !== "ambient") continue;
|
||||
const spec = requireSpecOf7(call)!;
|
||||
if (load.externalTypes.has(spec) || canonicalBuiltinModule(spec) !== null) continue;
|
||||
const lazy = inDeferredPosition7(call);
|
||||
const edgeTo = (dep: ts.SourceFile): CycleEdge =>
|
||||
lazy ? { dep, lazy: true, call } : { dep, call };
|
||||
const projectDep = resolveImport7(program, sf, spec, "require");
|
||||
if (projectDep !== null) {
|
||||
if (!projectDep.fileName.endsWith(".json")) deps.push(edgeTo(projectDep));
|
||||
continue;
|
||||
}
|
||||
if (isRelativeSpecifier(spec) || spec.startsWith("#")) continue;
|
||||
const npmReq = resolveNpmImport7(sf.fileName, spec, "require");
|
||||
if (npmReq !== null && isNpmStaticPackage(npmReq.packageName)) {
|
||||
const nDep = npmStaticProgramDep(program, npmReq.packageName, npmReq.typesFile);
|
||||
if (nDep !== null) deps.push(edgeTo(nDep));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5024,8 +5170,12 @@ function preflight7(load: LoadResult): {
|
||||
// reads are all let/const bindings with module storage — the
|
||||
// lowering checks each one (cycleEarlyBindings) and throws Node's
|
||||
// ReferenceError on a read before the declaration ran.
|
||||
const cycleAdmissionReason = makeCycleAdmission(program, (sf) => edges.get(sf) ?? []);
|
||||
const cycleAdmissionReason = makeCycleAdmission(program, (sf) => {
|
||||
const all = edges.get(sf) ?? [];
|
||||
return all.some((e) => e.call !== undefined) ? all.filter((e) => e.call === undefined) : all;
|
||||
});
|
||||
const order: ts.SourceFile[] = [];
|
||||
const inlineRequireCycleCalls = new Set<ts.Node>();
|
||||
const state = new Map<ts.SourceFile, "visiting" | "done">();
|
||||
const stack: string[] = [];
|
||||
const visit = (sf: ts.SourceFile): void => {
|
||||
@@ -5033,9 +5183,14 @@ function preflight7(load: LoadResult): {
|
||||
stack.push(sf.fileName);
|
||||
for (const e of edges.get(sf) ?? []) {
|
||||
if (e.dep === sf) continue; // self-import: a Node cache hit, not a cycle
|
||||
if (e.lazy === true) continue; // visited after this module completes
|
||||
const s = state.get(e.dep);
|
||||
if (s === "done") continue;
|
||||
if (s === "visiting") {
|
||||
if (e.call !== undefined) {
|
||||
inlineRequireCycleCalls.add(e.call);
|
||||
continue;
|
||||
}
|
||||
// A back-edge: Node answers it from the cache (the module is
|
||||
// already evaluating) — admitted by the cheap per-edge rule, the
|
||||
// declaration-only init window rule, or the exact initialization
|
||||
@@ -5059,6 +5214,11 @@ function preflight7(load: LoadResult): {
|
||||
stack.pop();
|
||||
state.set(sf, "done");
|
||||
order.push(sf);
|
||||
// Lazy inline requires run after this module's top level: their
|
||||
// modules join the order afterwards (cache hits when already loaded).
|
||||
for (const e of edges.get(sf) ?? []) {
|
||||
if (e.lazy === true && state.get(e.dep) === undefined) visit(e.dep);
|
||||
}
|
||||
};
|
||||
visit(entry);
|
||||
|
||||
@@ -5112,7 +5272,12 @@ function preflight7(load: LoadResult): {
|
||||
esmNamedImportLinkCheck(program, entry, order, diags),
|
||||
);
|
||||
|
||||
return { diags, moduleOrder: order, startupCrash: resolveCrash ?? linkCrash };
|
||||
return {
|
||||
diags,
|
||||
moduleOrder: order,
|
||||
startupCrash: resolveCrash ?? linkCrash,
|
||||
inlineRequireCycleCalls,
|
||||
};
|
||||
}
|
||||
|
||||
/* ── the CJS named-import link check ─────────────────────────────────────
|
||||
@@ -5815,6 +5980,37 @@ export function cjsExportAssignmentOf(
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The members and shared value of a CHAINED member export
|
||||
* `exports.a = exports.b = module.exports.c = value` (outermost name
|
||||
* first), or null when the assignment is not a chain of at least two
|
||||
* identifier-named export members or its value is undefined. */
|
||||
export function cjsExportChainOf(
|
||||
e: ts.BinaryExpression,
|
||||
): { names: ts.Identifier[]; value: ts.Expression } | null {
|
||||
const names: ts.Identifier[] = [];
|
||||
let cur: ts.Expression = e;
|
||||
for (;;) {
|
||||
while (ts.isParenthesizedExpression(cur)) cur = cur.expression;
|
||||
if (!ts.isBinaryExpression(cur) || cur.operatorToken.kind !== ts.SyntaxKind.EqualsToken) break;
|
||||
const left = cur.left;
|
||||
if (
|
||||
!ts.isPropertyAccessExpression(left) ||
|
||||
left.questionDotToken ||
|
||||
!ts.isIdentifier(left.name)
|
||||
)
|
||||
break;
|
||||
const recv = left.expression;
|
||||
if (!((ts.isIdentifier(recv) && recv.text === "exports") || isModuleExportsAccess(recv))) break;
|
||||
names.push(left.name);
|
||||
cur = cur.right;
|
||||
}
|
||||
// tsc's `exports.a = exports.b = void 0;` preamble is not a value chain
|
||||
// (the export-statement lowering owns it).
|
||||
const isVoid =
|
||||
cur.kind === ts.SyntaxKind.VoidExpression || (ts.isIdentifier(cur) && cur.text === "undefined");
|
||||
return names.length >= 2 && !isVoid ? { names, value: cur } : null;
|
||||
}
|
||||
|
||||
/** Why Node would DISCARD this CommonJS export statement, or null when it
|
||||
* survives (Node's export-object identity rules — checker-free, pure
|
||||
* AST): `module.exports = {...}` REPLACES the export object, so members
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
BOOL,
|
||||
bytesOf,
|
||||
canConvertToDyn,
|
||||
canDynCheckTo,
|
||||
CHILD_T,
|
||||
CRYPTOHASH_T,
|
||||
CRYPTOHMAC_T,
|
||||
@@ -920,6 +921,24 @@ function isDeclaredInAmbientNamespace(d: ts.Declaration, name: string): boolean
|
||||
return false;
|
||||
}
|
||||
|
||||
/** A type declared in the shipped WebAssembly namespace (scriptc.d.ts):
|
||||
* its modules, instances, memories, tables, globals, error classes and the
|
||||
* Exports/Imports records. */
|
||||
export function isWebAssemblyType(
|
||||
type: ts.Type,
|
||||
ctx: Pick<TypeMapperCtx, "checker" | "isStdlibFile">,
|
||||
): boolean {
|
||||
const symbol = type.getAliasSymbol() ?? type.getSymbol();
|
||||
if (!symbol) return false;
|
||||
return ctx.checker
|
||||
.declarationsOf(symbol)
|
||||
.some(
|
||||
(d) =>
|
||||
ctx.isStdlibFile(d.getSourceFile()) &&
|
||||
isDeclaredInAmbientNamespace(d as ts.Declaration, "WebAssembly"),
|
||||
);
|
||||
}
|
||||
|
||||
/** True when a type contains a record anywhere (the trigger for the
|
||||
* shape-mismatch diagnostic SC2002 — non-record mismatches are tsc's or
|
||||
* the validator's business). */
|
||||
@@ -1035,7 +1054,7 @@ export interface TypeMapperCtx {
|
||||
* inside members, self-referential member types), null outside any
|
||||
* mixin context (the type alone cannot name a call site). */
|
||||
mixinClassInstance?: (decl: ts.ClassLikeDeclaration) => IrType | null;
|
||||
localClassInstance?: (decl: ts.ClassLikeDeclaration) => IrType | null;
|
||||
localClassInstance?: (decl: ts.ClassLikeDeclaration, instanceType?: ts.Type) => IrType | null;
|
||||
/** Live class protocols retain identity and receiver dispatch rather than
|
||||
* copying an instance into an unrelated record layout. */
|
||||
classImplementedProtocol?: (type: ts.Type) => boolean;
|
||||
@@ -1419,6 +1438,14 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
return { kind: "object", className: "%Error" };
|
||||
}
|
||||
if (isOsConstantsType(type, ctx) || isLegacyUrlType(type, ctx)) return DYN;
|
||||
// The WebAssembly API's values have the checked-dynamic representation, so
|
||||
// declarations that merely mention them compile; each reached use of the
|
||||
// API itself is refused at its site (builtins/webassembly.ts).
|
||||
if (isWebAssemblyType(type, ctx)) return DYN;
|
||||
// A union over one of them (quickjs-wasi's `BufferSource | WebAssembly.Module`
|
||||
// option) holds that dynamic value too, so the whole union is dynamic.
|
||||
if (type.isUnionType() && ts.constituentTypes(type).some((t) => isWebAssemblyType(t, ctx)))
|
||||
return DYN;
|
||||
// Immutable<T> (@scriptc/types) is a deep-readonly view: the value is
|
||||
// still the T it maps.
|
||||
const immutableSource = immutableSourceType(type, ctx);
|
||||
@@ -1950,7 +1977,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
}
|
||||
}
|
||||
if (classExprNeedsContext(classDecl)) {
|
||||
const local = ctx.localClassInstance?.(classDecl);
|
||||
const local = ctx.localClassInstance?.(classDecl, widened);
|
||||
if (local) contextResolutions++;
|
||||
return local ?? null;
|
||||
}
|
||||
@@ -1989,7 +2016,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
}
|
||||
}
|
||||
if (classExprNeedsContext(classDecl)) {
|
||||
const local = ctx.localClassInstance?.(classDecl);
|
||||
const ctorSigs = checker.getConstructSignatures(widened);
|
||||
const local = ctx.localClassInstance?.(
|
||||
classDecl,
|
||||
ctorSigs.length === 1 ? checker.getReturnTypeOfSignature(ctorSigs[0]!) : undefined,
|
||||
);
|
||||
if (local?.kind === "object") {
|
||||
contextResolutions++;
|
||||
return { kind: "classval", className: local.className };
|
||||
@@ -3220,7 +3251,10 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
isStdlibInterface("IterableIterator") ||
|
||||
// An Iterator<T> annotation holds a single-pass iterator object: a
|
||||
// generator, or a native iterator adapted into one.
|
||||
isStdlibInterface("Iterator")
|
||||
isStdlibInterface("Iterator") ||
|
||||
// IteratorObject<T> is what the iterator helpers return: a helper
|
||||
// generator over its source (containers/iterator-helpers.ts).
|
||||
isStdlibInterface("IteratorObject")
|
||||
) {
|
||||
const args = checker.getTypeArguments(widened as ts.TypeReference);
|
||||
const channels = genChannels(args[0], args[1], args[2], ctx);
|
||||
@@ -3915,171 +3949,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
if (hybrid) return hybrid;
|
||||
}
|
||||
const callSigs = checker.getCallSignatures(widened);
|
||||
if (callSigs.length === 1) {
|
||||
const sig = callSigs[0]!;
|
||||
if (sig.getTypeParameters().length && ctx.erasingSignature !== sig) return null;
|
||||
// A SYNTHESIZED rest param (tsc's JS inference for a function body
|
||||
// reading `arguments` — `(...args: any[]) => any` whose args symbol
|
||||
// has no declaration): the dotDotDot check below can't see it, and a
|
||||
// fixed-arity func mapping would LIE about the value's calling
|
||||
// convention. Detected by the param-count mismatch against the
|
||||
// signature's own declaration, excluding an erased `this` parameter.
|
||||
{
|
||||
const sigDecl = checker.signatureDeclaration(sig);
|
||||
const declParams =
|
||||
sigDecl !== undefined && ts.isFunctionLike(sigDecl) ? sigDecl.parameters : undefined;
|
||||
if (
|
||||
declParams !== undefined &&
|
||||
runtimeParameterCount(declParams) !== sig.getParameters().length
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
// tsgo never SYNTHESIZES that rest param into the inferred signature
|
||||
// (5.9.3 did — the count mismatch above was the whole detector
|
||||
// there), so the declaration's own body answers directly.
|
||||
if (sigDecl !== undefined && ts.isFunctionLike(sigDecl) && bodyReadsArguments(sigDecl)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
const params: IrType[] = [];
|
||||
let typedRest = false;
|
||||
let dynRest = false;
|
||||
for (const p of sig.getParameters()) {
|
||||
const decl = checker.valueDeclarationOf(p);
|
||||
const rest = decl !== undefined && ts.isParameter(decl) && decl.dotDotDotToken !== undefined;
|
||||
|
||||
// arity ABI selected by lambdaSignature. Treating an inferred any[]
|
||||
// declaration as a typed packed-array closure would bypass its boxed
|
||||
// spread/error semantics and change coverage classification.
|
||||
if (rest && decl !== undefined && isJsSourceFile(decl.getSourceFile())) return null;
|
||||
const optional =
|
||||
decl !== undefined &&
|
||||
ts.isParameter(decl) &&
|
||||
(decl.questionToken !== undefined || decl.initializer !== undefined);
|
||||
let pt = mapType(checker.getTypeOfSymbol(p), ctx);
|
||||
if (
|
||||
decl &&
|
||||
ts.isParameter(decl) &&
|
||||
ts.isSetAccessorDeclaration(decl.parent) &&
|
||||
isJsSourceFile(decl.getSourceFile())
|
||||
)
|
||||
pt = DYN;
|
||||
// An inferred JavaScript binding pattern consumes an iterable; it
|
||||
// does not require the caller's array to have exactly this length.
|
||||
const executor = decl && ts.isParameter(decl) ? decl.parent : null;
|
||||
const construction = executor?.parent;
|
||||
const constructorSymbol =
|
||||
construction && ts.isNewExpression(construction)
|
||||
? checker.getSymbolAtLocation(construction.expression)
|
||||
: undefined;
|
||||
const nativeExecutor =
|
||||
executor &&
|
||||
(ts.isArrowFunction(executor) || ts.isFunctionExpression(executor)) &&
|
||||
construction &&
|
||||
ts.isNewExpression(construction) &&
|
||||
construction.arguments?.[0] === executor &&
|
||||
ts.isIdentifier(construction.expression) &&
|
||||
construction.expression.text === "Promise" &&
|
||||
constructorSymbol &&
|
||||
checker
|
||||
.declarationsOf(constructorSymbol)
|
||||
.some((site) => ctx.isStdlibFile(site.getSourceFile()));
|
||||
if (
|
||||
!nativeExecutor &&
|
||||
decl &&
|
||||
ts.isParameter(decl) &&
|
||||
!decl.dotDotDotToken &&
|
||||
!ts.isSetAccessorDeclaration(decl.parent) &&
|
||||
isJsSourceFile(decl.getSourceFile()) &&
|
||||
!decl.type &&
|
||||
!/@(?:param|type)\b/.test(
|
||||
decl.getSourceFile().text.slice(decl.parent?.pos ?? decl.pos, decl.getStart()),
|
||||
)
|
||||
)
|
||||
pt = DYN;
|
||||
if (pt) pt = jsOpenObjectType(decl, pt, ctx.shapes, ctx.unions);
|
||||
// Belt and braces for non-strict type worlds: an optional param's
|
||||
// ABI slot is always the undefined-armed union (strictNullChecks
|
||||
// already spells it that way; arm it here if the world didn't).
|
||||
if (optional && pt && pt.kind !== "void" && pt.kind !== "dyn") {
|
||||
const armed = withUndefinedArm(pt, ctx.unions);
|
||||
if (!armed) return null;
|
||||
pt = armed;
|
||||
}
|
||||
// `...args: unknown[]` / `...args: any[]`: the checked-dynamic
|
||||
// variadic ABI (no typed slot; the callee's hidden trailing dyn
|
||||
// array receives every surplus argument, as for JS rest params).
|
||||
if (
|
||||
rest &&
|
||||
(pt?.kind === "dyn" || (!pt && isAnyArrayType(checker.getTypeOfSymbol(p), checker)))
|
||||
) {
|
||||
dynRest = true;
|
||||
continue;
|
||||
}
|
||||
if (!pt) return null;
|
||||
if (rest) {
|
||||
if (pt.kind !== "array") return null;
|
||||
typedRest = true;
|
||||
}
|
||||
// `(value: void) => void` (Promise<void>'s resolve) is callable with
|
||||
// no arguments — a void param is dropped, not a mapping failure.
|
||||
if (pt.kind === "void") continue;
|
||||
params.push(pt);
|
||||
}
|
||||
const retT = checker.getReturnTypeOfSignature(sig);
|
||||
// `() => never` (a throw-only lambda's inferred type) is assignable to
|
||||
// `() => void` and its calls never produce a value — map the return
|
||||
// like declaredReturnType does for declarations. JS null/undefined
|
||||
// returns can read mutable fields, so callable values use the same
|
||||
// checked-native return ABI as their implementations.
|
||||
const sigDecl = checker.signatureDeclaration(sig);
|
||||
const jsUnitReturn =
|
||||
sigDecl !== undefined &&
|
||||
isJsSourceFile(sigDecl.getSourceFile()) &&
|
||||
(retT.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) !== 0;
|
||||
let ret =
|
||||
jsUnitReturn || (retT.flags & ts.TypeFlags.Any) !== 0
|
||||
? DYN
|
||||
: retT.flags & ts.TypeFlags.Never
|
||||
? VOID
|
||||
: mapType(retT, ctx);
|
||||
if (sigDecl && ts.isGetAccessorDeclaration(sigDecl) && isJsSourceFile(sigDecl.getSourceFile()))
|
||||
ret = DYN;
|
||||
if (sigDecl && isJsSourceFile(sigDecl.getSourceFile()) && ret?.kind === "array") ret = DYN;
|
||||
if (
|
||||
sigDecl &&
|
||||
ts.isFunctionLike(sigDecl) &&
|
||||
isJsSourceFile(sigDecl.getSourceFile()) &&
|
||||
!sigDecl.type &&
|
||||
!/@returns?\b/.test(sigDecl.getSourceFile().text.slice(sigDecl.pos, sigDecl.getStart())) &&
|
||||
ret?.kind === "record"
|
||||
)
|
||||
ret = DYN;
|
||||
if (
|
||||
sigDecl &&
|
||||
ts.isFunctionLike(sigDecl) &&
|
||||
isJsSourceFile(sigDecl.getSourceFile()) &&
|
||||
!sigDecl.type &&
|
||||
!/@returns?\b/.test(sigDecl.getSourceFile().text.slice(sigDecl.pos, sigDecl.getStart())) &&
|
||||
(ret?.kind === "date" ||
|
||||
(ret?.kind === "union" &&
|
||||
ctx.unions.get(ret.unionId)?.arms.some((arm) => arm.kind === "func")))
|
||||
)
|
||||
ret = DYN;
|
||||
if (
|
||||
ret &&
|
||||
sigDecl &&
|
||||
isJsSourceFile(sigDecl.getSourceFile()) &&
|
||||
functionCanReturnUndefined(sigDecl)
|
||||
)
|
||||
ret = withUndefinedArm(ret, ctx.unions) ?? ret;
|
||||
if (!ret) return null;
|
||||
return typedRest
|
||||
? { kind: "func", params, ret, rest: true, restAbi: "typed" }
|
||||
: dynRest
|
||||
? { kind: "func", params, ret, rest: true }
|
||||
: funcOf(params, ret);
|
||||
}
|
||||
if (callSigs.length === 1) return mapCallSignature(callSigs[0]!, ctx);
|
||||
// Records: object types whose members are all data properties (shorthand
|
||||
// methods in type position count — they're func-typed fields) with
|
||||
// mappable types; no call/construct signatures, no index signatures, not
|
||||
@@ -4353,6 +4223,175 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** One non-overloaded call signature's func type (mapTypeInner's
|
||||
* single-signature rule; erasedOverloadMemberType maps each overload of a
|
||||
* member through it). */
|
||||
function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null {
|
||||
const { checker } = ctx;
|
||||
if (sig.getTypeParameters().length && ctx.erasingSignature !== sig) return null;
|
||||
// A SYNTHESIZED rest param (tsc's JS inference for a function body
|
||||
// reading `arguments` — `(...args: any[]) => any` whose args symbol
|
||||
// has no declaration): the dotDotDot check below can't see it, and a
|
||||
// fixed-arity func mapping would LIE about the value's calling
|
||||
// convention. Detected by the param-count mismatch against the
|
||||
// signature's own declaration, excluding an erased `this` parameter.
|
||||
{
|
||||
const sigDecl = checker.signatureDeclaration(sig);
|
||||
const declParams =
|
||||
sigDecl !== undefined && ts.isFunctionLike(sigDecl) ? sigDecl.parameters : undefined;
|
||||
if (
|
||||
declParams !== undefined &&
|
||||
runtimeParameterCount(declParams) !== sig.getParameters().length
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
// tsgo never SYNTHESIZES that rest param into the inferred signature
|
||||
// (5.9.3 did — the count mismatch above was the whole detector
|
||||
// there), so the declaration's own body answers directly.
|
||||
if (sigDecl !== undefined && ts.isFunctionLike(sigDecl) && bodyReadsArguments(sigDecl)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
const params: IrType[] = [];
|
||||
let typedRest = false;
|
||||
let dynRest = false;
|
||||
for (const p of sig.getParameters()) {
|
||||
const decl = checker.valueDeclarationOf(p);
|
||||
const rest = decl !== undefined && ts.isParameter(decl) && decl.dotDotDotToken !== undefined;
|
||||
|
||||
// arity ABI selected by lambdaSignature. Treating an inferred any[]
|
||||
// declaration as a typed packed-array closure would bypass its boxed
|
||||
// spread/error semantics and change coverage classification.
|
||||
if (rest && decl !== undefined && isJsSourceFile(decl.getSourceFile())) return null;
|
||||
const optional =
|
||||
decl !== undefined &&
|
||||
ts.isParameter(decl) &&
|
||||
(decl.questionToken !== undefined || decl.initializer !== undefined);
|
||||
let pt = mapType(checker.getTypeOfSymbol(p), ctx);
|
||||
if (
|
||||
decl &&
|
||||
ts.isParameter(decl) &&
|
||||
ts.isSetAccessorDeclaration(decl.parent) &&
|
||||
isJsSourceFile(decl.getSourceFile())
|
||||
)
|
||||
pt = DYN;
|
||||
// An inferred JavaScript binding pattern consumes an iterable; it
|
||||
// does not require the caller's array to have exactly this length.
|
||||
const executor = decl && ts.isParameter(decl) ? decl.parent : null;
|
||||
const construction = executor?.parent;
|
||||
const constructorSymbol =
|
||||
construction && ts.isNewExpression(construction)
|
||||
? checker.getSymbolAtLocation(construction.expression)
|
||||
: undefined;
|
||||
const nativeExecutor =
|
||||
executor &&
|
||||
(ts.isArrowFunction(executor) || ts.isFunctionExpression(executor)) &&
|
||||
construction &&
|
||||
ts.isNewExpression(construction) &&
|
||||
construction.arguments?.[0] === executor &&
|
||||
ts.isIdentifier(construction.expression) &&
|
||||
construction.expression.text === "Promise" &&
|
||||
constructorSymbol &&
|
||||
checker
|
||||
.declarationsOf(constructorSymbol)
|
||||
.some((site) => ctx.isStdlibFile(site.getSourceFile()));
|
||||
if (
|
||||
!nativeExecutor &&
|
||||
decl &&
|
||||
ts.isParameter(decl) &&
|
||||
!decl.dotDotDotToken &&
|
||||
!ts.isSetAccessorDeclaration(decl.parent) &&
|
||||
isJsSourceFile(decl.getSourceFile()) &&
|
||||
!decl.type &&
|
||||
!/@(?:param|type)\b/.test(
|
||||
decl.getSourceFile().text.slice(decl.parent?.pos ?? decl.pos, decl.getStart()),
|
||||
)
|
||||
)
|
||||
pt = DYN;
|
||||
if (pt) pt = jsOpenObjectType(decl, pt, ctx.shapes, ctx.unions);
|
||||
// Belt and braces for non-strict type worlds: an optional param's
|
||||
// ABI slot is always the undefined-armed union (strictNullChecks
|
||||
// already spells it that way; arm it here if the world didn't).
|
||||
if (optional && pt && pt.kind !== "void" && pt.kind !== "dyn") {
|
||||
const armed = withUndefinedArm(pt, ctx.unions);
|
||||
if (!armed) return null;
|
||||
pt = armed;
|
||||
}
|
||||
// `...args: unknown[]` / `...args: any[]`: the checked-dynamic
|
||||
// variadic ABI (no typed slot; the callee's hidden trailing dyn
|
||||
// array receives every surplus argument, as for JS rest params).
|
||||
if (
|
||||
rest &&
|
||||
(pt?.kind === "dyn" || (!pt && isAnyArrayType(checker.getTypeOfSymbol(p), checker)))
|
||||
) {
|
||||
dynRest = true;
|
||||
continue;
|
||||
}
|
||||
if (!pt) return null;
|
||||
if (rest) {
|
||||
if (pt.kind !== "array") return null;
|
||||
typedRest = true;
|
||||
}
|
||||
// `(value: void) => void` (Promise<void>'s resolve) is callable with
|
||||
// no arguments — a void param is dropped, not a mapping failure.
|
||||
if (pt.kind === "void") continue;
|
||||
params.push(pt);
|
||||
}
|
||||
const retT = checker.getReturnTypeOfSignature(sig);
|
||||
// `() => never` (a throw-only lambda's inferred type) is assignable to
|
||||
// `() => void` and its calls never produce a value — map the return
|
||||
// like declaredReturnType does for declarations. JS null/undefined
|
||||
// returns can read mutable fields, so callable values use the same
|
||||
// checked-native return ABI as their implementations.
|
||||
const sigDecl = checker.signatureDeclaration(sig);
|
||||
const jsUnitReturn =
|
||||
sigDecl !== undefined &&
|
||||
isJsSourceFile(sigDecl.getSourceFile()) &&
|
||||
(retT.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) !== 0;
|
||||
let ret =
|
||||
jsUnitReturn || (retT.flags & ts.TypeFlags.Any) !== 0
|
||||
? DYN
|
||||
: retT.flags & ts.TypeFlags.Never
|
||||
? VOID
|
||||
: mapType(retT, ctx);
|
||||
if (sigDecl && ts.isGetAccessorDeclaration(sigDecl) && isJsSourceFile(sigDecl.getSourceFile()))
|
||||
ret = DYN;
|
||||
if (sigDecl && isJsSourceFile(sigDecl.getSourceFile()) && ret?.kind === "array") ret = DYN;
|
||||
if (
|
||||
sigDecl &&
|
||||
ts.isFunctionLike(sigDecl) &&
|
||||
isJsSourceFile(sigDecl.getSourceFile()) &&
|
||||
!sigDecl.type &&
|
||||
!/@returns?\b/.test(sigDecl.getSourceFile().text.slice(sigDecl.pos, sigDecl.getStart())) &&
|
||||
ret?.kind === "record"
|
||||
)
|
||||
ret = DYN;
|
||||
if (
|
||||
sigDecl &&
|
||||
ts.isFunctionLike(sigDecl) &&
|
||||
isJsSourceFile(sigDecl.getSourceFile()) &&
|
||||
!sigDecl.type &&
|
||||
!/@returns?\b/.test(sigDecl.getSourceFile().text.slice(sigDecl.pos, sigDecl.getStart())) &&
|
||||
(ret?.kind === "date" ||
|
||||
(ret?.kind === "union" &&
|
||||
ctx.unions.get(ret.unionId)?.arms.some((arm) => arm.kind === "func")))
|
||||
)
|
||||
ret = DYN;
|
||||
if (
|
||||
ret &&
|
||||
sigDecl &&
|
||||
isJsSourceFile(sigDecl.getSourceFile()) &&
|
||||
functionCanReturnUndefined(sigDecl)
|
||||
)
|
||||
ret = withUndefinedArm(ret, ctx.unions) ?? ret;
|
||||
if (!ret) return null;
|
||||
return typedRest
|
||||
? { kind: "func", params, ret, rest: true, restAbi: "typed" }
|
||||
: dynRest
|
||||
? { kind: "func", params, ret, rest: true }
|
||||
: funcOf(params, ret);
|
||||
}
|
||||
|
||||
/** Retain a literal discriminator before widening erases its values. A
|
||||
* structural match alone is insufficient: `{ kind: "loop", body: ... }`
|
||||
* also fits a smaller `{ kind: "empty" }` record. Choosing that layout
|
||||
@@ -5599,6 +5638,164 @@ export function erasedGenericMemberType(t: ts.Type, ctx: TypeMapperCtx): IrType
|
||||
return erased?.kind === "func" ? erased : undefined;
|
||||
}
|
||||
|
||||
/** An OVERLOADED callable member type (`on(event: "a", h: (x: number) =>
|
||||
* void): () => void; on(event: "b", h: (x: string) => void): () => void`
|
||||
* in a program interface or type literal): two or more call signatures and
|
||||
* nothing else (no data properties, construct signatures, or index
|
||||
* signatures), every one declared in TypeScript program source. Such a
|
||||
* member keeps a record slot holding its erased runtime function
|
||||
* (erasedOverloadMemberType); calls convert per their resolved overload
|
||||
* (lowerRecordFieldCall). */
|
||||
export function isOverloadedCallableMemberType(t: ts.Type, checker: ts.TypeChecker): boolean {
|
||||
const sigs = checker.getCallSignatures(t);
|
||||
if (sigs.length < 2) return false;
|
||||
if (
|
||||
checker.getConstructSignatures(t).length > 0 ||
|
||||
checker.getPropertiesOfType(t).length > 0 ||
|
||||
checker.getIndexInfosOfType(t).length > 0
|
||||
)
|
||||
return false;
|
||||
return sigs.every((sig) => {
|
||||
const decl = checker.signatureDeclaration(sig);
|
||||
if (decl === undefined) return false;
|
||||
if (!ts.isMethodSignatureDeclaration(decl) && !ts.isCallSignatureDeclaration(decl))
|
||||
return false;
|
||||
const file = decl.getSourceFile();
|
||||
return !file.isDeclarationFile && !isJsSourceFile(file);
|
||||
});
|
||||
}
|
||||
|
||||
/** Scalar arms an overload join may combine into one union. */
|
||||
const OVERLOAD_JOIN_SCALARS = new Set(["f64", "string", "bool", "undefinedT", "nullT"]);
|
||||
|
||||
/** The join of one position's types across an overload set: equal types
|
||||
* stay; scalar mixes form their union; anything else is `unknown` when
|
||||
* every joined type crosses the checked-dynamic boundary in both directions
|
||||
* (the call site converts into the slot, the implementation converts out
|
||||
* of it). Differing callbacks (`(x: number) => void` beside `(x: string) =>
|
||||
* void`) therefore travel as checked function values, which any
|
||||
* implementation's own callback type adapts from. Null when no join
|
||||
* exists. */
|
||||
function joinOverloadTypes(types: readonly IrType[], ctx: TypeMapperCtx): IrType | null {
|
||||
const first = types[0]!;
|
||||
if (types.every((t) => typeEquals(t, first))) return first;
|
||||
const getRecord = (id: string) => ctx.shapes.get(id);
|
||||
const getUnion = (id: string) => ctx.unions.get(id);
|
||||
const arms = new Map<string, IrType>();
|
||||
let scalar = true;
|
||||
for (const t of types) {
|
||||
const parts = t.kind === "union" ? (ctx.unions.get(t.unionId)?.arms ?? []) : [t];
|
||||
if (parts.length === 0) scalar = false;
|
||||
for (const part of parts) {
|
||||
if (!OVERLOAD_JOIN_SCALARS.has(part.kind)) scalar = false;
|
||||
arms.set(typeKey(part), part);
|
||||
}
|
||||
}
|
||||
if (scalar && [...arms.values()].some((arm) => !isUnitType(arm))) {
|
||||
const sorted = [...arms.values()].sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1));
|
||||
return { kind: "union", unionId: ctx.unions.intern(sorted) };
|
||||
}
|
||||
if (
|
||||
types.every(
|
||||
(t) =>
|
||||
t.kind === "dyn" ||
|
||||
(t.kind !== "void" &&
|
||||
canConvertToDyn(t, getRecord, getUnion) &&
|
||||
canDynCheckTo(t, getRecord, getUnion)),
|
||||
)
|
||||
)
|
||||
return DYN;
|
||||
return null;
|
||||
}
|
||||
|
||||
/** An overloaded member's runtime type (isOverloadedCallableMemberType).
|
||||
* JavaScript stores ONE function under the member; overload signatures only
|
||||
* constrain call sites. The slot's type joins the overloads per parameter
|
||||
* position and result (joinOverloadTypes), and a position some overloads
|
||||
* omit is optional. Each overload's own type parameters erase to `unknown`
|
||||
* first. Calls pick their overload statically, convert arguments into the
|
||||
* slot's parameter types and the result back to the overload's; an
|
||||
* implementation converts into the slot like any function value. Undefined
|
||||
* when the set does not join (rest parameters, a void result beside a
|
||||
* value result, a component outside the checked-dynamic boundary): the
|
||||
* member keeps its fence. */
|
||||
export function erasedOverloadMemberType(t: ts.Type, ctx: TypeMapperCtx): IrType | undefined {
|
||||
const funcs: (IrType & { kind: "func" })[] = [];
|
||||
for (const sig of ctx.checker.getCallSignatures(t)) {
|
||||
const own = new Set(sig.getTypeParameters());
|
||||
const outer = ctx.resolveTypeParam;
|
||||
const mapped = mapCallSignature(
|
||||
sig,
|
||||
own.size === 0
|
||||
? ctx
|
||||
: {
|
||||
...ctx,
|
||||
erasingSignature: sig,
|
||||
resolveTypeParam: (param) => (own.has(param) ? DYN : (outer?.(param) ?? null)),
|
||||
},
|
||||
);
|
||||
if (mapped?.kind !== "func" || mapped.rest === true || mapped.argumentsAll === true)
|
||||
return undefined;
|
||||
funcs.push(mapped);
|
||||
}
|
||||
const arity = Math.max(...funcs.map((f) => f.params.length));
|
||||
const params: IrType[] = [];
|
||||
for (let i = 0; i < arity; i++) {
|
||||
const present = funcs.filter((f) => i < f.params.length).map((f) => f.params[i]!);
|
||||
let param = joinOverloadTypes(present, ctx);
|
||||
if (param !== null && present.length < funcs.length && param.kind !== "dyn")
|
||||
param = withUndefinedArm(param, ctx.unions);
|
||||
if (param === null) return undefined;
|
||||
params.push(param);
|
||||
}
|
||||
const voids = funcs.filter((f) => f.ret.kind === "void").length;
|
||||
if (voids !== 0 && voids !== funcs.length) return undefined;
|
||||
const ret =
|
||||
voids === funcs.length
|
||||
? VOID
|
||||
: joinOverloadTypes(
|
||||
funcs.map((f) => f.ret),
|
||||
ctx,
|
||||
);
|
||||
if (ret === null) return undefined;
|
||||
return funcOf(params, ret);
|
||||
}
|
||||
|
||||
/** Why an overloaded member's set has no erased type
|
||||
* (erasedOverloadMemberType): the first overload, and within it the first
|
||||
* parameter or result, that does not map; otherwise the join itself. A
|
||||
* message tail after "its member 'name'". */
|
||||
function describeOverloadSetBlocker(t: ts.Type, ctx: TypeMapperCtx): string {
|
||||
const { checker } = ctx;
|
||||
for (const sig of checker.getCallSignatures(t)) {
|
||||
const own = new Set(sig.getTypeParameters());
|
||||
const outer = ctx.resolveTypeParam;
|
||||
const sigCtx: TypeMapperCtx =
|
||||
own.size === 0
|
||||
? ctx
|
||||
: {
|
||||
...ctx,
|
||||
erasingSignature: sig,
|
||||
resolveTypeParam: (param) => (own.has(param) ? DYN : (outer?.(param) ?? null)),
|
||||
};
|
||||
const mapped = mapCallSignature(sig, sigCtx);
|
||||
if (mapped?.kind === "func" && mapped.rest !== true && mapped.argumentsAll !== true) continue;
|
||||
const decl = checker.signatureDeclaration(sig);
|
||||
const spelled = decl ? decl.getText().replace(/\s+/g, " ").replace(/;$/, "") : "";
|
||||
const which = spelled ? `overload '${spelled}'` : "one overload";
|
||||
if (mapped?.kind === "func")
|
||||
return `is an overload set, and its ${which} has a rest parameter (an overload set stores one fixed-arity function)`;
|
||||
for (const param of sig.getParameters()) {
|
||||
const paramTs = checker.getTypeOfSymbol(param);
|
||||
if (!mapType(paramTs, sigCtx))
|
||||
return `is an overload set, and its ${which} has parameter '${param.name}' of type '${checker.typeToString(paramTs)}', which does not compile`;
|
||||
}
|
||||
const retTs = checker.getReturnTypeOfSignature(sig);
|
||||
return `is an overload set, and its ${which} returns '${checker.typeToString(retTs)}', which does not compile`;
|
||||
}
|
||||
return "is an overload set whose signatures do not join into one stored function (a void result beside a value result, or positions whose differing types do not cross the checked-dynamic boundary)";
|
||||
}
|
||||
|
||||
/** The computed pieces of a record shape mapRecordTypeInner hands back for
|
||||
* the OUTER frame to intern — or, when a back-reference minted a recursive
|
||||
* placeholder for the type mid-construction, to finalize INTO that
|
||||
@@ -5980,9 +6177,14 @@ function mapRecordTypeInner(
|
||||
// can hold them — see isGenericCallableMemberType): the shape keeps
|
||||
// its data fields, and calls of the member monomorphize per call
|
||||
// site against the defining object literal's declaration.
|
||||
// OVERLOADED members hold their erased runtime function (see
|
||||
// erasedOverloadMemberType); a set that does not join fails the
|
||||
// shape as before.
|
||||
let pt = isGenericCallableMemberType(fieldTs, checker)
|
||||
? erasedGenericMemberType(fieldTs, ctx)
|
||||
: mapType(fieldTs, ctx);
|
||||
: isOverloadedCallableMemberType(fieldTs, checker)
|
||||
? (erasedOverloadMemberType(fieldTs, ctx) ?? null)
|
||||
: mapType(fieldTs, ctx);
|
||||
if (pt === undefined) continue;
|
||||
// tsgo PANICS computing `readonly []` through the symbol-type query
|
||||
// (the TupleType conversion — the facade's panic fence answers
|
||||
@@ -6354,6 +6556,10 @@ export function describeRecordMemberBlocker(widened: ts.Type, ctx: TypeMapperCtx
|
||||
return null;
|
||||
}
|
||||
const fieldTs = checker.getTypeOfSymbol(p);
|
||||
if (isOverloadedCallableMemberType(fieldTs, checker)) {
|
||||
if (erasedOverloadMemberType(fieldTs, ctx) !== undefined) continue;
|
||||
return `the record shape is supported, but its member '${p.name}' ${describeOverloadSetBlocker(fieldTs, ctx)}`;
|
||||
}
|
||||
let pt = isGenericCallableMemberType(fieldTs, checker)
|
||||
? erasedGenericMemberType(fieldTs, ctx)
|
||||
: mapType(fieldTs, ctx);
|
||||
|
||||
@@ -137,6 +137,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"fetch.streamFrom",
|
||||
"num.toFixed",
|
||||
"num.toStringRadix",
|
||||
"num.toStringRadixF64",
|
||||
"insp.jsonDyn",
|
||||
// diagnostics_channel: publish runs subscribers synchronously (a throw
|
||||
// propagates — the documented divergence from triggerUncaughtException);
|
||||
|
||||
@@ -755,6 +755,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
"num.toFixed0": { argTypes: [F64], result: STRING },
|
||||
"num.toFixed": { argTypes: [F64, F64], result: STRING },
|
||||
"num.toStringRadix": { argTypes: [F64, DYN], result: STRING },
|
||||
/** toString(radix) with a number radix (scr_lib.c): truncates it as
|
||||
* ToIntegerOrInfinity does, THROWS RangeError outside 2..36. */
|
||||
"num.toStringRadixF64": { argTypes: [F64, F64], result: STRING },
|
||||
/** Object.is over two numbers — the spec's SameValue on doubles: NaN
|
||||
* equals NaN, +0 differs from -0, everything else is `===`. Plain bool
|
||||
* result; never throws. (Union-armed operands take unionEq's sameValue
|
||||
|
||||
@@ -1673,7 +1673,15 @@ export interface IrFunction {
|
||||
* out, `nextT` what `.next(v)` sends in (the yield expression's result
|
||||
* type). `async` alongside this field marks an async generator: its
|
||||
* resume methods queue requests and return promises. */
|
||||
generator?: { yieldT: IrType; nextT: IrType; resultType: IrType & { kind: "record" } };
|
||||
generator?: {
|
||||
yieldT: IrType;
|
||||
nextT: IrType;
|
||||
resultType: IrType & { kind: "record" };
|
||||
/** The first parameter is a generator this one reads from (an
|
||||
* iterator helper's source): `.return()` before the first `.next()`
|
||||
* closes it. */
|
||||
closesSource?: true;
|
||||
};
|
||||
/** Source function name for native error stack frames. */
|
||||
sourceName?: string;
|
||||
/** Ordinary authored function declarations/expressions own a prototype. */
|
||||
@@ -1946,6 +1954,11 @@ export type IrStmt =
|
||||
/** Resource-management cleanup: if the guarded body and cleanup both
|
||||
* throw, raise a SuppressedError instead of replacing the first error. */
|
||||
suppressFinallyErrors?: true;
|
||||
/** Iterator closing: if the guarded body threw and the cleanup throws
|
||||
* too, the cleanup's error is dropped and the first one propagates.
|
||||
* After a generator .return() the cleanup's error replaces it, and on
|
||||
* normal exits it propagates. */
|
||||
dropFinallyErrors?: true;
|
||||
loc: SrcLoc;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { analyzeWasm, isTranslatableFeature, isWasmHeader } from "./binary.js";
|
||||
|
||||
/** V8's verdict (the compiler's type world declares no WebAssembly value). */
|
||||
const v8Validate = (bytes: Uint8Array): boolean =>
|
||||
(
|
||||
globalThis as unknown as { WebAssembly: { validate(bytes: Uint8Array): boolean } }
|
||||
).WebAssembly.validate(bytes);
|
||||
|
||||
// A minimal module assembler: enough structure to aim at one decoder rule
|
||||
// per test. Every module is also checked against V8 (Node is the oracle).
|
||||
|
||||
function leb(n: number): number[] {
|
||||
const out: number[] = [];
|
||||
do {
|
||||
let b = n % 128;
|
||||
n = Math.floor(n / 128);
|
||||
if (n > 0) b |= 0x80;
|
||||
out.push(b);
|
||||
} while (n > 0);
|
||||
return out;
|
||||
}
|
||||
|
||||
function name(text: string): number[] {
|
||||
const bytes = [...new TextEncoder().encode(text)];
|
||||
return [...leb(bytes.length), ...bytes];
|
||||
}
|
||||
|
||||
function vec(items: number[][]): number[] {
|
||||
return [...leb(items.length), ...items.flat()];
|
||||
}
|
||||
|
||||
function section(id: number, body: number[]): number[] {
|
||||
return [id, ...leb(body.length), ...body];
|
||||
}
|
||||
|
||||
function module(...sections: number[][]): Uint8Array {
|
||||
return new Uint8Array([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, ...sections.flat()]);
|
||||
}
|
||||
|
||||
const I32 = 0x7f;
|
||||
const I64 = 0x7e;
|
||||
const F32 = 0x7d;
|
||||
const V128 = 0x7b;
|
||||
const EXTERNREF = 0x6f;
|
||||
|
||||
const funcType = (params: number[], results: number[]): number[] => [
|
||||
0x60,
|
||||
...vec(params.map((p) => [p])),
|
||||
...vec(results.map((r) => [r])),
|
||||
];
|
||||
const types = (...fts: number[][]): number[] => section(1, vec(fts));
|
||||
const funcs = (...typeIndices: number[]): number[] =>
|
||||
section(3, vec(typeIndices.map((t) => leb(t))));
|
||||
const body = (code: number[], locals: number[][] = []): number[] => {
|
||||
const content = [...vec(locals), ...code, 0x0b];
|
||||
return [...leb(content.length), ...content];
|
||||
};
|
||||
const code = (...bodies: number[][]): number[] => section(10, vec(bodies));
|
||||
const memory = (...limits: number[][]): number[] => section(5, vec(limits));
|
||||
const exportsOf = (...entries: number[][]): number[] => section(7, vec(entries));
|
||||
|
||||
function check(bytes: Uint8Array) {
|
||||
const summary = analyzeWasm(bytes);
|
||||
const accepted = summary.status === "valid" || summary.status === "unsupported";
|
||||
expect(accepted, summary.error ?? "").toBe(v8Validate(bytes));
|
||||
return summary;
|
||||
}
|
||||
|
||||
describe("analyzeWasm", () => {
|
||||
test("an empty module is valid and uses no proposal", () => {
|
||||
const s = check(module());
|
||||
expect(s).toMatchObject({ status: "valid", error: null, features: [], functions: 0, size: 8 });
|
||||
expect(isWasmHeader(module())).toBe(true);
|
||||
});
|
||||
|
||||
test("counts functions, imports, exports, memories, tables and globals", () => {
|
||||
const s = check(
|
||||
module(
|
||||
types(funcType([I32, I32], [I32]), funcType([], [])),
|
||||
section(
|
||||
2,
|
||||
vec([
|
||||
[...name("env"), ...name("log"), 0x00, 0x01],
|
||||
[...name("wasi_snapshot_preview1"), ...name("proc_exit"), 0x00, 0x01],
|
||||
]),
|
||||
),
|
||||
funcs(0),
|
||||
section(4, vec([[0x70, 0x00, 0x01]])),
|
||||
memory([0x00, 0x01]),
|
||||
section(6, vec([[I32, 0x01, 0x41, 0x00, 0x0b]])),
|
||||
exportsOf([...name("add"), 0x00, 0x02], [...name("memory"), 0x02, 0x00]),
|
||||
code(body([0x20, 0x00, 0x20, 0x01, 0x6a])),
|
||||
),
|
||||
);
|
||||
expect(s).toMatchObject({
|
||||
status: "valid",
|
||||
functions: 1,
|
||||
imports: 2,
|
||||
exports: 2,
|
||||
importModules: ["env", "wasi_snapshot_preview1"],
|
||||
memories: 1,
|
||||
tables: 1,
|
||||
globals: 1,
|
||||
});
|
||||
});
|
||||
|
||||
test.each([
|
||||
[
|
||||
"sign-extension",
|
||||
module(types(funcType([I32], [I32])), funcs(0), code(body([0x20, 0x00, 0xc0]))),
|
||||
],
|
||||
[
|
||||
"nontrapping-fptoint",
|
||||
module(types(funcType([F32], [I32])), funcs(0), code(body([0x20, 0x00, 0xfc, 0x00]))),
|
||||
],
|
||||
[
|
||||
"bulk-memory",
|
||||
module(
|
||||
types(funcType([], [])),
|
||||
funcs(0),
|
||||
memory([0x00, 0x01]),
|
||||
code(body([0x41, 0x00, 0x41, 0x00, 0x41, 0x00, 0xfc, 0x0b, 0x00])),
|
||||
),
|
||||
],
|
||||
[
|
||||
"multi-value",
|
||||
module(types(funcType([], [I32, I64])), funcs(0), code(body([0x41, 0x01, 0x42, 0x02]))),
|
||||
],
|
||||
["reference-types", module(section(4, vec([[EXTERNREF, 0x00, 0x01]])))],
|
||||
["extended-const", module(section(6, vec([[I32, 0x00, 0x41, 0x01, 0x41, 0x02, 0x6a, 0x0b]])))],
|
||||
[
|
||||
"mutable-globals",
|
||||
module(section(2, vec([[...name("env"), ...name("sp"), 0x03, I32, 0x01]]))),
|
||||
],
|
||||
])("detects %s and stays translatable", (feature, bytes) => {
|
||||
const s = check(bytes);
|
||||
expect(s.status).toBe("valid");
|
||||
expect(s.features).toContain(feature);
|
||||
expect(isTranslatableFeature(feature)).toBe(true);
|
||||
});
|
||||
|
||||
test.each([
|
||||
[
|
||||
"simd",
|
||||
module(
|
||||
types(funcType([], [V128])),
|
||||
funcs(0),
|
||||
code(body([0xfd, 0x0c, ...Array.from({ length: 16 }, () => 0)])),
|
||||
),
|
||||
],
|
||||
["threads", module(memory([0x03, 0x01, 0x01]))],
|
||||
["exceptions", module(types(funcType([], [])), section(13, vec([[0x00, 0x00]])))],
|
||||
["tail-call", module(types(funcType([], [])), funcs(0), code(body([0x12, 0x00])))],
|
||||
["memory64", module(memory([0x04, 0x01]))],
|
||||
["multi-memory", module(memory([0x00, 0x01], [0x00, 0x01]))],
|
||||
["gc", module(section(1, vec([[0x5f, 0x00]])))],
|
||||
])("reports %s as outside the translatable set", (feature, bytes) => {
|
||||
const s = check(bytes);
|
||||
expect(s.status).toBe("unsupported");
|
||||
expect(s.unsupported).toEqual([feature]);
|
||||
expect(isTranslatableFeature(feature)).toBe(false);
|
||||
});
|
||||
|
||||
test.each([
|
||||
["bad magic", new Uint8Array([0x00, 0x61, 0x73, 0x6e, 1, 0, 0, 0]), "malformed", "magic"],
|
||||
["bad version", new Uint8Array([0x00, 0x61, 0x73, 0x6d, 2, 0, 0, 0]), "malformed", "version"],
|
||||
["sections out of order", module(funcs(), types()), "malformed", "unexpected section"],
|
||||
["section size mismatch", module([0x01, 0x02, 0x00, 0x00]), "malformed", "section size"],
|
||||
[
|
||||
"overlong integer",
|
||||
module([0x01, 0x06, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00]),
|
||||
"malformed",
|
||||
"integer",
|
||||
],
|
||||
["invalid UTF-8 name", module(exportsOf([0x01, 0xff, 0x00, 0x00])), "malformed", "UTF-8"],
|
||||
[
|
||||
"function and code counts differ",
|
||||
module(types(funcType([], [])), funcs(0, 0), code(body([]))),
|
||||
"malformed",
|
||||
"inconsistent",
|
||||
],
|
||||
[
|
||||
"illegal opcode",
|
||||
module(types(funcType([], [])), funcs(0), code(body([0xff]))),
|
||||
"malformed",
|
||||
"illegal opcode",
|
||||
],
|
||||
[
|
||||
"memory.init without a data count section",
|
||||
module(
|
||||
types(funcType([], [])),
|
||||
funcs(0),
|
||||
memory([0x00, 0x01]),
|
||||
code(body([0x41, 0x00, 0x41, 0x00, 0x41, 0x00, 0xfc, 0x08, 0x00, 0x00])),
|
||||
),
|
||||
"malformed",
|
||||
"data count",
|
||||
],
|
||||
[
|
||||
"type mismatch",
|
||||
module(types(funcType([], [I32])), funcs(0), code(body([0x42, 0x00]))),
|
||||
"invalid",
|
||||
"type mismatch",
|
||||
],
|
||||
[
|
||||
"unknown function",
|
||||
module(types(funcType([], [])), funcs(0), code(body([0x10, 0x05]))),
|
||||
"invalid",
|
||||
"unknown function",
|
||||
],
|
||||
[
|
||||
"duplicate export",
|
||||
module(
|
||||
types(funcType([], [])),
|
||||
funcs(0),
|
||||
exportsOf([...name("f"), 0x00, 0x00], [...name("f"), 0x00, 0x00]),
|
||||
code(body([])),
|
||||
),
|
||||
"invalid",
|
||||
"duplicate export",
|
||||
],
|
||||
["memory above 4 GiB", module(memory([0x00, ...leb(65537)])), "invalid", "65536 pages"],
|
||||
[
|
||||
"undeclared function reference",
|
||||
module(types(funcType([], [])), funcs(0), code(body([0xd2, 0x00, 0x1a]))),
|
||||
"invalid",
|
||||
"undeclared",
|
||||
],
|
||||
])("rejects %s", (_label, bytes, status, message) => {
|
||||
const s = check(bytes);
|
||||
expect(s.status).toBe(status);
|
||||
expect(s.error).toContain(message);
|
||||
});
|
||||
|
||||
test("never throws on truncated input", () => {
|
||||
const whole = module(
|
||||
types(funcType([I32, I32], [I32])),
|
||||
funcs(0),
|
||||
code(body([0x20, 0x00, 0x20, 0x01, 0x6a])),
|
||||
);
|
||||
for (let n = 0; n < whole.length; n++) check(whole.subarray(0, n));
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,30 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { decodeBase64, packageRootOf } from "./discover.js";
|
||||
|
||||
describe("decodeBase64", () => {
|
||||
test("decodes standard and URL-safe spellings, with or without padding", () => {
|
||||
const bytes = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 0xfb, 0xff]);
|
||||
const standard = Buffer.from(bytes).toString("base64");
|
||||
expect(decodeBase64(standard)).toEqual(bytes);
|
||||
expect(decodeBase64(standard.replace(/=+$/, ""))).toEqual(bytes);
|
||||
expect(decodeBase64(Buffer.from(bytes).toString("base64url"))).toEqual(bytes);
|
||||
});
|
||||
|
||||
test("answers null for text that is not base64", () => {
|
||||
expect(decodeBase64("AGFzbQ*")).toBeNull();
|
||||
expect(decodeBase64("AGFzb")).toBeNull();
|
||||
expect(decodeBase64("AGFzbQ===")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("packageRootOf", () => {
|
||||
test("finds the innermost package, scoped or not", () => {
|
||||
expect(packageRootOf("/p/node_modules/quickjs-wasi/dist/index.js")).toBe(
|
||||
"/p/node_modules/quickjs-wasi",
|
||||
);
|
||||
expect(packageRootOf("/p/node_modules/a/node_modules/@scope/b/lib/x.js")).toBe(
|
||||
"/p/node_modules/a/node_modules/@scope/b",
|
||||
);
|
||||
expect(packageRootOf("/p/src/main.ts")).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,426 @@
|
||||
/* Build-time discovery of the WebAssembly modules a program can load.
|
||||
*
|
||||
* `scriptc coverage` reports every module whose bytes the build can see,
|
||||
* from four sources:
|
||||
*
|
||||
* 1. path: a `.wasm` path the compiler resolves from the source text
|
||||
* (`require.resolve`, `createRequire(...).resolve`, `import.meta.resolve`,
|
||||
* `new URL("./x.wasm", import.meta.url)`, and `path.join(__dirname, ...)`
|
||||
* or `__dirname + "/x.wasm"`);
|
||||
* 2. inline: a string literal that decodes as base64 to a module, or an
|
||||
* array literal of its bytes;
|
||||
* 3. package: every `.wasm` file shipped by a reached npm package whose
|
||||
* program files mention `WebAssembly` (this finds paths computed at run
|
||||
* time, such as a loader's default argument);
|
||||
* 4. config: the `scriptc.wasm` list in the entry's package.json and the
|
||||
* `--wasm <file>` flag.
|
||||
*
|
||||
* Each module is decoded and validated (binary.ts) and reported once, by
|
||||
* content, with every source that found it. Nothing here changes what a
|
||||
* build emits. */
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { dirname, isAbsolute, join, resolve } from "node:path";
|
||||
import * as ts from "../frontend/ts7/adapter.js";
|
||||
import {
|
||||
trackedAccessibleEntries,
|
||||
trackedFileExists,
|
||||
trackedReadFile,
|
||||
} from "../frontend/input-tracker.js";
|
||||
import { analyzeWasm, isWasmHeader, type WasmModuleSummary } from "./binary.js";
|
||||
|
||||
export type WasmSourceKind = "path" | "inline" | "package" | "config";
|
||||
|
||||
export interface WasmSource {
|
||||
kind: WasmSourceKind;
|
||||
/** The source file (path, inline) or the file that names the module. */
|
||||
file: string;
|
||||
/** 1-based line of the site, when there is one. */
|
||||
line?: number;
|
||||
}
|
||||
|
||||
/** One discovered module. JSON-shaped: the coverage verdict cache stores it. */
|
||||
export interface WasmModuleReport extends WasmModuleSummary {
|
||||
sha256: string;
|
||||
/** The module file, absent for inline modules. */
|
||||
path?: string;
|
||||
sources: WasmSource[];
|
||||
}
|
||||
|
||||
export interface WasmDiscoveryInput {
|
||||
/** The program's source files (entry and modules). */
|
||||
files: readonly ts.SourceFile[];
|
||||
/** The entry file. */
|
||||
entry: string;
|
||||
/** --wasm files, absolute. */
|
||||
configured: readonly string[];
|
||||
/** require.resolve from `fromFile`, or null when it does not resolve. */
|
||||
resolveRequest: (fromFile: string, specifier: string) => string | null;
|
||||
}
|
||||
|
||||
const SCAN_KINDS: ReadonlySet<ts.SyntaxKind> = new Set([
|
||||
ts.SyntaxKind.StringLiteral,
|
||||
ts.SyntaxKind.NoSubstitutionTemplateLiteral,
|
||||
ts.SyntaxKind.CallExpression,
|
||||
ts.SyntaxKind.NewExpression,
|
||||
ts.SyntaxKind.BinaryExpression,
|
||||
ts.SyntaxKind.ArrayLiteralExpression,
|
||||
]);
|
||||
|
||||
/** The base64 spelling of a module's first bytes ("\0asm"). */
|
||||
const BASE64_MAGIC = "AGFzbQ";
|
||||
const BYTES_MAGIC = /\[\s*(?:0|0x0+)\s*,\s*(?:97|0x61)\s*,\s*(?:115|0x73)\s*,\s*(?:109|0x6d)\s*,/i;
|
||||
/** Package directories deeper than this are not searched for modules. */
|
||||
const PACKAGE_SCAN_DEPTH = 6;
|
||||
/** Larger files are reported by size but not read. */
|
||||
const MAX_MODULE_BYTES = 256 * 1024 * 1024;
|
||||
|
||||
class Discovery {
|
||||
input: WasmDiscoveryInput;
|
||||
byHash = new Map<string, WasmModuleReport>();
|
||||
byPath = new Map<string, string>();
|
||||
order: string[] = [];
|
||||
|
||||
constructor(input: WasmDiscoveryInput) {
|
||||
this.input = input;
|
||||
}
|
||||
|
||||
/** Records a module and one source that found it; answers its hash. */
|
||||
addBytes(bytes: Uint8Array, path: string | undefined, source: WasmSource): string {
|
||||
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
||||
let report = this.byHash.get(sha256);
|
||||
if (report === undefined) {
|
||||
const summary = analyzeWasm(bytes);
|
||||
report = {
|
||||
size: summary.size,
|
||||
status: summary.status,
|
||||
error: summary.error,
|
||||
features: summary.features,
|
||||
unsupported: summary.unsupported,
|
||||
functions: summary.functions,
|
||||
imports: summary.imports,
|
||||
exports: summary.exports,
|
||||
importModules: summary.importModules,
|
||||
memories: summary.memories,
|
||||
tables: summary.tables,
|
||||
globals: summary.globals,
|
||||
customSections: summary.customSections,
|
||||
sha256,
|
||||
sources: [],
|
||||
};
|
||||
this.byHash.set(sha256, report);
|
||||
this.order.push(sha256);
|
||||
}
|
||||
if (report.path === undefined && path !== undefined) report.path = path;
|
||||
addSource(report, source);
|
||||
return sha256;
|
||||
}
|
||||
|
||||
addFile(path: string, source: WasmSource): void {
|
||||
path = resolve(path);
|
||||
const known = this.byPath.get(path);
|
||||
if (known !== undefined) {
|
||||
addSource(this.byHash.get(known)!, source);
|
||||
return;
|
||||
}
|
||||
// The tracked read makes the coverage verdict cache depend on the file.
|
||||
if (trackedReadFile(path) === null) return;
|
||||
let bytes: Uint8Array;
|
||||
try {
|
||||
bytes = new Uint8Array(readFileSync(path));
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (bytes.length > MAX_MODULE_BYTES) return;
|
||||
this.byPath.set(path, this.addBytes(bytes, path, source));
|
||||
}
|
||||
|
||||
// ── source 1 and 2: the program's source text ───────────────────────
|
||||
|
||||
scanFile(sf: ts.SourceFile): void {
|
||||
const text = sf.text;
|
||||
const paths = text.includes(".wasm");
|
||||
const base64 = text.includes(BASE64_MAGIC);
|
||||
const bytes = BYTES_MAGIC.test(text);
|
||||
if (!paths && !base64 && !bytes) return;
|
||||
const file = sf.fileName;
|
||||
const lineOf = (node: ts.Node): number =>
|
||||
ts.getLineAndCharacterOfPosition(sf, node.getStart()).line + 1;
|
||||
ts.walkPreorder(
|
||||
sf,
|
||||
(node) => {
|
||||
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
||||
if (base64 && node.text.startsWith(BASE64_MAGIC)) {
|
||||
const decoded = decodeBase64(node.text);
|
||||
if (decoded !== null && isWasmHeader(decoded)) {
|
||||
this.addBytes(decoded, undefined, { kind: "inline", file, line: lineOf(node) });
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
if (ts.isArrayLiteralExpression(node)) {
|
||||
if (bytes) {
|
||||
const literal = byteArrayLiteral(node);
|
||||
if (literal !== null && isWasmHeader(literal)) {
|
||||
this.addBytes(literal, undefined, { kind: "inline", file, line: lineOf(node) });
|
||||
return "skip";
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
if (!paths) return undefined;
|
||||
const path = this.resolvedPath(sf, node);
|
||||
if (path !== null) this.addFile(path, { kind: "path", file, line: lineOf(node) });
|
||||
return undefined;
|
||||
},
|
||||
SCAN_KINDS,
|
||||
);
|
||||
}
|
||||
|
||||
/** The module path a resolution expression names, when it ends in .wasm. */
|
||||
resolvedPath(sf: ts.SourceFile, node: ts.Node): string | null {
|
||||
const dir = dirname(sf.fileName);
|
||||
if (ts.isCallExpression(node)) {
|
||||
const callee = node.expression;
|
||||
const first = node.arguments[0];
|
||||
if (first === undefined) return null;
|
||||
// require.resolve / createRequire(...).resolve / import.meta.resolve
|
||||
if (ts.isPropertyAccessExpression(callee) && callee.name.text === "resolve") {
|
||||
const specifier = wasmLiteral(first);
|
||||
if (specifier === null) {
|
||||
return this.joinedPath(dir, node.arguments);
|
||||
}
|
||||
const receiver = callee.expression;
|
||||
if (ts.isMetaProperty(receiver) || isRequireLike(receiver)) {
|
||||
if (specifier.startsWith("./") || specifier.startsWith("../")) {
|
||||
return resolve(dir, specifier);
|
||||
}
|
||||
if (isAbsolute(specifier)) return specifier;
|
||||
return this.input.resolveRequest(sf.fileName, specifier);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
// path.join(__dirname, "x.wasm") and its resolve/bare spellings
|
||||
if (
|
||||
(ts.isPropertyAccessExpression(callee) &&
|
||||
(callee.name.text === "join" || callee.name.text === "resolve")) ||
|
||||
(ts.isIdentifier(callee) && (callee.text === "join" || callee.text === "resolve"))
|
||||
) {
|
||||
return this.joinedPath(dir, node.arguments);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (ts.isNewExpression(node)) {
|
||||
// new URL("./x.wasm", import.meta.url)
|
||||
if (!ts.isIdentifier(node.expression) || node.expression.text !== "URL") return null;
|
||||
const args = node.arguments;
|
||||
if (args === undefined || args.length !== 2) return null;
|
||||
const specifier = wasmLiteral(args[0]!);
|
||||
const base = args[1]!;
|
||||
if (specifier === null || !isImportMetaUrl(base)) return null;
|
||||
if (specifier.startsWith("./") || specifier.startsWith("../")) return resolve(dir, specifier);
|
||||
return null;
|
||||
}
|
||||
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
||||
// __dirname + "/x.wasm"
|
||||
if (!isDirname(node.left)) return null;
|
||||
const rest = wasmLiteral(node.right);
|
||||
return rest === null ? null : join(dir, rest);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
joinedPath(dir: string, args: readonly ts.Expression[]): string | null {
|
||||
if (args.length < 2 || !isDirname(args[0]!)) return null;
|
||||
const parts: string[] = [dir];
|
||||
for (let i = 1; i < args.length; i++) {
|
||||
const arg = args[i]!;
|
||||
if (!ts.isStringLiteral(arg) && !ts.isNoSubstitutionTemplateLiteral(arg)) return null;
|
||||
parts.push(arg.text);
|
||||
}
|
||||
if (!parts[parts.length - 1]!.endsWith(".wasm")) return null;
|
||||
let path = parts[0]!;
|
||||
for (let i = 1; i < parts.length; i++) path = resolve(path, parts[i]!);
|
||||
return path;
|
||||
}
|
||||
|
||||
// ── source 3: package files ─────────────────────────────────────────
|
||||
|
||||
scanPackages(): void {
|
||||
const roots = new Map<string, boolean>();
|
||||
for (const sf of this.input.files) {
|
||||
const root = packageRootOf(sf.fileName);
|
||||
if (root === null) continue;
|
||||
const mentions = sf.text.includes("WebAssembly");
|
||||
roots.set(root, (roots.get(root) ?? false) || mentions);
|
||||
}
|
||||
for (const [root, mentions] of roots) {
|
||||
if (!mentions) continue;
|
||||
const found: string[] = [];
|
||||
collectWasmFiles(root, 0, found);
|
||||
found.sort();
|
||||
for (const path of found)
|
||||
this.addFile(path, { kind: "package", file: join(root, "package.json") });
|
||||
}
|
||||
}
|
||||
|
||||
// ── source 4: configuration ─────────────────────────────────────────
|
||||
|
||||
scanConfig(): void {
|
||||
const manifest = nearestPackageJson(dirname(this.input.entry));
|
||||
if (manifest !== null) {
|
||||
const text = trackedReadFile(manifest);
|
||||
if (text !== null) {
|
||||
let listed: unknown;
|
||||
try {
|
||||
const parsed = JSON.parse(text) as { scriptc?: { wasm?: unknown } };
|
||||
listed = parsed.scriptc?.wasm;
|
||||
} catch {
|
||||
listed = undefined;
|
||||
}
|
||||
if (Array.isArray(listed)) {
|
||||
for (const entry of listed) {
|
||||
if (typeof entry === "string") {
|
||||
this.addFile(resolve(dirname(manifest), entry), { kind: "config", file: manifest });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const path of this.input.configured) {
|
||||
this.addFile(path, { kind: "config", file: path });
|
||||
}
|
||||
}
|
||||
|
||||
run(): WasmModuleReport[] {
|
||||
for (const sf of this.input.files) this.scanFile(sf);
|
||||
this.scanPackages();
|
||||
this.scanConfig();
|
||||
return this.order.map((sha) => this.byHash.get(sha)!);
|
||||
}
|
||||
}
|
||||
|
||||
function addSource(report: WasmModuleReport, source: WasmSource): void {
|
||||
for (const s of report.sources) {
|
||||
if (s.kind === source.kind && s.file === source.file && s.line === source.line) return;
|
||||
}
|
||||
report.sources.push(source);
|
||||
}
|
||||
|
||||
function wasmLiteral(node: ts.Expression): string | null {
|
||||
if (!ts.isStringLiteral(node) && !ts.isNoSubstitutionTemplateLiteral(node)) return null;
|
||||
return node.text.endsWith(".wasm") ? node.text : null;
|
||||
}
|
||||
|
||||
function isDirname(node: ts.Expression): boolean {
|
||||
return ts.isIdentifier(node) && node.text === "__dirname";
|
||||
}
|
||||
|
||||
function isImportMetaUrl(node: ts.Expression): boolean {
|
||||
return (
|
||||
ts.isPropertyAccessExpression(node) &&
|
||||
node.name.text === "url" &&
|
||||
ts.isMetaProperty(node.expression)
|
||||
);
|
||||
}
|
||||
|
||||
/** `require`, a `createRequire(...)` call, or a binding named like one. */
|
||||
function isRequireLike(node: ts.Expression): boolean {
|
||||
if (ts.isIdentifier(node)) return node.text === "require" || /require$/i.test(node.text);
|
||||
if (ts.isCallExpression(node)) {
|
||||
const callee = node.expression;
|
||||
return (
|
||||
(ts.isIdentifier(callee) && callee.text === "createRequire") ||
|
||||
(ts.isPropertyAccessExpression(callee) && callee.name.text === "createRequire")
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** The bytes of an array literal of integer literals, or null. */
|
||||
function byteArrayLiteral(node: ts.ArrayLiteralExpression): Uint8Array | null {
|
||||
const elements = node.elements;
|
||||
const out = new Uint8Array(elements.length);
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
const element = elements[i]!;
|
||||
if (!ts.isNumericLiteral(element)) return null;
|
||||
const value = Number(element.text);
|
||||
if (!Number.isInteger(value) || value < 0 || value > 255) return null;
|
||||
out[i] = value;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const BASE64_VALUES = (() => {
|
||||
const table = new Int16Array(128).fill(-1);
|
||||
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
for (let i = 0; i < alphabet.length; i++) table[alphabet.charCodeAt(i)] = i;
|
||||
// base64url spellings
|
||||
table["-".charCodeAt(0)] = 62;
|
||||
table["_".charCodeAt(0)] = 63;
|
||||
return table;
|
||||
})();
|
||||
|
||||
/** Decodes standard or URL-safe base64 (padding optional); null when the
|
||||
* text is not base64. */
|
||||
export function decodeBase64(text: string): Uint8Array | null {
|
||||
let end = text.length;
|
||||
while (end > 0 && text.charCodeAt(end - 1) === 61) end--;
|
||||
if (text.length - end > 2 || end % 4 === 1) return null;
|
||||
const out = new Uint8Array(Math.floor((end * 3) / 4));
|
||||
let acc = 0;
|
||||
let bits = 0;
|
||||
let j = 0;
|
||||
for (let i = 0; i < end; i++) {
|
||||
const c = text.charCodeAt(i);
|
||||
const v = c < 128 ? BASE64_VALUES[c]! : -1;
|
||||
if (v < 0) return null;
|
||||
acc = ((acc << 6) | v) & 0xffffff;
|
||||
bits += 6;
|
||||
if (bits >= 8) {
|
||||
bits -= 8;
|
||||
out[j++] = (acc >> bits) & 0xff;
|
||||
}
|
||||
}
|
||||
return j === out.length ? out : out.subarray(0, j);
|
||||
}
|
||||
|
||||
/** The root directory of the npm package that contains `file` (the
|
||||
* directory below its innermost node_modules), or null outside one. */
|
||||
export function packageRootOf(file: string): string | null {
|
||||
const marker = "/node_modules/";
|
||||
const at = file.lastIndexOf(marker);
|
||||
if (at < 0) return null;
|
||||
const rest = file.slice(at + marker.length);
|
||||
const segments = rest.split("/");
|
||||
const take = segments[0]!.startsWith("@") ? 2 : 1;
|
||||
if (segments.length <= take) return null;
|
||||
return file.slice(0, at + marker.length) + segments.slice(0, take).join("/");
|
||||
}
|
||||
|
||||
function collectWasmFiles(dir: string, depth: number, out: string[]): void {
|
||||
const entries = trackedAccessibleEntries(dir);
|
||||
if (entries === null) return;
|
||||
for (const name of entries.files) if (name.endsWith(".wasm")) out.push(join(dir, name));
|
||||
if (depth >= PACKAGE_SCAN_DEPTH) return;
|
||||
for (const name of entries.directories) {
|
||||
if (name === "node_modules" || name.startsWith(".")) continue;
|
||||
collectWasmFiles(join(dir, name), depth + 1, out);
|
||||
}
|
||||
}
|
||||
|
||||
function nearestPackageJson(dir: string): string | null {
|
||||
for (;;) {
|
||||
const candidate = join(dir, "package.json");
|
||||
if (trackedFileExists(candidate)) return candidate;
|
||||
const parent = dirname(dir);
|
||||
if (parent === dir) return null;
|
||||
dir = parent;
|
||||
}
|
||||
}
|
||||
|
||||
/** Every WebAssembly module the program's sources, packages and
|
||||
* configuration make visible to the build, in discovery order. */
|
||||
export function discoverWasmModules(input: WasmDiscoveryInput): WasmModuleReport[] {
|
||||
return new Discovery(input).run();
|
||||
}
|
||||
@@ -4464,7 +4464,7 @@
|
||||
"kind": "stdlib",
|
||||
"name": "Map.prototype.entries",
|
||||
"status": "static",
|
||||
"note": "live native iterators support next(), for...of, and drains; iterator helper methods are refused"
|
||||
"note": "live native iterators support next(), for...of, drains, and the iterator helper methods"
|
||||
},
|
||||
{
|
||||
"id": "stdlib.map.forEach",
|
||||
@@ -4489,7 +4489,7 @@
|
||||
"kind": "stdlib",
|
||||
"name": "Map.prototype.keys",
|
||||
"status": "static",
|
||||
"note": "live native iterators support next(), for...of, and drains; iterator helper methods are refused"
|
||||
"note": "live native iterators support next(), for...of, drains, and the iterator helper methods"
|
||||
},
|
||||
{
|
||||
"id": "stdlib.map.set",
|
||||
@@ -4502,7 +4502,7 @@
|
||||
"kind": "stdlib",
|
||||
"name": "Map.prototype.values",
|
||||
"status": "static",
|
||||
"note": "live native iterators support next(), for...of, and drains; iterator helper methods are refused"
|
||||
"note": "live native iterators support next(), for...of, drains, and the iterator helper methods"
|
||||
},
|
||||
{
|
||||
"id": "stdlib.math.E",
|
||||
@@ -5371,7 +5371,7 @@
|
||||
"kind": "stdlib",
|
||||
"name": "Set.prototype.entries",
|
||||
"status": "static",
|
||||
"note": "live native iterators support next(), for...of, and drains; iterator helper methods are refused"
|
||||
"note": "live native iterators support next(), for...of, drains, and the iterator helper methods"
|
||||
},
|
||||
{
|
||||
"id": "stdlib.set.forEach",
|
||||
@@ -5418,7 +5418,7 @@
|
||||
"kind": "stdlib",
|
||||
"name": "Set.prototype.keys",
|
||||
"status": "static",
|
||||
"note": "live native iterators support next(), for...of, and drains; iterator helper methods are refused"
|
||||
"note": "live native iterators support next(), for...of, drains, and the iterator helper methods"
|
||||
},
|
||||
{
|
||||
"id": "stdlib.set.symmetricDifference",
|
||||
@@ -5439,7 +5439,7 @@
|
||||
"kind": "stdlib",
|
||||
"name": "Set.prototype.values",
|
||||
"status": "static",
|
||||
"note": "live native iterators support next(), for...of, and drains; iterator helper methods are refused"
|
||||
"note": "live native iterators support next(), for...of, drains, and the iterator helper methods"
|
||||
},
|
||||
{
|
||||
"id": "stdlib.sharedArrayBuffer",
|
||||
@@ -5630,6 +5630,14 @@
|
||||
"status": "static",
|
||||
"note": "the lowered call form takes no arguments"
|
||||
},
|
||||
{
|
||||
"id": "stdlib.webassembly",
|
||||
"kind": "stdlib",
|
||||
"name": "globalThis.WebAssembly",
|
||||
"status": "unsupported",
|
||||
"code": "SC2020",
|
||||
"note": "WebAssembly support is planned as translation of each module whose bytes the build can see (an inline literal, or a file read from a path the compiler resolves) into native code; 'scriptc coverage' lists the modules this program would load (https://scriptc.dev/limitations)"
|
||||
},
|
||||
{
|
||||
"id": "syntax.async-generators",
|
||||
"kind": "syntax",
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/* Regenerates wasm-spec-cases.txt: the binary modules of the WebAssembly
|
||||
* specification test suite at the feature level the decoder validates
|
||||
* (src/wasm/binary.ts), each with the suite's verdict and V8's.
|
||||
*
|
||||
* node gen-wasm-spec-cases.mjs --wast2json <wabt>/bin/wast2json \
|
||||
* --spec <WebAssembly/spec at tag wg-2.0>/test/core \
|
||||
* --extended-const <WebAssembly/extended-const>/test/core > wasm-spec-cases.txt
|
||||
*
|
||||
* Inputs: every .wast of the 2.0 core suite except simd/ (SIMD is outside
|
||||
* the translatable set; the decoder only reports it), plus the extended
|
||||
* constant expression proposal's global, data and elem tests. Modules given
|
||||
* as text for assert_malformed are skipped (they test the text format).
|
||||
* Identical modules are kept once, at their first location.
|
||||
*
|
||||
* Line format: <suite verdict> <V8 verdict> <file>:<line> <base64>, where a
|
||||
* verdict is "valid", "invalid" or "malformed" (V8: "valid" or "invalid").
|
||||
* The suite's tests are Apache-2.0 licensed (WebAssembly Community Group). */
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { parseArgs } from "node:util";
|
||||
|
||||
const { values } = parseArgs({
|
||||
options: {
|
||||
wast2json: { type: "string" },
|
||||
spec: { type: "string" },
|
||||
"extended-const": { type: "string" },
|
||||
},
|
||||
});
|
||||
const inputs = [];
|
||||
for (const name of readdirSync(values.spec).sort()) {
|
||||
if (name.endsWith(".wast")) inputs.push({ label: name, path: join(values.spec, name) });
|
||||
}
|
||||
for (const name of ["global.wast", "data.wast", "elem.wast"]) {
|
||||
inputs.push({ label: `extended-const/${name}`, path: join(values["extended-const"], name) });
|
||||
}
|
||||
const seen = new Set();
|
||||
const lines = [
|
||||
"# WebAssembly spec test modules for src/wasm/binary.ts; generated by gen-wasm-spec-cases.mjs.",
|
||||
"# Sources: WebAssembly/spec wg-2.0 test/core (without simd/) and WebAssembly/extended-const",
|
||||
"# test/core global, data and elem, converted with wabt 1.0.37 wast2json (Apache-2.0).",
|
||||
"# <suite verdict> <V8 verdict> <file>:<line> <base64>",
|
||||
];
|
||||
for (const { label, path } of inputs) {
|
||||
const dir = mkdtempSync(join(tmpdir(), "wasm-spec-"));
|
||||
try {
|
||||
execFileSync(values.wast2json, [path, "-o", join(dir, "x.json")], { stdio: "pipe" });
|
||||
const json = JSON.parse(readFileSync(join(dir, "x.json"), "utf8"));
|
||||
for (const command of json.commands) {
|
||||
let verdict;
|
||||
if (["module", "assert_unlinkable", "assert_uninstantiable"].includes(command.type))
|
||||
verdict = "valid";
|
||||
else if (command.type === "assert_invalid") verdict = "invalid";
|
||||
else if (command.type === "assert_malformed") verdict = "malformed";
|
||||
else continue;
|
||||
if (command.module_type === "text" || !command.filename?.endsWith(".wasm")) continue;
|
||||
const bytes = readFileSync(join(dir, command.filename));
|
||||
const b64 = bytes.toString("base64");
|
||||
if (seen.has(b64)) continue;
|
||||
seen.add(b64);
|
||||
const v8 = WebAssembly.validate(bytes) ? "valid" : "invalid";
|
||||
lines.push(`${verdict} ${v8} ${label}:${command.line} ${b64}`);
|
||||
}
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
process.stdout.write(`${lines.join("\n")}\n`);
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5908-iterator-helper-objects.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5909-iterator-helper-objects-js.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5997-frame-records-in-record-unions.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6051-generic-class-factory-results.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6150-cjs-inline-require/lib.js",
|
||||
"<repo>/tests/corpus/6150-cjs-inline-require/inner.js",
|
||||
"<repo>/tests/corpus/6150-cjs-inline-require/reexport-inline.js",
|
||||
"<repo>/tests/corpus/6150-cjs-inline-require/reexport-binding.js",
|
||||
"<repo>/tests/corpus/6150-cjs-inline-require/type-error.js",
|
||||
"<repo>/tests/corpus/6150-cjs-inline-require/pick.js",
|
||||
"<repo>/tests/corpus/6150-cjs-inline-require/registry.js",
|
||||
"<repo>/tests/corpus/6150-cjs-inline-require/make-logger.js",
|
||||
"<repo>/tests/corpus/6150-cjs-inline-require/point.js",
|
||||
"<repo>/tests/corpus/6150-cjs-inline-require/shapes.js",
|
||||
"<repo>/tests/corpus/6150-cjs-inline-require/table.js",
|
||||
"<repo>/tests/corpus/6150-cjs-inline-require/verify.js",
|
||||
"<repo>/tests/corpus/6150-cjs-inline-require/fallback.js",
|
||||
"<repo>/tests/corpus/6150-cjs-inline-require/main.js",
|
||||
"<repo>/tests/corpus/6150-cjs-inline-require/late.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6151-cjs-export-chains/chain.js",
|
||||
"<repo>/tests/corpus/6151-cjs-export-chains/constants.js",
|
||||
"<repo>/tests/corpus/6151-cjs-export-chains/re.js",
|
||||
"<repo>/tests/corpus/6151-cjs-export-chains/sem.js",
|
||||
"<repo>/tests/corpus/6151-cjs-export-chains/main.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6152-cjs-tsc-interop/plain.js",
|
||||
"<repo>/tests/corpus/6152-cjs-tsc-interop/stamped.js",
|
||||
"<repo>/tests/corpus/6152-cjs-tsc-interop/fn.js",
|
||||
"<repo>/tests/corpus/6152-cjs-tsc-interop/main.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6153-cjs-exports-object/lib.js",
|
||||
"<repo>/tests/corpus/6153-cjs-exports-object/main.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6154-cjs-require-nested-destructure/errors.js",
|
||||
"<repo>/tests/corpus/6154-cjs-require-nested-destructure/constants.js",
|
||||
"<repo>/tests/corpus/6154-cjs-require-nested-destructure/main.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6155-cjs-export-table-methods/more.js",
|
||||
"<repo>/tests/corpus/6155-cjs-export-table-methods/lib.js",
|
||||
"<repo>/tests/corpus/6155-cjs-export-table-methods/main.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6156-cjs-function-members-from-ts/status.cjs",
|
||||
"<repo>/tests/corpus/6156-cjs-function-members-from-ts/consumer.cjs",
|
||||
"<repo>/tests/corpus/6156-cjs-function-members-from-ts/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6160-number-radix-typed.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6161-dispose-observer-gates.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6162-dispose-observer-weak-keys.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6163-dispose-observer-workers.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6164-regex-replace-typed-callback.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6165-regex-replace-callback-throws.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6170-flight-dev-stacks-dead.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6171-error-stack-capture-sites.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6172-unreachable-statements.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6180-interface-overload-events.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6181-interface-overload-arity.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6182-interface-overload-types.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6183-interface-overload-flow.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6230-optional-chain-unchecked-receivers.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/error-js-accessor-name-message.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/webassembly-feature-probe.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/webassembly-type-mentions.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/overloaded-members.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/webassembly-fences.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user