jz 0.5.1 → 0.7.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 (86) hide show
  1. package/README.md +315 -318
  2. package/bench/README.md +369 -0
  3. package/bench/bench.svg +102 -0
  4. package/cli.js +104 -30
  5. package/dist/interop.js +1 -0
  6. package/dist/jz.js +6024 -0
  7. package/index.d.ts +126 -0
  8. package/index.js +362 -84
  9. package/interop.js +159 -186
  10. package/jz.svg +5 -0
  11. package/jzify/arguments.js +97 -0
  12. package/jzify/bundler.js +382 -0
  13. package/jzify/classes.js +328 -0
  14. package/jzify/hoist-vars.js +177 -0
  15. package/jzify/index.js +51 -0
  16. package/jzify/names.js +37 -0
  17. package/jzify/switch.js +106 -0
  18. package/jzify/transform.js +349 -0
  19. package/layout.js +184 -0
  20. package/module/array.js +377 -176
  21. package/module/collection.js +639 -145
  22. package/module/console.js +55 -43
  23. package/module/core.js +264 -153
  24. package/module/date.js +15 -3
  25. package/module/function.js +73 -6
  26. package/module/index.js +2 -1
  27. package/module/json.js +226 -61
  28. package/module/math.js +551 -187
  29. package/module/number.js +327 -60
  30. package/module/object.js +474 -184
  31. package/module/regex.js +255 -25
  32. package/module/schema.js +24 -6
  33. package/module/simd.js +117 -0
  34. package/module/string.js +621 -226
  35. package/module/symbol.js +1 -1
  36. package/module/timer.js +9 -14
  37. package/module/typedarray.js +459 -73
  38. package/package.json +58 -14
  39. package/src/abi/index.js +39 -23
  40. package/src/abi/string.js +38 -41
  41. package/src/ast.js +460 -0
  42. package/src/autoload.js +29 -24
  43. package/src/bridge.js +111 -0
  44. package/src/compile/analyze-scans.js +824 -0
  45. package/src/compile/analyze.js +1600 -0
  46. package/src/compile/emit-assign.js +411 -0
  47. package/src/compile/emit.js +3512 -0
  48. package/src/compile/flow-types.js +103 -0
  49. package/src/{compile.js → compile/index.js} +778 -128
  50. package/src/{infer.js → compile/infer.js} +62 -98
  51. package/src/compile/loop-divmod.js +155 -0
  52. package/src/{narrow.js → compile/narrow.js} +409 -106
  53. package/src/compile/peel-stencil.js +261 -0
  54. package/src/compile/plan/advise.js +370 -0
  55. package/src/compile/plan/common.js +150 -0
  56. package/src/compile/plan/index.js +122 -0
  57. package/src/compile/plan/inline.js +682 -0
  58. package/src/compile/plan/literals.js +1199 -0
  59. package/src/compile/plan/loops.js +472 -0
  60. package/src/compile/plan/scope.js +649 -0
  61. package/src/compile/program-facts.js +423 -0
  62. package/src/ctx.js +220 -64
  63. package/src/ir.js +589 -172
  64. package/src/kind-traits.js +132 -0
  65. package/src/kind.js +524 -0
  66. package/src/op-policy.js +57 -0
  67. package/src/{optimize.js → optimize/index.js} +1432 -473
  68. package/src/optimize/vectorize.js +4660 -0
  69. package/src/param-reps.js +65 -0
  70. package/src/parse.js +44 -0
  71. package/src/{prepare.js → prepare/index.js} +649 -205
  72. package/src/prepare/lift-iife.js +149 -0
  73. package/src/reps.js +116 -0
  74. package/src/resolve.js +12 -3
  75. package/src/static.js +208 -0
  76. package/src/type.js +651 -0
  77. package/src/{assemble.js → wat/assemble.js} +275 -55
  78. package/src/wat/optimize.js +3938 -0
  79. package/transform.js +21 -0
  80. package/wasi.js +47 -5
  81. package/src/analyze.js +0 -3818
  82. package/src/emit.js +0 -3040
  83. package/src/jzify.js +0 -1580
  84. package/src/plan.js +0 -2132
  85. package/src/vectorize.js +0 -1088
  86. /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, FORWARDING_MASK, 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,35 +119,77 @@ 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() {
124
146
  const graph = ctx.core.stdlibDeps
147
+ const stdlib = ctx.core.stdlib
148
+ // Auto-derived deps: a stdlib template that calls `$__foo` (a registered stdlib
149
+ // func) depends on it, whether or not the hand-maintained `deps()` list says so.
150
+ // Scanning the *realized* template keeps the graph honest, so a missing manual
151
+ // entry can't silently drop a transitively-needed helper (the bug class the old
152
+ // blanket `inc('__mkptr','__alloc')` masked). Factory templates are realized
153
+ // (called) so feature-gated branches — `${hasExt ? '(call $__ext_prop …)' : ''}`
154
+ // — resolve before scanning; reading raw source would over-pull the dead branch.
155
+ // jz's templates are pure string builders, so realizing here (and again at
156
+ // emission) is side-effect-free. A `$__foo` naming a global (not a stdlib func)
157
+ // is skipped. Realization can fail if called before its inputs are ready — then
158
+ // we return nothing *without caching*, so a later pass retries. Memoized per compile.
159
+ const autoCache = ctx.core._autoDeps ??= new Map()
160
+ const autoDepsOf = (name) => {
161
+ let found = autoCache.get(name)
162
+ if (found !== undefined) return found
163
+ const v = stdlib[name]
164
+ let text
165
+ if (typeof v === 'string') text = v
166
+ else if (typeof v === 'function') { try { text = v() } catch { return [] } }
167
+ if (typeof text !== 'string') return (autoCache.set(name, []), [])
168
+ found = []
169
+ const seen = new Set()
170
+ for (const m of text.matchAll(/\$(__[A-Za-z0-9_]+)/g)) {
171
+ const d = m[1]
172
+ if (d !== name && stdlib[d] && !seen.has(d)) { seen.add(d); found.push(d) }
173
+ }
174
+ autoCache.set(name, found)
175
+ return found
176
+ }
125
177
  let changed = true
126
178
  while (changed) {
127
179
  changed = false
128
180
  for (const name of [...ctx.core.includes]) {
129
181
  const entry = graph[name]
130
182
  const deps = typeof entry === 'function' ? entry() : entry
131
- if (deps) for (const dep of deps) {
132
- if (!ctx.core.includes.has(dep)) {
133
- ctx.core.includes.add(dep)
134
- changed = true
135
- }
136
- }
183
+ const add = (dep) => { if (!ctx.core.includes.has(dep)) { ctx.core.includes.add(dep); changed = true } }
184
+ if (deps) for (const dep of deps) add(dep)
185
+ for (const dep of autoDepsOf(name)) add(dep)
137
186
  }
138
187
  }
139
188
  }
