jz 0.6.0 → 0.8.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 (63) hide show
  1. package/README.md +99 -72
  2. package/bench/README.md +253 -100
  3. package/bench/bench.svg +58 -81
  4. package/cli.js +85 -12
  5. package/dist/interop.js +1 -0
  6. package/dist/jz.js +6196 -0
  7. package/index.d.ts +126 -0
  8. package/index.js +222 -35
  9. package/interop.js +240 -141
  10. package/layout.js +34 -18
  11. package/module/array.js +99 -111
  12. package/module/collection.js +124 -30
  13. package/module/console.js +1 -1
  14. package/module/core.js +163 -19
  15. package/module/json.js +3 -3
  16. package/module/math.js +162 -3
  17. package/module/number.js +268 -13
  18. package/module/object.js +37 -5
  19. package/module/regex.js +8 -7
  20. package/module/simd.js +37 -5
  21. package/module/string.js +203 -168
  22. package/module/typedarray.js +565 -112
  23. package/package.json +26 -7
  24. package/src/abi/string.js +29 -29
  25. package/src/ast.js +19 -2
  26. package/src/autoload.js +3 -0
  27. package/src/compile/analyze-scans.js +174 -11
  28. package/src/compile/analyze.js +101 -4
  29. package/src/compile/cse-load.js +200 -0
  30. package/src/compile/emit-assign.js +82 -20
  31. package/src/compile/emit.js +592 -51
  32. package/src/compile/index.js +504 -89
  33. package/src/compile/infer.js +36 -1
  34. package/src/compile/loop-divmod.js +109 -0
  35. package/src/compile/loop-model.js +91 -0
  36. package/src/compile/loop-square.js +102 -0
  37. package/src/compile/narrow.js +275 -41
  38. package/src/compile/peel-stencil.js +215 -0
  39. package/src/compile/plan/advise.js +55 -1
  40. package/src/compile/plan/common.js +29 -0
  41. package/src/compile/plan/index.js +8 -1
  42. package/src/compile/plan/inline.js +180 -22
  43. package/src/compile/plan/literals.js +313 -24
  44. package/src/compile/plan/scope.js +115 -39
  45. package/src/compile/program-facts.js +21 -2
  46. package/src/ctx.js +96 -19
  47. package/src/helper-counters.js +137 -0
  48. package/src/ir.js +157 -20
  49. package/src/kind-traits.js +34 -3
  50. package/src/kind.js +79 -4
  51. package/src/op-policy.js +5 -2
  52. package/src/ops.js +119 -0
  53. package/src/optimize/index.js +1274 -151
  54. package/src/optimize/recurse.js +182 -0
  55. package/src/optimize/vectorize.js +4187 -253
  56. package/src/prepare/index.js +75 -14
  57. package/src/prepare/lift-iife.js +149 -0
  58. package/src/reps.js +6 -2
  59. package/src/static.js +9 -0
  60. package/src/type.js +63 -51
  61. package/src/wat/assemble.js +286 -21
  62. package/src/widen.js +21 -0
  63. package/src/wat/optimize.js +0 -3760
@@ -44,62 +44,138 @@ import { invalidateProgramFactsCache } from '../program-facts.js'
44
44
  // mixed ctors) clears the candidacy, keeping the read site polymorphic.
