jz 0.5.1 → 0.6.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.
Files changed (80) hide show
  1. package/README.md +288 -314
  2. package/bench/README.md +319 -0
  3. package/bench/bench.svg +112 -0
  4. package/cli.js +32 -24
  5. package/index.js +177 -55
  6. package/interop.js +88 -159
  7. package/jz.svg +5 -0
  8. package/jzify/arguments.js +97 -0
  9. package/jzify/bundler.js +382 -0
  10. package/jzify/classes.js +328 -0
  11. package/jzify/hoist-vars.js +177 -0
  12. package/jzify/index.js +51 -0
  13. package/jzify/names.js +37 -0
  14. package/jzify/switch.js +106 -0
  15. package/jzify/transform.js +349 -0
  16. package/layout.js +179 -0
  17. package/module/array.js +322 -153
  18. package/module/collection.js +603 -145
  19. package/module/console.js +55 -43
  20. package/module/core.js +266 -153
  21. package/module/date.js +15 -3
  22. package/module/function.js +73 -6
  23. package/module/index.js +2 -1
  24. package/module/json.js +226 -61
  25. package/module/math.js +414 -186
  26. package/module/number.js +306 -60
  27. package/module/object.js +448 -184
  28. package/module/regex.js +255 -25
  29. package/module/schema.js +24 -6
  30. package/module/simd.js +85 -0
  31. package/module/string.js +586 -220
  32. package/module/symbol.js +1 -1
  33. package/module/timer.js +9 -14
  34. package/module/typedarray.js +45 -48
  35. package/package.json +41 -12
  36. package/src/abi/index.js +39 -23
  37. package/src/abi/string.js +38 -41
  38. package/src/ast.js +460 -0
  39. package/src/autoload.js +26 -24
  40. package/src/bridge.js +111 -0
  41. package/src/compile/analyze-scans.js +661 -0
  42. package/src/compile/analyze.js +1565 -0
  43. package/src/compile/emit-assign.js +408 -0
  44. package/src/compile/emit.js +3201 -0
  45. package/src/compile/flow-types.js +103 -0
  46. package/src/{compile.js → compile/index.js} +497 -125
  47. package/src/{infer.js → compile/infer.js} +27 -98
  48. package/src/{narrow.js → compile/narrow.js} +302 -96
  49. package/src/compile/plan/advise.js +316 -0
  50. package/src/compile/plan/common.js +150 -0
  51. package/src/compile/plan/index.js +118 -0
  52. package/src/compile/plan/inline.js +679 -0
  53. package/src/compile/plan/literals.js +984 -0
  54. package/src/compile/plan/loops.js +472 -0
  55. package/src/compile/plan/scope.js +573 -0
  56. package/src/compile/program-facts.js +404 -0
  57. package/src/ctx.js +176 -58
  58. package/src/ir.js +540 -171
  59. package/src/kind-traits.js +105 -0
  60. package/src/kind.js +462 -0
  61. package/src/op-policy.js +57 -0
  62. package/src/{optimize.js → optimize/index.js} +1106 -446
  63. package/src/optimize/vectorize.js +1874 -0
  64. package/src/param-reps.js +65 -0
  65. package/src/parse.js +44 -0
  66. package/src/{prepare.js → prepare/index.js} +589 -202
  67. package/src/reps.js +115 -0
  68. package/src/resolve.js +12 -3
  69. package/src/static.js +199 -0
  70. package/src/type.js +647 -0
  71. package/src/{assemble.js → wat/assemble.js} +86 -48
  72. package/src/wat/optimize.js +3760 -0
  73. package/transform.js +21 -0
  74. package/wasi.js +47 -5
  75. package/src/analyze.js +0 -3818
  76. package/src/emit.js +0 -3040
  77. package/src/jzify.js +0 -1580
  78. package/src/plan.js +0 -2132
  79. package/src/vectorize.js +0 -1088
  80. /package/src/{codegen.js → wat/codegen.js} +0 -0
