jz 0.2.1 → 0.3.1

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
@@ -68,7 +68,7 @@ Options are passed as `jz(source, opts)` or `compile(source, opts)`. Common ones
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
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. |
@@ -82,7 +82,16 @@ Options are passed as `jz(source, opts)` or `compile(source, opts)`. Common ones
82
82
 
83
83
  ```sh
84
84
  # Compile
85
- jz program.js # → program.wasm
85
+ jz program.js # → program.wasm
86
+ jz program.js --wat # → program.wat
87
+ jz program.js -o out.wasm # custom output (- for stdout)
88
+
89
+ # Optimization level: -O0 off, -O1 size-only, -O2 default, -O3 aggressive
90
+ # aliases: -Os/--optimize size, -Ob/balanced, -Of/speed
91
+ jz program.js -O3
92
+
93
+ # Runtime-service lowering: js (default) or wasi
94
+ jz program.js --host wasi
86
95
 
87
96
  # Evaluate
88
97
  jz -e "1 + 2" # 3
@@ -91,33 +100,12 @@ jz -e "1 + 2" # 3
91
100
  jz --help
92
101
  ```
93
102
 
103
+ Other flags: `--strict` (no auto-`jzify`, reject dynamic fallbacks), `--jzify` (transform JS → jz, no compile), `--no-alloc` (omit `_alloc`/`_clear`), `--names` (emit wasm `name` section), `--resolve` (Node.js bare-specifier resolution), `--imports <file.json>` (host import specs).
104
+
94
105
  ## Language
95
106
 
96
107
  JZ is a strict functional JS subset. Built-in `jzify` transform extends support to legacy patterns.
97
108
 
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
109
  ```
122
110
  ┌────────────────────────────────────────────────────────────────────────┐
123
111
  │ JZify │
@@ -151,6 +139,10 @@ Not supported
151
139
 
152
140
  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
141
 
142
+ > [!WARNING] jz objects are fixed-layout schemas (like C structs), not dynamic key bags.
143
+ > `memory.Object({ x: 3, y: 4 })` expects the same key order as the jz source `{ x, y }`.
144
+ > `{ y: 4, x: 3 }` with reversed keys will produce wrong values.
145
+
154
146
  ```js
155
147
  const { exports, memory } = jz`
156
148
  export let greet = (s) => s.length
@@ -494,7 +486,7 @@ cc program.c -o program
494
486
  | [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
487
  | [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
488
  | [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 |
489
+ | [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 |
498
490
  | [watr](bench/watr/watr.js) | 1.04ms<br>169.8kB | 1.05ms<br>2.6kB | fails | — | — | — | — | — | — | — |
499
491
 
500
492
  _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)
@@ -28,23 +32,38 @@ Examples:
28
32
  jz program.js --wat # → program.wat
29
33
  jz program.js -o out.wasm # custom output name
30
34
  jz program.js -o - # write to stdout
35
+ jz program.js -O3 # aggressive optimization
36
+ jz program.js -Os # optimize for size
37
+ jz program.js --host wasi # emit WASI Preview 1 imports
31
38
  jz --strict program.js # strict mode
32
39
  jz --jzify lib.js # → lib.jz
33
40
  jz -e "1 + 2"
34
41
 
35
42
  Options:
36
43
  --output, -o <file> Output file (.wat, .wasm, or - for stdout)
37
- --strict Strict jz mode (no auto-transform)
44
+ -O<n>, --optimize <n> Optimization level: 0 off, 1 size-only, 2 default,
45
+ 3 aggressive. Aliases: -Os/size, -Ob/balanced, -Of/speed.
46
+ --host <js|wasi> Runtime-service lowering (default js)
47
+ --no-alloc Omit _alloc/_clear allocator exports (standalone wasm)
48
+ --names Emit wasm name section for profilers/debuggers
49
+ --strict Strict jz mode (no auto-transform), reject dynamic fallbacks
38
50
  --jzify Transform JS to jz (no compilation)
39
51
  --eval, -e Evaluate expression or file
40
52
  --wat Output WAT text instead of binary
41
53
  --resolve Resolve bare specifiers via Node.js module resolution
54
+ --imports <file> JSON file with host import specs (e.g. {"env":{"fn":{"params":2}}})
55
+ --version, -v Show version number
42
56
  `)