45
45
  export const inferModuleLetTypes = (ast) => {
46
46
  if (!ctx.scope.userGlobals) return
47
- // candidates: name { ctor: string|null, valid: true } | { valid: false }
48
- // valid=true with ctor=null means "still no positive evidence"; we promote
49
- // only when ctor is non-null at the end. Assignments to nullish (undef/null)
50
- // don't change ctor they're consistent with any typed-array value.
51
- // Per-candidate state in PARALLEL maps, not a shared mutable `{ctor, valid}` object
52
- // per name: `ctorOf` holds the positive ctor evidence (name absent not a candidate),
53
- // `invalid` the demoted names. A prior shape stored one mutable object per name in a
54
- // Map; under self-host those per-name objects aliased (every candidate ended up sharing
55
- // one record), so a second global of a different kind invalidated the typed one and any
56
- // two-global program lost its typed-array fast path. String/Set values can't alias.
57
- const ctorOf = new Map()
58
- const invalid = new Set()
59
- for (const name of ctx.scope.userGlobals) ctorOf.set(name, null)
47
+ // Build an assignment/alias graph over EVERY `=`/`let`/`const` binding in the
48
+ // program (globals and locals alike), then resolve each global's typed-array
49
+ // ctor by least-fixed-point. A single forward pass can't see the double-buffer
50
+ // swap idiom`let tmp = a; a = b; b = tmp` assigns `a` from `b` and `b` from
51
+ // a local `tmp` that aliases `a`, so neither ref resolves until its sibling is
52
+ // already known. The fixpoint closes that cycle: `a`/`b` each anchor on their
53
+ // `new Float64Array(...)` decl, the alias edges carry the ctor around the loop,
54
+ // and they promote to VAL.TYPED. Without it the swap poisoned both globals and
55
+ // every `a[i]` read forked __str_idx/__typed_idx, every `+` forked __str_concat.
56
+ //
57
+ // Lattice (per name): null (no evidence) < ctor < MIXED. `bad` evidence (a non-
58
+ // typed, non-alias RHS — number, string, call, arithmetic, compound-assign) jumps
59
+ // straight to MIXED; conflicting ctors join to MIXED. We promote a global only
60
+ // when its fixed point is a single concrete ctor — sound: every assignment then
61
+ // provably yields that typed-array kind or nullish.
62
+ const MIXED = MIXED_CTORS
63
+ const defs = new Map() // name → { ctors:Set<string>, refs:Set<string>, bad:bool }
64
+ const getDef = (name) => {
65
+ let d = defs.get(name)
66
+ if (!d) defs.set(name, d = { ctors: new Set(), refs: new Set(), bad: false })
67
+ return d
68
+ }
60
69
 
61
70
  const isNullishLit = (e) => e == null || e === 'undefined' || e === 'null'
62
71
  || (Array.isArray(e) && e[0] == null && (e[1] === undefined || e[1] === null))
63
72
 
64
- const observe = (name, rhs) => {
65
- if (!ctorOf.has(name) || invalid.has(name)) return
73
+ // User-function names a call to one is an alias edge to its return value
74
+ // (virtual node `@ret:<fn>`, populated from each `return`). Lets a global
75
+ // assigned `a = makeBuffer(n)` inherit makeBuffer's typed-array ctor without
76
+ // relying on the call being inlined (locals get it via inlining; globals,
77
+ // typed before inlining runs, did not). `@`/`:` can't occur in a JS identifier,
78
+ // so the virtual key never collides with a real binding.
79
+ const fnames = new Set()
80
+ for (const f of ctx.func.list) if (f.body && !f.raw && typeof f.name === 'string') fnames.add(f.name)
81
+ // Typed-array methods that preserve the receiver's element ctor: `.subarray`
82
+ // and `.slice` (same-kind view/copy), `.map` (same-kind, per propagateTyped).
83
+ const CTOR_PRESERVING = new Set(['subarray', 'slice', 'map'])
84
+
85
+ // Record one assignment `name = rhs` as evidence. Nullish contributes nothing
86
+ // (consistent with any typed-array value); a bare identifier, a ctor-preserving
87
+ // method on a name, or a call to a user function are alias edges; anything else
88
+ // that isn't a typed ctor poisons the name.
89
+ // Scope-qualified binding key. A module global is ONE node program-wide (bare
90
+ // name); a function-local is unique to its scope `sid`. Keying locals by bare
91
+ // name made a numeric counter `let s = 0` in one function poison a typed
92
+ // swap-temp `let s = a` in another — cascading MIXED into the double-buffer
93
+ // globals so every `a[i]` fell back to runtime __str_idx/__typed_idx dispatch
94
+ // (lbm: 3.9× slower than JS). `@ret:` virtual nodes stay bare (module-wide
95
+ // return-value anchors).
96
+ const key = (name, sid) => name[0] === '@' || ctx.scope.userGlobals.has(name) ? name : sid + '\x00' + name
97
+
98
+ const observe = (name, rhs, sid) => {
99
+ const d = getDef(key(name, sid))
66
100
  if (isNullishLit(rhs)) return
67
- // Resolve typed-array ctor from `new TypedArrayCtor(...)`, ternary of typed,
68
- // or a reference to a name we already know is typed.
69
- let ctor = typedElemCtor(rhs) ?? ternaryCtorOfRhs(rhs)
70
- if (ctor === MIXED_CTORS) { invalid.add(name); return }
71
- if (!ctor && typeof rhs === 'string') {
72
- if (ctx.scope.globalValTypes?.get(rhs) === VAL.TYPED)
73
- ctor = ctx.scope.globalTypedElem?.get(rhs) ?? null
101
+ const ctor = typedElemCtor(rhs) ?? ternaryCtorOfRhs(rhs)
102
+ if (ctor === MIXED) { d.bad = true; return }
103
+ if (ctor) { d.ctors.add(ctor); return }
104
+ if (typeof rhs === 'string') { d.refs.add(key(rhs, sid)); return }
105
+ if (Array.isArray(rhs) && rhs[0] === '()') {
106
+ const callee = rhs[1]
107
+ // `recv.subarray(...)` / `recv.slice(...)` / `recv.map(...)` inherit recv's ctor.
108
+ if (Array.isArray(callee) && callee[0] === '.' && typeof callee[1] === 'string'
109
+ && CTOR_PRESERVING.has(callee[2])) { d.refs.add(key(callee[1], sid)); return }
110
+ // `fn(...)` to a user function → inherit its return ctor.
111
+ if (typeof callee === 'string' && fnames.has(callee)) { d.refs.add('@ret:' + callee); return }
74
112
  }
75
- if (!ctor) { invalid.add(name); return }
76
- const prev = ctorOf.get(name)
77
- if (prev && prev !== ctor) { invalid.add(name); return }
78
- ctorOf.set(name, ctor)
113
+ d.bad = true
79
114
  }
80
115
 
81
- const walk = (node) => {
116
+ // Scope-aware walk. Every `=>` opens a fresh scope so same-named locals across
117
+ // functions (and sibling closures) stay distinct. A function bound to a name
118
+ // descends in a name-stable scope (`fn\0name`) so the ast descent and the
119
+ // func.list sweep below visit it identically (idempotent), and so `return`
120
+ // exprs anchor on `@ret:name`.
121
+ let sidc = 0
122
+ const walk = (node, sid, retFn) => {
82
123
  if (!Array.isArray(node)) return
83
124
  const op = node[0]
84
- if (op === '=' && typeof node[1] === 'string' && ctorOf.has(node[1])) observe(node[1], node[2])
125
+ if (op === '=>') { walk(node[2], 's' + (++sidc), null); return }
126
+ if (op === 'return' && retFn != null) observe('@ret:' + retFn, node[1], sid)
127
+ const assign = (name, rhs) => {
128
+ if (Array.isArray(rhs) && rhs[0] === '=>') { observe(name, rhs, sid); enterFn(rhs[2], name); return }
129
+ observe(name, rhs, sid); walk(rhs, sid, retFn)
130
+ }
131
+ if (op === '=' && typeof node[1] === 'string') return assign(node[1], node[2])
85
132
  if ((op === 'let' || op === 'const') && node.length > 1) {
86
133
  for (let i = 1; i < node.length; i++) {
87
134
  const d = node[i]
88
- if (Array.isArray(d) && d[0] === '=' && typeof d[1] === 'string' && ctorOf.has(d[1]))
89
- observe(d[1], d[2])
135
+ if (Array.isArray(d) && d[0] === '=' && typeof d[1] === 'string') assign(d[1], d[2])
136
+ else walk(d, sid, retFn)
90
137
  }
138
+ return
139
+ }
140
+ // Compound-assigns (`+=`, `++`, …) can't preserve a typed-array kind — poison.
141
+ if (ASSIGN_OPS.has(op) && typeof node[1] === 'string') { getDef(key(node[1], sid)).bad = true; walk(node[2], sid, retFn); return }
142
+ for (let i = 1; i < node.length; i++) walk(node[i], sid, retFn)
143
+ }
144
+ // Descend into a function body anchored on `@ret:fn`. An arrow expr-body IS the
145
+ // implicit return (`(n) => new Float64Array(n)`); a `{}` block uses explicit
146
+ // `return` nodes (captured in walk). Without the implicit-return capture, a
147
+ // global assigned `a = mk(n)` from an expr-body fn never inherited mk's ctor.
148
+ const enterFn = (body, fn) => {
149
+ if (Array.isArray(body) && body[0] !== '{}') observe('@ret:' + fn, body, 'fn\x00' + fn)
150
+ walk(body, 'fn\x00' + fn, fn)
151
+ }
152
+ walk(ast, 'mod', null)
153
+ // Defensive sweep: cover any func.list body not reachable by descent from `ast`
154
+ // (hoisted / submodule). Name-stable scope keeps it idempotent with the descent.
155
+ for (const f of ctx.func.list) if (f.body && !f.raw) enterFn(f.body, f.name)
156
+
157
+ // Least-fixed-point over the alias graph. join: null is bottom, MIXED is top.
158
+ const join = (a, b) => a === MIXED || b === MIXED ? MIXED : a == null ? b : b == null ? a : a === b ? a : MIXED
159
+ const state = new Map() // name → null | ctor | MIXED
160
+ // A ref to a name with no tracked defs resolves via an already-known typed
161
+ // global (const typed array / earlier-recorded rep); otherwise it's opaque → MIXED.
162
+ const refState = (r) => defs.has(r) ? (state.get(r) ?? null)
163
+ : ctx.scope.globalValTypes?.get(r) === VAL.TYPED ? (ctx.scope.globalTypedElem?.get(r) ?? MIXED)
164
+ : MIXED
165
+ let changed = true
166
+ while (changed) {
167
+ changed = false
168
+ for (const [name, d] of defs) {
169
+ let cur = d.bad ? MIXED : null
170
+ if (cur !== MIXED) for (const c of d.ctors) cur = join(cur, c)
171
+ if (cur !== MIXED) for (const r of d.refs) cur = join(cur, refState(r))
172
+ if (cur !== (state.get(name) ?? null)) { state.set(name, cur); changed = true }
91
173
  }
92
- // Compound-assigns (`+=`, etc.) to a typed-array binding can't preserve
93
- // the typed-array kind — invalidate.
94
- if (ASSIGN_OPS.has(op) && op !== '=' && typeof node[1] === 'string' && ctorOf.has(node[1]))
95
- invalid.add(node[1])
96
- for (let i = 1; i < node.length; i++) walk(node[i])
97
174
  }
98
- walk(ast)
99
- for (const f of ctx.func.list) if (f.body && !f.raw) walk(f.body)
100
175
 
101
- for (const [name, ctor] of ctorOf) {
102
- if (invalid.has(name) || !ctor) continue
176
+ for (const name of ctx.scope.userGlobals) {
177
+ const ctor = state.get(name)
178
+ if (!ctor || ctor === MIXED) continue
103
179
  if (ctx.scope.globalValTypes?.get(name) === VAL.TYPED) continue
104
180
  ;(ctx.scope.globalValTypes ||= new Map()).set(name, VAL.TYPED)
105
181
  ;(ctx.scope.globalTypedElem ||= new Map()).set(name, ctor)
@@ -23,6 +23,24 @@ export function observeNodeFacts(node, f) {
23
23
  if (PROP_WRITE_OPS.has(op) && Array.isArray(args[0]) &&
24
24
  (args[0][0] === '.' || args[0][0] === '?.') && typeof args[0][2] === 'string')
25
25
  f.writtenProps.add(args[0][2])
26
+ // Computed-key WRITES (`o[k]=v`, `o[k]+=v`, `o[k]++`) are the ONLY operations
27
+ // that add ENUMERABLE keys beyond the static schema — computed reads and dot-adds
28
+ // (`o.b=2`) do not enumerate in jz. Tracked separately from `dynVars` (which also
29
+ // counts reads) so for-in / Object.keys key pooling can trust the static schema
30
+ // for a receiver that is only computed-READ. (`isLiteralStr` excludes literal
31
+ // string keys, which place in fixed schema slots.)
32
+ if (PROP_WRITE_OPS.has(op) && Array.isArray(args[0]) && args[0][0] === '[]') {
33
+ const [, wobj, widx] = args[0]
34
+ // Flag the ROOT array var. `o[k]=v` → o; a NESTED write `o[i][j]=v` mutates an
35
+ // element of o, so walk the receiver chain to its root identifier and flag that
36
+ // too — else o's recorded (nested) element types would be wrongly trusted at a
37
+ // later `o[i][j]` read. Strictly more conservative for every dynWriteVars consumer.
38
+ if (!isLiteralStr(widx)) {
39
+ let root = wobj
40
+ while (Array.isArray(root) && root[0] === '[]') root = root[1]
41
+ if (typeof root === 'string') f.dynWriteVars?.add(root)
42
+ }
43
+ }
26
44
  if (op === '[]') {
27
45
  const [obj, idx] = args
28
46
  if (!isLiteralStr(idx)) { f.anyDyn = true; if (typeof obj === 'string') f.dynVars.add(obj) }
@@ -68,7 +86,7 @@ export function invalidateProgramFactsCache(...roots) {
68
86
 
69
87
  function emptyWalkFacts() {
70
88
  return {
71
- dynVars: new Set(), anyDyn: false, hasSchemaLiterals: false,
89
+ dynVars: new Set(), dynWriteVars: new Set(), anyDyn: false, hasSchemaLiterals: false,
72
90
  maxDef: 0, maxCall: 0, hasRest: false, hasSpread: false,
73
91
  propMap: new Map(), valueUsed: new Set(), callSites: [],
74
92
  writtenProps: new Set(),
@@ -78,6 +96,7 @@ function emptyWalkFacts() {
78
96
  function mergeWalkFacts(into, from) {
79
97
  if (from.anyDyn) into.anyDyn = true
80
98
  for (const v of from.dynVars) into.dynVars.add(v)
99
+ for (const v of from.dynWriteVars) into.dynWriteVars.add(v)
81
100
  if (from.hasSchemaLiterals) into.hasSchemaLiterals = true
82
101
  if (from.maxDef > into.maxDef) into.maxDef = from.maxDef
83
102
  if (from.maxCall > into.maxCall) into.maxCall = from.maxCall
@@ -234,7 +253,7 @@ export function collectProgramFacts(ast) {
234
253
  // ctx — a mutated prop name anywhere disqualifies sharing a static instance.
235
254
  ctx.module.writtenProps = f.writtenProps
236
255
  return {
237
- dynVars: f.dynVars, anyDyn: f.anyDyn, propMap, valueUsed, callSites,
256
+ dynVars: f.dynVars, dynWriteVars: f.dynWriteVars, anyDyn: f.anyDyn, propMap, valueUsed, callSites,
238
257
  maxDef: f.maxDef, maxCall: f.maxCall, hasRest: f.hasRest, hasSpread: f.hasSpread,
239
258
  paramReps, hasSchemaLiterals: f.hasSchemaLiterals, writtenProps: f.writtenProps,
240
259
  }
package/src/ctx.js CHANGED
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
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
+ export { HEAP, LAYOUT, PTR, ATOM, FORWARDING_MASK, nanPrefixHex, atomNanHex, ssoBitI64Hex, sliceBitI64Hex, ptrNanHex, ptrBoxPrefixBigInt, encodePtrHi, decodePtrType, decodePtrAux, ATOM_HI, oobNanLiteral, oobNanIR, followForwardingWat } from '../layout.js'
12
12
 
13
13
  // === Carrier layout ===
14
14
  // Canonical bit layout lives in layout.js (compiler-free). Re-exported above for
@@ -38,11 +38,11 @@ export { HEAP, LAYOUT, PTR, ATOM, nanPrefixHex, atomNanHex, ssoBitI64Hex, sliceB
38
38
  // |-----------|----------|---------------------------------|---------------------------|
39
39
  // | core | compile | reset, modules, inc(), emit* | emit, compile, modules |
40
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 |
41
+ // | scope | compile | analyze, compile, plan, modules, assemble | compile, emit |
42
+ // | func | function | compile, narrow, assemble | emit, modules |
43
43
  // | types | function | analyze, plan | emit, modules |
44
44
  // | schema | compile | prepare, analyze, compile | prepare, analyze, emit |
45
- // | closure | init | modules (fn plugin) | emit, compile |
45
+ // | closure | init | modules (fn plugin), plan, emit | emit, compile |
46
46
  // | runtime | compile | emit, modules | emit, compile |
47
47
  // | memory | compile | index.js | compile |
48
48
  // | error | compile | prepare, compile, emit | err() |
@@ -63,6 +63,11 @@ export { HEAP, LAYOUT, PTR, ATOM, nanPrefixHex, atomNanHex, ssoBitI64Hex, sliceB
63
63
  // narrow-phase writers: narrowSignatures (under plan) temporarily swaps
64
64
  // ctx.func.{localReps, locals, current} per-function with save/restore
65
65
  // so per-call-site signature inference sees the right scope.
66
+ // assemble-phase writers: buildStartFn (wat/assemble.js) re-owns the ctx.func frame
67
+ // (locals/stack/refinements/…) to emit the module-init `start` fn, save/restoring
68
+ // around it; the data pass also const-folds ctx.scope.globals (mut→false) and
69
+ // declares the __heap* globals. emit seeds ctx.closure.{paramTypes,paramTypedCtors}
70
+ // at direct-call sites (read by emitClosureBody); plan sets ctx.closure.{floor,width}.
66
71
  export const ctx = {
67
72
  core: {}, // emitter table + stdlib registry (seeded by reset + modules)
68
73
  module: {}, // module graph: imports, resolved sources, module-init blocks
@@ -92,9 +97,14 @@ export const ctx = {
92
97
  // the defaults; see reset() for the field list and who flips each.
93
98
  }
94
99
 
95
- /** Create a child scope via shallow flat copy (metacircular-safe: no prototype chain).
100
+ /** Create a child scope via shallow flat copy with NO prototype chain. Critical:
101
+ * `{ ...parent }` would inherit Object.prototype in V8 (jz.js), so a name-keyed lookup
102
+ * like `chain['valueOf']`/`emit['toString']` returns the inherited method instead of
103
+ * undefined — corrupting resolution of any identifier named like an Object method. The
104
+ * kernel's jz objects are already prototype-less, so this was a jz.js-ONLY footgun. A
105
+ * prototype-less dict (Object.create(null) + assign) is correct in both engines.
96
106
  * Mutations to the child do not affect the parent; lookups work via direct property access. */
97
- export const derive = (parent) => ({ ...parent })
107
+ export const derive = (parent) => Object.assign(Object.create(null), parent)
98
108
 
99
109
  /** Include stdlib names for emission. */
100
110
  export const inc = (...names) => names.forEach(n => ctx.core.includes.add(n))
@@ -133,29 +143,63 @@ export const emitter = (deps, fn) => {
133
143
  * carry it as `.argc`; bare ones expose it as the function's own `.length`. */
134
144
  export const emitArity = (h) => h?.argc ?? h?.length
135
145
 
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)
146
+ /** Register `fn` as a property-GETTER emitter for `key` — it yields a value when
147
+ * the property is *read* (`re.source`, `m.size`, `a.byteOffset`), so the `.`-read
148
+ * path fires it. (Untagged `ctx.core.emit` handlers are methods: a bare read of
149
+ * `m.values` must NOT invoke them — that would materialize a view they fire only
150
+ * from the method-call path.) Getter-ness lives in `ctx.core.getters` (a plain Set),
151
+ * NOT as a flag on the emitter closure: the self-host kernel can't reliably read a
152
+ * dynamic property off a closure returned via a dynamic-key lookup, so a closure tag
153
+ * silently read `undefined` and every getter fell through to `__dyn_get`. A Set
154
+ * key-lookup is kernel-safe. Dispatch (module/core.js) checks `ctx.core.getters.has(key)`. */
155
+ export const registerGetter = (key, fn) => {
156
+ ctx.core.emit[key] = fn
157
+ ctx.core.getters.add(key)
158
+ }
142
159
 
143
160
  /** Expand ctx.core.includes transitively via ctx.core.stdlibDeps. Call before WASM assembly.
144
161
  * Each module co-locates its own deps with its stdlib registrations at init time. */
145
162
  export function resolveIncludes() {
146
163
  const graph = ctx.core.stdlibDeps
164
+ const stdlib = ctx.core.stdlib
165
+ // Auto-derived deps: a stdlib template that calls `$__foo` (a registered stdlib
166
+ // func) depends on it, whether or not the hand-maintained `deps()` list says so.
167
+ // Scanning the *realized* template keeps the graph honest, so a missing manual
168
+ // entry can't silently drop a transitively-needed helper (the bug class the old
169
+ // blanket `inc('__mkptr','__alloc')` masked). Factory templates are realized
170
+ // (called) so feature-gated branches — `${hasExt ? '(call $__ext_prop …)' : ''}`
171
+ // — resolve before scanning; reading raw source would over-pull the dead branch.
172
+ // jz's templates are pure string builders, so realizing here (and again at
173
+ // emission) is side-effect-free. A `$__foo` naming a global (not a stdlib func)
174
+ // is skipped. Realization can fail if called before its inputs are ready — then
175
+ // we return nothing *without caching*, so a later pass retries. Memoized per compile.
176
+ const autoCache = ctx.core._autoDeps ??= new Map()
177
+ const autoDepsOf = (name) => {
178
+ let found = autoCache.get(name)
179
+ if (found !== undefined) return found
180
+ const v = stdlib[name]
181
+ let text
182
+ if (typeof v === 'string') text = v
183
+ else if (typeof v === 'function') { try { text = v() } catch { return [] } }
184
+ if (typeof text !== 'string') return (autoCache.set(name, []), [])
185
+ found = []
186
+ const seen = new Set()
187
+ for (const m of text.matchAll(/\$(__[A-Za-z0-9_]+)/g)) {
188
+ const d = m[1]
189
+ if (d !== name && stdlib[d] && !seen.has(d)) { seen.add(d); found.push(d) }
190
+ }
191
+ autoCache.set(name, found)
192
+ return found
193
+ }
147
194
  let changed = true
148
195
  while (changed) {
149
196
  changed = false
150
197
  for (const name of [...ctx.core.includes]) {
151
198
  const entry = graph[name]
152
199
  const deps = typeof entry === 'function' ? entry() : entry
153
- if (deps) for (const dep of deps) {
154
- if (!ctx.core.includes.has(dep)) {
155
- ctx.core.includes.add(dep)
156
- changed = true
157
- }
158
- }
200
+ const add = (dep) => { if (!ctx.core.includes.has(dep)) { ctx.core.includes.add(dep); changed = true } }
201
+ if (deps) for (const dep of deps) add(dep)
202
+ for (const dep of autoDepsOf(name)) add(dep)
159
203
  }
160
204
  }
161
205
  }
@@ -180,6 +224,13 @@ export function reset(proto, globals, bridge) {
180
224
  // `(import "env" "name" (global $name i64))` at assembly. Same
181
225
  // usage-gated pattern as jsstring — emit records, assembly owns
182
226
  // the ctx.module.imports write.
227
+ getters: new Set(), // keys of emit entries that are property getters — the
228
+ // kernel-safe authority for getter dispatch (a closure-attached
229
+ // flag was unreadable in the self-host kernel after a dynamic-key
230
+ // lookup, so every getter silently fell through to __dyn_get).
231
+ // MUST remain last: adding fields before stdlib/stdlibDeps/… shifts
232
+ // their slot indices and breaks the self-host compiled kernel's reads.
233
+ // Populated by registerGetter(); checked by module/core.js dispatch.
183
234
  }
184
235
 
185
236
 
@@ -207,6 +258,10 @@ export function reset(proto, globals, bridge) {
207
258
  globalTypedElem: null,
208
259
  globalReps: null, // Map<name, ValueRep> — module-level pointer reps (TYPED const globals stored as raw i32 offset, etc.)
209
260
  consts: null,
261
+ constInts: null, // Map<name, int> — module const folded to an integer literal (prepare/plan seed; static/ir read)
262
+ constStrs: null, // Map<name, string> — module const folded to a string literal
263
+ shapeStrs: null, // Map<expr, string> / shapeStrArrays: Map<name, string[]> — schema-shape string folds
264
+ shapeStrArrays: null,
210
265
  }
211
266
 
212
267
  ctx.func = {
@@ -214,7 +269,7 @@ export function reset(proto, globals, bridge) {
214
269
  names: new Set(), // Set<string> — known func names (list + imported funcs); populated at compile() start
215
270
  map: new Map(), // Map<string, func> — name → func entry; populated at compile() start
216
271
  multiProp: new Set(), // Set<"obj.prop"> — function-properties assigned >1× (wrapper composition); suppresses the static fn.prop() direct call
217
- exports: {},
272
+ exports: Object.create(null), // name-keyed: prototype-less (see derive) — `export let valueOf` must not hit Object.prototype
218
273
  current: null,
219
274
  locals: new Map(),
220
275
  localReps: null,
@@ -241,6 +296,7 @@ export function reset(proto, globals, bridge) {
241
296
  ctx.types = {
242
297
  typedElem: null,
243
298
  dynKeyVars: null,
299
+ dynWriteVars: null,
244
300
  anyDynKey: false,
245
301
  }
246
302
 
@@ -296,6 +352,8 @@ export function reset(proto, globals, bridge) {
296
352
  minArgc: null, // Map<closureBodyName, number> — fewest args any direct call passed.
297
353
  // A slot at index ≥ minArgc is omitted by some call (→ may be undefined),
298
354
  // so it must NOT be typed NUMBER, else `x === undefined` mis-folds to false.
355
+ floor: null, // min closure-table arity (modules: fn/timer/typedarray/array; read in plan). null ⇒ 0.
356
+ width: null, // closure call/make signature width (plan/scope sets; emit/assemble read). null ⇒ MAX_CLOSURE_ARITY.
299
357
  }
300
358
 
301
359
  ctx.runtime = {
@@ -316,6 +374,7 @@ export function reset(proto, globals, bridge) {
316
374
  ctx.memory = {
317
375
  shared: false,
318
376
  pages: 0,
377
+ max: 0, // 0 = unbounded; >0 emits a maximum on the memory type (cap growth)
319
378
  }
320
379
 
321
380
  ctx.error = {
@@ -341,6 +400,15 @@ export function reset(proto, globals, bridge) {
341
400
  inspect: false, // when true, compile() additionally populates ctx.inspect with the inferred
342
401
  // per-function signatures, locals, and JSON shapes — readable by editor
343
402
  // hosts for inlay hints / hover types without re-running the analyzer.
403
+ helperCounters: false, // internal profiling mode: export mutable i64 counters for selected
404
+ // runtime helpers and instrument their entry blocks. Build-time opt-in
405
+ // only; normal output is byte-identical and pays no counter cost.
406
+ helperCallsites: false, // profiling-only: export mutable i64 counters for selected runtime
407
+ // helper callsites after optimization, so hot helpers can be traced
408
+ // back to the compiled function that calls them.
409
+ loopXformId: 0, // monotonic id for the per-function loop transforms' generated locals
410
+ // (loop-model freshLoopId). Per-compile (reset here), not a module-global —
411
+ // so compile(P) is deterministic regardless of prior compiles in the process.
344
412
  }
345
413
 
346
414
  // Inspection sink. Populated by compile() only when transform.inspect is true.
@@ -440,6 +508,15 @@ export function warn(code, message, meta = {}, loc = null) {
440
508
  ctx.warnings.sink.entries.push(entry)
441
509
  }
442
510
 
511
+ /** Advise that an emit site fell back to generic runtime dispatch (the slow,
512
+ * un-inferred path). Called from the actual emission point so it fires only when
513
+ * inference/optimization truly couldn't fold it — never a false positive on a
514
+ * case that vectorized/unrolled/slot-folded. `ctx.error.loc` is the current AST
515
+ * node's byte offset (kept up to date by the emit walk), giving line/column. */
516
+ export function warnDeopt(code, message) {
517
+ warn(code, message, { fn: ctx.func.current?.name }, ctx.error.loc)
518
+ }
519
+
443
520
  /** Throw with source location context. */
444
521
  export function err(msg, cause) {
445
522
  let detail = msg
@@ -0,0 +1,137 @@
1
+ import { ctx, declGlobal, inc } from './ctx.js'
2
+ import { findBodyStart } from './ir.js'
3
+
4
+ export const HELPER_COUNTERS = [
5
+ ['__eq', 'eq'],
6
+ ['__same_value_zero', 'same_value_zero'],
7
+ ['__is_truthy', 'is_truthy'],
8
+ ['__ptr_type', 'ptr_type'],
9
+ ['__ptr_offset', 'ptr_offset'],
10
+ ['__len', 'len'],
11
+ ['__length', 'length'],
12
+ ['__typed_idx', 'typed_idx'],
13
+ ['__str_len', 'str_len'],
14
+ ['__str_eq', 'str_eq'],
15
+ ['__str_eq_cold', 'str_eq_cold'],
16
+ ['__str_hash', 'str_hash'],
17
+ ['__map_hash', 'map_hash'],
18
+ ['__hash_get_local', 'hash_get_local'],
19
+ ['__hash_set_local', 'hash_set_local'],
20
+ ['__ihash_get_local', 'ihash_get_local'],
21
+ ['__ihash_set_local', 'ihash_set_local'],
22
+ ['__dyn_get', 'dyn_get'],
23
+ ['__dyn_get_t', 'dyn_get_t'],
24
+ ['__dyn_get_t_h', 'dyn_get_t_h'],
25
+ ['__dyn_set', 'dyn_set'],
26
+ ['__arr_grow', 'arr_grow'],
27
+ ['__arr_grow_known', 'arr_grow_known'],
28
+ ['__arr_push1', 'arr_push1'],
29
+ ['__arr_shift', 'arr_shift'],
30
+ ['__alloc', 'alloc'],
31
+ ['__alloc_hdr', 'alloc_hdr'],
32
+ ['__alloc_hdr_n', 'alloc_hdr_n'],
33
+ ['__memgrow', 'memgrow'],
34
+ ]
35
+
36
+ const COUNTER_BY_HELPER = new Map(HELPER_COUNTERS.map(([helper, label]) => [helper, `__hc_${label}`]))
37
+ const LABEL_BY_WAT_HELPER = new Map(HELPER_COUNTERS.map(([helper, label]) => [`$${helper}`, label]))
38
+
39
+ export const HELPER_SITE_PREFIX = '__hcs_'
40
+
41
+ export const helperCounterName = helper => COUNTER_BY_HELPER.get(helper)
42
+
43
+ export function installHelperCounters() {
44
+ if (!ctx.transform.helperCounters) return
45
+ for (const counter of COUNTER_BY_HELPER.values()) {
46
+ if (!ctx.scope.globals.has(counter)) declGlobal(counter, 'i64', 0, { export: counter })
47
+ }
48
+ ctx.core.stdlib.__helper_counts_reset = `(func $__helper_counts_reset (export "__helper_counts_reset")
49
+ ${[...COUNTER_BY_HELPER.values()].map(counter => ` (global.set $${counter} (i64.const 0))`).join('\n')})`
50
+ inc('__helper_counts_reset')
51
+ }
52
+
53
+ // Bump the helper's counter once on entry. NOTE the semantics: this counts FUNCTION
54
+ // ENTRIES at runtime — a call site that jz inlined or specialized away (fusedRewrite,
55
+ // specializeMkptr/specializePtrBase, …) never enters the function and is NOT counted. So
56
+ // the numbers are a relative ranking / lower bound for picking hot helpers, not exact
57
+ // operation counts. Good enough to choose targets; don't read them as call totals.
58
+ export function instrumentHelperCounter(helper, fn) {
59
+ const counter = ctx.transform.helperCounters && helperCounterName(helper)
60
+ if (!counter || !Array.isArray(fn) || fn[0] !== 'func') return fn
61
+ fn.splice(findBodyStart(fn), 0,
62
+ ['global.set', `$${counter}`, ['i64.add', ['global.get', `$${counter}`], ['i64.const', 1]]])
63
+ return fn
64
+ }
65
+
66
+ const safeExportPart = name => String(name || 'anon')
67
+ .replace(/^\$/, '')
68
+ .replace(/[^A-Za-z0-9_.-]+/g, '_')
69
+ .slice(0, 80) || 'anon'
70
+
71
+ const helperSiteFilter = () => {
72
+ const opt = ctx.transform.helperCallsites
73
+ if (opt === true) return null
74
+ const raw = Array.isArray(opt) ? opt : String(opt || '').split(',')
75
+ const labels = raw.map(s => String(s).trim()).filter(Boolean).map(s => s.replace(/^\$?__/, ''))
76
+ return labels.length ? new Set(labels) : null
77
+ }
78
+
79
+ const funcResults = fn => {
80
+ const out = []
81
+ if (!Array.isArray(fn) || fn[0] !== 'func') return out
82
+ for (let i = 2; i < fn.length; i++) {
83
+ const n = fn[i]
84
+ if (Array.isArray(n) && n[0] === 'result') out.push(...n.slice(1))
85
+ }
86
+ return out
87
+ }
88
+
89
+ const bumpCounter = counter =>
90
+ ['global.set', `$${counter}`, ['i64.add', ['global.get', `$${counter}`], ['i64.const', 1]]]
91
+
92
+ // Profiling-only helper-callsite counters. Unlike instrumentHelperCounter(), this
93
+ // answers "which compiled function executed the helper call?" by wrapping each
94
+ // final `(call $__helper ...)` with a tiny counter block:
95
+ // (block (result T) (global.set $__hcs_N ...) (call $__helper ...))
96
+ //
97
+ // This intentionally runs after whole-module optimization. The profile should
98
+ // observe final codegen, while production output remains byte-identical because
99
+ // ctx.transform.helperCallsites is build-time opt-in.
100
+ export function instrumentHelperCallsites(funcs) {
101
+ if (!ctx.transform.helperCallsites) return 0
102
+ const only = helperSiteFilter()
103
+
104
+ const resultsByName = new Map()
105
+ for (const fn of funcs) {
106
+ if (Array.isArray(fn) && fn[0] === 'func' && typeof fn[1] === 'string')
107
+ resultsByName.set(fn[1], funcResults(fn))
108
+ }
109
+
110
+ let id = 0
111
+ const wrap = (node, owner) => {
112
+ if (!Array.isArray(node)) return node
113
+ for (let i = 1; i < node.length; i++) node[i] = wrap(node[i], owner)
114
+
115
+ if (node[0] !== 'call' || typeof node[1] !== 'string') return node
116
+ const label = LABEL_BY_WAT_HELPER.get(node[1])
117
+ if (!label) return node
118
+ if (only && !only.has(label) && !only.has(node[1].replace(/^\$?__/, ''))) return node
119
+ const results = resultsByName.get(node[1])
120
+ if (!results) return node
121
+
122
+ const counter = `${HELPER_SITE_PREFIX}${id++}`
123
+ const ownerPart = safeExportPart(owner)
124
+ declGlobal(counter, 'i64', 0, { export: `${counter}:${label}:${ownerPart}` })
125
+ const block = ['block']
126
+ for (const type of results) block.push(['result', type])
127
+ block.push(bumpCounter(counter), node)
128
+ return block
129
+ }
130
+
131
+ for (const fn of funcs) {
132
+ if (!Array.isArray(fn) || fn[0] !== 'func' || typeof fn[1] !== 'string') continue
133
+ const bodyStart = findBodyStart(fn)
134
+ for (let i = bodyStart; i < fn.length; i++) fn[i] = wrap(fn[i], fn[1])
135
+ }
136
+ return id
137
+ }