jz 0.2.0 → 0.3.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
@@ -32,7 +32,7 @@ JZ distills the modern functional core – the "good parts" ([Crockford](https:/
32
32
  | Parsing / transforms | Async / I/O-heavy logic |
33
33
  | WASM utilities | JavaScript runtime |
34
34
 
35
- Inspired by [porffor](https://github.com/CanadaHonk/porffor) and [piezo](https://github.com/dy/piezo).
35
+ <!-- Inspired by [porffor](https://github.com/CanadaHonk/porffor) and [piezo](https://github.com/dy/piezo). -->
36
36
  <!-- Used internally by: web-audio-api, color-space, audiojs -->
37
37
 
38
38
 
@@ -66,9 +66,9 @@ Options are passed as `jz(source, opts)` or `compile(source, opts)`. Common ones
66
66
  | `jzify: true` | Accept broader JS patterns such as `var`, `function`, `switch`, `arguments`, `==`, and `undefined` by lowering them to the JZ subset. |
67
67
  | `modules: { specifier: source }` | Bundle static ES imports into one WASM module. CLI import resolution does this from files automatically. |
68
68
  | `imports: { mod: host }` | Wire host namespaces/functions used by `import { fn } from "mod"`; functions may be plain JS functions or `{ fn, returns }` specs. |
69
- | `memory` | Pass `memory: N` to create owned memory with `N` initial pages, or pass `memory: jz.memory()` / `WebAssembly.Memory` to share memory across modules. `memoryPages` remains as a legacy alias for the page count. |
69
+ | `memory` | Pass `memory: N` to create owned memory with `N` initial pages, or pass `memory: jz.memory()` / `WebAssembly.Memory` to share memory across modules. |
70
70
  | `host: 'js' \| 'wasi'` | Select runtime-service lowering. Default `js` uses small `env.*` imports auto-wired by `jz()`; `wasi` emits WASI Preview 1 imports for wasmtime/wasmer/deno. |
71
- | `optimize` | `false`/`0` disables optimization, `1` keeps cheap size passes, `true`/`2` is the default, `3` enables aggressive experimental passes, object form overrides individual passes. |
71
+ | `optimize` | `false`/`0` disables optimization, `1` keeps cheap size passes, `true`/`2` is the default, `3` enables aggressive experimental passes. String aliases `'size'` (unroll/vectorize off, tight scalar caps — smallest wasm), `'balanced'` (= default), `'speed'` (full unroll + SIMD). Object form overrides individual passes/knobs (and accepts `level:` as a number or alias base). |
72
72
  | `strict: true` | Reject dynamic fallbacks such as unknown receiver method calls, `obj[k]`, and `for-in` instead of emitting JS-host dynamic dispatch. |
73
73
  | `alloc: false` | Omit raw allocator exports like `_alloc`/`_clear` when compiling standalone WASM that never marshals heap values across the host boundary. |
74
74
  | `wat: true` | `compile()` returns WAT text instead of a WASM binary. |
@@ -95,29 +95,6 @@ jz --help
95
95
 
96
96
  JZ is a strict functional JS subset. Built-in `jzify` transform extends support to legacy patterns.
97
97
 
98
- <!--
99
- ```mermaid
100
- %%{init: {'flowchart': {'titleTopMargin': 0, 'padding': 0, 'margin': 0}}}%%
101
- flowchart TB
102
- classDef plain fill:none,stroke:none,font-size:14px,font-weight:bold,padding:0px,margin:0px
103
-
104
- subgraph JS[JS — not supported]
105
- subgraph JZify[JZ + jzify]
106
- subgraph JZ[JZ strict]
107
- j1["let/const, arrows, default/rest params, flow, break/continue, try/catch/finally, a[]/a()/a.b, operators, strings, booleans, numbers, std, memory, host"]:::plain
108
- end
109
- z1["var, function, arguments, switch, new Foo(), ==, !=, instanceof"]:::plain
110
- end
111
- n1["async/await, Promise, generators, this, class, eval, Function, with, Proxy, Reflect, WeakMap, WeakSet, dynamic import, DOM, fetch, Intl, Node APIs"]:::plain
112
- end
113
-
114
- style JZ fill:#ffe0b2,stroke-width:0
115
- style JZify fill:#fff9c4,stroke-width:0
116
- style JS fill:#ffffff,stroke:#ccc,stroke-width:1px
117
- style n1 min-width:720px
118
- ```
119
- -->
120
-
121
98
  ```
122
99
  ┌────────────────────────────────────────────────────────────────────────┐
123
100
  │ JZify │
@@ -151,6 +128,10 @@ Not supported
151
128
 
152
129
  Numbers pass directly as f64, arrays of ≤ 8 elements return as plain JS arrays (multi-value). Strings, arrays, objects, and typed arrays are heap values — `inst.memory` provides read/write across the boundary:
153
130
 
131
+ > [!WARNING] jz objects are fixed-layout schemas (like C structs), not dynamic key bags.
132
+ > `memory.Object({ x: 3, y: 4 })` expects the same key order as the jz source `{ x, y }`.
133
+ > `{ y: 4, x: 3 }` with reversed keys will produce wrong values.
134
+
154
135
  ```js
155
136
  const { exports, memory } = jz`
156
137
  export let greet = (s) => s.length
@@ -494,8 +475,7 @@ cc program.c -o program
494
475
  | [bitwise](bench/bitwise/bitwise.js) | 0.98ms<br>1.3kB | 3.76ms<br>1005 B | fails | 8.79ms<br>1.5kB | 4.86ms<br>355 B | 1.30ms | 5.20ms | 4.15ms | 1.30ms | 14.72ms |
495
476
  | [poly](bench/poly/poly.js) | 0.27ms<br>1.4kB | 1.62ms<br>1014 B | fails | 0.73ms<br>1.3kB | 0.81ms<br>359 B | 0.57ms | 0.79ms | 0.89ms | 0.63ms | 0.60ms |
496
477
  | [callback](bench/callback/callback.js) | 0.03ms<br>1.6kB | 0.69ms<br>828 B | fails | 1.04ms<br>1.9kB | 0.24ms<br>267 B | 0.08ms | 0.23ms | 0.01ms | 0.12ms | 1.78ms |
497
- | [json](bench/json/json.js) | 0.24ms<br>11.0kB | 0.37ms<br>1.2kB | fails | — | — | 0.03ms | 1.05ms | <0.01ms | 0.03ms | 1.21ms |
498
- | [json-dynamic](bench/json-dynamic/json-dynamic.js) | 0.24ms<br>11.3kB | 0.39ms<br>1.2kB | — | — | — | — | — | — | — | — |
478
+ | [json](bench/json/json.js) | 0.25ms<br>10.9kB | 0.36ms<br>1.2kB | fails | — | — | 0.25ms | 1.16ms | 0.64ms | 0.65ms | 1.20ms |
499
479
  | [watr](bench/watr/watr.js) | 1.04ms<br>169.8kB | 1.05ms<br>2.6kB | fails | — | — | — | — | — | — | — |
500
480
 
501
481
  _Numbers from `node bench/bench.mjs` on Apple Silicon. Porffor cells were refreshed with `porf` 0.61.13; `fails` means the latest Porffor compiler/runtime did not complete that benchmark._
package/cli.js CHANGED
@@ -11,10 +11,14 @@ import { execFileSync } from 'child_process'
11
11
  import { parse } from 'subscript/feature/jessie'
12
12
  import jz, { compile } from './index.js'
13
13
  import jzifyFn, { codegen } from './src/jzify.js'
14
+ import { createRequire } from 'module'
15
+
16
+ const jzRequire = createRequire(import.meta.url)
17
+ const PKG = jzRequire('./package.json')
14
18
 
15
19
  function showHelp() {
16
20
  console.log(`
17
- jz - min JS → WASM compiler
21
+ jz v${PKG.version} - min JS → WASM compiler
18
22
 
19
23
  Usage:
20
24
  jz <file.js> Compile JS to WASM (auto-jzify)
@@ -39,12 +43,19 @@ Options:
39
43
  --eval, -e Evaluate expression or file
40
44
  --wat Output WAT text instead of binary
41
45
  --resolve Resolve bare specifiers via Node.js module resolution
46
+ --imports <file> JSON file with host import specs (e.g. {"env":{"fn":{"params":2}}})
47
+ --version, -v Show version number
42
48
  `)
43
49
  }
44
50
 
45
51
  async function main() {
46
52
  const args = process.argv.slice(2)
47
53
 
54
+ if (args.includes('--version') || args.includes('-v')) {
55
+ console.log(PKG.version)
56
+ return
57
+ }
58
+
48
59
  if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
49
60
  showHelp()
50
61
  return
@@ -99,13 +110,14 @@ async function handleJzify(args) {
99
110
  }
100
111
 
101
112
  async function handleCompile(args) {
102
- let inputFile = null, outputFile = null, wat = false, strict = false, resolveNode = false
113
+ let inputFile = null, outputFile = null, wat = false, strict = false, resolveNode = false, importsFile = null
103
114
 
104
115
  for (let i = 0; i < args.length; i++) {
105
116
  if (args[i] === '--output' || args[i] === '-o') outputFile = args[++i]
106
117
  else if (args[i] === '--wat') wat = true
107
118
  else if (args[i] === '--strict') strict = true
108
119
  else if (args[i] === '--resolve') resolveNode = true
120
+ else if (args[i] === '--imports') importsFile = args[++i]
109
121
  else if (!inputFile) inputFile = args[i]
110
122
  }
111
123
 
@@ -170,6 +182,11 @@ async function handleCompile(args) {
170
182
  ...(Object.keys(modules).length && { modules }),
171
183
  }
172
184
 
185
+ if (importsFile) {
186
+ const importsPath = resolve(importsFile)
187
+ opts.imports = JSON.parse(readFileSync(importsPath, 'utf8'))
188
+ }
189
+
173
190
  const result = compile(code, opts)
174
191
 
175
192
  if (outputFile === '-') {
package/index.js CHANGED
@@ -45,6 +45,7 @@ import prepare, { GLOBALS } from './src/prepare.js'
45
45
  import compile from './src/compile.js'
46
46
  import { emitter } from './src/emit.js'
47
47
  import { optimizeFunc, resolveOptimize } from './src/optimize.js'
48
+ import { detectOptimizeConfig } from './src/auto-config.js'
48
49
  import jzify from './src/jzify.js'
49
50
  import {
50
51
  memory as enhanceMemory, instantiate as instantiateRuntime,
@@ -163,8 +164,6 @@ jz.memory = enhanceMemory
163
164
  * receiver method calls) at compile time. Avoids pulling dynamic-dispatch stdlib
164
165
  * into output; large size win for static programs.
165
166
  * @param {WebAssembly.Memory|number} [opts.memory] - Shared memory import, or
166
- * initial page count for owned memory when a number is passed.
167
- * @param {number} [opts.memoryPages] - Legacy alias for owned/imported memory
168
167
  * initial page count. Prefer `memory: N` for owned memory.
169
168
  * @param {boolean} [opts.alloc=true] - Export raw allocator helpers
170
169
  * (`_alloc`, `_clear`) for JS memory wrapping. Set false for standalone host-run
@@ -194,7 +193,6 @@ jz.compile = (code, opts = {}) => {
194
193
 
195
194
  if (typeof opts.memory === 'number') ctx.memory.pages = opts.memory
196
195
  else if (opts.memory) ctx.memory.shared = true
197
- if (opts.memoryPages) ctx.memory.pages = opts.memoryPages
198
196
  if (opts.modules) ctx.module.importSources = opts.modules
199
197
  if (opts.imports) {
200
198
  ctx.module.hostImports = opts.imports
@@ -227,6 +225,16 @@ jz.compile = (code, opts = {}) => {
227
225
  let parsed = time('parse', () => parse(code))
228
226
  if (opts.jzify) parsed = time('jzify', () => jzify(parsed))
229
227
  const ast = time('prepare', () => prepare(parsed))
228
+
229
+ // Auto-detect optimization tuning from source characteristics when the user
230
+ // hasn't provided any optimize option.
231
+ if (opts.optimize == null) {
232
+ const autoCfg = detectOptimizeConfig(ast, code)
233
+ if (Object.keys(autoCfg).length) {
234
+ ctx.transform.optimize = resolveOptimize(autoCfg)
235
+ }
236
+ }
237
+
230
238
  const module = time('compile', () => compile(ast, profiler))
231
239
 
232
240
  // host: 'wasi' — error if the wasm would import any env.__ext_* helper. Those exist
@@ -252,8 +260,10 @@ jz.compile = (code, opts = {}) => {
252
260
  // Only valuable to re-run when watr ran (watr is what re-introduces the boundaries).
253
261
  if (cfg.watr) {
254
262
  const postCfg = { ...cfg, __phase: 'post' }
263
+ // Build global name→type map from ctx.scope.globalTypes for promoteGlobals
264
+ const globalTypesMap = ctx.scope.globalTypes ? new Map([...ctx.scope.globalTypes].map(([k, v]) => [`$${k}`, v])) : null
255
265
  time('watrReopt', () => {
256
- for (const node of optimized) if (Array.isArray(node) && node[0] === 'func') optimizeFunc(node, postCfg)
266
+ for (const node of optimized) if (Array.isArray(node) && node[0] === 'func') optimizeFunc(node, postCfg, globalTypesMap)
257
267
  })
258
268
  }
259
269
  if (opts.wat) return time('watrPrint', () => watrPrint(optimized))
package/module/array.js CHANGED
@@ -225,7 +225,8 @@ export default (ctx) => {
225
225
  '__ptr_offset',
226
226
  ...(needsArrayDynMove() ? ['__dyn_move', '__hash_new', '__ihash_set_local'] : []),
227
227
  ],
228
- __arr_set_idx_ptr: ['__arr_grow', '__ptr_offset', '__set_len'],
228
+ __arr_set_idx_ptr: ['__arr_grow', '__ptr_offset'],
229
+ __arr_push1: ['__arr_grow_known', '__ptr_offset'],
229
230
  __typed_idx: () => ctx.features.typedarray || ctx.features.external
230
231
  ? ['__len']
231
232
  : ['__len', '__ptr_offset'],
@@ -247,6 +248,16 @@ export default (ctx) => {
247
248
  ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${t}`]]], ['i32.const', PTR.ARRAY]]], 'i32')
