jz 0.0.0 → 0.1.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.
@@ -0,0 +1,1322 @@
1
+ /**
2
+ * Array module — literals, indexing, methods, push/pop.
3
+ *
4
+ * Type=1 (ARRAY): C-style header in memory.
5
+ * Layout: [-8:len(i32)][-4:cap(i32)][elem0:f64, elem1:f64, ...]
6
+ * offset points to elem0 (past header). len/cap mutable. Aliases see changes.
7
+ *
8
+ * @module array
9
+ */
10
+
11
+ import { typed, asF64, asI32, NULL_NAN, UNDEF_NAN, temp, tempI32, allocPtr, multiCount, arrayLoop, elemLoad, elemStore, truthyIR, extractF64Bits, appendStaticSlots, mkPtrIR, slotAddr } from '../src/ir.js'
12
+ import { emit, materializeMulti } from '../src/emit.js'
13
+ import { valTypeOf, lookupValType, VAL, extractParams, updateRep } from '../src/analyze.js'
14
+ import { ctx, inc, err, PTR } from '../src/ctx.js'
15
+ import { staticPropertyKey } from '../src/key.js'
16
+ import { strHashLiteral } from './collection.js'
17
+
18
+
19
+ /** Allocate ARRAY (type=1): header + n*8 data. Returns { local, setup, ptr } where local is data offset. */
20
+ function allocArray(len, cap) {
21
+ const a = allocPtr({ type: PTR.ARRAY, len, cap, tag: 'arr' })
22
+ return { local: a.local, setup: [a.init], ptr: a.ptr }
23
+ }
24
+
25
+ /** Pack literal i64 slots as a static ARRAY: writes [len][cap][slots...] into the data segment
26
+ * and returns a folded ARRAY pointer to the first slot. */
27
+ function staticArrayPtr(slots) {
28
+ if (!ctx.runtime.data) ctx.runtime.data = ''
29
+ while (ctx.runtime.data.length % 8 !== 0) ctx.runtime.data += '\0'
30
+ const headerOff = ctx.runtime.data.length
31
+ const len = slots.length
32
+ const hdr = new Uint8Array(8); new DataView(hdr.buffer).setInt32(0, len, true); new DataView(hdr.buffer).setInt32(4, len, true)
33
+ for (let i = 0; i < 8; i++) ctx.runtime.data += String.fromCharCode(hdr[i])
34
+ appendStaticSlots(slots)
35
+ return mkPtrIR(PTR.ARRAY, 0, headerOff + 8)
36
+ }
37
+
38
+ function hoistArrayValue(arr) {
39
+ const recv = temp('ar')
40
+ return {
41
+ setup: ['local.set', `$${recv}`, asF64(emit(arr))],
42
+ value: typed(['local.get', `$${recv}`], 'f64'),
43
+ }
44
+ }
45
+
46
+ // Pure-expression check: no statements, binders, control flow, or assignments.
47
+ // Inlining is only safe for these — anything else needs the full closure machinery.
48
+ const NOT_PURE_OPS = new Set([
49
+ ';', '{}', 'let', 'const', 'var', '=>', 'function', 'return', 'throw',
50
+ 'if', 'for', 'while', 'do', 'switch', 'case', 'default', 'break', 'continue',
51
+ 'try', 'catch', 'finally', '=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=',
52
+ '<<=', '>>=', '>>>=', '||=', '&&=', '??=', '++', '--', 'delete', 'yield', 'await',
53
+ ])
54
+ function isPureExpr(node) {
55
+ if (node == null || typeof node !== 'object' || !Array.isArray(node)) return true
56
+ const op = node[0]
57
+ if (op == null) return true
58
+ if (NOT_PURE_OPS.has(op)) return false
59
+ for (let i = 1; i < node.length; i++) if (!isPureExpr(node[i])) return false
60
+ return true
61
+ }
62
+
63
+ // Substitute variable references in a pure expression. Skips property names on `.` / `?.`
64
+ // and object-literal keys on `:`. Body must be pre-checked with isPureExpr.
65
+ function substExpr(node, mapping) {
66
+ if (typeof node === 'string') return mapping.has(node) ? mapping.get(node) : node
67
+ if (!Array.isArray(node)) return node
68
+ const op = node[0]
69
+ if (op === '.' || op === '?.') return [op, substExpr(node[1], mapping), node[2]]
70
+ if (op === ':') return [op, node[1], substExpr(node[2], mapping)]
71
+ const out = [op]
72
+ for (let i = 1; i < node.length; i++) out.push(substExpr(node[i], mapping))
73
+ return out
74
+ }
75
+
76
+ // Check whether a name is referenced inside a pure expression body.
77
+ // Mirrors substExpr's traversal — skips property names on '.'/'?.' and object keys on ':'.
78
+ function exprUses(node, name) {
79
+ if (typeof node === 'string') return node === name
80
+ if (!Array.isArray(node)) return false
81
+ const op = node[0]
82
+ if (op === '.' || op === '?.') return exprUses(node[1], name)
83
+ if (op === ':') return exprUses(node[2], name)
84
+ for (let i = 1; i < node.length; i++) if (exprUses(node[i], name)) return true
85
+ return false
86
+ }
87
+
88
+ // Callback factory: returns { setup, call, usedParams } where call(argExprs) emits the invocation.
89
+ // Fast path: literal arrow with simple-string params and pure expression body → inline,
90
+ // substituting param refs with fresh locals. Zero closure alloc, zero call_indirect, zero
91
+ // args-array alloc. Captures resolve naturally to outer locals.
92
+ // Slow path: fall back to ctx.closure.call (heap-allocated args array per iteration).
93
+ // usedParams: boolean array (fast path only) — callers can skip computing args for unused params.
94
+ function makeCallback(fn, argReps) {
95
+ if (Array.isArray(fn) && fn[0] === '=>') {
96
+ const raw = extractParams(fn[1])
97
+ const body = fn[2]
98
+ if (raw.every(p => typeof p === 'string') && isPureExpr(body)) {
99
+ const usedParams = raw.map(p => exprUses(body, p))
100
+ return {
101
+ setup: ['nop'],
102
+ usedParams,
103
+ call: (argExprs) => {
104
+ const stmts = []
105
+ const mapping = new Map()
106
+ const freshNames = []
107
+ for (let i = 0; i < raw.length; i++) {
108
+ if (!usedParams[i]) { freshNames.push(null); continue } // skip dead local + arg evaluation
109
+ const fresh = temp('inl')
110
+ mapping.set(raw[i], fresh)
111
+ freshNames.push(fresh)
112
+ const ae = i < argExprs.length && argExprs[i] != null
113
+ ? asF64(argExprs[i])
114
+ : typed(['f64.reinterpret_i64', ['i64.const', UNDEF_NAN]], 'f64')
115
+ stmts.push(['local.set', `$${fresh}`, ae])
116
+ }
117
+ // Apply argReps hints (caller knows recv elem val type) to inlined-param
118
+ // reps so emit(subst) sees `inl_i.val=NUMBER` and elides __to_num/__is_str_key.
119
+ if (argReps) {
120
+ for (let i = 0; i < raw.length && i < argReps.length; i++) {
121
+ const fresh = freshNames[i]
122
+ if (!fresh || !argReps[i]) continue
123
+ updateRep(fresh, argReps[i])
124
+ }
125
+ }
126
+ const subst = substExpr(body, mapping)
127
+ const result = emit(subst)
128
+ // Preserve i32 result type so callers (truthyIR, etc.) can skip f64↔i32 round-trips.
129
+ const ty = result.type === 'i32' ? 'i32' : 'f64'
130
+ return typed(['block', ['result', ty], ...stmts, result], ty)
131
+ },
132
+ }
133
+ }
134
+ }
135
+ // Fallback: closure call — all params are potentially used.
136
+ const cb = temp('af')
137
+ return {
138
+ setup: ['local.set', `$${cb}`, asF64(emit(fn))],
139
+ call: (argExprs) => ctx.closure.call(typed(['local.get', `$${cb}`], 'f64'), argExprs),
140
+ }
141
+ }
142
+
143
+ // Derive callback argReps from a receiver AST. For .map/.filter/etc., callbacks
144
+ // receive (item, idx, arr). idx is always a NUMBER. item depends on recv kind:
145
+ // - VAL.TYPED → NUMBER (BigInt typed-arrays excluded; we don't track elem prec
146
+ // here, but the .typed:[] path handles them, and __to_num elision is safe
147
+ // because BigInt's f64-cast in arithmetic still yields a Number).
148
+ // - VAL.ARRAY with rep.arrayElemValType set → that val.
149
+ // - else → no hint (slow path, runtime dispatch as today).
150
+ function callbackArgReps(arr) {
151
+ const idxRep = { val: VAL.NUMBER }
152
+ const arrRep = { val: VAL.ARRAY }
153
+ let itemRep = null
154
+ if (typeof arr === 'string') {
155
+ const vt = lookupValType(arr)
156
+ if (vt === VAL.TYPED) itemRep = { val: VAL.NUMBER }
157
+ else if (vt === VAL.ARRAY) {
158
+ const elemVt = ctx.func.repByLocal?.get(arr)?.arrayElemValType
159
+ if (elemVt) itemRep = { val: elemVt }
160
+ }
161
+ } else {
162
+ const vt = valTypeOf(arr)
163
+ if (vt === VAL.TYPED) itemRep = { val: VAL.NUMBER }
164
+ }
165
+ return [itemRep, idxRep, arrRep]
166
+ }
167
+
168
+ // Factory for simple arr→call stdlib patterns (mirrors strMethod in string.js)
169
+ const arrMethod = (name, nArgs = 0) => (...args) => {
170
+ inc(name)
171
+ const call = ['call', `$${name}`, ...args.slice(0, nArgs + 1).map(a => asF64(emit(a)))]
172
+ return typed(call, 'f64')
173
+ }
174
+
175
+ const needsArrayDynMove = () => ctx.core.includes.has('__dyn_set')
176
+ const arrayGrowDeps = (knownArray = false) => () => [
177
+ ...(knownArray ? [] : ['__ptr_type']),
178
+ '__ptr_offset', '__alloc_hdr', '__mkptr',
179
+ ...(needsArrayDynMove() ? ['__dyn_move'] : []),
180
+ ]
181
+
182
+ const maybeDynMoveIR = () => needsArrayDynMove()
183
+ ? '(call $__dyn_move (local.get $off) (local.get $newOff))'
184
+ : ''
185
+
186
+ export default (ctx) => {
187
+ Object.assign(ctx.core.stdlibDeps, {
188
+ __arr_idx: [],
189
+ __arr_grow: arrayGrowDeps(false),
190
+ __arr_grow_known: arrayGrowDeps(true),
191
+ __arr_shift: () => [
192
+ '__ptr_offset',
193
+ ...(needsArrayDynMove() ? ['__dyn_move'] : []),
194
+ ],
195
+ __arr_set_idx_ptr: ['__arr_grow', '__ptr_offset', '__set_len'],
196
+ __typed_idx: () => ctx.features.typedarray || ctx.features.external
197
+ ? ['__len']
198
+ : ['__len', '__ptr_offset'],
199
+ })
200
+
201
+ // Iteration methods (.map/.filter/.reduce/.forEach/...) invoke callbacks with
202
+ // (item, idx) internally — closure width must accommodate arity 2 even if no
203
+ // source-level closure has that arity.
204
+ ctx.closure.floor = Math.max(ctx.closure.floor ?? 0, 2)
205
+
206
+ inc('__ptr_offset', '__ptr_type', '__len', '__set_len', '__typed_idx', '__is_truthy')
207
+
208
+ // Array.isArray(x): check ptr_type === PTR.ARRAY
209
+ ctx.core.emit['Array.isArray'] = (x) => {
210
+ const v = asF64(emit(x))
211
+ const t = temp('t')
212
+ return typed(['i32.and',
213
+ ['f64.ne', ['local.tee', `$${t}`, v], ['local.get', `$${t}`]],
214
+ ['i32.eq', ['call', '$__ptr_type', ['local.get', `$${t}`]], ['i32.const', PTR.ARRAY]]], 'i32')
215
+ }
216
+
217
+ // ARRAY-only indexed read. Inline forwarding-follow + bounds check + load — avoids
218
+ // the redundant double pass through __len then __ptr_offset that both follow forwarding.
219
+ ctx.core.stdlib['__arr_idx'] = `(func $__arr_idx (param $ptr f64) (param $i i32) (result f64)
220
+ (local $bits i64) (local $off i32)
221
+ (local.set $bits (i64.reinterpret_f64 (local.get $ptr)))
222
+ (if (result f64)
223
+ (i32.ne
224
+ (i32.wrap_i64 (i64.and (i64.shr_u (local.get $bits) (i64.const 47)) (i64.const 0xF)))
225
+ (i32.const ${PTR.ARRAY}))
226
+ (then (f64.const nan:${UNDEF_NAN}))
227
+ (else
228
+ (local.set $off (i32.wrap_i64 (i64.and (local.get $bits) (i64.const 0xFFFFFFFF))))
229
+ (block $done
230
+ (loop $follow
231
+ (br_if $done (i32.lt_u (local.get $off) (i32.const 8)))
232
+ (br_if $done (i32.gt_u (local.get $off) (i32.shl (memory.size) (i32.const 16))))
233
+ (br_if $done (i32.ne (i32.load (i32.sub (local.get $off) (i32.const 4))) (i32.const -1)))
234
+ (local.set $off (i32.load (i32.sub (local.get $off) (i32.const 8))))
235
+ (br $follow)))
236
+ (if (result f64)
237
+ (i32.and
238
+ (i32.ge_u (local.get $off) (i32.const 8))
239
+ (i32.and
240
+ (i32.ge_s (local.get $i) (i32.const 0))
241
+ (i32.lt_u (local.get $i) (i32.load (i32.sub (local.get $off) (i32.const 8))))))
242
+ (then (f64.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3)))))
243
+ (else (f64.const nan:${UNDEF_NAN})))))) `
244
+
245
+ // Runtime-dispatch index: element-type aware load with bounds check + view indirection.
246
+ // Full body handles TYPED element types and view indirection since external host can
247
+ // pass typed arrays even when typedarray module isn't loaded. When features.typedarray
248
+ // and features.external are both off, collapses to ARRAY-only f64 indexing.
249
+ ctx.core.stdlib['__typed_idx'] = () => {
250
+ if (!ctx.features.typedarray && !ctx.features.external) {
251
+ return `(func $__typed_idx (param $ptr f64) (param $i i32) (result f64)
252
+ (local $len i32)
253
+ (local.set $len (call $__len (local.get $ptr)))
254
+ (if (result f64)
255
+ (i32.or
256
+ (i32.lt_s (local.get $i) (i32.const 0))
257
+ (i32.ge_u (local.get $i) (local.get $len)))
258
+ (then (f64.const nan:${UNDEF_NAN}))
259
+ (else (f64.load (i32.add (call $__ptr_offset (local.get $ptr)) (i32.shl (local.get $i) (i32.const 3)))))))`
260
+ }
261
+ // Hot (~37M calls in watr self-host). Type/aux/offset extracted once from $bits.
262
+ return `(func $__typed_idx (param $ptr f64) (param $i i32) (result f64)
263
+ (local $bits i64) (local $t i32) (local $off i32) (local $et i32) (local $len i32) (local $aux i32)
264
+ (local.set $bits (i64.reinterpret_f64 (local.get $ptr)))
265
+ (local.set $t (i32.wrap_i64 (i64.and (i64.shr_u (local.get $bits) (i64.const 47)) (i64.const 0xF))))
266
+ (local.set $aux (i32.wrap_i64 (i64.and (i64.shr_u (local.get $bits) (i64.const 32)) (i64.const 0x7FFF))))
267
+ (local.set $off (i32.wrap_i64 (i64.and (local.get $bits) (i64.const 0xFFFFFFFF))))
268
+ (if
269
+ (i32.and
270
+ (i32.eq (local.get $t) (i32.const ${PTR.TYPED}))
271
+ (i32.ne (i32.and (local.get $aux) (i32.const 8)) (i32.const 0)))
272
+ (then (local.set $off (i32.load (i32.add (local.get $off) (i32.const 4))))))
273
+ (local.set $len (call $__len (local.get $ptr)))
274
+ (if (result f64)
275
+ (i32.or
276
+ (i32.lt_s (local.get $i) (i32.const 0))
277
+ (i32.ge_u (local.get $i) (local.get $len)))
278
+ (then (f64.const nan:${UNDEF_NAN}))
279
+ (else
280
+ (if (result f64) (i32.eq (local.get $t) (i32.const ${PTR.TYPED}))
281
+ (then
282
+ (local.set $et (i32.and (local.get $aux) (i32.const 7)))
283
+ (if (result f64) (i32.ge_u (local.get $et) (i32.const 6))
284
+ (then (if (result f64) (i32.eq (local.get $et) (i32.const 7))
285
+ (then (f64.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3)))))
286
+ (else (f64.promote_f32 (f32.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 2))))))))
287
+ (else (if (result f64) (i32.ge_u (local.get $et) (i32.const 4))
288
+ (then (if (result f64) (i32.and (local.get $et) (i32.const 1))
289
+ (then (f64.convert_i32_u (i32.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 2))))))
290
+ (else (f64.convert_i32_s (i32.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 2))))))))
291
+ (else (if (result f64) (i32.ge_u (local.get $et) (i32.const 2))
292
+ (then (if (result f64) (i32.and (local.get $et) (i32.const 1))
293
+ (then (f64.convert_i32_u (i32.load16_u (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 1))))))
294
+ (else (f64.convert_i32_s (i32.load16_s (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 1))))))))
295
+ (else (if (result f64) (i32.and (local.get $et) (i32.const 1))
296
+ (then (f64.convert_i32_u (i32.load8_u (i32.add (local.get $off) (local.get $i)))))
297
+ (else (f64.convert_i32_s (i32.load8_s (i32.add (local.get $off) (local.get $i)))))))))))))
298
+ (else (f64.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3)))))))))`
299
+ }
300
+
301
+ // Array.from(src) — shallow copy of array (memory.copy of f64 elements)
302
+ ctx.core.stdlib['__arr_from'] = `(func $__arr_from (param $src f64) (result f64)
303
+ (local $len i32) (local $dst i32)
304
+ (local.set $len (call $__len (local.get $src)))
305
+ (local.set $dst (call $__alloc_hdr (local.get $len) (local.get $len) (i32.const 8)))
306
+ (memory.copy (local.get $dst) (call $__ptr_offset (local.get $src)) (i32.shl (local.get $len) (i32.const 3)))
307
+ (call $__mkptr (i32.const ${PTR.ARRAY}) (i32.const 0) (local.get $dst)))`
308
+
309
+ ctx.core.emit['Array.from'] = (src) => {
310
+ inc('__arr_from')
311
+ return typed(['call', '$__arr_from', asF64(emit(src))], 'f64')
312
+ }
313
+
314
+ // Grow array if capacity insufficient. Returns (possibly new) NaN-boxed pointer.
315
+ // Old storage is left behind as a forwarding header so existing aliases keep
316
+ // seeing the current backing store after growth.
317
+ ctx.core.stdlib['__arr_grow'] = () => `(func $__arr_grow (param $ptr f64) (param $minCap i32) (result f64)
318
+ (local $t i32) (local $off i32) (local $oldCap i32) (local $newCap i32) (local $newOff i32) (local $len i32)
319
+ (local.set $t (call $__ptr_type (local.get $ptr)))
320
+ (local.set $off (call $__ptr_offset (local.get $ptr)))
321
+ ;; Defensive path: invalid/non-array pointer -> create fresh array buffer.
322
+ (if
323
+ (i32.or
324
+ (i32.ne (local.get $t) (i32.const ${PTR.ARRAY}))
325
+ (i32.lt_u (local.get $off) (i32.const 8)))
326
+ (then
327
+ (local.set $newCap (select (local.get $minCap) (i32.const 4) (i32.gt_s (local.get $minCap) (i32.const 4))))
328
+ (local.set $newOff (call $__alloc_hdr (i32.const 0) (local.get $newCap) (i32.const 8)))
329
+ (return (call $__mkptr (i32.const ${PTR.ARRAY}) (i32.const 0) (local.get $newOff)))))
330
+ (local.set $oldCap (i32.load (i32.sub (local.get $off) (i32.const 4))))
331
+ (if (i32.ge_s (local.get $oldCap) (local.get $minCap))
332
+ (then (return (local.get $ptr))))
333
+ (local.set $newCap (select
334
+ (local.get $minCap)
335
+ (i32.shl (local.get $oldCap) (i32.const 1))
336
+ (i32.gt_s (local.get $minCap) (i32.shl (local.get $oldCap) (i32.const 1)))))
337
+ (local.set $len (i32.load (i32.sub (local.get $off) (i32.const 8))))
338
+ (local.set $newOff (call $__alloc_hdr (local.get $len) (local.get $newCap) (i32.const 8)))
339
+ (memory.copy (local.get $newOff) (local.get $off) (i32.shl (local.get $len) (i32.const 3)))
340
+ ${maybeDynMoveIR()}
341
+ (i32.store (i32.sub (local.get $off) (i32.const 8)) (local.get $newOff))
342
+ (i32.store (i32.sub (local.get $off) (i32.const 4)) (i32.const -1))
343
+ (call $__mkptr (i32.const ${PTR.ARRAY}) (i32.const 0) (local.get $newOff)))`
344
+
345
+ ctx.core.stdlib['__arr_grow_known'] = () => `(func $__arr_grow_known (param $ptr f64) (param $minCap i32) (result f64)
346
+ (local $off i32) (local $oldCap i32) (local $newCap i32) (local $newOff i32) (local $len i32)
347
+ (local.set $off (call $__ptr_offset (local.get $ptr)))
348
+ (local.set $oldCap (i32.load (i32.sub (local.get $off) (i32.const 4))))
349
+ (if (i32.ge_s (local.get $oldCap) (local.get $minCap))
350
+ (then (return (local.get $ptr))))
351
+ (local.set $newCap (select
352
+ (local.get $minCap)
353
+ (i32.shl (local.get $oldCap) (i32.const 1))
354
+ (i32.gt_s (local.get $minCap) (i32.shl (local.get $oldCap) (i32.const 1)))))
355
+ (local.set $len (i32.load (i32.sub (local.get $off) (i32.const 8))))
356
+ (local.set $newOff (call $__alloc_hdr (local.get $len) (local.get $newCap) (i32.const 8)))
357
+ (memory.copy (local.get $newOff) (local.get $off) (i32.shl (local.get $len) (i32.const 3)))
358
+ ${maybeDynMoveIR()}
359
+ (i32.store (i32.sub (local.get $off) (i32.const 8)) (local.get $newOff))
360
+ (i32.store (i32.sub (local.get $off) (i32.const 4)) (i32.const -1))
361
+ (call $__mkptr (i32.const ${PTR.ARRAY}) (i32.const 0) (local.get $newOff)))`
362
+
363
+ // Hot for arr[i] = val (~18M calls in watr self-host). Compute base via __ptr_offset
364
+ // once and read len from the inline header (i32.load base-8) — avoids __len's separate
365
+ // forwarding follow. On the rare grow path the base is recomputed after relocation.
366
+ ctx.core.stdlib['__arr_set_idx_ptr'] = `(func $__arr_set_idx_ptr (param $ptr f64) (param $i i32) (param $val f64) (result f64)
367
+ (local $base i32)
368
+ (if (i32.lt_s (local.get $i) (i32.const 0))
369
+ (then (return (local.get $ptr))))
370
+ (local.set $base (call $__ptr_offset (local.get $ptr)))
371
+ (if (i32.ge_u (local.get $i)
372
+ (i32.load (i32.sub (local.get $base) (i32.const 8))))
373
+ (then
374
+ (local.set $ptr (call $__arr_grow (local.get $ptr) (i32.add (local.get $i) (i32.const 1))))
375
+ (call $__set_len (local.get $ptr) (i32.add (local.get $i) (i32.const 1)))
376
+ (local.set $base (call $__ptr_offset (local.get $ptr)))))
377
+ (f64.store
378
+ (i32.add (local.get $base) (i32.shl (local.get $i) (i32.const 3)))
379
+ (local.get $val))
380
+ (local.get $ptr))`
381
+
382
+ // === Array literal ===
383
+
384
+ ctx.core.emit['['] = (...elems) => {
385
+ const hasSpread = elems.some(e => Array.isArray(e) && e[0] === '...')
386
+
387
+ if (!hasSpread) {
388
+ const len = elems.length
389
+ // R: Static data segment for arrays of pure-literal elements (own-memory only).
390
+ // Saves N×(alloc+store) instructions in __start; raw f64 bits embedded directly.
391
+ if (len >= 4 && !ctx.memory.shared) {
392
+ // asF64 folds i32.const → f64.const literally, so int-literal arrays also qualify.
393
+ const slots = elems.map(e => extractF64Bits(asF64(emit(e))))
394
+ if (slots.every(b => b !== null)) return staticArrayPtr(slots)
395
+ }
396
+ const a = allocArray(len, Math.max(len, 4)) // min cap=4 for small pushes
397
+ const body = [...a.setup]
398
+ for (let i = 0; i < len; i++)
399
+ body.push(['f64.store', slotAddr(a.local, i), asF64(emit(elems[i]))])
400
+ body.push(a.ptr)
401
+ return typed(['block', ['result', 'f64'], ...body], 'f64')
402
+ }
403
+
404
+ const a = allocArray(0, Math.max(elems.length, 4))
405
+ const out = temp('sa'), pos = tempI32('sp')
406
+ inc('__arr_set_idx_ptr')
407
+
408
+ const body = [
409
+ ...a.setup,
410
+ ['local.set', `$${out}`, a.ptr],
411
+ ['local.set', `$${pos}`, ['i32.const', 0]],
412
+ ]
413
+
414
+ for (const e of elems) {
415
+ if (Array.isArray(e) && e[0] === '...') {
416
+ const src = temp('ss'), slen = tempI32('sl'), si = tempI32('si')
417
+ const id = ctx.func.uniq++
418
+ const spreadVal = multiCount(e[1]) ? materializeMulti(e[1]) : asF64(emit(e[1]))
419
+ const spreadItem = ctx.module.modules['string']
420
+ ? ['if', ['result', 'f64'],
421
+ ['i32.or',
422
+ ['i32.eq', ['call', '$__ptr_type', ['local.get', `$${src}`]], ['i32.const', PTR.STRING]],
423
+ ['i32.eq', ['call', '$__ptr_type', ['local.get', `$${src}`]], ['i32.const', PTR.SSO]]],
424
+ ['then', (inc('__str_idx'), ['call', '$__str_idx', ['local.get', `$${src}`], ['local.get', `$${si}`]])],
425
+ ['else', (['call', '$__typed_idx', ['local.get', `$${src}`], ['local.get', `$${si}`]])]]
426
+ : (['call', '$__typed_idx', ['local.get', `$${src}`], ['local.get', `$${si}`]])
427
+
428
+ body.push(
429
+ ['local.set', `$${src}`, spreadVal],
430
+ ['local.set', `$${slen}`, ['call', '$__len', ['local.get', `$${src}`]]],
431
+ ['local.set', `$${si}`, ['i32.const', 0]],
432
+ ['block', `$brk${id}`, ['loop', `$loop${id}`,
433
+ ['br_if', `$brk${id}`, ['i32.ge_s', ['local.get', `$${si}`], ['local.get', `$${slen}`]]],
434
+ ['local.set', `$${out}`, ['call', '$__arr_set_idx_ptr', ['local.get', `$${out}`], ['local.get', `$${pos}`], spreadItem]],
435
+ ['local.set', `$${pos}`, ['i32.add', ['local.get', `$${pos}`], ['i32.const', 1]]],
436
+ ['local.set', `$${si}`, ['i32.add', ['local.get', `$${si}`], ['i32.const', 1]]],
437
+ ['br', `$loop${id}`]]])
438
+ } else {
439
+ body.push(
440
+ ['local.set', `$${out}`, ['call', '$__arr_set_idx_ptr', ['local.get', `$${out}`], ['local.get', `$${pos}`], asF64(emit(e))]],
441
+ ['local.set', `$${pos}`, ['i32.add', ['local.get', `$${pos}`], ['i32.const', 1]]])
442
+ }
443
+ }
444
+
445
+ body.push(['local.get', `$${out}`])
446
+ return typed(['block', ['result', 'f64'], ...body], 'f64')
447
+ }
448
+
449
+ // === Index read ===
450
+
451
+ ctx.core.emit['[]'] = (arr, idx) => {
452
+ // Hoist non-identifier arr so side-effecting sources (e.g. `foo.shift()[i]`) execute once.
453
+ // The rest of the handler inlines `emit(arr)` into multiple IR positions, which would
454
+ // otherwise re-execute the source expression per use at runtime.
455
+ if (typeof arr !== 'string' && !(Array.isArray(arr) && arr[0] === 'local.get')) {
456
+ const vtArr = valTypeOf(arr)
457
+ const h = temp('ai')
458
+ if (vtArr) updateRep(h, { val: vtArr })
459
+ const setup = ['local.set', `$${h}`, asF64(emit(arr))]
460
+ const result = ctx.core.emit['[]'](h, idx)
461
+ return typed(['block', ['result', 'f64'], setup, asF64(result)], 'f64')
462
+ }
463
+ const keyType = typeof idx === 'string' ? lookupValType(idx) : valTypeOf(idx)
464
+ const useRuntimeKeyDispatch = keyType == null || (typeof idx === 'string' && keyType !== VAL.STRING)
465
+ // TypedArray: type-aware load
466
+ if (typeof arr === 'string' && ctx.core.emit['.typed:[]'] &&
467
+ lookupValType(arr) === 'typed') {
468
+ const r = ctx.core.emit['.typed:[]'](arr, idx)
469
+ if (r) return r
470
+ }
471
+ // Literal string key on schema-known object → direct payload slot read (skip __dyn_get)
472
+ const litKey = Array.isArray(idx) && idx[0] === 'str' && typeof idx[1] === 'string' ? idx[1]
473
+ : typeof arr === 'string' && lookupValType(arr) === VAL.OBJECT ? staticPropertyKey(idx)
474
+ : null
475
+ if (litKey != null && typeof arr === 'string' && ctx.schema.find) {
476
+ const slot = ctx.schema.find(arr, litKey)
477
+ if (slot >= 0) {
478
+ inc('__ptr_offset')
479
+ return typed(['f64.load',
480
+ ['i32.add', ['call', '$__ptr_offset', asF64(emit(arr))], ['i32.const', slot * 8]]], 'f64')
481
+ }
482
+ }
483
+ if (litKey != null && typeof arr === 'string' && lookupValType(arr) === VAL.HASH) {
484
+ inc('__hash_get_local_h')
485
+ return typed(['call', '$__hash_get_local_h', asF64(emit(arr)), asF64(emit(['str', litKey])), ['i32.const', strHashLiteral(litKey)]], 'f64')
486
+ }
487
+ if (litKey != null && typeof arr !== 'string' && valTypeOf(arr) === VAL.HASH) {
488
+ inc('__hash_get_local_h')
489
+ return typed(['call', '$__hash_get_local_h', asF64(emit(arr)), asF64(emit(['str', litKey])), ['i32.const', strHashLiteral(litKey)]], 'f64')
490
+ }
491
+ // Multi-value calls are materialized at call site (see '()' handler), so
492
+ // func()[i] works naturally — func() returns a heap array pointer, [i] indexes it.
493
+ const vt = typeof arr === 'string' ? lookupValType(arr) : valTypeOf(arr)
494
+ const va = emit(arr), vi = asI32(emit(idx))
495
+ const ptrExpr = asF64(va)
496
+ const dynLoad = (objExpr, keyExpr) => {
497
+ if (ctx.transform.strict) err(`strict mode: dynamic property access \`${typeof arr === 'string' ? arr : '<expr>'}[<expr>]\` falls back to __dyn_get. Use a literal key or known typed-array receiver, or pass { strict: false }.`)
498
+ inc('__dyn_get')
499
+ return ['call', '$__dyn_get', objExpr, keyExpr]
500
+ }
501
+ const stringLoad = () => (inc('__str_idx'), ['call', '$__str_idx', ptrExpr, vi])
502
+ const arrayLoad = (['call', '$__typed_idx', ptrExpr, vi])
503
+ const emitDynamicKeyDispatch = (objExpr, numericLoad) => {
504
+ const keyTmp = temp()
505
+ inc('__is_str_key')
506
+ return typed(['block', ['result', 'f64'],
507
+ ['local.set', `$${keyTmp}`, asF64(emit(idx))],
508
+ ['if', ['result', 'f64'], ['call', '$__is_str_key', ['local.get', `$${keyTmp}`]],
509
+ ['then', dynLoad(objExpr, ['local.get', `$${keyTmp}`])],
510
+ ['else', numericLoad(['local.get', `$${keyTmp}`])]]], 'f64')
511
+ }
512
+ // Boxed object: string keys address the box, numeric keys address the inner array.
513
+ if (typeof arr === 'string' && ctx.schema.isBoxed?.(arr)) {
514
+ const inner = ctx.schema.emitInner(arr)
515
+ if (keyType === VAL.STRING) return typed(dynLoad(asF64(emit(arr)), asF64(emit(idx))), 'f64')
516
+ if (useRuntimeKeyDispatch)
517
+ return emitDynamicKeyDispatch(asF64(emit(arr)), keyExpr =>
518
+ ['f64.load', ['i32.add', ['call', '$__ptr_offset', inner], ['i32.shl', asI32(typed(keyExpr, 'f64')), ['i32.const', 3]]]])
519
+ return typed(
520
+ ['f64.load', ['i32.add', ['call', '$__ptr_offset', inner], ['i32.shl', vi, ['i32.const', 3]]]],
521
+ 'f64')
522
+ }
523
+ // Known array → direct f64 element load, skip string check
524
+ if (keyType === VAL.STRING)
525
+ return typed(dynLoad(ptrExpr, asF64(emit(idx))), 'f64')
526
+ if (vt === 'array') {
527
+ // Known-ARRAY → __arr_idx (single forwarding follow + inline bounds check),
528
+ // not __typed_idx (which does __len + __ptr_offset = two forwarding follows
529
+ // plus type-dispatch overhead irrelevant for plain arrays).
530
+ const keyIsNum = keyType === VAL.NUMBER
531
+ // Inline fast path: when arr has a known element schema and key is a
532
+ // known number, the type-tag check and bounds check inside __arr_idx are
533
+ // dead weight in hot kernels. Emit (f64.load (i32.add base (shl i 3)))
534
+ // directly. base goes through __ptr_offset (still does forwarding follow),
535
+ // and hoistAddrBase will CSE the (base, i) pair across the iteration body.
536
+ // Array<NUMBER>/<STRING>/<OBJECT> all use 8-byte f64 slot layout — direct
537
+ // f64.load is correct regardless of elem kind (returns raw f64 for NUMBER,
538
+ // NaN-boxed pointer for OBJECT/STRING; downstream typed() handles both).
539
+ // Fast path fires on schemaId (Array<{x,y,z}> shapes) OR plain elem-val
540
+ // (Array<NUMBER> from `.map(x => x*k)` etc.).
541
+ const rep = typeof arr === 'string' ? ctx.func.repByLocal?.get(arr) : null
542
+ const hasElemFact = rep?.arrayElemSchema != null || rep?.arrayElemValType != null
543
+ if (hasElemFact && keyIsNum) {
544
+ inc('__ptr_offset')
545
+ // __ptr_offset returns i32 — base local must be i32 (not the default
546
+ // f64 NaN-box temp). Flat tee form so downstream peepholes can fold
547
+ // `i32.wrap_i64 (i64.reinterpret_f64 (f64.load …))` → `i32.load …`
548
+ // when this load feeds a ptrUnboxed OBJECT field.
549
+ const baseI32 = tempI32('ab')
550
+ return typed(['f64.load',
551
+ ['i32.add',
552
+ ['local.tee', `$${baseI32}`,
553
+ ['call', '$__ptr_offset', ptrExpr]],
554
+ ['i32.shl', vi, ['i32.const', 3]]]], 'f64')
555
+ }
556
+ inc('__arr_idx')
557
+ const baseTmp = temp()
558
+ // Numeric key (literal or known-NUMBER name) → skip __is_str_key dispatch;
559
+ // arrays don't honor string-key access for numeric keys (keys aren't coerced
560
+ // back to numbers for ARRAY index reads). Mirrors the VAL.TYPED branch below.
561
+ if (useRuntimeKeyDispatch && !keyIsNum)
562
+ return typed(['block', ['result', 'f64'],
563
+ ['local.set', `$${baseTmp}`, ptrExpr],
564
+ emitDynamicKeyDispatch(typed(['local.get', `$${baseTmp}`], 'f64'), keyExpr => {
565
+ const keyI32 = asI32(typed(keyExpr, 'f64'))
566
+ return (['call', '$__arr_idx', ['local.get', `$${baseTmp}`], keyI32])
567
+ })], 'f64')
568
+ return typed(['block', ['result', 'f64'],
569
+ ['local.set', `$${baseTmp}`, ptrExpr],
570
+ (['call', '$__arr_idx', ['local.get', `$${baseTmp}`], vi])], 'f64')
571
+ }
572
+ // Known string → single-char SSO string
573
+ if (vt === 'string')
574
+ return typed(stringLoad(), 'f64')
575
+ // Known typed-array (ctor unknown — bimorphic call sites). Skip str-key dispatch
576
+ // since arr is provably never a string. Inner __typed_idx still ctor-dispatches.
577
+ // Key narrowing: if idx is provably NUMBER (via lookupValType on the name), the
578
+ // str-key check is dead — emit the direct __typed_idx call. Other key shapes keep
579
+ // the runtime str_key dispatch (rare for typed arrays but legal: arr['length']).
580
+ if (vt === 'typed') {
581
+ const keyIsNum = keyType === VAL.NUMBER
582
+ if (useRuntimeKeyDispatch && !keyIsNum)
583
+ return emitDynamicKeyDispatch(ptrExpr, keyExpr => {
584
+ const keyI32 = asI32(typed(keyExpr, 'f64'))
585
+ return (['call', '$__typed_idx', ptrExpr, keyI32])
586
+ })
587
+ return typed((['call', '$__typed_idx', ptrExpr, vi]), 'f64')
588
+ }
589
+ if (useRuntimeKeyDispatch)
590
+ return emitDynamicKeyDispatch(ptrExpr, keyExpr => {
591
+ const keyI32 = asI32(typed(keyExpr, 'f64'))
592
+ if (ctx.module.modules['string']) {
593
+ return ['if', ['result', 'f64'],
594
+ ['i32.or',
595
+ ['i32.eq', ['call', '$__ptr_type', ptrExpr], ['i32.const', PTR.STRING]],
596
+ ['i32.eq', ['call', '$__ptr_type', ptrExpr], ['i32.const', PTR.SSO]]],
597
+ ['then', (inc('__str_idx'), ['call', '$__str_idx', ptrExpr, keyI32])],
598
+ ['else', (['call', '$__typed_idx', ptrExpr, keyI32])]]
599
+ }
600
+ return (['call', '$__typed_idx', ptrExpr, keyI32])
601
+ })
602
+ // Unknown → runtime dispatch (string module loaded → check ptr_type)
603
+ if (ctx.module.modules['string'])
604
+ return typed(
605
+ ['if', ['result', 'f64'],
606
+ ['i32.or',
607
+ ['i32.eq', ['call', '$__ptr_type', ptrExpr], ['i32.const', PTR.STRING]],
608
+ ['i32.eq', ['call', '$__ptr_type', ptrExpr], ['i32.const', PTR.SSO]]],
609
+ ['then', stringLoad()],
610
+ ['else', arrayLoad]],
611
+ 'f64')
612
+ return typed(arrayLoad, 'f64')
613
+ }
614
+
615
+ // === Push/Pop (mutate in place) ===
616
+
617
+ // .push(val) → append, increment len, return array (possibly reallocated pointer)
618
+ ctx.core.emit['.push'] = (arr, ...vals) => {
619
+ const va = asF64(emit(arr))
620
+ const t = temp('pp'), len = tempI32('pl')
621
+
622
+ // Known ARRAY → inline len as `i32.load(off - 8)` (ARRAY branch of __len). Saves a
623
+ // full __ptr_type + dispatch per push site. The off<8 nullish guard in __len is
624
+ // unreachable here: .push on a nullish var is a JS error before we get here.
625
+ const vt = typeof arr === 'string' ? lookupValType(arr) : valTypeOf(arr)
626
+ const inlineLen = vt === VAL.ARRAY
627
+ const grow = inlineLen ? '__arr_grow_known' : '__arr_grow'
628
+ inc(grow)
629
+
630
+ const body = [
631
+ ['local.set', `$${t}`, va],
632
+ ]
633
+ const pushBase = tempI32('pb')
634
+ if (inlineLen) {
635
+ // Hoist offset once; reuse for len load, cap-fits check, store base, and
636
+ // post-grow rebase. On cap-fits (the common path) we skip __arr_grow's call
637
+ // dispatch and prologue entirely; on grow we re-extract offset because the
638
+ // alloc may have relocated the buffer.
639
+ body.push(
640
+ ['local.set', `$${pushBase}`, ['call', '$__ptr_offset', ['local.get', `$${t}`]]],
641
+ ['local.set', `$${len}`,
642
+ ['i32.load', ['i32.sub', ['local.get', `$${pushBase}`], ['i32.const', 8]]]],
643
+ ['if',
644
+ ['i32.lt_s',
645
+ ['i32.load', ['i32.sub', ['local.get', `$${pushBase}`], ['i32.const', 4]]],
646
+ ['i32.add', ['local.get', `$${len}`], ['i32.const', vals.length]]],
647
+ ['then',
648
+ ['local.set', `$${t}`, ['call', `$${grow}`, ['local.get', `$${t}`],
649
+ ['i32.add', ['local.get', `$${len}`], ['i32.const', vals.length]]]],
650
+ ['local.set', `$${pushBase}`, ['call', '$__ptr_offset', ['local.get', `$${t}`]]]]],
651
+ )
652
+ } else {
653
+ body.push(
654
+ ['local.set', `$${len}`, ['call', '$__len', ['local.get', `$${t}`]]],
655
+ // Grow if needed: ensure cap >= len + vals.length
656
+ ['local.set', `$${t}`, ['call', `$${grow}`, ['local.get', `$${t}`],
657
+ ['i32.add', ['local.get', `$${len}`], ['i32.const', vals.length]]]],
658
+ ['local.set', `$${pushBase}`, ['call', '$__ptr_offset', ['local.get', `$${t}`]]],
659
+ )
660
+ }
661
+
662
+ // Store each value and increment len
663
+ for (const val of vals) {
664
+ const vv = asF64(emit(val))
665
+ body.push(
666
+ ['f64.store',
667
+ ['i32.add', ['local.get', `$${pushBase}`], ['i32.shl', ['local.get', `$${len}`], ['i32.const', 3]]],
668
+ vv],
669
+ ['local.set', `$${len}`, ['i32.add', ['local.get', `$${len}`], ['i32.const', 1]]]
670
+ )
671
+ }
672
+
673
+ // Update length header, update source variable (pointer may have changed from grow), return new length
674
+ body.push(['call', '$__set_len', ['local.get', `$${t}`], ['local.get', `$${len}`]])
675
+ // Update the source variable if it's a named variable (so arr still points to valid memory)
676
+ if (typeof arr === 'string') {
677
+ if (ctx.func.boxed?.has(arr)) {
678
+ body.push(['f64.store', ['local.get', `$${ctx.func.boxed.get(arr)}`], ['local.get', `$${t}`]])
679
+ }
680
+ else if (ctx.scope.globals.has(arr) && !ctx.func.locals?.has(arr))
681
+ body.push(['global.set', `$${arr}`, ['local.get', `$${t}`]])
682
+ else
683
+ body.push(['local.set', `$${arr}`, ['local.get', `$${t}`]])
684
+ }
685
+ body.push(['f64.convert_i32_s', ['local.get', `$${len}`]])
686
+
687
+ return typed(['block', ['result', 'f64'], ...body], 'f64')
688
+ }
689
+
690
+ // .pop() → decrement len, return removed element
691
+ ctx.core.emit['.pop'] = (arr) => {
692
+ const va = asF64(emit(arr))
693
+ const t = temp('po'), len = tempI32('pl')
694
+ // Known ARRAY → inline len (skips __len dispatch tree).
695
+ const vt = typeof arr === 'string' ? lookupValType(arr) : valTypeOf(arr)
696
+ const rawLen = vt === VAL.ARRAY
697
+ ? ['i32.load', ['i32.sub', ['call', '$__ptr_offset', ['local.get', `$${t}`]], ['i32.const', 8]]]
698
+ : ['call', '$__len', ['local.get', `$${t}`]]
699
+ return typed(['block', ['result', 'f64'],
700
+ ['local.set', `$${t}`, va],
701
+ ['local.set', `$${len}`, ['i32.sub', rawLen, ['i32.const', 1]]],
702
+ ['call', '$__set_len', ['local.get', `$${t}`], ['local.get', `$${len}`]],
703
+ ['f64.load',
704
+ ['i32.add', ['call', '$__ptr_offset', ['local.get', `$${t}`]], ['i32.shl', ['local.get', `$${len}`], ['i32.const', 3]]]]], 'f64')
705
+ }
706
+
707
+ // .shift() → remove first element, shift remaining left, return removed
708
+ ctx.core.emit['.shift'] = arrMethod('__arr_shift')
709
+
710
+ ctx.core.stdlib['__arr_shift'] = () => `(func $__arr_shift (param $arr f64) (result f64)
711
+ (local $bits i64) (local $rawOff i32) (local $off i32) (local $newOff i32) (local $len i32) (local $cap i32) (local $val f64)
712
+ (local.set $bits (i64.reinterpret_f64 (local.get $arr)))
713
+ (local.set $rawOff (i32.wrap_i64 (i64.and (local.get $bits) (i64.const 0xFFFFFFFF))))
714
+ (local.set $off (call $__ptr_offset (local.get $arr)))
715
+ (if (result f64) (i32.lt_u (local.get $off) (i32.const 8))
716
+ (then (f64.const 0))
717
+ (else
718
+ (local.set $len (i32.load (i32.sub (local.get $off) (i32.const 8))))
719
+ (if (result f64) (i32.le_s (local.get $len) (i32.const 0))
720
+ (then (f64.const 0))
721
+ (else
722
+ (local.set $val (f64.load (local.get $off)))
723
+ (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
724
+ (local.set $newOff (i32.add (local.get $off) (i32.const 8)))
725
+ (i32.store (local.get $off) (i32.sub (local.get $len) (i32.const 1)))
726
+ (i32.store (i32.add (local.get $off) (i32.const 4))
727
+ (select (i32.sub (local.get $cap) (i32.const 1)) (i32.const 0) (i32.gt_s (local.get $cap) (i32.const 0))))
728
+ ${maybeDynMoveIR()}
729
+ (i32.store (i32.sub (local.get $off) (i32.const 8)) (local.get $newOff))
730
+ (i32.store (i32.sub (local.get $off) (i32.const 4)) (i32.const -1))
731
+ (if (i32.and (i32.ne (local.get $rawOff) (local.get $off)) (i32.ge_u (local.get $rawOff) (i32.const 8)))
732
+ (then
733
+ (i32.store (i32.sub (local.get $rawOff) (i32.const 8)) (local.get $newOff))
734
+ (i32.store (i32.sub (local.get $rawOff) (i32.const 4)) (i32.const -1))))
735
+ (local.get $val))))))`
736
+
737
+ // .splice(start) | .splice(start, deleteCount) → remove range, return removed as new array
738
+ ctx.core.emit['.splice'] = (arr, start, deleteCount) => {
739
+ const recv = hoistArrayValue(arr)
740
+ const va = recv.value
741
+ const vs = asI32(emit(start))
742
+ const s = tempI32('sps'), cnt = tempI32('spc'), len = tempI32('spl'), off = tempI32('spo'), j = tempI32('spj')
743
+ const out = allocPtr({ type: PTR.ARRAY, len: ['local.get', `$${cnt}`], tag: 'sp' })
744
+ const id = ctx.func.uniq++
745
+ // Known ARRAY → fuse len with offset (__len would re-compute __ptr_offset + dispatch).
746
+ const svt = typeof arr === 'string' ? lookupValType(arr) : valTypeOf(arr)
747
+ const lenInit = svt === VAL.ARRAY
748
+ ? ['local.set', `$${len}`, ['i32.load', ['i32.sub', ['local.get', `$${off}`], ['i32.const', 8]]]]
749
+ : ['local.set', `$${len}`, ['call', '$__len', va]]
750
+ const body = [
751
+ recv.setup,
752
+ ['local.set', `$${off}`, ['call', '$__ptr_offset', va]],
753
+ lenInit,
754
+ // clamp start to [0, len]
755
+ ['local.set', `$${s}`, vs],
756
+ ['if', ['i32.lt_s', ['local.get', `$${s}`], ['i32.const', 0]],
757
+ ['then',
758
+ ['local.set', `$${s}`, ['i32.add', ['local.get', `$${s}`], ['local.get', `$${len}`]]],
759
+ ['if', ['i32.lt_s', ['local.get', `$${s}`], ['i32.const', 0]],
760
+ ['then', ['local.set', `$${s}`, ['i32.const', 0]]]]]],
761
+ ['if', ['i32.gt_s', ['local.get', `$${s}`], ['local.get', `$${len}`]],
762
+ ['then', ['local.set', `$${s}`, ['local.get', `$${len}`]]]],
763
+ // compute count
764
+ deleteCount === undefined
765
+ ? ['local.set', `$${cnt}`, ['i32.sub', ['local.get', `$${len}`], ['local.get', `$${s}`]]]
766
+ : ['block',
767
+ ['local.set', `$${cnt}`, asI32(emit(deleteCount))],
768
+ ['if', ['i32.lt_s', ['local.get', `$${cnt}`], ['i32.const', 0]],
769
+ ['then', ['local.set', `$${cnt}`, ['i32.const', 0]]]],
770
+ ['if', ['i32.gt_s',
771
+ ['i32.add', ['local.get', `$${s}`], ['local.get', `$${cnt}`]],
772
+ ['local.get', `$${len}`]],
773
+ ['then', ['local.set', `$${cnt}`,
774
+ ['i32.sub', ['local.get', `$${len}`], ['local.get', `$${s}`]]]]]],
775
+ // allocate result array of size cnt
776
+ out.init,
777
+ // copy removed elements into new array
778
+ ['memory.copy',
779
+ ['local.get', `$${out.local}`],
780
+ ['i32.add', ['local.get', `$${off}`], ['i32.shl', ['local.get', `$${s}`], ['i32.const', 3]]],
781
+ ['i32.shl', ['local.get', `$${cnt}`], ['i32.const', 3]]],
782
+ // shift remaining elements left: copy arr[s+cnt..len] → arr[s..]
783
+ ['memory.copy',
784
+ ['i32.add', ['local.get', `$${off}`], ['i32.shl', ['local.get', `$${s}`], ['i32.const', 3]]],
785
+ ['i32.add', ['local.get', `$${off}`], ['i32.shl',
786
+ ['i32.add', ['local.get', `$${s}`], ['local.get', `$${cnt}`]], ['i32.const', 3]]],
787
+ ['i32.shl',
788
+ ['i32.sub', ['i32.sub', ['local.get', `$${len}`], ['local.get', `$${s}`]], ['local.get', `$${cnt}`]],
789
+ ['i32.const', 3]]],
790
+ // update length
791
+ ['call', '$__set_len', va, ['i32.sub', ['local.get', `$${len}`], ['local.get', `$${cnt}`]]],
792
+ out.ptr,
793
+ ]
794
+ return typed(['block', ['result', 'f64'], ...body], 'f64')
795
+ }
796
+
797
+ // .unshift(val) → prepend element, shift existing right
798
+ ctx.core.emit['.unshift'] = arrMethod('__arr_unshift', 1)
799
+
800
+ ctx.core.stdlib['__arr_unshift'] = `(func $__arr_unshift (param $arr f64) (param $val f64) (result f64)
801
+ (local $off i32) (local $len i32)
802
+ (local.set $arr (call $__arr_grow (local.get $arr) (i32.add (call $__len (local.get $arr)) (i32.const 1))))
803
+ (local.set $off (call $__ptr_offset (local.get $arr)))
804
+ (local.set $len (call $__len (local.get $arr)))
805
+ (memory.copy
806
+ (i32.add (local.get $off) (i32.const 8))
807
+ (local.get $off)
808
+ (i32.shl (local.get $len) (i32.const 3)))
809
+ (f64.store (local.get $off) (local.get $val))
810
+ (call $__set_len (local.get $arr) (i32.add (local.get $len) (i32.const 1)))
811
+ (f64.convert_i32_s (i32.add (local.get $len) (i32.const 1))))`
812
+
813
+ // .some(fn) → return 1 if any element passes, else 0 (early exit)
814
+ ctx.core.emit['.some'] = (arr, fn) => {
815
+ const recv = hoistArrayValue(arr)
816
+ const r = temp('sr')
817
+ const exit = `$exit${ctx.func.uniq++}`
818
+ const cb = makeCallback(fn, callbackArgReps(arr))
819
+ const loop = arrayLoop(recv.value, (_ptr, _len, i, item) => [
820
+ ['if', truthyIR(cb.call([item, idxArg(cb, i)])),
821
+ ['then', ['local.set', `$${r}`, ['f64.const', 1]], ['br', exit]]]
822
+ ])
823
+ return typed(['block', ['result', 'f64'],
824
+ recv.setup,
825
+ cb.setup,
826
+ ['local.set', `$${r}`, ['f64.const', 0]],
827
+ ['block', exit, ...loop],
828
+ ['local.get', `$${r}`]], 'f64')
829
+ }
830
+
831
+ // .every(fn) → return 1 if all elements pass, else 0 (early exit)
832
+ ctx.core.emit['.every'] = (arr, fn) => {
833
+ const recv = hoistArrayValue(arr)
834
+ const r = temp('ev')
835
+ const exit = `$exit${ctx.func.uniq++}`
836
+ const cb = makeCallback(fn, callbackArgReps(arr))
837
+ const loop = arrayLoop(recv.value, (_ptr, _len, i, item) => [
838
+ ['if', ['i32.eqz', truthyIR(cb.call([item, idxArg(cb, i)]))],
839
+ ['then', ['local.set', `$${r}`, ['f64.const', 0]], ['br', exit]]]
840
+ ])
841
+ return typed(['block', ['result', 'f64'],
842
+ recv.setup,
843
+ cb.setup,
844
+ ['local.set', `$${r}`, ['f64.const', 1]],
845
+ ['block', exit, ...loop],
846
+ ['local.get', `$${r}`]], 'f64')
847
+ }
848
+
849
+ // .findIndex(fn) → return index of first matching element, or -1 (early exit)
850
+ ctx.core.emit['.findIndex'] = (arr, fn) => {
851
+ const recv = hoistArrayValue(arr)
852
+ const r = temp('fi')
853
+ const exit = `$exit${ctx.func.uniq++}`
854
+ const cb = makeCallback(fn, callbackArgReps(arr))
855
+ const loop = arrayLoop(recv.value, (_ptr, _len, i, item) => [
856
+ ['if', truthyIR(cb.call([item, idxArg(cb, i)])),
857
+ ['then', ['local.set', `$${r}`, ['f64.convert_i32_s', ['local.get', `$${i}`]]], ['br', exit]]]
858
+ ])
859
+ return typed(['block', ['result', 'f64'],
860
+ recv.setup,
861
+ cb.setup,
862
+ ['local.set', `$${r}`, ['f64.const', -1]],
863
+ ['block', exit, ...loop],
864
+ ['local.get', `$${r}`]], 'f64')
865
+ }
866
+
867
+ // === Array methods ===
868
+
869
+ // Fusion is only semantics-preserving when callbacks are side-effect-free.
870
+ // A callback with calls or writes to outer state (e.g., `ctx.push(x)`) observes
871
+ // the iteration order; fusing filter().forEach() would interleave them.
872
+ // Conservative purity: no call-expressions (covers method calls, free fn calls);
873
+ // no assignments to names not declared locally in the callback.
874
+ function collectLocals(node, locals) {
875
+ if (!Array.isArray(node)) return
876
+ const [op, ...args] = node
877
+ if (op === '=>') return
878
+ if (op === 'let' || op === 'const' || op === 'var') {
879
+ for (const a of args) if (Array.isArray(a) && a[0] === '=' && typeof a[1] === 'string') locals.add(a[1])
880
+ }
881
+ for (const a of args) collectLocals(a, locals)
882
+ }
883
+ function isPureCallback(fn) {
884
+ if (!Array.isArray(fn) || fn[0] !== '=>') return false
885
+ const body = fn[2]
886
+ const params = new Set()
887
+ const p = fn[1]
888
+ const raw = p == null ? [] : Array.isArray(p) ? (p[0] === ',' ? p.slice(1) : p[0] === '()' ? (p[1] == null ? [] : Array.isArray(p[1]) && p[1][0] === ',' ? p[1].slice(1) : [p[1]]) : [p]) : [p]
889
+ for (const r of raw) params.add(Array.isArray(r) && r[0] === '...' ? r[1] : typeof r === 'string' ? r : Array.isArray(r) && r[0] === '=' ? r[1] : null)
890
+ const locals = new Set(params)
891
+ collectLocals(body, locals)
892
+ let pure = true
893
+ ;(function walk(node) {
894
+ if (!pure || !Array.isArray(node)) return
895
+ const [op, ...args] = node
896
+ if (op === '=>') return
897
+ if (op === '()' || op === '?.()' || op === 'new') { pure = false; return }
898
+ if (op === '++' || op === '--') { pure = false; return }
899
+ if (op === '=' || op === '+=' || op === '-=' || op === '*=' || op === '/=' || op === '%=' || op === '&=' || op === '|=' || op === '^=' || op === '>>=' || op === '<<=' || op === '>>>=' || op === '||=' || op === '&&=' || op === '??=') {
900
+ const t = args[0]
901
+ if (typeof t === 'string') { if (!locals.has(t)) { pure = false; return } }
902
+ else { pure = false; return }
903
+ }
904
+ for (const a of args) walk(a)
905
+ })(body)
906
+ return pure
907
+ }
908
+
909
+ // Detect fuseable chain: arr.map(f).filter(g) etc.
910
+ // Returns {source, method, fn} or null.
911
+ function detectUpstream(arr) {
912
+ if (!Array.isArray(arr) || arr[0] !== '()') return null
913
+ const [, callee, ...callArgs] = arr
914
+ if (!Array.isArray(callee) || callee[0] !== '.' || callArgs.length !== 1) return null
915
+ const [, source, method] = callee
916
+ if (method !== 'map' && method !== 'filter') return null
917
+ if (!isPureCallback(callArgs[0])) return null
918
+ return { source, method, fn: callArgs[0] }
919
+ }
920
+
921
+ function idxF64(i) { return typed(['f64.convert_i32_s', ['local.get', `$${i}`]], 'f64') }
922
+ // Skip f64-convert when callback's index param is unused — saves per-iteration conversion.
923
+ function idxArg(cb, i, slot = 1) {
924
+ return cb.usedParams && !cb.usedParams[slot] ? null : idxF64(i)
925
+ }
926
+
927
+ ctx.core.emit['.map'] = (arr, fn) => {
928
+ // .filter(f).map(g) → single loop: test f, apply g if passes
929
+ const up = detectUpstream(arr)
930
+ if (up && up.method === 'filter' && isPureCallback(fn)) {
931
+ const recv = hoistArrayValue(up.source)
932
+ const count = tempI32('fc'), maxLen = tempI32('fm')
933
+ const upReps = callbackArgReps(up.source)
934
+ const filterCb = makeCallback(up.fn, upReps), mapCb = makeCallback(fn, upReps)
935
+ const out = allocPtr({ type: PTR.ARRAY, len: 0, cap: ['local.get', `$${maxLen}`], tag: 'fm' })
936
+ const loop = arrayLoop(recv.value, (_p, _l, i, item) => [
937
+ ['if', truthyIR(filterCb.call([item, idxArg(filterCb, i)])),
938
+ ['then',
939
+ elemStore(out.local, count, asF64(mapCb.call([item, idxArg(mapCb, count)]))),
940
+ ['local.set', `$${count}`, ['i32.add', ['local.get', `$${count}`], ['i32.const', 1]]]]]
941
+ ], maxLen)
942
+ return typed(['block', ['result', 'f64'],
943
+ recv.setup, filterCb.setup, mapCb.setup,
944
+ ['local.set', `$${maxLen}`, ['call', '$__len', recv.value]],
945
+ out.init, ['local.set', `$${count}`, ['i32.const', 0]],
946
+ ...loop,
947
+ ['i32.store', ['i32.sub', ['local.get', `$${out.local}`], ['i32.const', 8]], ['local.get', `$${count}`]],
948
+ out.ptr], 'f64')
949
+ }
950
+ const recv = hoistArrayValue(arr)
951
+ const len = tempI32('ml')
952
+ const cb = makeCallback(fn, callbackArgReps(arr))
953
+ const lenIR = ['local.get', `$${len}`]
954
+ const out = allocPtr({ type: PTR.ARRAY, len: lenIR, tag: 'mo' })
955
+ // Reuse the precomputed len local in arrayLoop (skip its internal load).
956
+ const loop = arrayLoop(recv.value, (_ptr, _len, i, item) => [
957
+ elemStore(out.local, i, asF64(cb.call([item, idxArg(cb, i)])))
958
+ ], len)
959
+ return typed(['block', ['result', 'f64'],
960
+ recv.setup,
961
+ cb.setup,
962
+ ['local.set', `$${len}`, ['call', '$__len', recv.value]],
963
+ out.init,
964
+ ...loop,
965
+ out.ptr], 'f64')
966
+ }
967
+
968
+ ctx.core.emit['.filter'] = (arr, fn) => {
969
+ // .map(f).filter(g) → single loop: apply f, test g, store if passes
970
+ const up = detectUpstream(arr)
971
+ if (up && up.method === 'map' && isPureCallback(fn)) {
972
+ const recv = hoistArrayValue(up.source)
973
+ const count = tempI32('fc'), maxLen = tempI32('fm'), mapped = temp('mv')
974
+ const upReps = callbackArgReps(up.source)
975
+ const mapCb = makeCallback(up.fn, upReps), filterCb = makeCallback(fn)
976
+ const out = allocPtr({ type: PTR.ARRAY, len: 0, cap: ['local.get', `$${maxLen}`], tag: 'mf' })
977
+ const loop = arrayLoop(recv.value, (_p, _l, i, item) => [
978
+ ['local.set', `$${mapped}`, asF64(mapCb.call([item, idxArg(mapCb, i)]))],
979
+ ['if', truthyIR(filterCb.call([typed(['local.get', `$${mapped}`], 'f64'), idxArg(filterCb, i)])),
980
+ ['then',
981
+ ['f64.store', ['i32.add', ['local.get', `$${out.local}`], ['i32.shl', ['local.get', `$${count}`], ['i32.const', 3]]], ['local.get', `$${mapped}`]],
982
+ ['local.set', `$${count}`, ['i32.add', ['local.get', `$${count}`], ['i32.const', 1]]]]]
983
+ ], maxLen)
984
+ return typed(['block', ['result', 'f64'],
985
+ recv.setup, mapCb.setup, filterCb.setup,
986
+ ['local.set', `$${maxLen}`, ['call', '$__len', recv.value]],
987
+ out.init, ['local.set', `$${count}`, ['i32.const', 0]],
988
+ ...loop,
989
+ ['i32.store', ['i32.sub', ['local.get', `$${out.local}`], ['i32.const', 8]], ['local.get', `$${count}`]],
990
+ out.ptr], 'f64')
991
+ }
992
+ const recv = hoistArrayValue(arr)
993
+ const count = tempI32('fc'), maxLen = tempI32('fm')
994
+ const cb = makeCallback(fn, callbackArgReps(arr))
995
+ const out = allocPtr({ type: PTR.ARRAY, len: 0, cap: ['local.get', `$${maxLen}`], tag: 'fo' })
996
+ const loop = arrayLoop(recv.value, (_ptr, _len, i, item) => [
997
+ ['if', truthyIR(cb.call([item, idxArg(cb, i)])),
998
+ ['then',
999
+ ['f64.store', ['i32.add', ['local.get', `$${out.local}`], ['i32.shl', ['local.get', `$${count}`], ['i32.const', 3]]], item],
1000
+ ['local.set', `$${count}`, ['i32.add', ['local.get', `$${count}`], ['i32.const', 1]]]]]
1001
+ ], maxLen)
1002
+ return typed(['block', ['result', 'f64'],
1003
+ recv.setup,
1004
+ cb.setup,
1005
+ ['local.set', `$${maxLen}`, ['call', '$__len', recv.value]],
1006
+ out.init,
1007
+ ['local.set', `$${count}`, ['i32.const', 0]],
1008
+ ...loop,
1009
+ // Patch actual length into header (data start - 8).
1010
+ ['i32.store', ['i32.sub', ['local.get', `$${out.local}`], ['i32.const', 8]], ['local.get', `$${count}`]],
1011
+ out.ptr], 'f64')
1012
+ }
1013
+
1014
+ ctx.core.emit['.reduce'] = (arr, fn, init) => {
1015
+ const up = detectUpstream(arr)
1016
+ // .map(f).reduce(g, init) → single loop: apply f, accumulate with g
1017
+ if (up && up.method === 'map') {
1018
+ const recv = hoistArrayValue(up.source)
1019
+ const acc = temp('ra'), mapped = temp('mv')
1020
+ const upReps = callbackArgReps(up.source)
1021
+ const mapCb = makeCallback(up.fn, upReps), redCb = makeCallback(fn)
1022
+ const loop = arrayLoop(recv.value, (_p, _l, i, item) => [
1023
+ ['local.set', `$${mapped}`, asF64(mapCb.call([item, idxArg(mapCb, i)]))],
1024
+ ['local.set', `$${acc}`, asF64(redCb.call([typed(['local.get', `$${acc}`], 'f64'), typed(['local.get', `$${mapped}`], 'f64')]))]
1025
+ ])
1026
+ return typed(['block', ['result', 'f64'],
1027
+ recv.setup, mapCb.setup, redCb.setup,
1028
+ ['local.set', `$${acc}`, init ? asF64(emit(init)) : ['f64.const', 0]],
1029
+ ...loop, ['local.get', `$${acc}`]], 'f64')
1030
+ }
1031
+ // .filter(f).reduce(g, init) → single loop: test f, accumulate with g if passes
1032
+ if (up && up.method === 'filter') {
1033
+ const recv = hoistArrayValue(up.source)
1034
+ const acc = temp('ra')
1035
+ const upReps = callbackArgReps(up.source)
1036
+ const filterCb = makeCallback(up.fn, upReps)
1037
+ // reduce cb signature: (acc, item, idx). Item rep mirrors upstream's item rep.
1038
+ const redCb = makeCallback(fn, [null, upReps[0], { val: VAL.NUMBER }])
1039
+ const loop = arrayLoop(recv.value, (_p, _l, i, item) => [
1040
+ ['if', truthyIR(filterCb.call([item, idxArg(filterCb, i)])),
1041
+ ['then', ['local.set', `$${acc}`, asF64(redCb.call([typed(['local.get', `$${acc}`], 'f64'), item]))]]]
1042
+ ])
1043
+ return typed(['block', ['result', 'f64'],
1044
+ recv.setup, filterCb.setup, redCb.setup,
1045
+ ['local.set', `$${acc}`, init ? asF64(emit(init)) : ['f64.const', 0]],
1046
+ ...loop, ['local.get', `$${acc}`]], 'f64')
1047
+ }
1048
+ const recv = hoistArrayValue(arr)
1049
+ const acc = temp('ra')
1050
+ // reduce cb signature: (acc, item, idx). Item rep mirrors recv's elem val type.
1051
+ const reps = callbackArgReps(arr)
1052
+ const cb = makeCallback(fn, [null, reps[0], { val: VAL.NUMBER }])
1053
+ const loop = arrayLoop(recv.value, (_ptr, _len, _i, item) => [
1054
+ ['local.set', `$${acc}`, asF64(cb.call([typed(['local.get', `$${acc}`], 'f64'), item]))]
1055
+ ])
1056
+ return typed(['block', ['result', 'f64'],
1057
+ recv.setup,
1058
+ cb.setup,
1059
+ ['local.set', `$${acc}`, init ? asF64(emit(init)) : ['f64.const', 0]],
1060
+ ...loop,
1061
+ ['local.get', `$${acc}`]], 'f64')
1062
+ }
1063
+
1064
+ ctx.core.emit['.forEach'] = (arr, fn) => {
1065
+ // .map(f).forEach(g) → single loop: apply f, call g — no intermediate array
1066
+ const up = detectUpstream(arr)
1067
+ if (up && up.method === 'map' && isPureCallback(fn)) {
1068
+ const recv = hoistArrayValue(up.source)
1069
+ const mapped = temp('mv'), tmp = temp('ft')
1070
+ const upReps = callbackArgReps(up.source)
1071
+ const mapCb = makeCallback(up.fn, upReps), forCb = makeCallback(fn)
1072
+ const loop = arrayLoop(recv.value, (_p, _l, i, item) => [
1073
+ ['local.set', `$${mapped}`, asF64(mapCb.call([item, idxArg(mapCb, i)]))],
1074
+ ['local.set', `$${tmp}`, asF64(forCb.call([typed(['local.get', `$${mapped}`], 'f64'), idxArg(forCb, i)]))]
1075
+ ])
1076
+ return typed(['block', ['result', 'f64'], recv.setup, mapCb.setup, forCb.setup, ...loop, ['f64.const', 0]], 'f64')
1077
+ }
1078
+ if (up && up.method === 'filter') {
1079
+ const recv = hoistArrayValue(up.source)
1080
+ const tmp = temp('ft')
1081
+ const upReps = callbackArgReps(up.source)
1082
+ const filterCb = makeCallback(up.fn, upReps), forCb = makeCallback(fn, upReps)
1083
+ const loop = arrayLoop(recv.value, (_p, _l, i, item) => [
1084
+ ['if', truthyIR(filterCb.call([item, idxArg(filterCb, i)])),
1085
+ ['then', ['local.set', `$${tmp}`, asF64(forCb.call([item, idxArg(forCb, i)]))]]]
1086
+ ])
1087
+ return typed(['block', ['result', 'f64'], recv.setup, filterCb.setup, forCb.setup, ...loop, ['f64.const', 0]], 'f64')
1088
+ }
1089
+ const recv = hoistArrayValue(arr)
1090
+ const tmp = temp('ft')
1091
+ const cb = makeCallback(fn, callbackArgReps(arr))
1092
+ const loop = arrayLoop(recv.value, (_ptr, _len, i, item) => [
1093
+ ['local.set', `$${tmp}`, asF64(cb.call([item, idxArg(cb, i)]))]
1094
+ ])
1095
+ return typed(['block', ['result', 'f64'], recv.setup, cb.setup, ...loop, ['f64.const', 0]], 'f64')
1096
+ }
1097
+
1098
+ ctx.core.emit['.find'] = (arr, fn) => {
1099
+ const recv = hoistArrayValue(arr)
1100
+ const result = temp('ff')
1101
+ const exit = `$exit${ctx.func.uniq++}`
1102
+ const cb = makeCallback(fn, callbackArgReps(arr))
1103
+ const loop = arrayLoop(recv.value, (_ptr, _len, i, item) => [
1104
+ ['if', truthyIR(cb.call([item, idxArg(cb, i)])),
1105
+ ['then', ['local.set', `$${result}`, item], ['br', exit]]]
1106
+ ])
1107
+ return typed(['block', ['result', 'f64'],
1108
+ recv.setup,
1109
+ cb.setup,
1110
+ ['local.set', `$${result}`, ['f64.reinterpret_i64', ['i64.const', NULL_NAN]]],
1111
+ ['block', exit, ...loop],
1112
+ ['local.get', `$${result}`]], 'f64')
1113
+ }
1114
+
1115
+ ctx.core.emit['.indexOf'] = (arr, val) => {
1116
+ const recv = hoistArrayValue(arr)
1117
+ const vv = asF64(emit(val))
1118
+ const result = tempI32('ix')
1119
+ const exit = `$exit${ctx.func.uniq++}`
1120
+ const loop = arrayLoop(recv.value, (_ptr, _len, i, item) => [
1121
+ ['if', ['f64.eq', item, vv],
1122
+ ['then', ['local.set', `$${result}`, ['local.get', `$${i}`]], ['br', exit]]]
1123
+ ])
1124
+ return typed(['block', ['result', 'f64'],
1125
+ recv.setup,
1126
+ ['local.set', `$${result}`, ['i32.const', -1]],
1127
+ ['block', exit, ...loop],
1128
+ ['f64.convert_i32_s', ['local.get', `$${result}`]]], 'f64')
1129
+ }
1130
+
1131
+ ctx.core.emit['.includes'] = (arr, val) => {
1132
+ const recv = hoistArrayValue(arr)
1133
+ const vv = asF64(emit(val))
1134
+ const result = tempI32('ic')
1135
+ const exit = `$exit${ctx.func.uniq++}`
1136
+ const loop = arrayLoop(recv.value, (_ptr, _len, i, item) => [
1137
+ ['if', ['f64.eq', item, vv],
1138
+ ['then', ['local.set', `$${result}`, ['i32.const', 1]], ['br', exit]]]
1139
+ ])
1140
+ return typed(['block', ['result', 'f64'],
1141
+ recv.setup,
1142
+ ['local.set', `$${result}`, ['i32.const', 0]],
1143
+ ['block', exit, ...loop],
1144
+ ['f64.convert_i32_s', ['local.get', `$${result}`]]], 'f64')
1145
+ }
1146
+
1147
+ // .at(i) → array element with negative index support
1148
+ ctx.core.emit['.array:at'] = (arr, idx) => {
1149
+ const t = tempI32('ai'), a = temp('aa')
1150
+ return typed(['block', ['result', 'f64'],
1151
+ ['local.set', `$${a}`, asF64(emit(arr))],
1152
+ ['local.set', `$${t}`, asI32(emit(idx))],
1153
+ // Negative index: t += length
1154
+ ['if', ['i32.lt_s', ['local.get', `$${t}`], ['i32.const', 0]],
1155
+ ['then', ['local.set', `$${t}`, ['i32.add', ['local.get', `$${t}`],
1156
+ ['call', '$__len', ['local.get', `$${a}`]]]]]],
1157
+ ['f64.load', ['i32.add', ['call', '$__ptr_offset', ['local.get', `$${a}`]],
1158
+ ['i32.shl', ['local.get', `$${t}`], ['i32.const', 3]]]]], 'f64')
1159
+ }
1160
+
1161
+ ctx.core.emit['.slice'] = (arr, start, end) => {
1162
+ // BUFFER slice → byte-level copy handled in typedarray module.
1163
+ if (typeof arr === 'string') {
1164
+ const vt = lookupValType(arr)
1165
+ if (vt === 'buffer' && ctx.core.emit['.buf:slice']) return ctx.core.emit['.buf:slice'](arr, start, end)
1166
+ }
1167
+ const recv = hoistArrayValue(arr)
1168
+ const s = tempI32('ss'), e = tempI32('se'), len = tempI32('sl'), outLen = tempI32('sn'), ptr = tempI32('sp')
1169
+ const rawStart = start == null ? ['i32.const', 0] : asI32(emit(start))
1170
+ const rawEnd = end == null ? ['local.get', `$${len}`] : asI32(emit(end))
1171
+ const out = allocPtr({ type: PTR.ARRAY, len: ['local.get', `$${outLen}`], tag: 'so' })
1172
+ return typed(['block', ['result', 'f64'],
1173
+ recv.setup,
1174
+ ['local.set', `$${ptr}`, ['call', '$__ptr_offset', recv.value]],
1175
+ ['local.set', `$${len}`, ['i32.load', ['i32.sub', ['local.get', `$${ptr}`], ['i32.const', 8]]]],
1176
+ ['local.set', `$${s}`, rawStart],
1177
+ ['if', ['i32.lt_s', ['local.get', `$${s}`], ['i32.const', 0]],
1178
+ ['then', ['local.set', `$${s}`, ['i32.add', ['local.get', `$${s}`], ['local.get', `$${len}`]]]]],
1179
+ ['if', ['i32.lt_s', ['local.get', `$${s}`], ['i32.const', 0]], ['then', ['local.set', `$${s}`, ['i32.const', 0]]]],
1180
+ ['if', ['i32.gt_s', ['local.get', `$${s}`], ['local.get', `$${len}`]], ['then', ['local.set', `$${s}`, ['local.get', `$${len}`]]]],
1181
+ ['local.set', `$${e}`, rawEnd],
1182
+ ['if', ['i32.lt_s', ['local.get', `$${e}`], ['i32.const', 0]],
1183
+ ['then', ['local.set', `$${e}`, ['i32.add', ['local.get', `$${e}`], ['local.get', `$${len}`]]]]],
1184
+ ['if', ['i32.lt_s', ['local.get', `$${e}`], ['i32.const', 0]], ['then', ['local.set', `$${e}`, ['i32.const', 0]]]],
1185
+ ['if', ['i32.gt_s', ['local.get', `$${e}`], ['local.get', `$${len}`]], ['then', ['local.set', `$${e}`, ['local.get', `$${len}`]]]],
1186
+ ['local.set', `$${outLen}`, ['i32.sub', ['local.get', `$${e}`], ['local.get', `$${s}`]]],
1187
+ ['if', ['i32.lt_s', ['local.get', `$${outLen}`], ['i32.const', 0]], ['then', ['local.set', `$${outLen}`, ['i32.const', 0]]]],
1188
+ out.init,
1189
+ ['memory.copy',
1190
+ ['local.get', `$${out.local}`],
1191
+ ['i32.add', ['local.get', `$${ptr}`], ['i32.shl', ['local.get', `$${s}`], ['i32.const', 3]]],
1192
+ ['i32.shl', ['local.get', `$${outLen}`], ['i32.const', 3]]],
1193
+ out.ptr], 'f64')
1194
+ }
1195
+
1196
+ // .concat(...others) → concatenate arrays
1197
+ ctx.core.emit['.array:concat'] = (arr, ...others) => {
1198
+ const len = tempI32('len'), pos = tempI32('pos')
1199
+ const recv = hoistArrayValue(arr)
1200
+ const va = recv.value
1201
+ const out = allocPtr({ type: PTR.ARRAY, len: ['local.get', `$${len}`], tag: 'res' })
1202
+ const result = out.local
1203
+
1204
+ // Calculate total length
1205
+ const body = [
1206
+ recv.setup,
1207
+ ['local.set', `$${len}`, ['call', '$__len', va]],
1208
+ ]
1209
+
1210
+ const otherVals = []
1211
+ for (const other of others) {
1212
+ const vo = asF64(emit(other))
1213
+ otherVals.push(vo)
1214
+ body.push(['local.set', `$${len}`, ['i32.add', ['local.get', `$${len}`], ['call', '$__len', vo]]])
1215
+ }
1216
+
1217
+ body.push(out.init)
1218
+
1219
+ // Copy source array
1220
+ const srcOff = tempI32('co')
1221
+ body.push(
1222
+ ['local.set', `$${pos}`, ['i32.const', 0]],
1223
+ ['local.set', `$${len}`, ['call', '$__len', va]],
1224
+ ['local.set', `$${srcOff}`, ['call', '$__ptr_offset', va]]
1225
+ )
1226
+ const id = ctx.func.uniq++
1227
+ body.push(
1228
+ ['block', `$done${id}`, ['loop', `$loop${id}`,
1229
+ ['br_if', `$done${id}`, ['i32.ge_s', ['local.get', `$${pos}`], ['local.get', `$${len}`]]],
1230
+ ['f64.store',
1231
+ ['i32.add', ['local.get', `$${result}`], ['i32.shl', ['local.get', `$${pos}`], ['i32.const', 3]]],
1232
+ ['f64.load', ['i32.add', ['local.get', `$${srcOff}`], ['i32.shl', ['local.get', `$${pos}`], ['i32.const', 3]]]]],
1233
+ ['local.set', `$${pos}`, ['i32.add', ['local.get', `$${pos}`], ['i32.const', 1]]],
1234
+ ['br', `$loop${id}`]]]
1235
+ )
1236
+
1237
+ // Copy each other array
1238
+ const offset = tempI32('off')
1239
+ body.push(['local.set', `$${offset}`, ['call', '$__len', va]])
1240
+
1241
+ const otherOff = tempI32('co2')
1242
+ for (let i = 0; i < otherVals.length; i++) {
1243
+ const vo = otherVals[i]
1244
+ const id2 = ctx.func.uniq++
1245
+ body.push(
1246
+ ['local.set', `$${pos}`, ['i32.const', 0]],
1247
+ ['local.set', `$${len}`, ['call', '$__len', vo]],
1248
+ ['local.set', `$${otherOff}`, ['call', '$__ptr_offset', vo]],
1249
+ ['block', `$done${id2}`, ['loop', `$loop${id2}`,
1250
+ ['br_if', `$done${id2}`, ['i32.ge_s', ['local.get', `$${pos}`], ['local.get', `$${len}`]]],
1251
+ ['f64.store',
1252
+ ['i32.add', ['local.get', `$${result}`], ['i32.shl', ['i32.add', ['local.get', `$${offset}`], ['local.get', `$${pos}`]], ['i32.const', 3]]],
1253
+ ['f64.load', ['i32.add', ['local.get', `$${otherOff}`], ['i32.shl', ['local.get', `$${pos}`], ['i32.const', 3]]]]],
1254
+ ['local.set', `$${pos}`, ['i32.add', ['local.get', `$${pos}`], ['i32.const', 1]]],
1255
+ ['br', `$loop${id2}`]]],
1256
+ ['local.set', `$${offset}`, ['i32.add', ['local.get', `$${offset}`], ['local.get', `$${len}`]]]
1257
+ )
1258
+ }
1259
+
1260
+ body.push(out.ptr)
1261
+ return typed(['block', ['result', 'f64'], ...body], 'f64')
1262
+ }
1263
+
1264
+ // .flat() → flatten one level of nested arrays
1265
+ ctx.core.stdlib['__arr_flat'] = `(func $__arr_flat (param $src f64) (result f64)
1266
+ (local $len i32) (local $off i32) (local $i i32) (local $total i32) (local $dst i32) (local $pos i32)
1267
+ (local $elem f64) (local $subLen i32) (local $subOff i32) (local $j i32)
1268
+ (local.set $off (call $__ptr_offset (local.get $src)))
1269
+ (local.set $len (call $__len (local.get $src)))
1270
+ ;; First pass: count total elements
1271
+ (local.set $total (i32.const 0)) (local.set $i (i32.const 0))
1272
+ (block $c1 (loop $cl1
1273
+ (br_if $c1 (i32.ge_s (local.get $i) (local.get $len)))
1274
+ (local.set $elem (f64.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3)))))
1275
+ (if (i32.and (f64.ne (local.get $elem) (local.get $elem))
1276
+ (i32.eq (call $__ptr_type (local.get $elem)) (i32.const ${PTR.ARRAY})))
1277
+ (then (local.set $total (i32.add (local.get $total) (call $__len (local.get $elem)))))
1278
+ (else (local.set $total (i32.add (local.get $total) (i32.const 1)))))
1279
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
1280
+ (br $cl1)))
1281
+ ;; Allocate result
1282
+ (local.set $dst (call $__alloc (i32.add (i32.const 8) (i32.shl (local.get $total) (i32.const 3)))))
1283
+ (i32.store (local.get $dst) (local.get $total))
1284
+ (i32.store (i32.add (local.get $dst) (i32.const 4)) (local.get $total))
1285
+ (local.set $dst (i32.add (local.get $dst) (i32.const 8)))
1286
+ ;; Second pass: copy
1287
+ (local.set $pos (i32.const 0)) (local.set $i (i32.const 0))
1288
+ (block $c2 (loop $cl2
1289
+ (br_if $c2 (i32.ge_s (local.get $i) (local.get $len)))
1290
+ (local.set $elem (f64.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3)))))
1291
+ (if (i32.and (f64.ne (local.get $elem) (local.get $elem))
1292
+ (i32.eq (call $__ptr_type (local.get $elem)) (i32.const ${PTR.ARRAY})))
1293
+ (then
1294
+ (local.set $subOff (call $__ptr_offset (local.get $elem)))
1295
+ (local.set $subLen (call $__len (local.get $elem)))
1296
+ (local.set $j (i32.const 0))
1297
+ (block $s (loop $sl
1298
+ (br_if $s (i32.ge_s (local.get $j) (local.get $subLen)))
1299
+ (f64.store (i32.add (local.get $dst) (i32.shl (local.get $pos) (i32.const 3)))
1300
+ (f64.load (i32.add (local.get $subOff) (i32.shl (local.get $j) (i32.const 3)))))
1301
+ (local.set $pos (i32.add (local.get $pos) (i32.const 1)))
1302
+ (local.set $j (i32.add (local.get $j) (i32.const 1)))
1303
+ (br $sl))))
1304
+ (else
1305
+ (f64.store (i32.add (local.get $dst) (i32.shl (local.get $pos) (i32.const 3))) (local.get $elem))
1306
+ (local.set $pos (i32.add (local.get $pos) (i32.const 1)))))
1307
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
1308
+ (br $cl2)))
1309
+ (call $__mkptr (i32.const ${PTR.ARRAY}) (i32.const 0) (local.get $dst)))`
1310
+
1311
+ ctx.core.emit['.flat'] = arrMethod('__arr_flat')
1312
+
1313
+ // .flatMap(fn) → map then flatten
1314
+ ctx.core.emit['.flatMap'] = (arr, fn) => {
1315
+ const mapped = ctx.core.emit['.map'](arr, fn)
1316
+ inc('__arr_flat')
1317
+ return typed(['call', '$__arr_flat', asF64(mapped)], 'f64')
1318
+ }
1319
+
1320
+ // .join(sep) → concatenate array elements with separator string
1321
+ ctx.core.emit['.join'] = arrMethod('__str_join', 1)
1322
+ }