43
57
  }
44
58
 
45
59
  async function main() {
46
60
  const args = process.argv.slice(2)
47
61
 
62
+ if (args.includes('--version') || args.includes('-v')) {
63
+ console.log(PKG.version)
64
+ return
65
+ }
66
+
48
67
  if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
49
68
  showHelp()
50
69
  return
@@ -98,15 +117,31 @@ async function handleJzify(args) {
98
117
  }
99
118
  }
100
119
 
120
+ // -O<n>/-Os/-Ob/-Of and --optimize <val> → value accepted by compile()'s `optimize` opt
121
+ const OPT_ALIAS = { s: 'size', b: 'balanced', f: 'speed' }
122
+ function parseOptimize(v) {
123
+ if (v == null) return undefined
124
+ if (/^\d+$/.test(v)) return +v
125
+ return OPT_ALIAS[v] ?? v
126
+ }
127
+
101
128
  async function handleCompile(args) {
102
- let inputFile = null, outputFile = null, wat = false, strict = false, resolveNode = false
129
+ let inputFile = null, outputFile = null, wat = false, strict = false, resolveNode = false, importsFile = null
130
+ let optimize, host, alloc = true, names = false
103
131
 
104
132
  for (let i = 0; i < args.length; i++) {
105
- if (args[i] === '--output' || args[i] === '-o') outputFile = args[++i]
106
- else if (args[i] === '--wat') wat = true
107
- else if (args[i] === '--strict') strict = true
108
- else if (args[i] === '--resolve') resolveNode = true
109
- else if (!inputFile) inputFile = args[i]
133
+ const a = args[i]
134
+ if (a === '--output' || a === '-o') outputFile = args[++i]
135
+ else if (a === '--wat') wat = true
136
+ else if (a === '--strict') strict = true
137
+ else if (a === '--resolve') resolveNode = true
138
+ else if (a === '--imports') importsFile = args[++i]
139
+ else if (a === '--optimize' || a === '-O') optimize = parseOptimize(args[++i])
140
+ else if (/^-O.+/.test(a)) optimize = parseOptimize(a.slice(2))
141
+ else if (a === '--host') host = args[++i]
142
+ else if (a === '--no-alloc') alloc = false
143
+ else if (a === '--names') names = true
144
+ else if (!inputFile) inputFile = a
110
145
  }
111
146
 
112
147
  if (!inputFile) throw new Error('No input file specified')
@@ -166,10 +201,20 @@ async function handleCompile(args) {
166
201
  const opts = {
167
202
  wat,
168
203
  jzify: !strict && !inputFile.endsWith('.jz'),
204
+ strict,
169
205
  importMetaUrl: pathToFileURL(resolve(inputFile)).href,
206
+ ...(optimize !== undefined && { optimize }),
207
+ ...(host && { host }),
208
+ ...(alloc === false && { alloc: false }),
209
+ ...(names && { profileNames: true }),
170
210
  ...(Object.keys(modules).length && { modules }),
171
211
  }
172
212
 
213
+ if (importsFile) {
214
+ const importsPath = resolve(importsFile)
215
+ opts.imports = JSON.parse(readFileSync(importsPath, 'utf8'))
216
+ }
217
+
173
218
  const result = compile(code, opts)
174
219
 
175
220
  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,
@@ -224,6 +225,16 @@ jz.compile = (code, opts = {}) => {
224
225
  let parsed = time('parse', () => parse(code))
225
226
  if (opts.jzify) parsed = time('jzify', () => jzify(parsed))
226
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
+
227
238
  const module = time('compile', () => compile(ast, profiler))
228
239
 
229
240
  // host: 'wasi' — error if the wasm would import any env.__ext_* helper. Those exist
@@ -249,8 +260,10 @@ jz.compile = (code, opts = {}) => {
249
260
  // Only valuable to re-run when watr ran (watr is what re-introduces the boundaries).
250
261
  if (cfg.watr) {
251
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
252
265
  time('watrReopt', () => {
253
- 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)
254
267
  })
255
268
  }
256
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)