248
249
  }
249
250
 
251
+ ctx.core.emit['new.Array'] = (len) => {
252
+ const n = tempI32('alen')
253
+ const nIR = ['local.get', `$${n}`]
254
+ const out = allocPtr({ type: PTR.ARRAY, len: nIR, cap: nIR, tag: 'newarr' })
255
+ return typed(['block', ['result', 'f64'],
256
+ ['local.set', `$${n}`, len == null ? ['i32.const', 0] : asI32(emit(len))],
257
+ out.init,
258
+ out.ptr], 'f64')
259
+ }
260
+
250
261
  // ARRAY-only indexed read. Inline forwarding-follow + bounds check + load — avoids
251
262
  // the redundant double pass through __len then __ptr_offset that both follow forwarding.
252
263
  ctx.core.stdlib['__arr_idx'] = `(func $__arr_idx (param $ptr i64) (param $i i32) (result f64)
@@ -313,8 +324,21 @@ export default (ctx) => {
313
324
  return `(func $__typed_idx (param $ptr i64) (param $i i32) (result f64)
314
325
  (local $t i32) (local $off i32) (local $et i32) (local $len i32) (local $aux i32)
315
326
  (local.set $t (i32.wrap_i64 (i64.and (i64.shr_u (local.get $ptr) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
316
- (local.set $aux (i32.wrap_i64 (i64.and (i64.shr_u (local.get $ptr) (i64.const ${LAYOUT.AUX_SHIFT})) (i64.const ${LAYOUT.AUX_MASK}))))
317
327
  (local.set $off (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ${LAYOUT.OFFSET_MASK}))))
328
+ ;; ARRAY fast path: follow forwarding inline, bounds-check against header len, f64.load — no $__len call.
329
+ (if (i32.and (i32.eq (local.get $t) (i32.const ${PTR.ARRAY})) (i32.ge_u (local.get $off) (i32.const 8)))
330
+ (then
331
+ (block $done
332
+ (loop $follow
333
+ (br_if $done (i32.gt_u (local.get $off) (i32.shl (memory.size) (i32.const 16))))
334
+ (br_if $done (i32.ne (i32.load (i32.sub (local.get $off) (i32.const 4))) (i32.const -1)))
335
+ (local.set $off (i32.load (i32.sub (local.get $off) (i32.const 8))))
336
+ (br $follow)))
337
+ (return (if (result f64)
338
+ (i32.and (i32.ge_s (local.get $i) (i32.const 0)) (i32.lt_u (local.get $i) (i32.load (i32.sub (local.get $off) (i32.const 8)))))
339
+ (then (f64.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3)))))
340
+ (else (f64.const nan:${UNDEF_NAN}))))))
341
+ (local.set $aux (i32.wrap_i64 (i64.and (i64.shr_u (local.get $ptr) (i64.const ${LAYOUT.AUX_SHIFT})) (i64.const ${LAYOUT.AUX_MASK}))))
318
342
  (if
319
343
  (i32.and
320
344
  (i32.eq (local.get $t) (i32.const ${PTR.TYPED}))
@@ -461,13 +485,29 @@ export default (ctx) => {
461
485
  (i32.load (i32.sub (local.get $base) (i32.const 8))))
462
486
  (then
463
487
  (local.set $p (call $__arr_grow (local.get $ptr) (i32.add (local.get $i) (i32.const 1))))
464
- (call $__set_len (i64.reinterpret_f64 (local.get $p)) (i32.add (local.get $i) (i32.const 1)))
465
- (local.set $base (call $__ptr_offset (i64.reinterpret_f64 (local.get $p))))))
488
+ (local.set $base (call $__ptr_offset (i64.reinterpret_f64 (local.get $p))))
489
+ (i32.store (i32.sub (local.get $base) (i32.const 8)) (i32.add (local.get $i) (i32.const 1)))))
466
490
  (f64.store
467
491
  (i32.add (local.get $base) (i32.shl (local.get $i) (i32.const 3)))
468
492
  (local.get $val))