package/src/ctx.js CHANGED
@@ -7,9 +7,13 @@
7
7
  * Refactored into focused sub-contexts for better maintainability.
8
8
  */
9
9
 
10
- import { DEFAULTS as ABI_DEFAULTS } from './abi/index.js'
10
+ import { makeAbi } from './abi/index.js'
11
+ export { HEAP, LAYOUT, PTR, ATOM, nanPrefixHex, atomNanHex, ssoBitI64Hex, sliceBitI64Hex, ptrNanHex, ptrBoxPrefixBigInt, encodePtrHi, decodePtrType, decodePtrAux, ATOM_HI, oobNanLiteral, oobNanIR, followForwardingWat } from '../layout.js'
11
12
 
12
13
  // === Carrier layout ===
14
+ // Canonical bit layout lives in layout.js (compiler-free). Re-exported above for
15
+ // backward-compatible `import { LAYOUT, PTR } from './ctx.js'`.
16
+ //
13
17
  // i64 carrier holds either:
14
18
  // - raw f64 number bits (any non-NaN-shape pattern), discriminated by
15
19
  // `f64.eq(f, f)` — true for real numbers, false for NaN-shape pointers.
@@ -19,38 +23,6 @@ import { DEFAULTS as ABI_DEFAULTS } from './abi/index.js'
19
23
  // `${LAYOUT.TAG_SHIFT}` etc. so a layout change propagates by re-evaluation.
20
24
  // Hot dispatch (__ptr_type/__ptr_aux/__ptr_offset) keeps the inline expansion
21
25
  // for codegen size; those sites are commented as LAYOUT-tied.
22
- export const LAYOUT = {
23
- TAG_SHIFT: 47,
24
- TAG_MASK: 0xF, // 4-bit tag (16 type slots)
25
- AUX_SHIFT: 32,
26
- AUX_MASK: 0x7FFF, // 15-bit aux (schemaId, elemType, atomId, …)
27
- OFFSET_MASK: 0xFFFFFFFF, // 32-bit offset (4GB heap)
28
- NAN_PREFIX: 0x7FF8, // top 13 bits of any NaN-shape pointer
29
- NAN_PREFIX_BITS: 0x7FF8000000000000n, // pre-shifted for i64 OR
30
- SSO_BIT: 0x4000, // STRING aux bit 14: 1=inline (≤4 ASCII chars in offset), 0=heap
31
- SLICE_BIT: 0x2000, // STRING aux bit 13: 1=view (no length header — len in aux[12:0],
32
- // offset points into a parent buffer), 0=own heap string.
33
- SLICE_LEN_MASK: 0x1FFF, // aux[12:0] — view length (≤8191; larger slices fall back to copy)
34
- }
35
-
36
- // === Tagged-pointer type codes ===
37
- // 4-bit tag (LAYOUT.TAG_MASK = 16 slots).
38
- // To retire a type, gate emission behind a feature flag and drop its dispatch
39
- // branches; to add, renumber with care — hardcoded branches in module/* must update.
40
- export const PTR = {
41
- ATOM: 0, // null, undefined, booleans, symbols (aux=atomId)
42
- ARRAY: 1, // heap-allocated arrays
43
- BUFFER: 2, // ArrayBuffer: [-8:byteLen][-4:byteCap][bytes]
44
- TYPED: 3, // TypedArrays (Float64Array, etc.)
45
- STRING: 4, // strings (heap or inline-SSO; aux bit LAYOUT.SSO_BIT distinguishes)
46
- // 5: free
47
- OBJECT: 6, // plain objects
48
- HASH: 7, // dynamic objects (Map-like)
49
- SET: 8, // Set collections
50
- MAP: 9, // Map collections
51
- CLOSURE: 10, // first-class functions
52
- EXTERNAL: 11, // JS host object refs (aux=0, offset→extMap index)
53
- }
54
26
 
55
27
  // === Global context with nested sub-contexts ===
