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.
- package/LICENSE +21 -0
- package/README.md +381 -0
- package/cli.js +163 -0
- package/index.js +217 -0
- package/module/array.js +1317 -0
- package/module/collection.js +791 -0
- package/module/console.js +190 -0
- package/module/core.js +642 -0
- package/module/function.js +180 -0
- package/module/index.js +15 -0
- package/module/json.js +504 -0
- package/module/math.js +389 -0
- package/module/number.js +605 -0
- package/module/object.js +357 -0
- package/module/regex.js +913 -0
- package/module/schema.js +104 -0
- package/module/string.js +928 -0
- package/module/symbol.js +54 -0
- package/module/timer.js +253 -0
- package/module/typedarray.js +711 -0
- package/package.json +54 -5
- package/src/analyze.js +1906 -0
- package/src/compile.js +2175 -0
- package/src/ctx.js +243 -0
- package/src/emit.js +2095 -0
- package/src/host.js +524 -0
- package/src/ir.js +649 -0
- package/src/jzify.js +391 -0
- package/src/optimize.js +1352 -0
- package/src/prepare.js +1598 -0
- package/wasi.js +74 -0
package/module/array.js
ADDED
|
@@ -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
|
+
}
|