@loopdive/js2 0.57.0 → 0.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -57,7 +57,7 @@ STATUS.md rather than duplicating numbers that go stale. Standalone
57
57
  README until the current standalone regression is fixed.
58
58
 
59
59
  <!-- AUTO:conformance-start -->
60
- **test262 conformance**: 32,456 / 43,135 (75.2 %)
60
+ **test262 conformance**: 32,704 / 43,135 (75.8 %)
61
61
  <!-- AUTO:conformance-end -->
62
62
 
63
63
  ## Current Status
package/dist/cli.js CHANGED
@@ -13,7 +13,7 @@ const args = process.argv.slice(2);
13
13
  if (args.includes("--ts7")) {
14
14
  process.env.JS2WASM_TS7 = "1";
15
15
  }
16
- const { compile } = await import("./index.js");
16
+ const { compile, compileProject, entryHasRelativeImports } = await import("./index.js");
17
17
  const { buildDefaultDefines } = await import("./define-substitution-BcUeKC2A.js").then((n) => n.d);
18
18
  if (args.includes("--version") || args.includes("-v")) {
19
19
  console.log(getCliVersion());
@@ -66,13 +66,17 @@ Options:
66
66
  is ON by default; this restores the pre-#1950 behaviour.
67
67
  (No-op when binaryen/wasm-opt is unavailable — that path
68
68
  already degrades to a one-line note, never a failure.)
69
- --link-node-shims (WASI, #2625/#2633) Emit the per-module linkable node:<mod>
70
- shims instead of inlining the host APIs. Std-IO goes through
71
- node:fs: the module imports readSync/writeSync + its memory
72
- from node:fs (no wasi_snapshot_preview1 for stream IO) and
73
- links node-fs.wasm. console.log / process.std*.write lower to
74
- writeSync(1|2, …); stdin is readSync(0, …). Off by default —
75
- the inline fd_read/fd_write path is self-contained.
69
+ --link <ns> (WASI, #2783) Leave the external namespace <ns> as link-time
70
+ imports (repeatable) instead of inline-lowering it. Satisfied
71
+ at instantiation by a preloaded provider module (e.g.
72
+ 'wasmtime --preload <ns>=provider.wasm'). Any namespace works
73
+ (leave-as-import is universal); '--link node:fs' additionally
74
+ selects the import-and-link std-IO path: the module imports
75
+ readSync/writeSync + its memory from node:fs (no
76
+ wasi_snapshot_preview1 for stream IO) and links node-fs.wasm.
77
+ console.log / process.std*.write lower to writeSync(1|2, ...),
78
+ stdin is readSync(0, ...). Off by default — every namespace is
79
+ inline-lowered into a self-contained module.
76
80
  --emulate <env> Emulate a host runtime's globals so they type-check without
77
81
  @types/node. 'node' = ambient process/etc.; 'none' = off.
78
82
  Auto-enabled (type-level only) when the source imports a
@@ -127,7 +131,7 @@ let allowFs = false;
127
131
  let quiet = false;
128
132
  let utf8Storage = false;
129
133
  let strictNoHostImports;
130
- let linkNodeShims = false;
134
+ const linkedNamespaces = /* @__PURE__ */ new Set();
131
135
  let emulateNode = false;
132
136
  let emulateExplicit = false;
133
137
  let platform;
@@ -186,8 +190,13 @@ for (let i = 0; i < args.length; i++) {
186
190
  quiet = true;
187
191
  } else if (arg === "--utf8-storage") {
188
192
  utf8Storage = true;
189
- } else if (arg === "--link-node-shims") {
190
- linkNodeShims = true;
193
+ } else if (arg === "--link" || arg.startsWith("--link=")) {
194
+ const ns = arg.startsWith("--link=") ? arg.slice("--link=".length) : args[++i];
195
+ if (!ns) {
196
+ console.error("--link requires a namespace argument (e.g. --link node:fs)");
197
+ process.exit(1);
198
+ }
199
+ linkedNamespaces.add(ns);
191
200
  } else if (arg === "--emulate" || arg.startsWith("--emulate=")) {
192
201
  const env = arg.startsWith("--emulate=") ? arg.slice("--emulate=".length) : args[++i];
193
202
  if (env === "node") {
@@ -276,20 +285,21 @@ if (!emulateExplicit && !emulateNode && /['"]node:[A-Za-z0-9_./-]+['"]/.test(sou
276
285
  }
277
286
  const name = basename(absInput, ".ts");
278
287
  const dir = outDir ? resolve(outDir) : dirname(absInput);
279
- const result = await compile(source, {
288
+ const compileOptions = {
280
289
  ...optimize ? { optimize } : {},
281
290
  ...target ? { target } : {},
282
291
  ...allocator ? { allocator } : {},
283
292
  ...emitWit ? { wit: witPackageName ? { packageName: witPackageName } : true } : {},
284
293
  ...allowFs ? { allowFs: true } : {},
285
294
  ...utf8Storage ? { utf8Storage: true } : {},
286
- ...linkNodeShims ? { linkNodeShims: true } : {},
295
+ ...linkedNamespaces.size ? { link: [...linkedNamespaces] } : {},
287
296
  ...emulateNode ? { emulateNode: true } : {},
288
297
  ...platform ? { platform } : {},
289
298
  fileName: absInput,
290
299
  ...strictNoHostImports !== void 0 ? { strictNoHostImports } : {},
291
300
  ...Object.keys(defines).length > 0 ? { define: defines } : {}
292
- });
301
+ };
302
+ const result = entryHasRelativeImports(source) ? await compileProject(absInput, compileOptions) : await compile(source, compileOptions);
293
303
  if (!result.success) {
294
304
  for (const e of result.errors) {
295
305
  const severity = e.severity === "warning" ? "warning" : "error";
@@ -1,21 +1,35 @@
1
1
  import { ts } from '../ts-api.js';
2
2
  /**
3
- * Return true if `expr` provably produces a 32-bit signed integer at runtime,
4
- * given that `i32Locals` is the set of locals already known to hold i32.
3
+ * Return true if `expr` provably produces a **canonical** signed-32-bit integer
4
+ * at runtime — a value `v [-2^31, 2^31)` whose f64 image is bit-identical
5
+ * to `v` (so `v` is not `-0`, not fractional, not NaN/±Inf, not `|v| ≥ 2^31`).
6
+ * `i32Locals` is the set of locals already known to hold i32.
5
7
  *
6
- * Recognised i32-safe forms (mirrors `isI32SafeExpr` in function-body.ts but
7
- * is intentionally narrower we err on the side of disqualification):
8
- * - integer numeric literal in [-2^31, 2^31)
9
- * - identifier referencing a known-i32 local
10
- * - bitwise `|`, `&`, `^`, `<<`, `>>` (always produce int32 per ECMAScript)
11
- * - comparison ops (return boolean = i32)
12
- * - unary `+` / `-` / `~` of an i32-safe operand
13
- * - `+` / `-` / `*` of two i32-safe operands (overflow wraps; receiver is i32)
14
- * - parenthesised / `as`-cast / non-null-asserted i32-safe expr
8
+ * The "canonical" property is the soundness contract for #2789: when EVERY write
9
+ * to a packed array yields a canonical i32, the i32-stored value read back as f64
10
+ * equals what the f64 backing would have stored, so NO read can observe a
11
+ * distinction i32 erases. That discharges the read-side proof obligation in the
12
+ * hybrid fast-path audit (Row 3) for free there is no distinction to observe.
15
13
  *
16
- * Note: `>>>` is intentionally excluded it produces uint32 which can sit
17
- * above 2^31 and would be reinterpreted as a negative i32 on store. The
18
- * conservative choice is to disqualify (the array would then stay f64).
14
+ * Recognised canonical-i32 forms (mirrors `isI32SafeExpr` in function-body.ts;
15
+ * intentionally narrow we err on the side of disqualification):
16
+ * - integer numeric literal in [-2^31, 2^31) (always +0, never -0)
17
+ * - identifier referencing a known-i32 local (physically i32 ⇒ canonical)
18
+ * - bitwise `|`, `&`, `^`, `<<`, `>>` (ECMAScript defines these to yield a
19
+ * value that equals ToInt32 of itself ⇒ canonical regardless of operands)
20
+ * - comparison ops (return boolean = i32 0/1)
21
+ * - `~x` (canonical int32) and unary `+x` of an i32-safe operand
22
+ * - unary `-<non-zero integer literal>` only (a `-1`-style sentinel) — NOT
23
+ * `-x` / `-(expr)`, which can be `-0` (#2789)
24
+ * - parenthesised / `as`-cast / non-null-asserted canonical-i32 expr
25
+ *
26
+ * Deliberately EXCLUDED (would break canonicality → MISCOMPILE if packed):
27
+ * - `+` / `-` / `*` arithmetic — f64 result stored via `i32.trunc_sat_f64_s`,
28
+ * which SATURATES on overflow rather than yielding the spec-correct f64
29
+ * (#2789, mirrors the #1236 scalar-local fix). Wrap-canonicalising patterns
30
+ * like `(a*b) | 0` still qualify via the top-level bitwise op.
31
+ * - `>>>` — produces uint32 which can sit above 2^31, reinterpreted as a
32
+ * negative i32 on store.
19
33
  */
20
34
  export declare function isI32SafeExprForArray(expr: ts.Expression | undefined, i32Locals: ReadonlySet<string>, depth?: number): boolean;
21
35
  /**
@@ -55,22 +55,29 @@ export interface CodegenOptions {
55
55
  /** WASI target: emit WASI imports (fd_write, proc_exit) instead of JS host imports */
56
56
  wasi?: boolean;
57
57
  /**
58
- * #2524 / #2633 route std-IO through a separately compiled, linkable
59
- * `node:fs` shim instead of inlining the `wasi_snapshot_preview1.fd_read`/
60
- * `fd_write` glue. When set (WASI only), the user module imports
61
- * `readSync`/`writeSync` plus its linear memory from `node:fs` and carries NO
58
+ * #2783the dynamic-linking axis: namespaces to leave as link-time imports
59
+ * (satisfied by a preloaded provider) instead of inline-lowering. `["node:fs"]`
60
+ * routes std-IO through the `node:fs` shim (the user module imports
61
+ * `readSync`/`writeSync` + its linear memory from `node:fs` and carries NO
62
62
  * `wasi_snapshot_preview1` import for the stream IO path; console.log /
63
- * process.std*.write lower to `writeSync(1|2, …)`. `node-fs.wasm` implements
64
- * the interface over WASI. The bespoke `js2wasm:node-process` shim was retired
65
- * (#2633). Default off — the inline fd_read/fd_write path stays as fallback.
63
+ * process.std*.write lower to `writeSync(1|2, …)`; `node-fs.wasm` implements
64
+ * the interface over WASI). WASI-gated in `create-context.ts` (ignored for
65
+ * non-WASI targets). Default empty — the inline fd_read/fd_write path stays.
66
66
  */
67
- linkNodeShims?: boolean;
67
+ link?: string[];
68
68
  /** Standalone target (#1470): pure WasmGC, no JS host imports and no WASI
69
69
  * runtime. Implies `nativeStrings: true` and refuses to emit any
70
70
  * `wasm:js-string` namespace or `env::__concat_*` / `__extern_toString` /
71
71
  * `__unbox_string` JS-host string imports. Used so the compiled module is
72
72
  * runnable under pure-Wasm engines (wasmtime, wasmer) without a JS host. */
73
73
  standalone?: boolean;
74
+ /** (#2796) Diff-test-harness fidelity: in JS-host mode, export the top-level
75
+ * `__module_init` and do NOT run it via the wasm `start` section, so the host
76
+ * invokes it AFTER `setExports` (symmetric with the standalone `_start`
77
+ * model). Default false → top-level runs in the start section. See
78
+ * `CompileOptions.deferTopLevelInit`. WASI is unaffected (it already exports
79
+ * `_start`). */
80
+ deferTopLevelInit?: boolean;
74
81
  /**
75
82
  * Experimental: route a narrow set of functions through the middle-end IR
76
83
  * (see `src/ir/`). Defaults to **on** since #1131 (the front-end driver
@@ -842,6 +849,24 @@ export interface CodegenContext {
842
849
  */
843
850
  holeTypeIdx: number;
844
851
  holeGlobalIdx: number | undefined;
852
+ /**
853
+ * (#2800) The mutable i32 `__in_module_init` flag — 1 for the duration of
854
+ * `__module_init` (the Wasm `start` section in gc/host mode, which runs INSIDE
855
+ * `WebAssembly.instantiate`, BEFORE the host wires struct getters via
856
+ * `__setExports`), 0 otherwise. The delete-aware `any`-receiver READ
857
+ * (`tryEmitDeleteAwareDynamicGet`) branches on it: while init runs (host
858
+ * `__extern_get` can't reach `__sget_<field>` → returns undefined for every
859
+ * struct field), read the slot HOST-FREE via the `__get_member_<name>`
860
+ * dispatcher; at runtime use the tombstone-aware host `__extern_get`.
861
+ *
862
+ * `inModuleInitFlagReads` collects the `global.get` flag-read Instr objects
863
+ * emitted at read sites (with a placeholder index); `finalizeInModuleInitFlag`
864
+ * allocates the i32 global AFTER every import settles and patches their
865
+ * `.index` + records the final slot in `inModuleInitGlobalIdx`. Undefined/empty
866
+ * for delete-free / standalone / WASI modules (byte-identical).
867
+ */
868
+ inModuleInitFlagReads: Instr[] | undefined;
869
+ inModuleInitGlobalIdx: number | undefined;
845
870
  /**
846
871
  * (#2580 M0) Value-rep dynamic-read substrate. Set true by a call site that
847
872
  * needs the runtime property-presence read primitives (`__dyn_has` /
@@ -1675,6 +1700,11 @@ export interface CodegenContext {
1675
1700
  * `__unbox_string`, `__str_from_mem`, `__str_to_mem`,
1676
1701
  * `__str_extern_len`). Implies `nativeStrings === true`. */
1677
1702
  standalone: boolean;
1703
+ /** (#2796) Diff-test-harness fidelity: in JS-host mode, export the top-level
1704
+ * `__module_init` and do NOT wire the wasm `start` section to it, so the host
1705
+ * runs it after `setExports` (symmetric with the standalone `_start` model).
1706
+ * Default false. WASI is unaffected. */
1707
+ deferTopLevelInit: boolean;
1678
1708
  /** (#2179) True when the module body contains any `delete` of a property or
1679
1709
  * element access (e.g. `delete o.a` / `delete o[k]`). Pre-scanned once at
1680
1710
  * module setup. When true, `any`/`unknown`-typed property READS in JS-host
@@ -1745,10 +1775,27 @@ export interface CodegenContext {
1745
1775
  * `node:fs` `readSync`/`writeSync` calls (over a shim-owned, imported linear
1746
1776
  * memory) instead of inline `fd_read`/`fd_write`. console.log/warn/error and
1747
1777
  * process.std*.write lower to `writeSync(1|2, …)`; the bespoke
1748
- * `js2wasm:node-process` shim was retired (#2633). See `linkNodeShims` in
1749
- * `CodegenOptions`.
1778
+ * `js2wasm:node-process` shim was retired (#2633). Driven by the `link` set
1779
+ * (`["node:fs"]`).
1780
+ *
1781
+ * #2783 — an INTERNAL convenience boolean, **derived** from
1782
+ * `linkedNamespaces.has("node:fs")` (there is no user-facing `linkNodeShims`
1783
+ * option anymore — `link: string[]` is the only input). The two
1784
+ * are computed together in `create-context.ts` from the same (WASI-gated)
1785
+ * `link` set so they can never drift. Keeping this boolean lets the ~30
1786
+ * existing `ctx.linkNodeShims` read sites stay zero-churn while the underlying
1787
+ * state generalizes to an arbitrary set of linked namespaces.
1750
1788
  */
1751
1789
  linkNodeShims: boolean;
1790
+ /**
1791
+ * #2783 — the set of external namespaces left as **link-time imports** for
1792
+ * this compile (WASI-gated; empty for non-WASI targets). `node:fs` membership
1793
+ * additionally drives the import-and-link std-IO codegen path (see
1794
+ * `linkNodeShims`, derived from this set). For an arbitrary namespace,
1795
+ * membership only permits its imports past the strict `--no-host-imports` /
1796
+ * WASI leaked-host-import gate (`assertNoLeakedHostImports`).
1797
+ */
1798
+ linkedNamespaces: ReadonlySet<string>;
1752
1799
  /** #2631/#2633: func index of the imported `node:fs::readSync` (fd,ptr,len)->i32 (-1 = not registered). */
1753
1800
  nodeFsReadSyncIdx: number;
1754
1801
  /** #2631/#2633: func index of the imported `node:fs::writeSync` (fd,ptr,len)->i32 (-1 = not registered). */
@@ -1819,6 +1866,17 @@ export interface CodegenContext {
1819
1866
  * Wasm byte-identical.
1820
1867
  */
1821
1868
  fnctorEscapeGate?: import('../fnctor-escape-gate.js').FnctorEscapeGateResult;
1869
+ /**
1870
+ * #2773 S1 (keystone) — fnctor name → reserved `$__fnctor_<Name>` struct type
1871
+ * index. Populated up-front by `reserveFnctorStructTypes` (index.ts) at the
1872
+ * deterministic type-init phase so the index is IDENTICAL across the hoist pass
1873
+ * and the emit pass (the on-demand registration at the `new F()` site landed at
1874
+ * a pass-dependent index → `ref.test`/`struct.get` desync). When a name is
1875
+ * present here, `compileNewFunctionDeclaration` FILLS the reserved slot in place
1876
+ * instead of pushing a new type (which would re-shift every downstream typeIdx).
1877
+ * Empty for fnctor-free modules ⇒ byte-identical no-op.
1878
+ */
1879
+ fnctorReservedTypeIdx: Map<string, number>;
1822
1880
  /**
1823
1881
  * #1886 Slice B — Func index of the lazily-emitted
1824
1882
  * `__lin_u8_alloc(len:i32)->i32` bump allocator for linear-backed Uint8Array
@@ -1,4 +1,5 @@
1
1
  import { ts } from '../ts-api.js';
2
+ import { FieldDef } from '../ir/types.js';
2
3
  import { CodegenContext, FunctionContext } from './context/types.js';
3
4
  /** Classification of a `new F()` fnctor allocation site. */
4
5
  export type FnctorGateClass =
@@ -52,6 +53,28 @@ export interface FnctorEscapeGateResult {
52
53
  * `resolveReceiverStruct`; no lowering reads it, so emitted Wasm is byte-identical.
53
54
  */
54
55
  readonly receiverStruct: ReadonlyMap<ts.Expression, string>;
56
+ /**
57
+ * #2773 S2b — fnctor NAMES that own a `new this()` reconstruct site (a
58
+ * `new this()` inside a static / prototype method classified as an `F`
59
+ * reconstruct). The #2773 S1 up-front struct-type reservation
60
+ * (`reserveFnctorStructTypes`) unions this with {@link approvedNames} so a
61
+ * `Parser` reconstructed via `new this()` also gets a reserved
62
+ * `$__fnctor_Parser` slot. **S1 ships this as an EMPTY set** — S1 only READS it
63
+ * (so the reservation union is a no-op today); S2b populates it. Landing the
64
+ * field shape here keeps S2b purely additive.
65
+ */
66
+ readonly newThisOwnerNames: ReadonlySet<string>;
67
+ /**
68
+ * #2773 S1 — fnctor NAME → its function-like declaration (the body-bearer whose
69
+ * `this.<field> = …` writes derive the `$__fnctor_<Name>` struct shape). Covers
70
+ * EVERY fnctor `new F()` site seen (not just approved ones) so
71
+ * `reserveFnctorStructTypes` can resolve a name to the SAME declaration the
72
+ * on-demand `compileNewFunctionDeclaration` path uses — guaranteeing identical
73
+ * field derivation. A name with ≥2 distinct declarations keeps the first
74
+ * (deterministic by source order); ambiguity here only affects WHICH body shapes
75
+ * the reserved slot (matching the on-demand resolution at the dominant site).
76
+ */
77
+ readonly ctorDeclByName: ReadonlyMap<string, ts.FunctionDeclaration | ts.FunctionExpression>;
55
78
  }
56
79
  /**
57
80
  * Whether `expr` resolves to a plain function constructor (fnctor) rather than a
@@ -62,6 +85,43 @@ export interface FnctorEscapeGateResult {
62
85
  * `undefined`.
63
86
  */
64
87
  export declare function resolveFnctorSymbol(checker: ts.TypeChecker, calleeExpr: ts.Expression): ts.Symbol | undefined;
88
+ /**
89
+ * #2681/#2686 — resolve the fnctor `F` that OWNS the enclosing method a node sits
90
+ * in, for a `new this(…)` site or a lifted method body. `this` inside a method
91
+ * `F.method = function(){…}` / `F.prototype.m = function(){…}` / aliased `var pp =
92
+ * F.prototype; pp.m = function(){…}` binds to `F` (static) or an `F` instance
93
+ * (prototype). Walks up to the nearest non-arrow function (arrows do not rebind
94
+ * `this`) and resolves its defining assignment's holder to a fnctor symbol.
95
+ *
96
+ * Returns `{ name, sym, viaPrototype }` where `viaPrototype` is true for a
97
+ * prototype/aliased method (`this` is an INSTANCE — the read-dispatch case) and
98
+ * false for a direct static method (`this` is the CONSTRUCTOR — the `new this()`
99
+ * reconstruct case). `undefined` when the enclosing function is not a fnctor
100
+ * method, or the holder does not resolve to a user fnctor.
101
+ */
102
+ export declare function resolveEnclosingFnctorOwner(checker: ts.TypeChecker, node: ts.Node): {
103
+ name: string;
104
+ sym: ts.Symbol;
105
+ viaPrototype: boolean;
106
+ } | undefined;
107
+ /**
108
+ * #2681/#2686 A3 — the `__fnctor_<F>` struct name a lifted PROTOTYPE method's
109
+ * `this` receiver resolves to, when `F` is approved for reconstruction. Sets
110
+ * `FunctionContext.thisStructName` (closures.ts) so the dynamic `this.<field>`
111
+ * read dispatch (property-access.ts) routes through the finalize-filled
112
+ * `__get_member_<name>` dispatcher.
113
+ *
114
+ * Deliberately NOT gated on `ctx.structMap.has(__fnctor_<F>)`: the reader method
115
+ * frequently compiles BEFORE the `new this()` site that registers the struct
116
+ * (acorn defines `pp.parseExprAtom` long before the static `Parser.parse`). The
117
+ * dispatcher is reserved at the read site and FILLED at finalize over the
118
+ * COMPLETE type table, so a struct registered later is still enumerated — pinning
119
+ * on `approvedNames` (frozen pre-codegen at index.ts) is order-independent and
120
+ * correct, while a `structMap.has` gate would race the compile order and miss.
121
+ * Excludes static methods (`viaPrototype === false`) — their `this` is the
122
+ * constructor function-value, not an instance.
123
+ */
124
+ export declare function resolveLiftedMethodThisStruct(ctx: CodegenContext, fn: ts.FunctionExpression | ts.ArrowFunction): string | undefined;
65
125
  /**
66
126
  * #2660 PART-1 — resolve the WasmGC struct a member-access RECEIVER expression
67
127
  * concretely is, for the dynamic read/write/compound dispatch to PIN to.
@@ -90,3 +150,25 @@ export declare function resolveReceiverStruct(ctx: CodegenContext, fctx: Functio
90
150
  * sites exist (so the pass is a no-op for class-only / fnctor-free code).
91
151
  */
92
152
  export declare function analyzeFnctorEscapeGate(checker: ts.TypeChecker, sourceFile: ts.SourceFile): FnctorEscapeGateResult;
153
+ /**
154
+ * #2773 S1 (keystone) — derive the WasmGC field shape of a fnctor's
155
+ * `$__fnctor_<Name>` struct from its constructor body's `this.<field> = …`
156
+ * assignments. This is the **single source of truth** for the field set,
157
+ * EXTRACTED verbatim from the on-demand inline logic that lived in
158
+ * `compileNewFunctionDeclaration` (new-super.ts) so both the up-front reservation
159
+ * pass and the legacy on-demand fallback produce the SAME shape — divergent field
160
+ * order would give `struct.new` a different arity than the reserved type and trap.
161
+ *
162
+ * Mirrors the original logic exactly:
163
+ * - collects EVERY `this.<field>` LHS across (possibly CHAINED) assignments
164
+ * (`this.a = this.b = expr`), recursing into if/else and loop blocks;
165
+ * - prefers the RHS type when the LHS is `any` (externref) — the RHS carries the
166
+ * concrete type (e.g. number → f64);
167
+ * - widens non-null `ref` fields to `ref_null` so `struct.new`'s `ref.null`
168
+ * default-init is well-typed (a struct.new can't default a non-null ref).
169
+ *
170
+ * @param ctx codegen context (for the checker + `resolveWasmType`)
171
+ * @param funcDecl the fnctor's function-like declaration (its body is read)
172
+ * @returns the ordered field set, or `[]` for a body-less / empty-body fnctor.
173
+ */
174
+ export declare function deriveFnctorFields(ctx: CodegenContext, funcDecl: ts.FunctionDeclaration | ts.FunctionExpression): FieldDef[];
@@ -80,7 +80,7 @@ export declare function lookupAllowlistEntry(name: string): HostImportAllowlistE
80
80
  * `nativeStrings` mode which strict mode auto-enables; they should not
81
81
  * appear when strict mode is on.)
82
82
  */
83
- export declare function isHostImportAllowed(module: string, name: string): {
83
+ export declare function isHostImportAllowed(module: string, name: string, linkedNamespaces?: ReadonlySet<string>): {
84
84
  allowed: true;
85
85
  } | {
86
86
  allowed: false;
@@ -129,7 +129,7 @@ export interface LeakedHostImport {
129
129
  export declare function scanForLeakedHostImports(imports: ReadonlyArray<{
130
130
  module: string;
131
131
  name: string;
132
- }>): LeakedHostImport[];
132
+ }>, linkedNamespaces?: ReadonlySet<string>): LeakedHostImport[];
133
133
  /**
134
134
  * (#2094) Build the structured compile error for a host import that leaked
135
135
  * into a finished standalone/strict binary. Distinct from
@@ -89,6 +89,23 @@ export declare function generateModule(ast: TypedAST, options?: CodegenOptions):
89
89
  message: string;
90
90
  }[];
91
91
  };
92
+ /**
93
+ * Emit __vec_get(externref, i32) -> externref and __vec_len(externref) -> i32
94
+ * exports so the runtime can iterate WasmGC vec structs that were coerced to
95
+ * externref (e.g. arrays stored in `any`-typed variables).
96
+ *
97
+ * For each registered vec type, emits ref.test/ref.cast dispatch to extract
98
+ * the length or the indexed element, boxing the result to externref.
99
+ */
100
+ /**
101
+ * (#2784 S3) Reserve a `__vec_push` / `__vec_pop` helper funcIdx UP FRONT so the
102
+ * native-vec method dispatch (calls.ts) can bake the call at compile time — the
103
+ * helper bodies are only built in the finalize `emitVecAccessExports` pass, which
104
+ * runs AFTER the method-call site compiles. Pushes a valid placeholder body +
105
+ * export + funcMap entry (shift-tracked); the finalize pass FILLS the body in
106
+ * place (fill-or-build in `_emitVecAccessExportsInner`). Idempotent.
107
+ */
108
+ export declare function reserveVecMethodHelper(ctx: CodegenContext, kind: "push" | "pop" | "get"): number;
92
109
  /**
93
110
  * Compile multiple typed source files into a single WasmModule IR.
94
111
  * All source files share the same codegen context (funcMap, structMap, etc.).
@@ -183,6 +200,41 @@ export declare function reserveTypedArraySubviewTypes(ctx: CodegenContext): void
183
200
  * fallback can't fire without a class).
184
201
  */
185
202
  export declare function reserveObjVecArrType(ctx: CodegenContext): void;
203
+ /**
204
+ * #2773 S1 (KEYSTONE) — reserve every reconstructed-fnctor `$__fnctor_<Name>`
205
+ * struct type at the deterministic up-front type-init phase (the same stable
206
+ * point as `reserveTypedArraySubviewTypes` / `reserveObjVecArrType`), so the type
207
+ * index is IDENTICAL across the hoist pass and the emit pass.
208
+ *
209
+ * ROOT CAUSE this fixes: the on-demand registration at the `new F()` call site
210
+ * (`compileNewFunctionDeclaration`, new-super.ts — `ctx.mod.types.length`) assigns
211
+ * the index at a NON-deterministic mid-compile point that depends on which
212
+ * function the compiler reached first. The two-pass type numbering then desyncs:
213
+ * a typed-receiver `ref.test $__fnctor_<Name>` baked in the hoist pass misses the
214
+ * emit-pass `struct.new` index, and a read site compiled before the `new` site is
215
+ * excluded from `findAlternateStructsForField`'s candidate set. Reserving up-front
216
+ * collapses BOTH facets — the index is pass-invariant AND the candidate set is
217
+ * complete at every read site. This is the one thing the #2674 finalize
218
+ * dispatcher cannot retroactively fix (it can't rewrite a baked typeIdx).
219
+ *
220
+ * Two sub-passes — the ORDER is load-bearing:
221
+ * (1) reserve ALL indices + names FIRST (placeholder struct, `structMap`,
222
+ * `typeIdxToStructName`, `fnctorReservedTypeIdx`), so a cross-fnctor ref
223
+ * field in sub-pass 2 (a `Parser` field typed `Scope` →
224
+ * `(ref null $__fnctor_Scope)`) resolves against an already-registered
225
+ * `structMap` entry. Do NOT collapse the two sub-passes.
226
+ * (2) FILL each placeholder's fields via the shared `deriveFnctorFields`
227
+ * (single source of truth — identical to the on-demand derivation) and
228
+ * record `structFields` for candidate-set completeness.
229
+ *
230
+ * Determinism: the name set is SORTED, and the call-site position is fixed, so the
231
+ * reserved index is identical across the hoist pass and the emit pass (the entire
232
+ * point of the slice). Gated on a non-empty approved set ⇒ fnctor-free modules are
233
+ * byte-identical (a true no-op). Runs in BOTH host and standalone — the on-demand
234
+ * struct path is target-independent. A reserved-but-never-constructed placeholder
235
+ * is unreferenced ⇒ `dead-elimination` prunes + renumbers it cleanly.
236
+ */
237
+ export declare function reserveFnctorStructTypes(ctx: CodegenContext): void;
186
238
  export declare function ensureLinearU8AllocHelper(ctx: CodegenContext): number;
187
239
  /**
188
240
  * #1618: Ensure __wasi_write_any_string(s: ref NativeString) -> void exists and
@@ -210,7 +262,7 @@ export declare function ensureWasiWriteAnyStringHelper(ctx: CodegenContext, useS
210
262
  * it to the *runtime* fd. This backs the STRING overload of `node:fs`
211
263
  * `writeSync(fd, str, position?, encoding?)`, where the fd is an arbitrary
212
264
  * integer (not just stdout/stderr). Two modes (#2655):
213
- * - shim (`--link-node-shims`): `writeSync(fd, ptr, len)` returns the byte count.
265
+ * - shim (`--link node:fs`): `writeSync(fd, ptr, len)` returns the byte count.
214
266
  * - direct (standalone `--target wasi`): build a `{ base=ptr, len }` iovec at
215
267
  * memory[0..7], call `fd_write(fd, iovs=0, 1, nwritten=8)`, load nwritten.
216
268
  */
@@ -42,6 +42,33 @@ export declare function compileObjectLiteralAsExternref(ctx: CodegenContext, fct
42
42
  * value semantics suffice; plain data structs keep `extern.convert_any`.
43
43
  */
44
44
  export declare function materializeStructAsDynamicObject(ctx: CodegenContext, fctx: FunctionContext, structTypeIdx: number): boolean;
45
+ /**
46
+ * (#2714 / #2804) True when a spread-containing object literal must be built via
47
+ * the host plain-object (`$Object`/externref) path rather than a closed struct,
48
+ * because its evaluation context does not pin a CONCRETE object SHAPE the struct
49
+ * path could faithfully build/enumerate: `any` / `unknown` / `object`, NO
50
+ * contextual type at all, or a shapeless object type with zero own properties
51
+ * (e.g. the `object` param of `Object.keys`).
52
+ *
53
+ * This is the single source of truth for the routing decision below AND for the
54
+ * variable-declaration local typing (statements/variables.ts, index.ts var
55
+ * hoist). Keeping them in lockstep is what fixes #2804: `const b = { ...a, z: 3 }`
56
+ * (no annotation) has NO contextual type, so the literal takes the host path —
57
+ * but the receiving variable's INFERRED type is a concrete struct `{x;y;z}`, so
58
+ * without this shared predicate the local was typed as that struct while the
59
+ * initializer produced a host `$Object`, and the externref→struct coercion
60
+ * (ref.test/ref.cast) failed at runtime → `b.x` read NaN / null. The variable
61
+ * sites consult this predicate and force an externref local so the local
62
+ * representation matches the host-object value (and `b.x` routes through
63
+ * `__extern_get`, preserving the spread's insertion-order keys + values, which
64
+ * the struct path cannot — its field order follows TS's own-prop-first inferred
65
+ * type, not the runtime CopyDataProperties insertion order).
66
+ *
67
+ * A CONCRETE annotated target (`const x: { a: number } = { ...o }`, ≥1 property)
68
+ * has a specific contextual type → returns false → keeps the struct path so
69
+ * typed consumers still receive a struct (#2714 control).
70
+ */
71
+ export declare function objectLiteralSpreadTakesHostPath(ctx: CodegenContext, expr: ts.ObjectLiteralExpression): boolean;
45
72
  export declare function compileObjectLiteral(ctx: CodegenContext, fctx: FunctionContext, expr: ts.ObjectLiteralExpression): ValType | null;
46
73
  /**
47
74
  * Try to evaluate an expression to a constant numeric or string value at compile time.
@@ -1,4 +1,4 @@
1
- import { CodegenContext } from './context/types.js';
1
+ import { CodegenContext, FunctionContext } from './context/types.js';
2
2
  /**
3
3
  * Reserve (or fetch) the member-set dispatcher `__set_member_<name>(recv, val)`
4
4
  * funcIdx with a placeholder body. The real body is built by
@@ -14,7 +14,7 @@ import { CodegenContext } from './context/types.js';
14
14
  * - `__box_number`/`__unbox_number` (union imports — the per-struct arms may
15
15
  * unbox the externref value into an f64/i32 field via `coercionInstrs`).
16
16
  */
17
- export declare function reserveMemberSetDispatch(ctx: CodegenContext, propName: string, strict: boolean): number | undefined;
17
+ export declare function reserveMemberSetDispatch(ctx: CodegenContext, propName: string, strict: boolean, fctx?: FunctionContext): number | undefined;
18
18
  /**
19
19
  * Fill every reserved `__set_member_<name>` dispatcher body at FINALIZE, after
20
20
  * every struct type (incl. late-registered fnctor structs) is known. READ-ONLY
@@ -289,6 +289,6 @@ export declare function emitThisReceiverGuardConvert(ctx: CodegenContext, fctx:
289
289
  * `ref.is_null`, and emit the index + read only in the non-null arm.
290
290
  */
291
291
  export declare function compileOptionalElementAccess(ctx: CodegenContext, fctx: FunctionContext, expr: ts.ElementAccessExpression): ValType | null;
292
- export declare function compileElementAccess(ctx: CodegenContext, fctx: FunctionContext, expr: ts.ElementAccessExpression): ValType | null;
292
+ export declare function compileElementAccess(ctx: CodegenContext, fctx: FunctionContext, expr: ts.ElementAccessExpression, expectedType?: ValType): ValType | null;
293
293
  /** Inner element access logic — assumes objType is on the stack and non-null */
294
- export declare function compileElementAccessBody(ctx: CodegenContext, fctx: FunctionContext, expr: ts.ElementAccessExpression, objType: ValType): ValType | null;
294
+ export declare function compileElementAccessBody(ctx: CodegenContext, fctx: FunctionContext, expr: ts.ElementAccessExpression, objType: ValType, expectedType?: ValType): ValType | null;
@@ -1,4 +1,4 @@
1
- import { Import } from '../../ir/types.js';
1
+ import { Import, Instr } from '../../ir/types.js';
2
2
  import { CodegenContext } from '../context/types.js';
3
3
  /**
4
4
  * Register an import (`module.name`) on the current module.
@@ -37,6 +37,24 @@ export declare function addImport(ctx: CodegenContext, module: string, name: str
37
37
  export declare function addStringConstantGlobal(ctx: CodegenContext, value: string): void;
38
38
  /** Return the absolute Wasm global index for a new module-defined global. */
39
39
  export declare function nextModuleGlobalIdx(ctx: CodegenContext): number;
40
+ /**
41
+ * (#2800) Record a `global.get __in_module_init` instruction for FINALIZE-time
42
+ * index resolution. Returns a fresh `global.get` Instr with a PLACEHOLDER index
43
+ * and registers it on `ctx.inModuleInitFlagReads`; the caller bakes this exact
44
+ * object into its body. `finalizeInModuleInitFlag` (codegen/index.ts) allocates
45
+ * the i32 flag global AFTER every import global has settled and patches each
46
+ * recorded instr's `.index` to the final slot — so no read can desync when a
47
+ * later string-constant import shifts the module-global range (the live-baked
48
+ * index hazard #2043 across closure bodies the per-add fixup can miss).
49
+ *
50
+ * The flag is 1 only while `__module_init` runs; the delete-aware `any`-receiver
51
+ * read branches on it (init → host-free `__get_member_<name>` slot dispatcher;
52
+ * runtime → tombstone-aware host `__extern_get`). gc/host runs `__module_init`
53
+ * via the Wasm `start` section INSIDE `WebAssembly.instantiate`, before the host
54
+ * wires struct getters (`__setExports`), so the host read returns undefined for
55
+ * every struct field at init — this flag is what makes init reads correct.
56
+ */
57
+ export declare function recordInModuleInitFlagRead(ctx: CodegenContext): Instr;
40
58
  /** Convert an absolute Wasm global index to a local module-globals array index. */
41
59
  export declare function localGlobalIdx(ctx: CodegenContext, absIdx: number): number;
42
60
  /**
@@ -64,6 +64,30 @@ export declare function flushLateImportShifts(ctx: CodegenContext, fctx: Functio
64
64
  * Check if a ValType is the any-value boxed type used for TS `any`.
65
65
  */
66
66
  export declare function isAnyValue(type: ValType, ctx: CodegenContext): boolean;
67
+ /**
68
+ * (#2770, S5b of #2773) Brand an extern-method result ValType as a *boolean*
69
+ * when the method's declared TS return type is exactly `boolean`, so the
70
+ * `any`/return coercion boxes it via `__box_boolean` (→ `true`/`false`) instead
71
+ * of `__box_number` (→ `1`/`0`).
72
+ *
73
+ * Why a per-call-site wrap (not just registration): a boolean extern method's
74
+ * func type carries an `i32` result, and `funcTypeKey` (registry/types.ts) keys
75
+ * results on `.kind` only — so a branded `{i32,boolean:true}` result func type
76
+ * dedups to a pre-existing *unbranded* `…->i32` type. `getWasmFuncReturnType`
77
+ * then reads back the deduped unbranded i32 and the brand is lost at every
78
+ * `getWasmFuncReturnType(ctx, idx) ?? resolveWasmType(ctx, retType)` dispatch
79
+ * site. Re-branding from the call's TS return type here recovers it regardless
80
+ * of dedup. (Registration is also branded so the direct `methodInfo.results[0]`
81
+ * path in extern.ts is honest at source.)
82
+ *
83
+ * Over-boxing guards — idempotent, never widens:
84
+ * - only a *bare* `i32` is touched (f64/externref/ref/ref_null pass through);
85
+ * - an already `{i32,boolean:true}` value short-circuits (idempotent);
86
+ * - only an *exactly*-`boolean` declared return is branded (numbers, unions,
87
+ * `void`, etc. pass through unchanged — `map.get`/`indexOf`/`.size` stay
88
+ * numbers).
89
+ */
90
+ export declare function brandExternMethodResult(_ctx: CodegenContext, tsReturnType: ts.Type | undefined, valType: ValType): ValType;
67
91
  type EnsureAnyHelpersFn = (ctx: CodegenContext) => void;
68
92
  export declare function registerEnsureAnyHelpers(fn: EnsureAnyHelpersFn): void;
69
93
  type AddUnionImportsFn = (ctx: CodegenContext) => void;
@@ -1,6 +1,36 @@
1
1
  import { ts } from '../../ts-api.js';
2
2
  import { CodegenContext, FunctionContext } from '../context/types.js';
3
3
  export declare function compileWhileStatement(ctx: CodegenContext, fctx: FunctionContext, stmt: ts.WhileStatement): void;
4
+ /**
5
+ * #1196: Detect mutations of the loop index or array binding inside a for-loop
6
+ * body. Used by the bounds-check elimination pass — we can only elide bounds
7
+ * checks for `arr[i]` if both `i` and `arr` are stable across every iteration.
8
+ *
9
+ * Returns `true` if the body contains anything that could mutate either
10
+ * binding:
11
+ * - Direct assignment / compound assignment to `i` or `arr`
12
+ * (`i = …`, `i += …`, `arr = …`, etc.)
13
+ * - `i++ / ++i / i-- / --i` or the same on `arr`
14
+ * - Method calls on `arr` (`arr.push()`, `arr.length = …`, etc.)
15
+ * - `arr.length = …` assignment
16
+ * - Any nested function / arrow / class — closures could capture and mutate
17
+ * either binding outside our static view (conservative).
18
+ *
19
+ * Notes:
20
+ * - `arr[k] = v` writes through the array but does not change the binding
21
+ * itself or `arr.length` (when `k < arr.length`), so element writes are
22
+ * allowed — they're the whole point of the optimisation.
23
+ */
24
+ export declare function loopBodyMutatesIndexOrArray(body: ts.Statement, indexName: string, arrayName: string): boolean;
25
+ /**
26
+ * #2682: the increment must strictly INCREASE `i` so that, combined with a
27
+ * non-negative init and the strict `i < recv.length` condition, `0 <= i < len`
28
+ * holds at every body point. `i++`/`++i` and `i += <positive int literal>`
29
+ * qualify; `i--`/`i -= k`/`i += <non-positive>` do NOT (would break the proof).
30
+ * Narrower than `detectI32LoopVar`'s incrementor check, which also accepts the
31
+ * decreasing forms.
32
+ */
33
+ export declare function isIncreasingStep(incr: ts.Expression | undefined, name: string): boolean;
4
34
  export declare function compileForStatement(ctx: CodegenContext, fctx: FunctionContext, stmt: ts.ForStatement): void;
5
35
  export declare function compileDoWhileStatement(ctx: CodegenContext, fctx: FunctionContext, stmt: ts.DoStatement): void;
6
36
  export declare function compileForOfStatement(ctx: CodegenContext, fctx: FunctionContext, stmt: ts.ForOfStatement): void;