140
189
 
141
190
  /** Reset all compilation state. Called once per jz() invocation. */
142
- export function reset(proto, globals) {
191
+ export function reset(proto, globals, bridge) {
192
+ ctx.bridge = bridge
143
193
  ctx.core = {
144
194
  emit: derive(proto),
145
195
  stdlib: {},
@@ -152,6 +202,11 @@ export function reset(proto, globals) {
152
202
  // Drained at module-assembly time into `(import "wasm:js-string" "name" …)`
153
203
  // nodes; host wires JS-side polyfills via interop's
154
204
  // env builder for engines without builtin support.
205
+ hostGlobals: new Set(), // host globals (globalThis/process/WebAssembly/…) referenced as
206
+ // values. Recorded by emit on first use; drained into
207
+ // `(import "env" "name" (global $name i64))` at assembly. Same
208
+ // usage-gated pattern as jsstring — emit records, assembly owns
209
+ // the ctx.module.imports write.
155
210
  }
156
211
 
157
212
 
@@ -159,6 +214,8 @@ export function reset(proto, globals) {
159
214
  imports: [],
160
215
  modules: {},
161
216
  importSources: null,
217
+ importAsts: null, // self-host: pre-parsed [specifier, ast] pairs (the kernel can't parse).
218
+ // Consulted by prepareModule before falling back to ctx.transform.parse(source).
162
219
  hostImports: null,
163
220
  hostImportValTypes: new Map(),
164
221
  resolvedModules: new Map(),
@@ -170,7 +227,7 @@ export function reset(proto, globals) {
170
227
 
171
228
  ctx.scope = {
172
229
  chain: derive(globals),
173
- globals: new Map(),
230
+ globals: new Map(), // name → { type, mut, init, export } records (see declGlobal)
174
231
  userGlobals: new Set(),
175
232
  globalTypes: new Map(),
176
233
  globalValTypes: null,
@@ -190,6 +247,7 @@ export function reset(proto, globals) {
190
247
  localReps: null,
191
248
  refinements: new Map(), // flow-sensitive: name → {val?: VAL.*, notString?: true} inside a type-guarded branch
192
249
  boxed: new Map(),
250
+ cellTypes: new Set(), // boxed vars whose CELL stores raw i32 (closure-capture narrowing)
193
251
  stack: [],
194
252
  uniq: 0,
195
253
  inTry: false,
@@ -201,17 +259,29 @@ export function reset(proto, globals) {
201
259
  // by the pass owners so re-entrant analyzeBody calls don't clobber each other.
202
260
  localValTypesOverlay: null,
203
261
  localTypedElemsOverlay: null,
262
+ _ccBody: null, // memo key: body node last scanned by inBoundsCharCodeAt (src/type.js)
263
+ ccInBounds: null, // memo value: Set of in-bounds charCodeAt callee nodes for _ccBody
264
+ _aiBody: null, // memo key: body node last scanned by inBoundsArrIdx (src/type.js)
265
+ aiInBounds: null, // memo value: Set of in-bounds "recv\0idx" array-read keys for _aiBody
204
266
  }
205
267
 
206
268
  ctx.types = {
207
269
  typedElem: null,
208
270
  dynKeyVars: null,
271
+ dynWriteVars: null,
209
272
  anyDynKey: false,
210
273
  }
211
274
 
212
275
  ctx.schema = {
213
276
  list: [],
214
277
  vars: new Map(),
278
+ poisoned: new Set(), // names whose assignments disagree on shape (literal +
279
+ // non-literal, or two different literals). A poisoned
280
+ // name never (re)binds in schema.vars: fixed-slot reads
281
+ // against ONE literal's layout would misread the other
282
+ // sources' objects. Populated by prepare's `=` handler;
283
+ // end-of-prepare state is what compile reads, so the
284
+ // conflict is order-insensitive.
215
285
  register: null,
216
286
  find: null,
217
287
  targetStack: [],
@@ -246,6 +316,14 @@ export function reset(proto, globals) {
246
316
  bodies: null,
247
317
  make: null,
248
318
  call: null,
319
+ numericReturn: null, // Set<closureBodyName> proven to return a plain number — lets
320
+ // callers skip the __to_num result coercion (function.js seeds it).
321
+ paramTypes: null, // Map<closureBodyName, bool[]> — per-param "every direct call site
322
+ // passed a number" lattice; emitClosureBody marks such params
323
+ // VAL.NUMBER so their body uses skip __to_num (tryDirectClosureCall seeds).
324
+ minArgc: null, // Map<closureBodyName, number> — fewest args any direct call passed.
325
+ // A slot at index ≥ minArgc is omitted by some call (→ may be undefined),
326
+ // so it must NOT be typed NUMBER, else `x === undefined` mis-folds to false.
249
327
  }
250
328
 
251
329
  ctx.runtime = {
@@ -258,11 +336,15 @@ export function reset(proto, globals) {
258
336
  throws: false,
259
337
  userThrows: false, // user wrote `throw`/`try`/`catch`/`finally` — keep runtime declared
260
338
  // even when all throws are dead-code-eliminated (JS-side ABI contract).
339
+ staticPtrSlots: null, // [byteOffset] data-segment slots holding NaN-boxed ptrs (host relocates); lazy-init in ir.js
340
+ staticDataLen: 0, // byte length of the address-0 static string block (seeded by module/number staticStr)
341
+ typeofStrs: null, // [str] interned typeof result strings; lazy-init in module/core `typeof`
261
342
  }
262
343
 
263
344
  ctx.memory = {
264
345
  shared: false,
265
346
  pages: 0,
347
+ max: 0, // 0 = unbounded; >0 emits a maximum on the memory type (cap growth)
266
348
  }
267
349
 
268
350
  ctx.error = {
@@ -294,6 +376,9 @@ export function reset(proto, globals) {
294
376
  // Shape: { abi, functions: { [name]: { exported, params, results, ptrKind?, locals, callerReps } }, schemas }.
295
377
  ctx.inspect = null
296
378
 
379
+ // Advisory sink. Populated when compile() receives opts.warnings.
380
+ ctx.warnings = null
381
+
297
382
  // Feature flags: capabilities the compiled module may exercise at runtime.
298
383
  // Set true by producer sites (import points, auto-imports, dynamic call sites).
299
384
  // Read by stdlib template factories and deps graph at resolveIncludes() time to
@@ -309,7 +394,7 @@ export function reset(proto, globals) {
309
394
  // (b) a capability needs an opt-in A/B switch against the default path
310
395
  // (SSO is the planned first user — default string-literal emission
311
396
  // currently forces SSO for ≤4 ASCII chars at string.js:49)
312
- ctx.abi = ABI_DEFAULTS
397
+ ctx.abi = makeAbi()
313
398
 
314
399
  // Only flags actually read by codegen live here. Hash/regex/json substrates
315
400
  // are pulled organically by inc(__*) — no flag mediates them, so no flag exists.
@@ -325,8 +410,76 @@ export function reset(proto, globals) {
325
410
  }
326
411
  }
327
412
 
413
+ /** Debug-mode invariant checks. Encodes the writers/readers contract documented
414
+ * above as runtime asserts so a bad refactor surfaces at the phase boundary
415
+ * instead of as a distant nondeterministic failure. No-op unless
416
+ * `JZ_DEBUG_INVARIANTS=1`; designed so phase-boundary callers can sprinkle
417
+ * `assertCtxInvariants('post-prepare')` without runtime cost in production.
418
+ *
419
+ * Phases checked:
420
+ * - `post-reset` : every sub-context exists; Maps/Sets initialized.
421
+ * - `post-prepare` : module + scope populated; func.list possibly empty.
422
+ * - `pre-emit` : func.current set; locals Map present; rep maps live.
423
+ * - `post-compile` : no transient temps leaked (func.uniq stable across calls). */
424
+ const DBG_INVARIANTS = typeof process !== 'undefined' && process.env?.JZ_DEBUG_INVARIANTS === '1'
425
+ export function assertCtxInvariants(phase) {
426
+ if (!DBG_INVARIANTS) return
427
+ const fail = msg => { throw new Error(`[ctx invariant] ${phase}: ${msg}`) }
428
+ const must = (cond, msg) => { if (!cond) fail(msg) }
429
+
430
+ must(ctx.core && ctx.module && ctx.scope && ctx.func && ctx.transform && ctx.features,
431
+ 'sub-contexts present')
432
+ if (phase !== 'pre-reset') {
433
+ must(ctx.core.includes instanceof Set, 'core.includes is Set')
434
+ must(ctx.core.emit && typeof ctx.core.emit === 'object', 'core.emit table')
435
+ must(Array.isArray(ctx.func.list), 'func.list array')
436
+ must(ctx.func.locals instanceof Map, 'func.locals Map')
437
+ must(ctx.func.refinements instanceof Map, 'func.refinements Map')
438
+ }
439
+ if (phase === 'pre-emit') {
440
+ must(ctx.func.current, 'func.current set before emit')
441
+ must(ctx.func.locals.size != null, 'locals open for writes')
442
+ }
443
+ }
444
+
445
+ /** Enable compile-time advisories. Pass `opts.warnings` (mirrors `opts.profile`). */
446
+ export function initWarnings(sink) {
447
+ if (sink == null) {
448
+ ctx.warnings = null
449
+ return
450
+ }
451
+ sink.entries ||= []
452
+ ctx.warnings = { sink, seen: new Set() }
453
+ }
454
+
455
+ /** Record one advisory; `loc` is a source byte offset used only to derive
456
+ * line/column — it is never persisted on the entry. No-op unless
457
+ * `initWarnings` wired a sink. */
458
+ export function warn(code, message, meta = {}, loc = null) {
459
+ if (!ctx.warnings) return
460
+ const key = `${code}:${meta.fn || ''}:${meta.line || ''}`
461
+ if (ctx.warnings.seen.has(key)) return
462
+ ctx.warnings.seen.add(key)
463
+ const entry = { code, message, ...meta }
464
+ if (loc != null && ctx.error.src) {
465
+ const before = ctx.error.src.slice(0, loc)
466
+ entry.line = before.split('\n').length
467
+ entry.column = loc - before.lastIndexOf('\n')
468
+ }
469
+ ctx.warnings.sink.entries.push(entry)
470
+ }
471
+
472
+ /** Advise that an emit site fell back to generic runtime dispatch (the slow,
473
+ * un-inferred path). Called from the actual emission point so it fires only when
474
+ * inference/optimization truly couldn't fold it — never a false positive on a
475
+ * case that vectorized/unrolled/slot-folded. `ctx.error.loc` is the current AST
476
+ * node's byte offset (kept up to date by the emit walk), giving line/column. */
477
+ export function warnDeopt(code, message) {
478
+ warn(code, message, { fn: ctx.func.current?.name }, ctx.error.loc)
479
+ }
480
+
328
481
  /** Throw with source location context. */
329
- export function err(msg) {
482
+ export function err(msg, cause) {
330
483
  let detail = msg
331
484
 
332
485
  if (ctx.error.loc != null && ctx.error.src) {
@@ -345,7 +498,10 @@ export function err(msg) {
345
498
  detail += `\n current AST: ${formatErrorNode(ctx.error.node)}`
346
499
  }
347
500
 
348
- const e = new Error(detail)
501
+ // Preserve the triggering error (if any) as the cause: when an internal jz bug
502
+ // is wrapped, the original stack — pointing at the actual codegen site — survives
503
+ // in the chain (`Error: … [cause]: …`) instead of being replaced by this frame.
504
+ const e = cause !== undefined ? new Error(detail, { cause }) : new Error(detail)
349
505
  const stackLines = e.stack.split('\n')
350
506
  const firstFrame = stackLines.findIndex(line => line.trimStart().startsWith('at '))
351
507
  const frames = firstFrame >= 0 ? stackLines.slice(firstFrame) : stackLines.slice(1)