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