469
493
  (local.get $p))`
470
494
 
495
+ // Out-of-line .push(val) for known-ARRAY receivers — keeps each call site to a
496
+ // single call + var update instead of ~30 inlined instructions. Returns the
497
+ // (possibly relocated) array pointer; caller derives the new length if needed.
498
+ ctx.core.stdlib['__arr_push1'] = `(func $__arr_push1 (param $ptr i64) (param $val f64) (result f64)
499
+ (local $p f64) (local $base i32) (local $len i32)
500
+ (local.set $p (f64.reinterpret_i64 (local.get $ptr)))
501
+ (local.set $base (call $__ptr_offset (local.get $ptr)))
502
+ (local.set $len (i32.load (i32.sub (local.get $base) (i32.const 8))))
503
+ (if (i32.lt_s (i32.load (i32.sub (local.get $base) (i32.const 4))) (i32.add (local.get $len) (i32.const 1)))
504
+ (then
505
+ (local.set $p (call $__arr_grow_known (local.get $ptr) (i32.add (local.get $len) (i32.const 1))))
506
+ (local.set $base (call $__ptr_offset (i64.reinterpret_f64 (local.get $p))))))
507
+ (f64.store (i32.add (local.get $base) (i32.shl (local.get $len) (i32.const 3))) (local.get $val))
508
+ (i32.store (i32.sub (local.get $base) (i32.const 8)) (i32.add (local.get $len) (i32.const 1)))
509
+ (local.get $p))`
510
+
471
511
  // === Array literal ===
472
512
 
473
513
  ctx.core.emit['['] = (...elems) => {
@@ -725,6 +765,21 @@ export default (ctx) => {
725
765
 
726
766
  // .push(val) → append, increment len, return array (possibly reallocated pointer)
727
767
  ctx.core.emit['.push'] = (arr, ...vals) => {
768
+ // Out-of-line fast path: single value, named known-ARRAY receiver. One call +
769
+ // var update instead of ~30 inlined instructions — the dominant size cost of
770
+ // push-heavy code (e.g. watr's WASM emitter).
771
+ if (vals.length === 1 && typeof arr === 'string' && lookupValType(arr) === VAL.ARRAY) {
772
+ inc('__arr_push1')
773
+ const box = ctx.func.boxed?.get(arr)
774
+ const isGlobal = !box && ctx.scope.globals.has(arr) && !ctx.func.locals?.has(arr)
775
+ const readVar = box ? ['f64.load', ['local.get', `$${box}`]] : isGlobal ? ['global.get', `$${arr}`] : ['local.get', `$${arr}`]
776
+ const writeVar = v => box ? ['f64.store', ['local.get', `$${box}`], v] : isGlobal ? ['global.set', `$${arr}`, v] : ['local.set', `$${arr}`, v]
777
+ const vv = asF64(emit(vals[0]))
778
+ return typed(['block', ['result', 'f64'],
779
+ writeVar(['call', '$__arr_push1', ['i64.reinterpret_f64', readVar], vv]),
780
+ ['f64.convert_i32_s', ['i32.load', ['i32.sub',
781
+ ['call', '$__ptr_offset', ['i64.reinterpret_f64', readVar]], ['i32.const', 8]]]]], 'f64')
782
+ }
728
783
  const va = asF64(emit(arr))
729
784
  const t = temp('pp'), len = tempI32('pl')
730
785
 
@@ -779,8 +834,10 @@ export default (ctx) => {
779
834
  )
780
835
  }
781
836
 
782
- // Update length header, update source variable (pointer may have changed from grow), return new length
783
- body.push(['call', '$__set_len', ['i64.reinterpret_f64', ['local.get', `$${t}`]], ['local.get', `$${len}`]])
837
+ // Update length header (write directly via the offset we already hold
838
+ // skips __set_len's tag/forward dispatch), update source variable (pointer
839
+ // may have changed from grow), return new length
840
+ body.push(['i32.store', ['i32.sub', ['local.get', `$${pushBase}`], ['i32.const', 8]], ['local.get', `$${len}`]])
784
841
  // Update the source variable if it's a named variable (so arr still points to valid memory)
785
842
  if (typeof arr === 'string') {
786
843
  if (ctx.func.boxed?.has(arr)) {
@@ -898,8 +955,8 @@ export default (ctx) => {
898
955
  ['i32.shl',
899
956
  ['i32.sub', ['i32.sub', ['local.get', `$${len}`], ['local.get', `$${s}`]], ['local.get', `$${cnt}`]],
900
957
  ['i32.const', 3]]],
901
- // update length
902
- ['call', '$__set_len', ['i64.reinterpret_f64', va], ['i32.sub', ['local.get', `$${len}`], ['local.get', `$${cnt}`]]],
958
+ // update length (write directly via the offset we already hold)
959
+ ['i32.store', ['i32.sub', ['local.get', `$${off}`], ['i32.const', 8]], ['i32.sub', ['local.get', `$${len}`], ['local.get', `$${cnt}`]]],
903
960
  out.ptr,
904
961
  ]
905
962
  return typed(['block', ['result', 'f64'], ...body], 'f64')
@@ -919,7 +976,7 @@ export default (ctx) => {
919
976
  (local.get $off)
920
977
  (i32.shl (local.get $len) (i32.const 3)))
921
978
  (f64.store (local.get $off) (local.get $val))
922
- (call $__set_len (i64.reinterpret_f64 (local.get $a)) (i32.add (local.get $len) (i32.const 1)))
979
+ (i32.store (i32.sub (local.get $off) (i32.const 8)) (i32.add (local.get $len) (i32.const 1)))
923
980
  (f64.convert_i32_s (i32.add (local.get $len) (i32.const 1))))`