56
28
  // Each namespace has a single lifecycle phase and clear ownership. Violating
@@ -62,21 +34,35 @@ export const PTR = {
62
34
  // function — per function being lowered
63
35
  // emit — transient during a single AST→IR dispatch
64
36
  //
65
- // | Namespace | Phase | Writers | Readers |
66
- // |-----------|----------|---------------------------|----------------------------|
67
- // | core | compile | reset, modules, inc() | emit, compile, modules |
68
- // | module | compile | prepare, index.js | prepare, compile, emit |
69
- // | scope | compile | analyze, compile | compile, emit |
70
- // | func | function | compile | emit, modules |
71
- // | types | function | analyze | emit, modules |
72
- // | schema | compile | prepare, analyze, compile | prepare, analyze, emit |
73
- // | closure | init | modules (fn plugin) | emit, compile |
74
- // | runtime | compile | emit, modules | emit, compile |
75
- // | memory | compile | index.js | compile |
76
- // | error | compile | prepare, compile, emit | err() |
77
- // | transform | compile | index.js | prepare |
78
- // | features | compile | emit, modules, prepare | compile (resolveIncludes), |
79
- // | | | | stdlib factories |
37
+ // | Namespace | Phase | Writers | Readers |
38
+ // |-----------|----------|---------------------------------|---------------------------|
39
+ // | core | compile | reset, modules, inc(), emit* | emit, compile, modules |
40
+ // | module | compile | prepare, index.js | prepare, compile, emit |
41
+ // | scope | compile | analyze, compile, plan, modules | compile, emit |
42
+ // | func | function | compile, narrow | emit, modules |
43
+ // | types | function | analyze, plan | emit, modules |
44
+ // | schema | compile | prepare, analyze, compile | prepare, analyze, emit |
45
+ // | closure | init | modules (fn plugin) | emit, compile |
46
+ // | runtime | compile | emit, modules | emit, compile |
47
+ // | memory | compile | index.js | compile |
48
+ // | error | compile | prepare, compile, emit | err() |
49
+ // | transform | compile | index.js | prepare, compile, emit |
50
+ // | features | compile | emit, modules, prepare | compile, stdlib factories |
51
+ // | abi | compile | reset (makeAbi) | ir.js codegen, optimizer |
52
+ // | bridge | compile | reset (bridge.js) | bridge.js → emit, modules |
53
+ //
54
+ // *emit's only `core` write is ctx.core.hostGlobals (a bare host-global reference),
55
+ // drained to env imports at compile (compile/index.js) — NOT to ctx.scope, so emit
56
+ // never writes scope. The stdlib module factories DO write ctx.scope.globals
57
+ // directly (core/string register __heap, __strBase, __tof_* there at compile phase).
58
+ //
59
+ // plan-phase writers (extending compile-phase): plan writes
60
+ // ctx.scope.{globalValTypes, globalTypedElem, globals, globalTypes} via
61
+ // inferModuleLetTypes / unboxConstTypedGlobals / inferModuleIntGlobals,
62
+ // and ctx.types.{dynKeyVars, anyDynKey} from collectProgramFacts results.
63
+ // narrow-phase writers: narrowSignatures (under plan) temporarily swaps
64
+ // ctx.func.{localReps, locals, current} per-function with save/restore
65
+ // so per-call-site signature inference sees the right scope.
80
66
  export const ctx = {
81
67
  core: {}, // emitter table + stdlib registry (seeded by reset + modules)
82
68
  module: {}, // module graph: imports, resolved sources, module-init blocks
@@ -88,11 +74,22 @@ export const ctx = {
88
74
  runtime: {}, // runtime state: data segments, string pool, atom table, throws flag
89
75
  memory: {}, // module memory config (pages, shared)
90
76
  error: {}, // source location carried through emit for err() messages
91
- transform: {}, // compile-time options (jzify, etc.)
77
+ transform: {}, // compile-time options + injected services. Three categories:
78
+ // user opts : noTailCall, strict, alloc, importMetaUrl, host, inspect
79
+ // derived cfg : optimize (resolved by resolveOptimize from user input)
80
+ // services : parse, resolveUrl, jzify (when set to a function by the
81
+ // host pipeline; boolean form is a user opt). Service
82
+ // injection is the pattern that lets the self-host kernel
83
+ // run without a parser — it omits these and prepare uses
84
+ // ctx.module.importAsts instead.
92
85
  abi: {}, // per-type rep lookup (see abi/index.js). { number: rep, string: rep, ... }
93
86
  // Set by reset() to the default carrier bundle. Read by codegen sites
94
87
  // that delegate rep-specific behavior — today just the optimizer's
95
88
  // peephole hook; expanding as per-site narrowing tags individual sites.
89
+ bridge: {}, // emit/flat/wat dispatch, bound by reset() (see bridge.js). Lets every
90
+ // module call emit() without importing the emitter — breaks the cycle.
91
+ features: {}, // codegen capability flags (external, sso, typedarray, …), reset() seeds
92
+ // the defaults; see reset() for the field list and who flips each.
96
93
  }
97
94
 
98
95
  /** Create a child scope via shallow flat copy (metacircular-safe: no prototype chain).
@@ -102,6 +99,17 @@ export const derive = (parent) => ({ ...parent })
102
99
  /** Include stdlib names for emission. */
103
100
  export const inc = (...names) => names.forEach(n => ctx.core.includes.add(n))
104
101
 
102
+ /** Declare a module global as a structured record — the single shape behind
103
+ * every `ctx.scope.globals` entry:
104
+ * { type: 'i32'|'i64'|'f64', mut: bool, init: number|string, export: string|null }
105
+ * `init` is a number or a watr const literal (`-1`, `nan:0x…`, hex). Replaces
106
+ * the old WAT-text strings: type queries are field reads, emission builds IR
107
+ * directly (no parse-back), and `globalTypes` is set in the same move. */
108
+ export const declGlobal = (name, type, init = 0, opts) => {
109
+ ctx.scope.globals.set(name, { type, mut: opts?.mut !== false, init, export: opts?.export ?? null })
110
+ ctx.scope.globalTypes.set(name, type)
111
+ }
112
+
105
113
  /** Wrap an emit handler with a declarative stdlib-dependency list. The deps
106
114
  * become data — exposed as `.deps` (tabulatable, analyzable) — and are `inc`'d
107
115
  * on every call, while the body `fn` stays a pure `args → IR` builder (also
@@ -111,13 +119,27 @@ export const emitter = (deps, fn) => {
111
119
  const run = (...args) => (inc(...deps), fn(...args))
112
120
  run.deps = deps
113
121
  run.pure = fn
114
- // Preserve the body's arity: `typeof Math.x` folding keys off emitter `.length`
115
- // to tell callable builtins (sin) from constant ones (PI). The rest-param
116
- // wrapper would otherwise report 0 for everything.
117
- Object.defineProperty(run, 'length', { value: fn.length, configurable: true })
122
+ // Carry the body's parameter count as `.argc`: the rest-param wrapper above
123
+ // reports `.length` 0, so a handler's logical arity must travel as plain data
124
+ // (read back via `emitArity`, never the masked function `.length`). Two
125
+ // consumers need it — `typeof Math.x` folding (callable builtin vs constant)
126
+ // and the `.`-emit property/method split (arity-1 reads as a value; arity ≥2
127
+ // is call-only).
128
+ run.argc = fn.length
118
129
  return run
119
130
  }
120
131
 
132
+ /** Logical arity of an emit handler: wrapped handlers (emitter/call/method/dual)
133
+ * carry it as `.argc`; bare ones expose it as the function's own `.length`. */
134
+ export const emitArity = (h) => h?.argc ?? h?.length
135
+
136
+ /** Tag an emit handler as a property getter — it yields a value when the
137
+ * property is *read* (`re.source`, `m.size`), so the `.`-read path may fire it.
138
+ * Untagged handlers are methods: a bare read of `m.values` must not invoke them
139
+ * (that would materialize a view instead of reading the `"values"` property);
140
+ * they fire only from the method-call path. Apply outermost: `getter(emitter(…))`. */
141
+ export const getter = (fn) => (fn.getter = true, fn)
142
+
121
143
  /** Expand ctx.core.includes transitively via ctx.core.stdlibDeps. Call before WASM assembly.
122
144
  * Each module co-locates its own deps with its stdlib registrations at init time. */
123
145
  export function resolveIncludes() {
@@ -139,7 +161,8 @@ export function resolveIncludes() {
139
161
  }
140
162
 
141
163
  /** Reset all compilation state. Called once per jz() invocation. */
142
- export function reset(proto, globals) {
164
+ export function reset(proto, globals, bridge) {
165
+ ctx.bridge = bridge
143
166
  ctx.core = {
144
167
  emit: derive(proto),
145
168
  stdlib: {},
@@ -152,6 +175,11 @@ export function reset(proto, globals) {
152
175
  // Drained at module-assembly time into `(import "wasm:js-string" "name" …)`
153
176
  // nodes; host wires JS-side polyfills via interop's
154
177
  // env builder for engines without builtin support.
178
+ hostGlobals: new Set(), // host globals (globalThis/process/WebAssembly/…) referenced as
179
+ // values. Recorded by emit on first use; drained into
180
+ // `(import "env" "name" (global $name i64))` at assembly. Same
181
+ // usage-gated pattern as jsstring — emit records, assembly owns
182
+ // the ctx.module.imports write.
155
183
  }
156
184
 
157
185
 
@@ -159,6 +187,8 @@ export function reset(proto, globals) {
159
187
  imports: [],
160
188
  modules: {},
161
189
  importSources: null,
190
+ importAsts: null, // self-host: pre-parsed [specifier, ast] pairs (the kernel can't parse).
191
+ // Consulted by prepareModule before falling back to ctx.transform.parse(source).
162
192
  hostImports: null,
163
193
  hostImportValTypes: new Map(),
164
194
  resolvedModules: new Map(),
@@ -170,7 +200,7 @@ export function reset(proto, globals) {
170
200
 
171
201
  ctx.scope = {
172
202
  chain: derive(globals),
173
- globals: new Map(),
203
+ globals: new Map(), // name → { type, mut, init, export } records (see declGlobal)
174
204
  userGlobals: new Set(),
175
205
  globalTypes: new Map(),
176
206
  globalValTypes: null,
@@ -190,6 +220,7 @@ export function reset(proto, globals) {
190
220
  localReps: null,
191
221
  refinements: new Map(), // flow-sensitive: name → {val?: VAL.*, notString?: true} inside a type-guarded branch
192
222
  boxed: new Map(),
223
+ cellTypes: new Set(), // boxed vars whose CELL stores raw i32 (closure-capture narrowing)
193
224
  stack: [],
194
225
  uniq: 0,
195
226
  inTry: false,
@@ -201,6 +232,10 @@ export function reset(proto, globals) {
201
232
  // by the pass owners so re-entrant analyzeBody calls don't clobber each other.
202
233
  localValTypesOverlay: null,
203
234
  localTypedElemsOverlay: null,
235
+ _ccBody: null, // memo key: body node last scanned by inBoundsCharCodeAt (src/type.js)
236
+ ccInBounds: null, // memo value: Set of in-bounds charCodeAt callee nodes for _ccBody
237
+ _aiBody: null, // memo key: body node last scanned by inBoundsArrIdx (src/type.js)
238
+ aiInBounds: null, // memo value: Set of in-bounds "recv\0idx" array-read keys for _aiBody
204
239
  }
205
240
 
206
241
  ctx.types = {
@@ -212,6 +247,13 @@ export function reset(proto, globals) {
212
247
  ctx.schema = {
213
248
  list: [],
214
249
  vars: new Map(),
250
+ poisoned: new Set(), // names whose assignments disagree on shape (literal +
251
+ // non-literal, or two different literals). A poisoned
252
+ // name never (re)binds in schema.vars: fixed-slot reads
253
+ // against ONE literal's layout would misread the other
254
+ // sources' objects. Populated by prepare's `=` handler;
255
+ // end-of-prepare state is what compile reads, so the
256
+ // conflict is order-insensitive.
215
257
  register: null,
216
258
  find: null,
217
259
  targetStack: [],
@@ -246,6 +288,14 @@ export function reset(proto, globals) {
246
288
  bodies: null,
247
289
  make: null,
248
290
  call: null,
291
+ numericReturn: null, // Set<closureBodyName> proven to return a plain number — lets
292
+ // callers skip the __to_num result coercion (function.js seeds it).
293
+ paramTypes: null, // Map<closureBodyName, bool[]> — per-param "every direct call site
294
+ // passed a number" lattice; emitClosureBody marks such params
295
+ // VAL.NUMBER so their body uses skip __to_num (tryDirectClosureCall seeds).
296
+ minArgc: null, // Map<closureBodyName, number> — fewest args any direct call passed.
297
+ // A slot at index ≥ minArgc is omitted by some call (→ may be undefined),
298
+ // so it must NOT be typed NUMBER, else `x === undefined` mis-folds to false.
249
299
  }
250
300
 
251
301
  ctx.runtime = {
@@ -258,6 +308,9 @@ export function reset(proto, globals) {
258
308
  throws: false,
259
309
  userThrows: false, // user wrote `throw`/`try`/`catch`/`finally` — keep runtime declared
260
310
  // even when all throws are dead-code-eliminated (JS-side ABI contract).
311
+ staticPtrSlots: null, // [byteOffset] data-segment slots holding NaN-boxed ptrs (host relocates); lazy-init in ir.js
312
+ staticDataLen: 0, // byte length of the address-0 static string block (seeded by module/number staticStr)
313
+ typeofStrs: null, // [str] interned typeof result strings; lazy-init in module/core `typeof`
261
314
  }
262
315
 
263
316
  ctx.memory = {
@@ -294,6 +347,9 @@ export function reset(proto, globals) {
294
347
  // Shape: { abi, functions: { [name]: { exported, params, results, ptrKind?, locals, callerReps } }, schemas }.
295
348
  ctx.inspect = null
296
349
 
350
+ // Advisory sink. Populated when compile() receives opts.warnings.
351
+ ctx.warnings = null
352
+
297
353
  // Feature flags: capabilities the compiled module may exercise at runtime.
298
354
  // Set true by producer sites (import points, auto-imports, dynamic call sites).
299
355
  // Read by stdlib template factories and deps graph at resolveIncludes() time to
@@ -309,7 +365,7 @@ export function reset(proto, globals) {
309
365
  // (b) a capability needs an opt-in A/B switch against the default path
310
366
  // (SSO is the planned first user — default string-literal emission
311
367
  // currently forces SSO for ≤4 ASCII chars at string.js:49)
312
- ctx.abi = ABI_DEFAULTS
368
+ ctx.abi = makeAbi()
313
369
 
314
370
  // Only flags actually read by codegen live here. Hash/regex/json substrates
315
371
  // are pulled organically by inc(__*) — no flag mediates them, so no flag exists.
@@ -325,8 +381,67 @@ export function reset(proto, globals) {
325
381
  }
326
382
  }
327
383
 
384
+ /** Debug-mode invariant checks. Encodes the writers/readers contract documented
385
+ * above as runtime asserts so a bad refactor surfaces at the phase boundary
386
+ * instead of as a distant nondeterministic failure. No-op unless
387
+ * `JZ_DEBUG_INVARIANTS=1`; designed so phase-boundary callers can sprinkle
388
+ * `assertCtxInvariants('post-prepare')` without runtime cost in production.
389
+ *
390
+ * Phases checked:
391
+ * - `post-reset` : every sub-context exists; Maps/Sets initialized.
392
+ * - `post-prepare` : module + scope populated; func.list possibly empty.
393
+ * - `pre-emit` : func.current set; locals Map present; rep maps live.
394
+ * - `post-compile` : no transient temps leaked (func.uniq stable across calls). */
395
+ const DBG_INVARIANTS = typeof process !== 'undefined' && process.env?.JZ_DEBUG_INVARIANTS === '1'
396
+ export function assertCtxInvariants(phase) {
397
+ if (!DBG_INVARIANTS) return
398
+ const fail = msg => { throw new Error(`[ctx invariant] ${phase}: ${msg}`) }
399
+ const must = (cond, msg) => { if (!cond) fail(msg) }
400
+
401
+ must(ctx.core && ctx.module && ctx.scope && ctx.func && ctx.transform && ctx.features,
402
+ 'sub-contexts present')
403
+ if (phase !== 'pre-reset') {
404
+ must(ctx.core.includes instanceof Set, 'core.includes is Set')
405
+ must(ctx.core.emit && typeof ctx.core.emit === 'object', 'core.emit table')
406
+ must(Array.isArray(ctx.func.list), 'func.list array')
407
+ must(ctx.func.locals instanceof Map, 'func.locals Map')
408
+ must(ctx.func.refinements instanceof Map, 'func.refinements Map')
409
+ }
410
+ if (phase === 'pre-emit') {
411
+ must(ctx.func.current, 'func.current set before emit')
412
+ must(ctx.func.locals.size != null, 'locals open for writes')
413
+ }
414
+ }
415
+
416
+ /** Enable compile-time advisories. Pass `opts.warnings` (mirrors `opts.profile`). */
417
+ export function initWarnings(sink) {
418
+ if (sink == null) {
419
+ ctx.warnings = null
420
+ return
421
+ }
422
+ sink.entries ||= []
423
+ ctx.warnings = { sink, seen: new Set() }
424
+ }
425
+
426
+ /** Record one advisory; `loc` is a source byte offset used only to derive
427
+ * line/column — it is never persisted on the entry. No-op unless
428
+ * `initWarnings` wired a sink. */
429
+ export function warn(code, message, meta = {}, loc = null) {
430
+ if (!ctx.warnings) return
431
+ const key = `${code}:${meta.fn || ''}:${meta.line || ''}`
432
+ if (ctx.warnings.seen.has(key)) return
433
+ ctx.warnings.seen.add(key)
434
+ const entry = { code, message, ...meta }
435
+ if (loc != null && ctx.error.src) {
436
+ const before = ctx.error.src.slice(0, loc)
437
+ entry.line = before.split('\n').length
438
+ entry.column = loc - before.lastIndexOf('\n')
439
+ }
440
+ ctx.warnings.sink.entries.push(entry)
441
+ }
442
+
328
443
  /** Throw with source location context. */
329
- export function err(msg) {
444
+ export function err(msg, cause) {
330
445
  let detail = msg
331
446
 
332
447
  if (ctx.error.loc != null && ctx.error.src) {
@@ -345,7 +460,10 @@ export function err(msg) {
345
460
  detail += `\n current AST: ${formatErrorNode(ctx.error.node)}`
346
461
  }
347
462
 
348
- const e = new Error(detail)
463
+ // Preserve the triggering error (if any) as the cause: when an internal jz bug
464
+ // is wrapped, the original stack — pointing at the actual codegen site — survives
465
+ // in the chain (`Error: … [cause]: …`) instead of being replaced by this frame.
466
+ const e = cause !== undefined ? new Error(detail, { cause }) : new Error(detail)
349
467
  const stackLines = e.stack.split('\n')
350
468
  const firstFrame = stackLines.findIndex(line => line.trimStart().startsWith('at '))
351
469
  const frames = firstFrame >= 0 ? stackLines.slice(firstFrame) : stackLines.slice(1)