924
981
 
925
982
  // .some(fn) → return 1 if any element passes, else 0 (early exit)
@@ -44,6 +44,16 @@ function numConstLiteral(expr) {
44
44
  const sameValueZeroEq = '(call $__same_value_zero (i64.load (i32.add (local.get $slot) (i32.const 8))) (local.get $key))'
45
45
  const strEq = '(call $__str_eq (i64.load (i32.add (local.get $slot) (i32.const 8))) (local.get $key))'
46
46
 
47
+ // Open-addressing probe walked additively by entrySize: avoids an i32.mul + mask per
48
+ // step (vs recomputing slot = off + idx*entrySize). Needs $off/$cap/$h set and $end/$slot
49
+ // locals declared. `idxExpr` is the first-slot index (defaults to h mod cap; cap is pow2).
50
+ const probeStart = (entrySize, idxExpr = '(i32.and (local.get $h) (i32.sub (local.get $cap) (i32.const 1)))') =>
51
+ `(local.set $end (i32.add (local.get $off) (i32.mul (local.get $cap) (i32.const ${entrySize}))))
52
+ (local.set $slot (i32.add (local.get $off) (i32.mul ${idxExpr} (i32.const ${entrySize}))))`
53
+ const probeNext = (entrySize) =>
54
+ `(local.set $slot (i32.add (local.get $slot) (i32.const ${entrySize})))
55
+ (if (i32.ge_u (local.get $slot) (local.get $end)) (then (local.set $slot (local.get $off))))`
56
+
47
57
  /** Generate upsert (add/set) probe function. hasVal: store value at slot+16.
48
58
  * hasExt: emit EXTERNAL fallthrough (call $__ext_set on non-matching type).
49
59
  * Gated off → type mismatch just returns coll unchanged. */
@@ -62,14 +72,13 @@ function genUpsert(name, entrySize, hashFn, eqExpr, expectedType, hasVal, hasExt
62
72
  ? `(if (i32.ne ${tExpr} (i32.const ${expectedType})) (then (if (i32.eq ${tExpr} (i32.const ${PTR.EXTERNAL})) ${extBranch}) (return (local.get $coll))))`
63
73
  : `(if (i32.ne ${tExpr} (i32.const ${expectedType})) (then (return (local.get $coll))))`
64
74
  return `(func $${name} (param $coll i64) (param $key i64) ${valParam}(result i64)
65
- (local $off i32) (local $cap i32) (local $h i32) (local $idx i32) (local $slot i32)
75
+ (local $off i32) (local $cap i32) (local $h i32) (local $end i32) (local $slot i32)
66
76
  ${typeGuard}
67
77
  (local.set $off (i32.wrap_i64 (i64.and (local.get $coll) (i64.const ${LAYOUT.OFFSET_MASK}))))
68
78
  (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
69
79
  (local.set $h (call ${hashFn} (local.get $key)))
70
- (local.set $idx (i32.and (local.get $h) (i32.sub (local.get $cap) (i32.const 1))))
80
+ ${probeStart(entrySize)}
71
81
  (block $done (loop $probe
72
- (local.set $slot (i32.add (local.get $off) (i32.mul (local.get $idx) (i32.const ${entrySize}))))
73
82
  (if (i64.eqz (i64.load (local.get $slot)))
74
83
  (then
75
84
  (i64.store (local.get $slot) (i64.extend_i32_u (local.get $h)))
@@ -78,7 +87,7 @@ function genUpsert(name, entrySize, hashFn, eqExpr, expectedType, hasVal, hasExt
78
87
  (i32.add (i32.load (i32.sub (local.get $off) (i32.const 8))) (i32.const 1)))
79
88
  (br $done)))
80
89
  (if ${eqExpr} ${onMatch})
81
- (local.set $idx (i32.and (i32.add (local.get $idx) (i32.const 1)) (i32.sub (local.get $cap) (i32.const 1))))
90
+ ${probeNext(entrySize)}
82
91
  (br $probe)))
83
92
  (local.get $coll))`
84
93
  }
@@ -108,17 +117,16 @@ function genLookup(name, entrySize, hashFn, eqExpr, expectedType, wantValue, has
108
117
  : `(if (i32.ne ${tExpr} (i32.const ${expectedType})) (then ${onEmpty}))`
109
118
 
110
119
  return `(func $${name} (param $coll i64) (param $key i64) (result ${rt})
111
- (local $off i32) (local $cap i32) (local $h i32) (local $idx i32) (local $slot i32) (local $tries i32)
120
+ (local $off i32) (local $cap i32) (local $h i32) (local $end i32) (local $slot i32) (local $tries i32)
112
121
  ${typeGuard}
113
122
  (local.set $off (i32.wrap_i64 (i64.and (local.get $coll) (i64.const ${LAYOUT.OFFSET_MASK}))))
114
123
  (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
115
124
  (local.set $h (call ${hashFn} (local.get $key)))
116
- (local.set $idx (i32.and (local.get $h) (i32.sub (local.get $cap) (i32.const 1))))
125
+ ${probeStart(entrySize)}
117
126
  (block $done (loop $probe
118
- (local.set $slot (i32.add (local.get $off) (i32.mul (local.get $idx) (i32.const ${entrySize}))))
119
127
  (if (i64.eqz (i64.load (local.get $slot))) (then ${onEmpty}))
120
128
  (if ${eqExpr} (then ${onFound}))
121
- (local.set $idx (i32.and (i32.add (local.get $idx) (i32.const 1)) (i32.sub (local.get $cap) (i32.const 1))))
129
+ ${probeNext(entrySize)}
122
130
  (local.set $tries (i32.add (local.get $tries) (i32.const 1)))
123
131
  (br_if $done (i32.ge_s (local.get $tries) (local.get $cap)))
124
132
  (br $probe)))
@@ -128,21 +136,20 @@ function genLookup(name, entrySize, hashFn, eqExpr, expectedType, wantValue, has
128
136
  /** Generate delete probe function. Zero out entry on match. */
129
137
  function genDelete(name, entrySize, hashFn, eqExpr, expectedType) {
130
138
  return `(func $${name} (param $coll i64) (param $key i64) (result i32)
131
- (local $off i32) (local $cap i32) (local $h i32) (local $idx i32) (local $slot i32) (local $tries i32)
139
+ (local $off i32) (local $cap i32) (local $h i32) (local $end i32) (local $slot i32) (local $tries i32)
132
140
  (if (i32.ne (call $__ptr_type (local.get $coll)) (i32.const ${expectedType})) (then (return (i32.const 0))))
133
141
  (local.set $off (call $__ptr_offset (local.get $coll)))
134
142
  (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
135
143
  (local.set $h (call ${hashFn} (local.get $key)))
136
- (local.set $idx (i32.and (local.get $h) (i32.sub (local.get $cap) (i32.const 1))))
144
+ ${probeStart(entrySize)}
137
145
  (block $done (loop $probe
138
- (local.set $slot (i32.add (local.get $off) (i32.mul (local.get $idx) (i32.const ${entrySize}))))
139
146
  (if (i64.eqz (i64.load (local.get $slot))) (then (return (i32.const 0))))
140
147
  (if ${eqExpr}
141
148
  (then
142
149
  (i64.store (local.get $slot) (i64.const 0))
143
150
  (i64.store (i32.add (local.get $slot) (i32.const 8)) (i64.const 0))
144
151
  (return (i32.const 1))))
145
- (local.set $idx (i32.and (i32.add (local.get $idx) (i32.const 1)) (i32.sub (local.get $cap) (i32.const 1))))
152
+ ${probeNext(entrySize)}
146
153
  (local.set $tries (i32.add (local.get $tries) (i32.const 1)))
147
154
  (br_if $done (i32.ge_s (local.get $tries) (local.get $cap)))
148
155
  (br $probe)))
@@ -168,7 +175,7 @@ function genUpsertGrow(name, entrySize, hashFn, eqExpr, typeConst, strict = fals
168
175
  ${nonHashFallback}
169
176
  (return (local.get $obj))))`
170
177
  return `(func $${name} (param $obj i64) (param $key i64) (param $val i64) (result i64)
171
- (local $off i32) (local $cap i32) (local $h i32) (local $idx i32) (local $slot i32)
178
+ (local $off i32) (local $cap i32) (local $h i32) (local $end i32) (local $slot i32)
172
179
  (local $size i32) (local $newptr i32) (local $newcap i32) (local $i i32)
173
180
  (local $oldslot i32) (local $newidx i32) (local $newslot i32)
174
181
  ${typeGuard}
@@ -205,9 +212,8 @@ function genUpsertGrow(name, entrySize, hashFn, eqExpr, typeConst, strict = fals
205
212
  (local.set $obj (i64.reinterpret_f64 (call $__mkptr (i32.const ${typeConst}) (i32.const 0) (local.get $newptr))))))
206
213
  ;; Insert/update
207
214
  (local.set $h (call ${hashFn} (local.get $key)))
208
- (local.set $idx (i32.and (local.get $h) (i32.sub (local.get $cap) (i32.const 1))))
215
+ ${probeStart(entrySize)}
209
216
  (block $done (loop $probe
210
- (local.set $slot (i32.add (local.get $off) (i32.mul (local.get $idx) (i32.const ${entrySize}))))
211
217
  (if (i64.eqz (i64.load (local.get $slot)))
212
218
  (then
213
219
  (i64.store (local.get $slot) (i64.extend_i32_u (local.get $h)))
@@ -220,14 +226,14 @@ function genUpsertGrow(name, entrySize, hashFn, eqExpr, typeConst, strict = fals
220
226
  (then
221
227
  (i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))
222
228
  (br $done)))
223
- (local.set $idx (i32.and (i32.add (local.get $idx) (i32.const 1)) (i32.sub (local.get $cap) (i32.const 1))))
229
+ ${probeNext(entrySize)}
224
230
  (br $probe)))
225
231
  (local.get $obj))`
226
232
  }
227
233
 
228
234
  function genLookupStrict(name, entrySize, hashFn, eqExpr, expectedType, missing = UNDEF_NAN) {
229
235
  return `(func $${name} (param $coll i64) (param $key i64) (result i64)
230
- (local $off i32) (local $cap i32) (local $h i32) (local $idx i32) (local $slot i32) (local $tries i32)
236
+ (local $off i32) (local $cap i32) (local $h i32) (local $end i32) (local $slot i32) (local $tries i32)
231
237
  (if (i32.ne
232
238
  (i32.wrap_i64 (i64.and (i64.shr_u (local.get $coll) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK})))
233
239
  (i32.const ${expectedType}))
@@ -235,14 +241,13 @@ function genLookupStrict(name, entrySize, hashFn, eqExpr, expectedType, missing
235
241
  (local.set $off (i32.wrap_i64 (i64.and (local.get $coll) (i64.const ${LAYOUT.OFFSET_MASK}))))
236
242
  (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
237
243
  (local.set $h (call ${hashFn} (local.get $key)))
238
- (local.set $idx (i32.and (local.get $h) (i32.sub (local.get $cap) (i32.const 1))))
244
+ ${probeStart(entrySize)}
239
245
  (block $done (loop $probe
240
- (local.set $slot (i32.add (local.get $off) (i32.mul (local.get $idx) (i32.const ${entrySize}))))
241
246
  (if (i64.eqz (i64.load (local.get $slot)))
242
247
  (then (return (i64.const ${missing}))))
243
248
  (if ${eqExpr}
244
249
  (then (return (i64.load (i32.add (local.get $slot) (i32.const 16))))))
245
- (local.set $idx (i32.and (i32.add (local.get $idx) (i32.const 1)) (i32.sub (local.get $cap) (i32.const 1))))
250
+ ${probeNext(entrySize)}
246
251
  (local.set $tries (i32.add (local.get $tries) (i32.const 1)))
247
252
  (br_if $done (i32.ge_s (local.get $tries) (local.get $cap)))
248
253
  (br $probe)))
@@ -260,18 +265,17 @@ function genLookupStrictPrehashed(name, entrySize, eqExpr, expectedType, missing
260
265
  : `(if (i32.ne ${tExpr} (i32.const ${expectedType}))
261
266
  (then (return (i64.const ${missing}))))`
262
267
  return `(func $${name} (param $coll i64) (param $key i64) (param $h i32) (result i64)
263
- (local $off i32) (local $cap i32) (local $idx i32) (local $slot i32) (local $tries i32)
268
+ (local $off i32) (local $cap i32) (local $end i32) (local $slot i32) (local $tries i32)
264
269
  ${typeGuard}
265
270
  (local.set $off (i32.wrap_i64 (i64.and (local.get $coll) (i64.const ${LAYOUT.OFFSET_MASK}))))
266
271
  (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
267
- (local.set $idx (i32.and (local.get $h) (i32.sub (local.get $cap) (i32.const 1))))
272
+ ${probeStart(entrySize)}
268
273
  (block $done (loop $probe
269
- (local.set $slot (i32.add (local.get $off) (i32.mul (local.get $idx) (i32.const ${entrySize}))))
270
274
  (if (i64.eqz (i64.load (local.get $slot)))
271
275
  (then (return (i64.const ${missing}))))
272
276
  (if ${eqExpr}
273
277
  (then (return (i64.load (i32.add (local.get $slot) (i32.const 16))))))
274
- (local.set $idx (i32.and (i32.add (local.get $idx) (i32.const 1)) (i32.sub (local.get $cap) (i32.const 1))))
278
+ ${probeNext(entrySize)}
275
279
  (local.set $tries (i32.add (local.get $tries) (i32.const 1)))
276
280
  (br_if $done (i32.ge_s (local.get $tries) (local.get $cap)))
277
281
  (br $probe)))
@@ -280,16 +284,15 @@ function genLookupStrictPrehashed(name, entrySize, eqExpr, expectedType, missing
280
284
 
281
285
  function genUpsertStrictPrehashed(name, entrySize, eqExpr, expectedType) {
282
286
  return `(func $${name} (param $obj i64) (param $key i64) (param $h i32) (param $val i64) (result i64)
283
- (local $off i32) (local $cap i32) (local $idx i32) (local $slot i32)
287
+ (local $off i32) (local $cap i32) (local $end i32) (local $slot i32)
284
288
  (if (i32.ne
285
289
  (i32.wrap_i64 (i64.and (i64.shr_u (local.get $obj) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK})))
286
290
  (i32.const ${expectedType}))
287
291
  (then (return (local.get $obj))))
288
292
  (local.set $off (i32.wrap_i64 (i64.and (local.get $obj) (i64.const ${LAYOUT.OFFSET_MASK}))))
289
293
  (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
290
- (local.set $idx (i32.and (local.get $h) (i32.sub (local.get $cap) (i32.const 1))))
294
+ ${probeStart(entrySize)}
291
295
  (block $done (loop $probe
292
- (local.set $slot (i32.add (local.get $off) (i32.mul (local.get $idx) (i32.const ${entrySize}))))
293
296
  (if (i64.eqz (i64.load (local.get $slot)))
294
297
  (then
295
298
  (i64.store (local.get $slot) (i64.extend_i32_u (local.get $h)))
@@ -302,7 +305,7 @@ function genUpsertStrictPrehashed(name, entrySize, eqExpr, expectedType) {
302
305
  (then
303
306
  (i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))
304
307
  (br $done)))
305
- (local.set $idx (i32.and (i32.add (local.get $idx) (i32.const 1)) (i32.sub (local.get $cap) (i32.const 1))))
308
+ ${probeNext(entrySize)}
306
309
  (br $probe)))
307
310
  (local.get $obj))`
308
311
  }
@@ -340,9 +343,11 @@ export default (ctx) => {
340
343
  __hash_set_local: ['__str_hash', '__str_eq'],
341
344
  __ihash_get_local: ['__map_hash'],
342
345
  __ihash_set_local: ['__map_hash', '__alloc_hdr', '__mkptr'],
343
- __dyn_get_t: ['__ihash_get_local', '__str_hash', '__str_eq', '__is_nullish'],
346
+ __dyn_get_t: ['__dyn_get_t_h', '__str_hash'],
347
+ __dyn_get_t_h: ['__ihash_get_local', '__str_eq', '__is_nullish'],
344
348
  __dyn_get: ['__dyn_get_t', '__ptr_type'],
345
349
  __dyn_get_expr_t: ['__dyn_get_t', '__hash_get_local'],
350
+ __dyn_get_expr_t_h: ['__dyn_get_t_h', '__hash_get_local_h'],
346
351
  __dyn_get_expr: ['__dyn_get_expr_t', '__ptr_type'],
347
352
  __dyn_get_any: ['__dyn_get_any_t', '__ptr_type'],
348
353
  __dyn_get_any_t: () => ctx.features.external
@@ -666,11 +671,22 @@ export default (ctx) => {
666
671
  ctx.core.stdlib['__dyn_get'] = `(func $__dyn_get (param $obj i64) (param $key i64) (result i64)
667
672
  (call $__dyn_get_t (local.get $obj) (local.get $key) (call $__ptr_type (local.get $obj))))`
668
673
 
669
- ctx.core.stdlib['__dyn_get_t'] = () => `(func $__dyn_get_t (param $obj i64) (param $key i64) (param $type i32) (result i64)
674
+ // Thin wrapper: hash the key once, delegate to the prehashed body. Constant-key
675
+ // call sites bypass this and call $__dyn_get_t_h directly with strHashLiteral().
676
+ ctx.core.stdlib['__dyn_get_t'] = `(func $__dyn_get_t (param $obj i64) (param $key i64) (param $type i32) (result i64)
677
+ (call $__dyn_get_t_h (local.get $obj) (local.get $key) (local.get $type) (call $__str_hash (local.get $key))))`
678
+
679
+ ctx.core.stdlib['__dyn_get_t_h'] = () => `(func $__dyn_get_t_h (param $obj i64) (param $key i64) (param $type i32) (param $h i32) (result i64)
670
680
  (local $props i64) (local $off i32)
671
- (local $poff i32) (local $pcap i32) (local $h i32) (local $idx i32) (local $slot i32) (local $tries i32)
681
+ (local $poff i32) (local $pcap i32) (local $pend i32) (local $idx i32) (local $slot i32) (local $tries i32)
672
682
  ${buildObjectSchemaLocals()}
673
683
  (local.set $off (i32.wrap_i64 (i64.and (local.get $obj) (i64.const ${LAYOUT.OFFSET_MASK}))))
684
+ ;; CLOSURE with no env (offset 0): many function refs share offset 0, so key the
685
+ ;; global __dyn_props hash on the function table index (negative — can't collide
686
+ ;; with real heap/data offsets). Closures *with* env keep their unique env ptr.
687
+ (if (i32.and (i32.eq (local.get $type) (i32.const ${PTR.CLOSURE})) (i32.eqz (local.get $off)))
688
+ (then (local.set $off (i32.sub (i32.const -1)
689
+ (i32.wrap_i64 (i64.and (i64.shr_u (local.get $obj) (i64.const ${LAYOUT.AUX_SHIFT})) (i64.const ${LAYOUT.AUX_MASK})))))))
674
690
  (if (i32.eq (local.get $type) (i32.const ${PTR.ARRAY}))
675
691
  (then
676
692
  (block $done
@@ -735,14 +751,14 @@ export default (ctx) => {
735
751
  (global.set $__dyn_get_cache_props (f64.reinterpret_i64 (local.get $props))))))))
736
752
  (local.set $poff (i32.wrap_i64 (i64.and (local.get $props) (i64.const ${LAYOUT.OFFSET_MASK}))))
737
753
  (local.set $pcap (i32.load (i32.sub (local.get $poff) (i32.const 4))))
738
- (local.set $h (call $__str_hash (local.get $key)))
739
- (local.set $idx (i32.and (local.get $h) (i32.sub (local.get $pcap) (i32.const 1))))
754
+ (local.set $pend (i32.add (local.get $poff) (i32.mul (local.get $pcap) (i32.const ${MAP_ENTRY}))))
755
+ (local.set $slot (i32.add (local.get $poff) (i32.mul (i32.and (local.get $h) (i32.sub (local.get $pcap) (i32.const 1))) (i32.const ${MAP_ENTRY}))))
740
756
  (block $hdone (loop $hprobe
741
- (local.set $slot (i32.add (local.get $poff) (i32.mul (local.get $idx) (i32.const ${MAP_ENTRY}))))
742
757
  (br_if $dynDone (i64.eqz (i64.load (local.get $slot))))
743
758
  (if (call $__str_eq (i64.load (i32.add (local.get $slot) (i32.const 8))) (local.get $key))
744
759
  (then (return (i64.load (i32.add (local.get $slot) (i32.const 16))))))
745
- (local.set $idx (i32.and (i32.add (local.get $idx) (i32.const 1)) (i32.sub (local.get $pcap) (i32.const 1))))
760
+ (local.set $slot (i32.add (local.get $slot) (i32.const ${MAP_ENTRY})))
761
+ (if (i32.ge_u (local.get $slot) (local.get $pend)) (then (local.set $slot (local.get $poff))))
746
762
  (local.set $tries (i32.add (local.get $tries) (i32.const 1)))
747
763
  (br_if $hdone (i32.ge_s (local.get $tries) (local.get $pcap)))
748
764
  (br $hprobe))))${buildObjectSchemaArm()}
@@ -770,6 +786,19 @@ export default (ctx) => {
770
786
  (then (call $__hash_get_local (local.get $obj) (local.get $key)))
771
787
  (else (i64.const ${NULL_NAN}))))))`
772
788
 
789
+ // Prehashed variant of __dyn_get_expr_t for constant string keys: the FNV hash
790
+ // is folded at compile time (strHashLiteral), so no __str_hash call at runtime.
791
+ ctx.core.stdlib['__dyn_get_expr_t_h'] = `(func $__dyn_get_expr_t_h (param $obj i64) (param $key i64) (param $t i32) (param $h i32) (result i64)
792
+ (local $val i64)
793
+ (local.set $val (call $__dyn_get_t_h (local.get $obj) (local.get $key) (local.get $t) (local.get $h)))
794
+ (if (result i64)
795
+ (i64.ne (local.get $val) (i64.const ${UNDEF_NAN}))
796
+ (then (local.get $val))
797
+ (else
798
+ (if (result i64) (i32.eq (local.get $t) (i32.const ${PTR.HASH}))
799
+ (then (call $__hash_get_local_h (local.get $obj) (local.get $key) (local.get $h)))
800
+ (else (i64.const ${NULL_NAN}))))))`
801
+
773
802
  // Like __dyn_get_expr but also resolves EXTERNAL host objects via __ext_prop.
774
803
  // Used at call sites where receiver type is statically unknown.
775
804
  // When features.external is off, collapses to __dyn_get_expr shape (no EXTERNAL probe).
@@ -809,6 +838,10 @@ export default (ctx) => {
809
838
  (local $off i32) (local $type i32) ${buildObjectSchemaSetLocals()}
810
839
  (local.set $off (i32.wrap_i64 (i64.and (local.get $obj) (i64.const ${LAYOUT.OFFSET_MASK}))))
811
840
  (local.set $type (i32.wrap_i64 (i64.and (i64.shr_u (local.get $obj) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
841
+ ;; CLOSURE with no env (offset 0): key __dyn_props on the function table index — see __dyn_get_t.
842
+ (if (i32.and (i32.eq (local.get $type) (i32.const ${PTR.CLOSURE})) (i32.eqz (local.get $off)))
843
+ (then (local.set $off (i32.sub (i32.const -1)
844
+ (i32.wrap_i64 (i64.and (i64.shr_u (local.get $obj) (i64.const ${LAYOUT.AUX_SHIFT})) (i64.const ${LAYOUT.AUX_MASK})))))))
812
845
  (if (i32.eq (local.get $type) (i32.const ${PTR.ARRAY}))
813
846
  (then
814
847
  (block $done