jz 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +315 -318
- package/bench/README.md +369 -0
- package/bench/bench.svg +102 -0
- package/cli.js +104 -30
- package/dist/interop.js +1 -0
- package/dist/jz.js +6024 -0
- package/index.d.ts +126 -0
- package/index.js +362 -84
- package/interop.js +159 -186
- package/jz.svg +5 -0
- package/jzify/arguments.js +97 -0
- package/jzify/bundler.js +382 -0
- package/jzify/classes.js +328 -0
- package/jzify/hoist-vars.js +177 -0
- package/jzify/index.js +51 -0
- package/jzify/names.js +37 -0
- package/jzify/switch.js +106 -0
- package/jzify/transform.js +349 -0
- package/layout.js +184 -0
- package/module/array.js +377 -176
- package/module/collection.js +639 -145
- package/module/console.js +55 -43
- package/module/core.js +264 -153
- package/module/date.js +15 -3
- package/module/function.js +73 -6
- package/module/index.js +2 -1
- package/module/json.js +226 -61
- package/module/math.js +551 -187
- package/module/number.js +327 -60
- package/module/object.js +474 -184
- package/module/regex.js +255 -25
- package/module/schema.js +24 -6
- package/module/simd.js +117 -0
- package/module/string.js +621 -226
- package/module/symbol.js +1 -1
- package/module/timer.js +9 -14
- package/module/typedarray.js +459 -73
- package/package.json +58 -14
- package/src/abi/index.js +39 -23
- package/src/abi/string.js +38 -41
- package/src/ast.js +460 -0
- package/src/autoload.js +29 -24
- package/src/bridge.js +111 -0
- package/src/compile/analyze-scans.js +824 -0
- package/src/compile/analyze.js +1600 -0
- package/src/compile/emit-assign.js +411 -0
- package/src/compile/emit.js +3512 -0
- package/src/compile/flow-types.js +103 -0
- package/src/{compile.js → compile/index.js} +778 -128
- package/src/{infer.js → compile/infer.js} +62 -98
- package/src/compile/loop-divmod.js +155 -0
- package/src/{narrow.js → compile/narrow.js} +409 -106
- package/src/compile/peel-stencil.js +261 -0
- package/src/compile/plan/advise.js +370 -0
- package/src/compile/plan/common.js +150 -0
- package/src/compile/plan/index.js +122 -0
- package/src/compile/plan/inline.js +682 -0
- package/src/compile/plan/literals.js +1199 -0
- package/src/compile/plan/loops.js +472 -0
- package/src/compile/plan/scope.js +649 -0
- package/src/compile/program-facts.js +423 -0
- package/src/ctx.js +220 -64
- package/src/ir.js +589 -172
- package/src/kind-traits.js +132 -0
- package/src/kind.js +524 -0
- package/src/op-policy.js +57 -0
- package/src/{optimize.js → optimize/index.js} +1432 -473
- package/src/optimize/vectorize.js +4660 -0
- package/src/param-reps.js +65 -0
- package/src/parse.js +44 -0
- package/src/{prepare.js → prepare/index.js} +649 -205
- package/src/prepare/lift-iife.js +149 -0
- package/src/reps.js +116 -0
- package/src/resolve.js +12 -3
- package/src/static.js +208 -0
- package/src/type.js +651 -0
- package/src/{assemble.js → wat/assemble.js} +275 -55
- package/src/wat/optimize.js +3938 -0
- package/transform.js +21 -0
- package/wasi.js +47 -5
- package/src/analyze.js +0 -3818
- package/src/emit.js +0 -3040
- package/src/jzify.js +0 -1580
- package/src/plan.js +0 -2132
- package/src/vectorize.js +0 -1088
- /package/src/{codegen.js → wat/codegen.js} +0 -0
package/module/collection.js
CHANGED
|
@@ -8,11 +8,13 @@
|
|
|
8
8
|
* @module collection
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { typed, asF64, asI64, asI32, NULL_NAN, UNDEF_NAN, temp, tempI32, tempI64, allocPtr, undefExpr } from '../src/ir.js'
|
|
12
|
-
import { emit,
|
|
13
|
-
import { valTypeOf
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
11
|
+
import { typed, asF64, asI64, asI32, NULL_NAN, UNDEF_NAN, temp, tempI32, tempI64, allocPtr, undefExpr, mkPtrIR, ptrTypeEq, elemStore, elemLoad } from '../src/ir.js'
|
|
12
|
+
import { emit, deps, call } from '../src/bridge.js'
|
|
13
|
+
import { valTypeOf } from '../src/kind.js'
|
|
14
|
+
import { VAL, lookupValType } from '../src/reps.js'
|
|
15
|
+
import { hasOwnContinue, isBlockBody } from '../src/ast.js'
|
|
16
|
+
import { ctx, inc, PTR, LAYOUT, getter, declGlobal } from '../src/ctx.js'
|
|
17
|
+
import { STR_INTERN_BIT } from '../layout.js'
|
|
16
18
|
|
|
17
19
|
const SET_ENTRY = 16 // hash + key
|
|
18
20
|
const MAP_ENTRY = 24 // hash + key + value
|
|
@@ -45,6 +47,20 @@ function numConstLiteral(expr) {
|
|
|
45
47
|
const sameValueZeroEq = '(call $__same_value_zero (i64.load (i32.add (local.get $slot) (i32.const 8))) (local.get $key))'
|
|
46
48
|
const strEq = '(call $__str_eq (i64.load (i32.add (local.get $slot) (i32.const 8))) (local.get $key))'
|
|
47
49
|
|
|
50
|
+
// Hash-first probe guard: each slot stores the key's hash in its low 32 bits
|
|
51
|
+
// (high 32 = insertion seq), so a collision step can reject on one i32 compare
|
|
52
|
+
// instead of a full key compare. Equal keys always have equal stored hashes
|
|
53
|
+
// (the insert wrote strHash/mapHash of that very key), so the guard never
|
|
54
|
+
// skips a true match — it only skips the ~all-of-them unequal probes that were
|
|
55
|
+
// walking shared-prefix bytes through __str_eq (31M unequal content-compares
|
|
56
|
+
// per self-host bench run before this). If-expression for short-circuit:
|
|
57
|
+
// i32.and would still evaluate the call.
|
|
58
|
+
const hashGuard = (eqExpr) =>
|
|
59
|
+
`(if (result i32) (i32.eq (i32.load (local.get $slot)) (local.get $h))
|
|
60
|
+
(then ${eqExpr}) (else (i32.const 0)))`
|
|
61
|
+
const strEqG = hashGuard(strEq)
|
|
62
|
+
const sameValueZeroEqG = hashGuard(sameValueZeroEq)
|
|
63
|
+
|
|
48
64
|
// Open-addressing probe walked additively by entrySize: avoids an i32.mul + mask per
|
|
49
65
|
// step (vs recomputing slot = off + idx*entrySize). Needs $off/$cap/$h set and $end/$slot
|
|
50
66
|
// locals declared. `idxExpr` is the first-slot index (defaults to h mod cap; cap is pow2).
|
|
@@ -55,15 +71,36 @@ const probeNext = (entrySize) =>
|
|
|
55
71
|
`(local.set $slot (i32.add (local.get $slot) (i32.const ${entrySize})))
|
|
56
72
|
(if (i32.ge_u (local.get $slot) (local.get $end)) (then (local.set $slot (local.get $off))))`
|
|
57
73
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
74
|
+
// Store a fresh entry's hash word, packing a monotonic insertion sequence
|
|
75
|
+
// (global $__seq) into its free high 32 bits. The hash itself only ever occupies
|
|
76
|
+
// the low 32 (always ≥2), so "empty slot ⇔ word==0" and the i32.wrap_i64
|
|
77
|
+
// home-bucket math are untouched; rehash/back-shift copy the whole word, so the
|
|
78
|
+
// sequence rides along for free. Iteration reads it back (via __coll_order) to
|
|
79
|
+
// restore JS insertion order. Emitted only on the insert-new branch — updates
|
|
80
|
+
// keep the original entry (and its sequence) in place.
|
|
81
|
+
const seqStore = `(i64.store (local.get $slot)
|
|
82
|
+
(i64.or (i64.extend_i32_u (local.get $h)) (i64.shl (i64.extend_i32_u (global.get $__seq)) (i64.const 32))))
|
|
83
|
+
(global.set $__seq (i32.add (global.get $__seq) (i32.const 1)))`
|
|
84
|
+
|
|
85
|
+
/** Generate upsert (add/set) probe for a growable collection (Set/Map). hasVal: store
|
|
86
|
+
* value at slot+16. hasExt: emit EXTERNAL fallthrough (call $__ext_set on non-matching
|
|
87
|
+
* type). Gated off → type mismatch just returns coll unchanged.
|
|
88
|
+
*
|
|
89
|
+
* The table grows at 75% load by allocating a 2× table, rehashing, and forward-marking
|
|
90
|
+
* the old header (cap=-1 sentinel, new offset at -8) — the array growth idiom. The boxed
|
|
91
|
+
* pointer the caller holds is returned UNCHANGED; future ops resolve it through
|
|
92
|
+
* __ptr_offset, which follows the chain. This is why Set/Map (held in caller locals, and
|
|
93
|
+
* possibly aliased) forward rather than remint like HASH (whose pointer lives in a single
|
|
94
|
+
* owner's propsPtr slot that genUpsertGrow can rewrite). */
|
|
61
95
|
function genUpsert(name, entrySize, hashFn, eqExpr, expectedType, hasVal, hasExt) {
|
|
62
96
|
const valParam = hasVal ? '(param $val i64) ' : ''
|
|
63
97
|
const storeVal = hasVal ? `\n (i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))` : ''
|
|
64
98
|
const onMatch = hasVal
|
|
65
99
|
? `(then\n (i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))\n (br $done))`
|
|
66
100
|
: `(then (br $done))`
|
|
101
|
+
const rehashVal = hasVal
|
|
102
|
+
? `\n (i64.store (i32.add (local.get $newslot) (i32.const 16)) (i64.load (i32.add (local.get $oldslot) (i32.const 16))))`
|
|
103
|
+
: ''
|
|
67
104
|
|
|
68
105
|
const extBranch = hasVal
|
|
69
106
|
? '(then (call $__ext_set (local.get $coll) (local.get $key) (local.get $val)) drop)'
|
|
@@ -74,15 +111,47 @@ function genUpsert(name, entrySize, hashFn, eqExpr, expectedType, hasVal, hasExt
|
|
|
74
111
|
: `(if (i32.ne ${tExpr} (i32.const ${expectedType})) (then (return (local.get $coll))))`
|
|
75
112
|
return `(func $${name} (param $coll i64) (param $key i64) ${valParam}(result i64)
|
|
76
113
|
(local $off i32) (local $cap i32) (local $h i32) (local $end i32) (local $slot i32)
|
|
114
|
+
(local $size i32) (local $newptr i32) (local $newcap i32) (local $i i32)
|
|
115
|
+
(local $oldslot i32) (local $newidx i32) (local $newslot i32)
|
|
77
116
|
${typeGuard}
|
|
78
|
-
(local.set $off (
|
|
117
|
+
(local.set $off (call $__ptr_offset (local.get $coll)))
|
|
79
118
|
(local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
|
|
119
|
+
(local.set $size (i32.load (i32.sub (local.get $off) (i32.const 8))))
|
|
120
|
+
;; Grow at 75% load (size*4 >= cap*3): 2× table, rehash, forward-mark old header.
|
|
121
|
+
(if (i32.ge_s (i32.mul (local.get $size) (i32.const 4)) (i32.mul (local.get $cap) (i32.const 3)))
|
|
122
|
+
(then
|
|
123
|
+
(local.set $newcap (i32.shl (local.get $cap) (i32.const 1)))
|
|
124
|
+
(local.set $newptr (call $__alloc_hdr_n (i32.const 0) (local.get $newcap) (i32.const ${entrySize})))
|
|
125
|
+
(i64.store (i32.sub (local.get $newptr) (i32.const 16)) (i64.load (i32.sub (local.get $off) (i32.const 16))))
|
|
126
|
+
(local.set $i (i32.const 0))
|
|
127
|
+
(block $rd (loop $rl
|
|
128
|
+
(br_if $rd (i32.ge_s (local.get $i) (local.get $cap)))
|
|
129
|
+
(local.set $oldslot (i32.add (local.get $off) (i32.mul (local.get $i) (i32.const ${entrySize}))))
|
|
130
|
+
(if (i64.ne (i64.load (local.get $oldslot)) (i64.const 0))
|
|
131
|
+
(then
|
|
132
|
+
(local.set $h (call ${hashFn} (i64.load (i32.add (local.get $oldslot) (i32.const 8)))))
|
|
133
|
+
(local.set $newidx (i32.and (local.get $h) (i32.sub (local.get $newcap) (i32.const 1))))
|
|
134
|
+
(block $ins (loop $probe2
|
|
135
|
+
(local.set $newslot (i32.add (local.get $newptr) (i32.mul (local.get $newidx) (i32.const ${entrySize}))))
|
|
136
|
+
(br_if $ins (i64.eqz (i64.load (local.get $newslot))))
|
|
137
|
+
(local.set $newidx (i32.and (i32.add (local.get $newidx) (i32.const 1)) (i32.sub (local.get $newcap) (i32.const 1))))
|
|
138
|
+
(br $probe2)))
|
|
139
|
+
(i64.store (local.get $newslot) (i64.load (local.get $oldslot)))
|
|
140
|
+
(i64.store (i32.add (local.get $newslot) (i32.const 8)) (i64.load (i32.add (local.get $oldslot) (i32.const 8))))${rehashVal}
|
|
141
|
+
(i32.store (i32.sub (local.get $newptr) (i32.const 8))
|
|
142
|
+
(i32.add (i32.load (i32.sub (local.get $newptr) (i32.const 8))) (i32.const 1)))))
|
|
143
|
+
(local.set $i (i32.add (local.get $i) (i32.const 1)))
|
|
144
|
+
(br $rl)))
|
|
145
|
+
(i32.store (i32.sub (local.get $off) (i32.const 8)) (local.get $newptr))
|
|
146
|
+
(i32.store (i32.sub (local.get $off) (i32.const 4)) (i32.const -1))
|
|
147
|
+
(local.set $off (local.get $newptr))
|
|
148
|
+
(local.set $cap (local.get $newcap))))
|
|
80
149
|
(local.set $h (call ${hashFn} (local.get $key)))
|
|
81
150
|
${probeStart(entrySize)}
|
|
82
151
|
(block $done (loop $probe
|
|
83
152
|
(if (i64.eqz (i64.load (local.get $slot)))
|
|
84
153
|
(then
|
|
85
|
-
|
|
154
|
+
${seqStore}
|
|
86
155
|
(i64.store (i32.add (local.get $slot) (i32.const 8)) (local.get $key))${storeVal}
|
|
87
156
|
(i32.store (i32.sub (local.get $off) (i32.const 8))
|
|
88
157
|
(i32.add (i32.load (i32.sub (local.get $off) (i32.const 8))) (i32.const 1)))
|
|
@@ -117,11 +186,14 @@ function genLookup(name, entrySize, hashFn, eqExpr, expectedType, wantValue, has
|
|
|
117
186
|
: '(call $__ext_has (local.get $coll) (local.get $key))'}))
|
|
118
187
|
(else ${onEmpty}))))`
|
|
119
188
|
: `(if (i32.ne ${tExpr} (i32.const ${expectedType})) (then ${onEmpty}))`
|
|
189
|
+
// SET/MAP/HASH all grow by forward-marking the old header (genUpsert / genUpsertGrow
|
|
190
|
+
// with forward=true), so a boxed pointer may be stale → resolve through the chain.
|
|
191
|
+
const offExpr = '(call $__ptr_offset (local.get $coll))'
|
|
120
192
|
|
|
121
193
|
return `(func $${name} (param $coll i64) (param $key i64) (result ${rt})
|
|
122
194
|
(local $off i32) (local $cap i32) (local $h i32) (local $end i32) (local $slot i32) (local $tries i32)
|
|
123
195
|
${typeGuard}
|
|
124
|
-
(local.set $off
|
|
196
|
+
(local.set $off ${offExpr})
|
|
125
197
|
(local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
|
|
126
198
|
(local.set $h (call ${hashFn} (local.get $key)))
|
|
127
199
|
${probeStart(entrySize)}
|
|
@@ -135,27 +207,55 @@ function genLookup(name, entrySize, hashFn, eqExpr, expectedType, wantValue, has
|
|
|
135
207
|
${notFound})`
|
|
136
208
|
}
|
|
137
209
|
|
|
138
|
-
/** Generate delete probe function.
|
|
210
|
+
/** Generate delete probe function. Backward-shift deletion: after removing an entry,
|
|
211
|
+
* pull back any following entry whose home slot lies outside the opened gap, so the
|
|
212
|
+
* "empty slot ⇒ end of probe chain" invariant holds without tombstones. Returns 1 if
|
|
213
|
+
* the key was present (and len decremented), 0 otherwise. Home slots are recomputed
|
|
214
|
+
* from the stored hash (low 32 bits), so no rehash of the key is needed during the shift. */
|
|
139
215
|
function genDelete(name, entrySize, hashFn, eqExpr, expectedType) {
|
|
140
216
|
return `(func $${name} (param $coll i64) (param $key i64) (result i32)
|
|
141
217
|
(local $off i32) (local $cap i32) (local $h i32) (local $end i32) (local $slot i32) (local $tries i32)
|
|
218
|
+
(local $i i32) (local $j i32) (local $k i32) (local $n i32)
|
|
142
219
|
(if (i32.ne (call $__ptr_type (local.get $coll)) (i32.const ${expectedType})) (then (return (i32.const 0))))
|
|
143
220
|
(local.set $off (call $__ptr_offset (local.get $coll)))
|
|
144
221
|
(local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
|
|
145
222
|
(local.set $h (call ${hashFn} (local.get $key)))
|
|
146
223
|
${probeStart(entrySize)}
|
|
147
|
-
(block $
|
|
148
|
-
(
|
|
149
|
-
|
|
150
|
-
(then
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
(
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
(
|
|
224
|
+
(block $found
|
|
225
|
+
(block $absent (loop $probe
|
|
226
|
+
(if (i64.eqz (i64.load (local.get $slot))) (then (br $absent)))
|
|
227
|
+
(if ${eqExpr} (then (br $found)))
|
|
228
|
+
${probeNext(entrySize)}
|
|
229
|
+
(local.set $tries (i32.add (local.get $tries) (i32.const 1)))
|
|
230
|
+
(br_if $absent (i32.ge_s (local.get $tries) (local.get $cap)))
|
|
231
|
+
(br $probe)))
|
|
232
|
+
(return (i32.const 0)))
|
|
233
|
+
;; $slot holds the entry to remove. Walk forward; move back any entry whose home
|
|
234
|
+
;; is not cyclically within (i, j], else it would become unreachable from its home.
|
|
235
|
+
(local.set $i (local.get $slot))
|
|
236
|
+
(local.set $j (local.get $slot))
|
|
237
|
+
(block $stop (loop $shift
|
|
238
|
+
(local.set $j (i32.add (local.get $j) (i32.const ${entrySize})))
|
|
239
|
+
(if (i32.ge_u (local.get $j) (local.get $end)) (then (local.set $j (local.get $off))))
|
|
240
|
+
(br_if $stop (i64.eqz (i64.load (local.get $j))))
|
|
241
|
+
;; Empty slot ends the cluster (load < 100%). A 100%-full table has none — lookups
|
|
242
|
+
;; tolerate that via the $tries<cap bound, so delete must too: after $cap advances $j
|
|
243
|
+
;; has cycled back to the gap origin; stop and clear the final gap.
|
|
244
|
+
(local.set $n (i32.add (local.get $n) (i32.const 1)))
|
|
245
|
+
(br_if $stop (i32.ge_u (local.get $n) (local.get $cap)))
|
|
246
|
+
(local.set $k (i32.add (local.get $off)
|
|
247
|
+
(i32.mul (i32.and (i32.wrap_i64 (i64.load (local.get $j))) (i32.sub (local.get $cap) (i32.const 1))) (i32.const ${entrySize}))))
|
|
248
|
+
(if (i32.le_u (local.get $i) (local.get $j))
|
|
249
|
+
(then (br_if $shift (i32.and (i32.lt_u (local.get $i) (local.get $k)) (i32.le_u (local.get $k) (local.get $j)))))
|
|
250
|
+
(else (br_if $shift (i32.or (i32.lt_u (local.get $i) (local.get $k)) (i32.le_u (local.get $k) (local.get $j))))))
|
|
251
|
+
(memory.copy (local.get $i) (local.get $j) (i32.const ${entrySize}))
|
|
252
|
+
(local.set $i (local.get $j))
|
|
253
|
+
(br $shift)))
|
|
254
|
+
(i64.store (local.get $i) (i64.const 0))
|
|
255
|
+
(i64.store (i32.add (local.get $i) (i32.const 8)) (i64.const 0))
|
|
256
|
+
(i32.store (i32.sub (local.get $off) (i32.const 8))
|
|
257
|
+
(i32.sub (i32.load (i32.sub (local.get $off) (i32.const 8))) (i32.const 1)))
|
|
258
|
+
(i32.const 1))`
|
|
159
259
|
}
|
|
160
260
|
|
|
161
261
|
/** Generate growable upsert. Grows table at 75% load, rehashes, then inserts.
|
|
@@ -163,7 +263,7 @@ function genDelete(name, entrySize, hashFn, eqExpr, expectedType) {
|
|
|
163
263
|
* strict=false: EXTERNAL → __ext_set, other non-HASH types → __dyn_set (global props).
|
|
164
264
|
* The non-strict fallback is critical for untyped variables (e.g. arrays from
|
|
165
265
|
* Object.create) that receive property writes — without it writes silently vanish. */
|
|
166
|
-
function genUpsertGrow(name, entrySize, hashFn, eqExpr, typeConst, strict = false, hasExt = false) {
|
|
266
|
+
function genUpsertGrow(name, entrySize, hashFn, eqExpr, typeConst, strict = false, hasExt = false, forward = false) {
|
|
167
267
|
const nonHashFallback = hasExt
|
|
168
268
|
? `(if (i32.eq (call $__ptr_type (local.get $obj)) (i32.const ${PTR.EXTERNAL}))
|
|
169
269
|
(then (call $__ext_set (local.get $obj) (local.get $key) (local.get $val)) drop)
|
|
@@ -209,16 +309,27 @@ function genUpsertGrow(name, entrySize, hashFn, eqExpr, typeConst, strict = fals
|
|
|
209
309
|
(i32.add (i32.load (i32.sub (local.get $newptr) (i32.const 8))) (i32.const 1)))))
|
|
210
310
|
(local.set $i (i32.add (local.get $i) (i32.const 1)))
|
|
211
311
|
(br $rl)))
|
|
312
|
+
${forward
|
|
313
|
+
// Forward-mark the old header (cap=-1 sentinel at -4, new offset at -8) and
|
|
314
|
+
// keep the boxed pointer the caller holds: any alias resolves through
|
|
315
|
+
// __ptr_offset. This preserves JS reference identity for a grown dict held in
|
|
316
|
+
// multiple places (e.g. ctx.core.emit), which remint cannot.
|
|
317
|
+
? `(i32.store (i32.sub (local.get $off) (i32.const 8)) (local.get $newptr))
|
|
318
|
+
(i32.store (i32.sub (local.get $off) (i32.const 4)) (i32.const -1))
|
|
212
319
|
(local.set $off (local.get $newptr))
|
|
320
|
+
(local.set $cap (local.get $newcap))`
|
|
321
|
+
// Remint: hand back a fresh boxed pointer. Only safe when a single owner
|
|
322
|
+
// (a local threaded via the return, or the global __dyn_props) is updated.
|
|
323
|
+
: `(local.set $off (local.get $newptr))
|
|
213
324
|
(local.set $cap (local.get $newcap))
|
|
214
|
-
(local.set $obj (i64.reinterpret_f64 (call $__mkptr (i32.const ${typeConst}) (i32.const 0) (local.get $newptr))))))
|
|
325
|
+
(local.set $obj (i64.reinterpret_f64 (call $__mkptr (i32.const ${typeConst}) (i32.const 0) (local.get $newptr))))`}))
|
|
215
326
|
;; Insert/update
|
|
216
327
|
(local.set $h (call ${hashFn} (local.get $key)))
|
|
217
328
|
${probeStart(entrySize)}
|
|
218
329
|
(block $done (loop $probe
|
|
219
330
|
(if (i64.eqz (i64.load (local.get $slot)))
|
|
220
331
|
(then
|
|
221
|
-
|
|
332
|
+
${seqStore}
|
|
222
333
|
(i64.store (i32.add (local.get $slot) (i32.const 8)) (local.get $key))
|
|
223
334
|
(i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))
|
|
224
335
|
(i32.store (i32.sub (local.get $off) (i32.const 8))
|
|
@@ -240,7 +351,7 @@ function genLookupStrict(name, entrySize, hashFn, eqExpr, expectedType, missing
|
|
|
240
351
|
(i32.wrap_i64 (i64.and (i64.shr_u (local.get $coll) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK})))
|
|
241
352
|
(i32.const ${expectedType}))
|
|
242
353
|
(then (return (i64.const ${missing}))))
|
|
243
|
-
(local.set $off (
|
|
354
|
+
(local.set $off (call $__ptr_offset (local.get $coll)))
|
|
244
355
|
(local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
|
|
245
356
|
(local.set $h (call ${hashFn} (local.get $key)))
|
|
246
357
|
${probeStart(entrySize)}
|
|
@@ -266,10 +377,12 @@ function genLookupStrictPrehashed(name, entrySize, eqExpr, expectedType, missing
|
|
|
266
377
|
(else (return (i64.const ${missing}))))))`
|
|
267
378
|
: `(if (i32.ne ${tExpr} (i32.const ${expectedType}))
|
|
268
379
|
(then (return (i64.const ${missing}))))`
|
|
380
|
+
// SET/MAP/HASH grow by forward-marking; a boxed pointer may be stale → follow the chain.
|
|
381
|
+
const offExpr = '(call $__ptr_offset (local.get $coll))'
|
|
269
382
|
return `(func $${name} (param $coll i64) (param $key i64) (param $h i32) (result i64)
|
|
270
383
|
(local $off i32) (local $cap i32) (local $end i32) (local $slot i32) (local $tries i32)
|
|
271
384
|
${typeGuard}
|
|
272
|
-
(local.set $off
|
|
385
|
+
(local.set $off ${offExpr})
|
|
273
386
|
(local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
|
|
274
387
|
${probeStart(entrySize)}
|
|
275
388
|
(block $done (loop $probe
|
|
@@ -291,13 +404,13 @@ function genUpsertStrictPrehashed(name, entrySize, eqExpr, expectedType) {
|
|
|
291
404
|
(i32.wrap_i64 (i64.and (i64.shr_u (local.get $obj) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK})))
|
|
292
405
|
(i32.const ${expectedType}))
|
|
293
406
|
(then (return (local.get $obj))))
|
|
294
|
-
(local.set $off (
|
|
407
|
+
(local.set $off (call $__ptr_offset (local.get $obj)))
|
|
295
408
|
(local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
|
|
296
409
|
${probeStart(entrySize)}
|
|
297
410
|
(block $done (loop $probe
|
|
298
411
|
(if (i64.eqz (i64.load (local.get $slot)))
|
|
299
412
|
(then
|
|
300
|
-
|
|
413
|
+
${seqStore}
|
|
301
414
|
(i64.store (i32.add (local.get $slot) (i32.const 8)) (local.get $key))
|
|
302
415
|
(i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))
|
|
303
416
|
(i32.store (i32.sub (local.get $off) (i32.const 8))
|
|
@@ -317,17 +430,18 @@ export default (ctx) => {
|
|
|
317
430
|
// Feature-gated deps: EXTERNAL-dependent symbols are only pulled when features.external.
|
|
318
431
|
// Evaluated lazily at resolveIncludes() time — after emission has finalized ctx.features.
|
|
319
432
|
const ifExt = (name) => () => ctx.features.external ? [name] : []
|
|
320
|
-
|
|
433
|
+
deps({
|
|
321
434
|
__same_value_zero: ['__str_eq'],
|
|
322
435
|
__map_hash: ['__hash', '__str_hash'],
|
|
323
|
-
__set_add: () => ctx.features.external ? ['__map_hash', '__same_value_zero', '__ext_set'] : ['__map_hash', '__same_value_zero'],
|
|
324
|
-
__set_has: () => ctx.features.external ? ['__map_hash', '__same_value_zero', '__ext_has'] : ['__map_hash', '__same_value_zero'],
|
|
436
|
+
__set_add: () => ctx.features.external ? ['__map_hash', '__same_value_zero', '__ptr_offset', '__alloc_hdr_n', '__ext_set'] : ['__map_hash', '__same_value_zero', '__ptr_offset', '__alloc_hdr_n'],
|
|
437
|
+
__set_has: () => ctx.features.external ? ['__map_hash', '__same_value_zero', '__ptr_offset', '__ext_has'] : ['__map_hash', '__same_value_zero', '__ptr_offset'],
|
|
325
438
|
__set_delete: ['__map_hash', '__same_value_zero'],
|
|
326
|
-
__map_set: () => ctx.features.external ? ['__map_hash', '__same_value_zero', '__ext_set'] : ['__map_hash', '__same_value_zero'],
|
|
327
|
-
__map_get: () => ctx.features.external ? ['__ext_prop', '__map_set'] : ['__map_set'],
|
|
328
|
-
__map_get_h: () => ctx.features.external ? ['__ext_prop', '__same_value_zero'] : ['__same_value_zero'],
|
|
329
|
-
__map_has: () => ctx.features.external ? ['__map_hash', '__same_value_zero', '__ext_has'] : ['__map_hash', '__same_value_zero'],
|
|
439
|
+
__map_set: () => ctx.features.external ? ['__map_hash', '__same_value_zero', '__ptr_offset', '__alloc_hdr_n', '__ext_set'] : ['__map_hash', '__same_value_zero', '__ptr_offset', '__alloc_hdr_n'],
|
|
440
|
+
__map_get: () => ctx.features.external ? ['__ext_prop', '__map_set', '__ptr_offset'] : ['__map_set', '__ptr_offset'],
|
|
441
|
+
__map_get_h: () => ctx.features.external ? ['__ext_prop', '__same_value_zero', '__ptr_offset'] : ['__same_value_zero', '__ptr_offset'],
|
|
442
|
+
__map_has: () => ctx.features.external ? ['__map_hash', '__same_value_zero', '__ptr_offset', '__ext_has'] : ['__map_hash', '__same_value_zero', '__ptr_offset'],
|
|
330
443
|
__map_delete: ['__map_hash', '__same_value_zero'],
|
|
444
|
+
__map_from: ['__ptr_type', '__ptr_offset', '__len', '__typed_idx', '__map_set', '__mkptr', '__alloc_hdr_n', '__coll_order'],
|
|
331
445
|
__hash_set: () => ctx.features.external
|
|
332
446
|
? ['__str_hash', '__str_eq', '__ptr_type', '__ext_set', '__dyn_set']
|
|
333
447
|
: ['__str_hash', '__str_eq', '__ptr_type', '__dyn_set'],
|
|
@@ -368,8 +482,15 @@ export default (ctx) => {
|
|
|
368
482
|
|
|
369
483
|
inc('__ptr_offset', '__cap')
|
|
370
484
|
|
|
485
|
+
// Monotonic insertion counter packed into each entry's hash-word high 32 bits
|
|
486
|
+
// (see seqStore). Restores JS insertion order at iteration without growing
|
|
487
|
+
// entries or touching the lookup/delete hot paths. i32: wraps after 2^32 total
|
|
488
|
+
// inserts — unreachable in practice; fresh per wasm instance.
|
|
489
|
+
if (!ctx.scope.globals.has('__seq'))
|
|
490
|
+
declGlobal('__seq', 'i32')
|
|
491
|
+
|
|
371
492
|
if (!ctx.scope.globals.has('__dyn_props'))
|
|
372
|
-
|
|
493
|
+
declGlobal('__dyn_props', 'f64')
|
|
373
494
|
// 1-slot inline cache for the global __dyn_props lookup. Hot path for
|
|
374
495
|
// metacircular workloads (watr WAT parser): ~96% of execution sits in
|
|
375
496
|
// __dyn_get_t / __ihash_get_local. Caches last-seen (off → propsPtr) at
|
|
@@ -377,15 +498,15 @@ export default (ctx) => {
|
|
|
377
498
|
// propsPtr is replaced (rehash on grow). Sentinel cache_off = -1 cannot
|
|
378
499
|
// collide with a real memory offset (always non-negative i32).
|
|
379
500
|
if (!ctx.scope.globals.has('__dyn_get_cache_off'))
|
|
380
|
-
|
|
501
|
+
declGlobal('__dyn_get_cache_off', 'i32', -1)
|
|
381
502
|
if (!ctx.scope.globals.has('__dyn_get_cache_props'))
|
|
382
|
-
|
|
503
|
+
declGlobal('__dyn_get_cache_props', 'f64')
|
|
383
504
|
// Schema name table for __dyn_get's OBJECT-schema fallback (polymorphic-receiver
|
|
384
505
|
// `.prop` access). Same declaration as json.js — defined here too so collection
|
|
385
506
|
// doesn't transitively require json. compile.js's schemaInit populates it when
|
|
386
507
|
// schema list is non-empty AND (__stringify OR __dyn_get) is included.
|
|
387
508
|
if (!ctx.scope.globals.has('__schema_tbl'))
|
|
388
|
-
|
|
509
|
+
declGlobal('__schema_tbl', 'i32')
|
|
389
510
|
|
|
390
511
|
// __ext_* imports carry NaN-boxed pointers across the env boundary as i64
|
|
391
512
|
// (not f64) to dodge V8's f64 NaN canonicalization at the wasm↔JS edge —
|
|
@@ -455,16 +576,17 @@ export default (ctx) => {
|
|
|
455
576
|
const out = allocPtr({ type: PTR.SET, len: 0, cap: INIT_CAP, stride: SET_ENTRY, tag: 'set' })
|
|
456
577
|
return typed(['block', ['result', 'f64'], out.init, out.ptr], 'f64')
|
|
457
578
|
}
|
|
458
|
-
// new Set(iterable):
|
|
459
|
-
//
|
|
460
|
-
//
|
|
579
|
+
// new Set(iterable): __iter_arr normalizes any iterable to an index-iterable
|
|
580
|
+
// dense array (Set→keys, Map→[k,v] entries, Array/String/TypedArray pass
|
|
581
|
+
// through), so a Set/Map/Array source all seed uniformly. __set_add does
|
|
582
|
+
// SameValueZero dedup + −0 normalization. A non-iterable normalizes to a
|
|
583
|
+
// non-array value — the ptr_type guard zeroes the length so the loop is skipped.
|
|
461
584
|
//
|
|
462
|
-
// __set_add
|
|
463
|
-
//
|
|
464
|
-
// the smallest power of two
|
|
465
|
-
//
|
|
466
|
-
//
|
|
467
|
-
// and floors at INIT_CAP for the empty/short case.
|
|
585
|
+
// __set_add grows on demand, but pre-sizing the table to fit the source array
|
|
586
|
+
// skips the rehash churn of building it up from INIT_CAP. cap = 1 << (32 −
|
|
587
|
+
// clz(m−1)) with m = 2*len + INIT_CAP is the smallest power of two > 2*len:
|
|
588
|
+
// distinct entries ≤ len, so the table lands ≤50% full and never needs to grow
|
|
589
|
+
// while seeding. Floors at INIT_CAP for the empty/short case.
|
|
468
590
|
inc('__set_add', '__ptr_type', '__len', '__typed_idx')
|
|
469
591
|
const setL = temp('nss'), arrL = temp('nsa')
|
|
470
592
|
const iL = tempI32('nsi'), lenL = tempI32('nsl')
|
|
@@ -475,7 +597,7 @@ export default (ctx) => {
|
|
|
475
597
|
['i32.const', 1]]]]]
|
|
476
598
|
const out = allocPtr({ type: PTR.SET, len: 0, cap: capExpr, stride: SET_ENTRY, tag: 'set' })
|
|
477
599
|
return typed(['block', ['result', 'f64'],
|
|
478
|
-
['local.set', `$${arrL}`, asF64(emit(iterExpr))],
|
|
600
|
+
['local.set', `$${arrL}`, asF64(emit(['()', '__iter_arr', iterExpr]))],
|
|
479
601
|
['local.set', `$${lenL}`, ['i32.const', 0]],
|
|
480
602
|
['if', ['i32.eq',
|
|
481
603
|
['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${arrL}`]]],
|
|
@@ -498,28 +620,34 @@ export default (ctx) => {
|
|
|
498
620
|
['local.get', `$${setL}`]], 'f64')
|
|
499
621
|
}
|
|
500
622
|
|
|
501
|
-
ctx.core.emit['.add'] = (
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
623
|
+
ctx.core.emit['.add'] = call('__set_add', 'II', 'i64')
|
|
624
|
+
|
|
625
|
+
// `.has` / `.delete` exist on BOTH Set and Map, which differ only in entry
|
|
626
|
+
// stride (16 vs 24). A receiver of unproven kind (e.g. a Map read off a nested
|
|
627
|
+
// object field like `ctx.scope.globals`) must resolve MAP vs SET at runtime:
|
|
628
|
+
// the Set probe carries a PTR.SET type guard that rejects a Map outright, so
|
|
629
|
+
// routing a Map through `__set_has`/`__set_delete` makes every lookup/delete
|
|
630
|
+
// silently report absent. Mirrors collViewDyn below; typed receivers skip it.
|
|
631
|
+
const collProbeDyn = (mapFn, setFn) => (collExpr, key) => {
|
|
632
|
+
inc(mapFn, setFn, '__ptr_type')
|
|
633
|
+
const o = temp('cp'), k = tempI64('cpk')
|
|
634
|
+
return typed(['block', ['result', 'f64'],
|
|
635
|
+
['local.set', `$${o}`, asF64(emit(collExpr))],
|
|
636
|
+
['local.set', `$${k}`, asI64(emit(key))],
|
|
637
|
+
['f64.convert_i32_s', ['if', ['result', 'i32'],
|
|
638
|
+
ptrTypeEq(['local.get', `$${o}`], PTR.MAP),
|
|
639
|
+
['then', ['call', `$${mapFn}`, ['i64.reinterpret_f64', ['local.get', `$${o}`]], ['local.get', `$${k}`]]],
|
|
640
|
+
['else', ['call', `$${setFn}`, ['i64.reinterpret_f64', ['local.get', `$${o}`]], ['local.get', `$${k}`]]]]]], 'f64')
|
|
514
641
|
}
|
|
642
|
+
ctx.core.emit['.has'] = collProbeDyn('__map_has', '__set_has')
|
|
643
|
+
ctx.core.emit['.delete'] = collProbeDyn('__map_delete', '__set_delete')
|
|
644
|
+
ctx.core.emit[`.${VAL.SET}:has`] = call('__set_has', 'II', 'i32')
|
|
645
|
+
ctx.core.emit[`.${VAL.SET}:delete`] = call('__set_delete', 'II', 'i32')
|
|
515
646
|
|
|
516
647
|
// Map.prototype.clear / Set.prototype.clear — drop every entry. `.clear` only
|
|
517
648
|
// exists on Map/Set in JS, so a single generic emitter is unambiguous; the
|
|
518
649
|
// stdlib reads the ptr tag to pick the entry stride.
|
|
519
|
-
ctx.core.emit['.clear'] = (
|
|
520
|
-
inc('__coll_clear')
|
|
521
|
-
return typed(['call', '$__coll_clear', asI64(emit(collExpr))], 'f64')
|
|
522
|
-
}
|
|
650
|
+
ctx.core.emit['.clear'] = call('__coll_clear', 'I')
|
|
523
651
|
|
|
524
652
|
// Zero every slot (so probes see empty) and reset the length header. Entry
|
|
525
653
|
// stride is 16 for a SET, 24 for a MAP — `(t == MAP) << 3` adds the 8-byte
|
|
@@ -538,41 +666,47 @@ export default (ctx) => {
|
|
|
538
666
|
(i32.store (i32.sub (local.get $off) (i32.const 8)) (i32.const 0))))
|
|
539
667
|
(f64.reinterpret_i64 (i64.const ${UNDEF_NAN})))`
|
|
540
668
|
|
|
541
|
-
ctx.core.emit['.size'] = (expr) => {
|
|
669
|
+
ctx.core.emit['.size'] = getter((expr) => {
|
|
542
670
|
return typed(['f64.convert_i32_s', ['call', '$__len', ['i64.reinterpret_f64', asF64(emit(expr))]]], 'f64')
|
|
543
|
-
}
|
|
671
|
+
})
|
|
544
672
|
|
|
545
673
|
// x instanceof Map / Set — typed-pointer predicates emitted by jzify. NaN-check
|
|
546
674
|
// first (non-pointer numbers must report false), then compare __ptr_type tag.
|
|
547
675
|
// Mirrors module/array.js's Array.isArray inline form. Result is i32 (boolean).
|
|
548
676
|
ctx.core.emit['__is_map'] = (x) => {
|
|
549
|
-
inc('__ptr_type')
|
|
550
677
|
const v = asF64(emit(x))
|
|
551
678
|
const t = temp('imap')
|
|
552
679
|
return typed(['i32.and',
|
|
553
680
|
['f64.ne', ['local.tee', `$${t}`, v], ['local.get', `$${t}`]],
|
|
554
|
-
['
|
|
681
|
+
ptrTypeEq(['local.get', `$${t}`], PTR.MAP)], 'i32')
|
|
555
682
|
}
|
|
556
683
|
ctx.core.emit['__is_set'] = (x) => {
|
|
557
|
-
inc('__ptr_type')
|
|
558
684
|
const v = asF64(emit(x))
|
|
559
685
|
const t = temp('iset')
|
|
560
686
|
return typed(['i32.and',
|
|
561
687
|
['f64.ne', ['local.tee', `$${t}`, v], ['local.get', `$${t}`]],
|
|
562
|
-
['
|
|
688
|
+
ptrTypeEq(['local.get', `$${t}`], PTR.SET)], 'i32')
|
|
563
689
|
}
|
|
564
690
|
|
|
565
691
|
// Generated Set probe functions
|
|
566
|
-
ctx.core.stdlib['__set_add'] = () => genUpsert('__set_add', SET_ENTRY, '$__map_hash',
|
|
567
|
-
ctx.core.stdlib['__set_has'] = () => genLookup('__set_has', SET_ENTRY, '$__map_hash',
|
|
568
|
-
ctx.core.stdlib['__set_delete'] = genDelete('__set_delete', SET_ENTRY, '$__map_hash',
|
|
692
|
+
ctx.core.stdlib['__set_add'] = () => genUpsert('__set_add', SET_ENTRY, '$__map_hash', sameValueZeroEqG, PTR.SET, false, ctx.features.external)
|
|
693
|
+
ctx.core.stdlib['__set_has'] = () => genLookup('__set_has', SET_ENTRY, '$__map_hash', sameValueZeroEqG, PTR.SET, false, ctx.features.external)
|
|
694
|
+
ctx.core.stdlib['__set_delete'] = genDelete('__set_delete', SET_ENTRY, '$__map_hash', sameValueZeroEqG, PTR.SET)
|
|
569
695
|
|
|
570
696
|
// === Map ===
|
|
571
697
|
|
|
572
|
-
ctx.core.emit['new.Map'] = () => {
|
|
698
|
+
ctx.core.emit['new.Map'] = (iterExpr) => {
|
|
573
699
|
ctx.features.map = true
|
|
574
|
-
|
|
575
|
-
|
|
700
|
+
if (iterExpr == null) {
|
|
701
|
+
const out = allocPtr({ type: PTR.MAP, len: 0, cap: INIT_CAP, stride: MAP_ENTRY, tag: 'map' })
|
|
702
|
+
return typed(['block', ['result', 'f64'], out.init, out.ptr], 'f64')
|
|
703
|
+
}
|
|
704
|
+
// new Map(iterable): seed from another Map or an array of [key, value] pairs.
|
|
705
|
+
// Delegated to a stdlib helper (vs inlined like Set) — `new Map(x)` is heavily
|
|
706
|
+
// used (the compiler copies fact Maps per function), so one shared helper keeps
|
|
707
|
+
// output small. Non-Map/Array args yield an empty map (guarded in the helper).
|
|
708
|
+
inc('__map_from')
|
|
709
|
+
return typed(['call', '$__map_from', asI64(emit(iterExpr))], 'f64')
|
|
576
710
|
}
|
|
577
711
|
|
|
578
712
|
ctx.core.emit['.set'] = (mapExpr, key, val) => {
|
|
@@ -595,22 +729,139 @@ export default (ctx) => {
|
|
|
595
729
|
ctx.core.emit['.get'] = emitMapGet
|
|
596
730
|
ctx.core.emit[`.${VAL.MAP}:get`] = emitMapGet
|
|
597
731
|
|
|
598
|
-
ctx.core.emit[`.${VAL.MAP}:has`] = (
|
|
599
|
-
|
|
600
|
-
|
|
732
|
+
ctx.core.emit[`.${VAL.MAP}:has`] = call('__map_has', 'II', 'i32')
|
|
733
|
+
ctx.core.emit[`.${VAL.MAP}:delete`] = call('__map_delete', 'II', 'i32')
|
|
734
|
+
|
|
735
|
+
// Map/Set iteration views: keys() / values() / entries() materialize a dense
|
|
736
|
+
// Array snapshot (jz models iterators as arrays — for-of/spread consume them
|
|
737
|
+
// directly). Set keys===values===elements; Set entries yield [v, v] pairs.
|
|
738
|
+
// Registered per concrete type; an unproven receiver resolves SET vs MAP at
|
|
739
|
+
// runtime via `.keys`/`.values`/`.entries` below.
|
|
740
|
+
const collView = (walk) => (expr) => {
|
|
741
|
+
const t = temp('cv')
|
|
742
|
+
return typed(['block', ['result', 'f64'], ['local.set', `$${t}`, asF64(emit(expr))], walk(t)], 'f64')
|
|
601
743
|
}
|
|
602
|
-
|
|
603
|
-
ctx.core.emit[`.${VAL.MAP}:
|
|
604
|
-
|
|
605
|
-
|
|
744
|
+
ctx.core.emit[`.${VAL.MAP}:keys`] = collView(t => collKeysFromTemp(t, MAP_ENTRY, 8))
|
|
745
|
+
ctx.core.emit[`.${VAL.MAP}:values`] = collView(t => collKeysFromTemp(t, MAP_ENTRY, 16))
|
|
746
|
+
ctx.core.emit[`.${VAL.MAP}:entries`] = collView(t => collEntriesFromTemp(t, MAP_ENTRY, 8, 16))
|
|
747
|
+
ctx.core.emit[`.${VAL.SET}:keys`] = collView(t => collKeysFromTemp(t, SET_ENTRY, 8))
|
|
748
|
+
ctx.core.emit[`.${VAL.SET}:values`] = ctx.core.emit[`.${VAL.SET}:keys`]
|
|
749
|
+
ctx.core.emit[`.${VAL.SET}:entries`] = collView(t => collEntriesFromTemp(t, SET_ENTRY, 8, 8))
|
|
750
|
+
|
|
751
|
+
// Generic keys()/values()/entries() for a receiver whose collection kind isn't
|
|
752
|
+
// statically proven (e.g. a Map read off an object field): resolve MAP vs SET
|
|
753
|
+
// vs ARRAY once at runtime. ARRAY.values() is the array itself; .keys() yields
|
|
754
|
+
// indices; .entries() yields [i, el]. Any other receiver passes through.
|
|
755
|
+
// A typed array (PTR.TYPED) is NOT PTR.ARRAY, so without an explicit branch it fell
|
|
756
|
+
// through to `local.get $t` (return the receiver). For .values that is correct (the
|
|
757
|
+
// receiver iterates its values), but .keys then yielded values instead of indices and
|
|
758
|
+
// .entries yielded scalars instead of [i, value] pairs. The typedWalk arg restores the
|
|
759
|
+
// right behavior for typed receivers (keys → index array, entries → typed-aware pairs).
|
|
760
|
+
const collViewDyn = (mapWalk, setWalk, arrWalk, typedWalk) => (expr) => {
|
|
761
|
+
inc('__ptr_type')
|
|
762
|
+
const t = temp('cd')
|
|
763
|
+
const pt = () => ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]
|
|
764
|
+
const branch = (tag, walk, rest) =>
|
|
765
|
+
['if', ['result', 'f64'], ['i32.eq', pt(), ['i32.const', tag]], ['then', walk(t)], ['else', rest]]
|
|
766
|
+
const tree = branch(PTR.MAP, mapWalk, branch(PTR.SET, setWalk,
|
|
767
|
+
branch(PTR.ARRAY, arrWalk, branch(PTR.TYPED, typedWalk, ['local.get', `$${t}`]))))
|
|
768
|
+
return typed(['block', ['result', 'f64'], ['local.set', `$${t}`, asF64(emit(expr))], tree], 'f64')
|
|
769
|
+
}
|
|
770
|
+
// keys: index array [0..len-1] for both plain and typed (length-based, no element reads).
|
|
771
|
+
ctx.core.emit['.keys'] = collViewDyn(
|
|
772
|
+
t => collKeysFromTemp(t, MAP_ENTRY, 8), t => collKeysFromTemp(t, SET_ENTRY, 8), arrIdxFromTemp, arrIdxFromTemp)
|
|
773
|
+
// values: the receiver iterates its own elements (correct for plain and typed alike).
|
|
774
|
+
ctx.core.emit['.values'] = collViewDyn(
|
|
775
|
+
t => collKeysFromTemp(t, MAP_ENTRY, 16), t => collKeysFromTemp(t, SET_ENTRY, 8), t => ['local.get', `$${t}`], t => ['local.get', `$${t}`])
|
|
776
|
+
// entries: [i, element] pairs; the typed variant reads elements width/kind-aware.
|
|
777
|
+
ctx.core.emit['.entries'] = collViewDyn(
|
|
778
|
+
t => collEntriesFromTemp(t, MAP_ENTRY, 8, 16), t => collEntriesFromTemp(t, SET_ENTRY, 8, 8), arrEntriesFromTemp, arrEntriesFromTempTyped)
|
|
779
|
+
|
|
780
|
+
// Map/Set forEach(cb): invoke cb(value, key) per live entry in insertion order.
|
|
781
|
+
// Map yields (value=val@16, key=key@8); Set yields (value=key@8, key@8) — the
|
|
782
|
+
// spec passes the element as both value and key. The trailing collection arg is
|
|
783
|
+
// dropped (as array/typedarray forEach drop the array arg) so we never exceed
|
|
784
|
+
// the uniform closure width (forEach autoloads array → closure floor 2). Uses
|
|
785
|
+
// the closure-call path like typedarray:forEach — forEach isn't a hot path.
|
|
786
|
+
const collForEach = (stride, valOff, keyOff) => (expr, fn) => {
|
|
787
|
+
inc('__ptr_offset', '__cap', '__len', '__coll_order')
|
|
788
|
+
const t = temp('fe'), cb = temp('fecb')
|
|
789
|
+
const off = tempI32('feo'), cap = tempI32('fec'), n = tempI32('fen')
|
|
790
|
+
const i = tempI32('fei'), ord = tempI32('fer'), slot = tempI32('fes')
|
|
791
|
+
const id = ctx.func.uniq++
|
|
792
|
+
const at = (o) => typed(['f64.load', ['i32.add', ['local.get', `$${slot}`], ['i32.const', o]]], 'f64')
|
|
793
|
+
return typed(['block', ['result', 'f64'],
|
|
794
|
+
['local.set', `$${t}`, asF64(emit(expr))],
|
|
795
|
+
['local.set', `$${cb}`, asF64(emit(fn))],
|
|
796
|
+
['local.set', `$${n}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
|
|
797
|
+
['local.set', `$${off}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
|
|
798
|
+
['local.set', `$${cap}`, ['call', '$__cap', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
|
|
799
|
+
['local.set', `$${ord}`, ['call', '$__coll_order', ['local.get', `$${off}`], ['local.get', `$${cap}`], ['i32.const', stride]]],
|
|
800
|
+
['local.set', `$${i}`, ['i32.const', 0]],
|
|
801
|
+
['block', `$febrk${id}`, ['loop', `$feloop${id}`,
|
|
802
|
+
['br_if', `$febrk${id}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${n}`]]],
|
|
803
|
+
['local.set', `$${slot}`, ['i32.load', ['i32.add', ['local.get', `$${ord}`],
|
|
804
|
+
['i32.shl', ['local.get', `$${i}`], ['i32.const', 2]]]]],
|
|
805
|
+
['drop', asF64(ctx.closure.call(typed(['local.get', `$${cb}`], 'f64'),
|
|
806
|
+
[at(valOff), at(keyOff)]))],
|
|
807
|
+
['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
|
|
808
|
+
['br', `$feloop${id}`]]],
|
|
809
|
+
['f64.const', 0]], 'f64')
|
|
606
810
|
}
|
|
811
|
+
ctx.core.emit[`.${VAL.MAP}:forEach`] = collForEach(MAP_ENTRY, 16, 8)
|
|
812
|
+
ctx.core.emit[`.${VAL.SET}:forEach`] = collForEach(SET_ENTRY, 8, 8)
|
|
607
813
|
|
|
608
814
|
// Generated Map probe functions
|
|
609
|
-
ctx.core.stdlib['__map_set'] = () => genUpsert('__map_set', MAP_ENTRY, '$__map_hash',
|
|
610
|
-
ctx.core.stdlib['__map_get'] = () => genLookup('__map_get', MAP_ENTRY, '$__map_hash',
|
|
611
|
-
ctx.core.stdlib['__map_get_h'] = () => genLookupStrictPrehashed('__map_get_h', MAP_ENTRY,
|
|
612
|
-
ctx.core.stdlib['__map_has'] = () => genLookup('__map_has', MAP_ENTRY, '$__map_hash',
|
|
613
|
-
ctx.core.stdlib['__map_delete'] = genDelete('__map_delete', MAP_ENTRY, '$__map_hash',
|
|
815
|
+
ctx.core.stdlib['__map_set'] = () => genUpsert('__map_set', MAP_ENTRY, '$__map_hash', sameValueZeroEqG, PTR.MAP, true, ctx.features.external)
|
|
816
|
+
ctx.core.stdlib['__map_get'] = () => genLookup('__map_get', MAP_ENTRY, '$__map_hash', sameValueZeroEqG, PTR.MAP, true, ctx.features.external)
|
|
817
|
+
ctx.core.stdlib['__map_get_h'] = () => genLookupStrictPrehashed('__map_get_h', MAP_ENTRY, sameValueZeroEqG, PTR.MAP, UNDEF_NAN, ctx.features.external)
|
|
818
|
+
ctx.core.stdlib['__map_has'] = () => genLookup('__map_has', MAP_ENTRY, '$__map_hash', sameValueZeroEqG, PTR.MAP, false, ctx.features.external)
|
|
819
|
+
ctx.core.stdlib['__map_delete'] = genDelete('__map_delete', MAP_ENTRY, '$__map_hash', sameValueZeroEqG, PTR.MAP)
|
|
820
|
+
|
|
821
|
+
// new Map(iterable) seeder. Source is another Map (copy live [key,val] slots) or
|
|
822
|
+
// an array of [key,value] pairs (`new Map([["a",1],…])`); any other arg yields an
|
|
823
|
+
// empty map. Pre-sizes cap to fit (smallest pow2 > 2·n, floor INIT_CAP) so seeding
|
|
824
|
+
// never triggers a rehash. Occupied MAP slot ⇔ hash word ≠ 0 (genDelete shift-back
|
|
825
|
+
// writes 0, leaving no tombstones — matches the rehash loop's own occupancy test).
|
|
826
|
+
ctx.core.stdlib['__map_from'] = `(func $__map_from (param $src i64) (result f64)
|
|
827
|
+
(local $map i64) (local $t i32) (local $off i32) (local $cap i32)
|
|
828
|
+
(local $i i32) (local $n i32) (local $slot i32) (local $entry i64) (local $newcap i32) (local $ord i32)
|
|
829
|
+
(local.set $t (call $__ptr_type (local.get $src)))
|
|
830
|
+
(if (i32.eq (local.get $t) (i32.const ${PTR.MAP}))
|
|
831
|
+
(then
|
|
832
|
+
(local.set $off (call $__ptr_offset (local.get $src)))
|
|
833
|
+
(local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
|
|
834
|
+
(local.set $n (i32.load (i32.sub (local.get $off) (i32.const 8)))))
|
|
835
|
+
(else (if (i32.eq (local.get $t) (i32.const ${PTR.ARRAY}))
|
|
836
|
+
(then (local.set $n (call $__len (local.get $src)))))))
|
|
837
|
+
(local.set $newcap (i32.shl (i32.const 1)
|
|
838
|
+
(i32.sub (i32.const 32) (i32.clz
|
|
839
|
+
(i32.sub (i32.add (i32.shl (local.get $n) (i32.const 1)) (i32.const ${INIT_CAP})) (i32.const 1))))))
|
|
840
|
+
(local.set $map (i64.reinterpret_f64 (call $__mkptr (i32.const ${PTR.MAP}) (i32.const 0)
|
|
841
|
+
(call $__alloc_hdr_n (i32.const 0) (local.get $newcap) (i32.const ${MAP_ENTRY})))))
|
|
842
|
+
(if (i32.eq (local.get $t) (i32.const ${PTR.MAP}))
|
|
843
|
+
(then
|
|
844
|
+
;; Copy in source insertion order so the new map enumerates identically.
|
|
845
|
+
(local.set $ord (call $__coll_order (local.get $off) (local.get $cap) (i32.const ${MAP_ENTRY})))
|
|
846
|
+
(block $dm (loop $lm
|
|
847
|
+
(br_if $dm (i32.ge_s (local.get $i) (local.get $n)))
|
|
848
|
+
(local.set $slot (i32.load (i32.add (local.get $ord) (i32.shl (local.get $i) (i32.const 2)))))
|
|
849
|
+
(local.set $map (call $__map_set (local.get $map)
|
|
850
|
+
(i64.load (i32.add (local.get $slot) (i32.const 8)))
|
|
851
|
+
(i64.load (i32.add (local.get $slot) (i32.const 16)))))
|
|
852
|
+
(local.set $i (i32.add (local.get $i) (i32.const 1)))
|
|
853
|
+
(br $lm))))
|
|
854
|
+
(else (if (i32.eq (local.get $t) (i32.const ${PTR.ARRAY}))
|
|
855
|
+
(then
|
|
856
|
+
(block $da (loop $la
|
|
857
|
+
(br_if $da (i32.ge_s (local.get $i) (local.get $n)))
|
|
858
|
+
(local.set $entry (i64.reinterpret_f64 (call $__typed_idx (local.get $src) (local.get $i))))
|
|
859
|
+
(local.set $map (call $__map_set (local.get $map)
|
|
860
|
+
(i64.reinterpret_f64 (call $__typed_idx (local.get $entry) (i32.const 0)))
|
|
861
|
+
(i64.reinterpret_f64 (call $__typed_idx (local.get $entry) (i32.const 1)))))
|
|
862
|
+
(local.set $i (i32.add (local.get $i) (i32.const 1)))
|
|
863
|
+
(br $la)))))))
|
|
864
|
+
(f64.reinterpret_i64 (local.get $map)))`
|
|
614
865
|
|
|
615
866
|
// === HASH — dynamic string-keyed object (type=7) ===
|
|
616
867
|
|
|
@@ -636,6 +887,11 @@ export default (ctx) => {
|
|
|
636
887
|
(local.set $i (i32.add (local.get $i) (i32.const 1)))
|
|
637
888
|
(br $ls))))
|
|
638
889
|
(else
|
|
890
|
+
;; canonical interned static: cached post-clamp FNV at -8 (see layout.js)
|
|
891
|
+
(if (i32.and (i32.eq (local.get $t) (i32.const ${PTR.STRING}))
|
|
892
|
+
(i32.and (i32.ge_u (local.get $off) (i32.const 8))
|
|
893
|
+
(i32.eq (i32.and (local.get $aux) (i32.const ${LAYOUT.SSO_BIT | LAYOUT.SLICE_BIT | STR_INTERN_BIT})) (i32.const ${STR_INTERN_BIT}))))
|
|
894
|
+
(then (return (i32.load (i32.sub (local.get $off) (i32.const 8))))))
|
|
639
895
|
(if (i32.and (i32.eq (local.get $t) (i32.const ${PTR.STRING})) (i32.ge_u (local.get $off) (i32.const 4)))
|
|
640
896
|
(then (local.set $len (i32.load (i32.sub (local.get $off) (i32.const 4))))))
|
|
641
897
|
;; 4-byte unrolled FNV-1a: each iter loads i32, mixes 4 bytes (little-endian) sequentially.
|
|
@@ -675,13 +931,13 @@ export default (ctx) => {
|
|
|
675
931
|
(call $__mkptr (i32.const ${PTR.HASH}) (i32.const 0)
|
|
676
932
|
(call $__alloc_hdr_n (i32.const 0) (i32.const ${smallCap}) (i32.const ${MAP_ENTRY}))))`
|
|
677
933
|
|
|
678
|
-
ctx.core.stdlib['__hash_get_local'] = genLookupStrict('__hash_get_local', MAP_ENTRY, '$__str_hash',
|
|
679
|
-
ctx.core.stdlib['__hash_get_local_h'] = genLookupStrictPrehashed('__hash_get_local_h', MAP_ENTRY,
|
|
680
|
-
ctx.core.stdlib['__hash_set_local_h'] = genUpsertStrictPrehashed('__hash_set_local_h', MAP_ENTRY,
|
|
681
|
-
ctx.core.stdlib['__hash_set_local'] = genUpsertGrow('__hash_set_local', MAP_ENTRY, '$__str_hash',
|
|
934
|
+
ctx.core.stdlib['__hash_get_local'] = genLookupStrict('__hash_get_local', MAP_ENTRY, '$__str_hash', strEqG, PTR.HASH)
|
|
935
|
+
ctx.core.stdlib['__hash_get_local_h'] = genLookupStrictPrehashed('__hash_get_local_h', MAP_ENTRY, strEqG, PTR.HASH)
|
|
936
|
+
ctx.core.stdlib['__hash_set_local_h'] = genUpsertStrictPrehashed('__hash_set_local_h', MAP_ENTRY, strEqG, PTR.HASH)
|
|
937
|
+
ctx.core.stdlib['__hash_set_local'] = genUpsertGrow('__hash_set_local', MAP_ENTRY, '$__str_hash', strEqG, PTR.HASH, true, false, true)
|
|
682
938
|
// Tombstones an entry in a HASH (string keys). Returns 1 if found+deleted, 0 otherwise.
|
|
683
939
|
// Used as the bucket-level primitive for __dyn_del.
|
|
684
|
-
ctx.core.stdlib['__hash_del_local'] = genDelete('__hash_del_local', MAP_ENTRY, '$__str_hash',
|
|
940
|
+
ctx.core.stdlib['__hash_del_local'] = genDelete('__hash_del_local', MAP_ENTRY, '$__str_hash', strEqG, PTR.HASH)
|
|
685
941
|
// Outer __dyn_props hash: keyed by object offset (i32 as f64 bits), value is per-object props hash.
|
|
686
942
|
// Uses bit-hash + i64.eq — no string allocation for the unique integer key.
|
|
687
943
|
ctx.core.stdlib['__ihash_get_local'] = genLookupStrict('__ihash_get_local', MAP_ENTRY, '$__map_hash', '(i64.eq (i64.load (i32.add (local.get $slot) (i32.const 8))) (local.get $key))', PTR.HASH)
|
|
@@ -713,10 +969,14 @@ export default (ctx) => {
|
|
|
713
969
|
// literals — single bit-eq decides). Falls back to __str_eq when bits differ
|
|
714
970
|
// so runtime-registered schemas (e.g. JSON.parse OBJECTs whose keys are
|
|
715
971
|
// freshly heap-allocated by __jp_str) still resolve correctly.
|
|
972
|
+
// If-expression, NOT i32.or: `or` evaluates both arms, calling __str_eq even
|
|
973
|
+
// when the bit-eq already decided — that bare call per schema-key step was
|
|
974
|
+
// the hottest __str_eq producer in the self-host (the kernel includes __jp
|
|
975
|
+
// for optJSON, so the fallback arm is always compiled in).
|
|
716
976
|
const schemaKeyEq = (storedKey, userKey) => ctx.core.includes.has('__jp_obj') || ctx.core.includes.has('__jp')
|
|
717
|
-
? `(i32.
|
|
718
|
-
(
|
|
719
|
-
(call $__str_eq ${storedKey} ${userKey}))`
|
|
977
|
+
? `(if (result i32) (i64.eq ${storedKey} ${userKey})
|
|
978
|
+
(then (i32.const 1))
|
|
979
|
+
(else (call $__str_eq ${storedKey} ${userKey})))`
|
|
720
980
|
: `(i64.eq ${storedKey} ${userKey})`
|
|
721
981
|
const buildObjectSchemaArm = () => (ctx.schema.list.length > 0 || ctx.core.includes.has('__jp_obj')) ? `
|
|
722
982
|
(if (i32.eq (local.get $type) (i32.const ${PTR.OBJECT}))
|
|
@@ -761,9 +1021,7 @@ export default (ctx) => {
|
|
|
761
1021
|
(local.set $idx (i32.const 0))
|
|
762
1022
|
(block $schemaSetDone (loop $schemaSetLoop
|
|
763
1023
|
(br_if $schemaSetDone (i32.ge_s (local.get $idx) (local.get $nkeys)))
|
|
764
|
-
(if (
|
|
765
|
-
(i64.load (i32.add (local.get $koff) (i32.shl (local.get $idx) (i32.const 3))))
|
|
766
|
-
(local.get $key))
|
|
1024
|
+
(if ${schemaKeyEq(`(i64.load (i32.add (local.get $koff) (i32.shl (local.get $idx) (i32.const 3))))`, `(local.get $key)`)}
|
|
767
1025
|
(then
|
|
768
1026
|
(i64.store (i32.add (local.get $off) (i32.shl (local.get $idx) (i32.const 3))) (local.get $val))
|
|
769
1027
|
(br $schemaSetDone)))
|
|
@@ -782,6 +1040,10 @@ export default (ctx) => {
|
|
|
782
1040
|
(local $props i64) (local $off i32)
|
|
783
1041
|
(local $poff i32) (local $pcap i32) (local $pend i32) (local $idx i32) (local $slot i32) (local $tries i32)
|
|
784
1042
|
${buildObjectSchemaLocals()}
|
|
1043
|
+
;; Real-number receiver (f===f — pointers are NaN-boxed) has no props: bail before
|
|
1044
|
+
;; treating its bits as a heap offset (\`(5).foo\`/\`(5)[k]\` → undefined, not OOB).
|
|
1045
|
+
(if (f64.eq (f64.reinterpret_i64 (local.get $obj)) (f64.reinterpret_i64 (local.get $obj)))
|
|
1046
|
+
(then (return (i64.const ${UNDEF_NAN}))))
|
|
785
1047
|
(local.set $off (i32.wrap_i64 (i64.and (local.get $obj) (i64.const ${LAYOUT.OFFSET_MASK}))))
|
|
786
1048
|
;; CLOSURE with no env (offset 0): many function refs share offset 0, so key the
|
|
787
1049
|
;; global __dyn_props hash on the function table index (negative — can't collide
|
|
@@ -827,13 +1089,23 @@ export default (ctx) => {
|
|
|
827
1089
|
(local.set $props (i64.load (i32.sub (local.get $off) (i32.const 16))))
|
|
828
1090
|
(br_if $dynDone (i64.eqz (local.get $props)))
|
|
829
1091
|
(br $haveProps)))
|
|
830
|
-
;;
|
|
1092
|
+
;; HASH: a plain dict whose string keys ARE its own bucket entries — the
|
|
1093
|
+
;; receiver IS its props table, so probe it directly (a HASH never carries
|
|
1094
|
+
;; an off-16 dyn sidecar). A statically HASH-typed h[k] already inlines
|
|
1095
|
+
;; __hash_get; this path serves receivers whose HASH type is only known at
|
|
1096
|
+
;; runtime — e.g. a value read back through a function return, as in
|
|
1097
|
+
;; derive(emitter)[op]. Without it, dyn-get reads the (absent) sidecar and
|
|
1098
|
+
;; reports every key missing.
|
|
1099
|
+
(if (i32.eq (local.get $type) (i32.const ${PTR.HASH}))
|
|
1100
|
+
(then
|
|
1101
|
+
(local.set $props (local.get $obj))
|
|
1102
|
+
(br $haveProps)))
|
|
1103
|
+
;; Other header types (TYPED/SET/MAP) carry propsPtr at off-16
|
|
831
1104
|
;; directly, bypassing the global __dyn_props hash.
|
|
832
1105
|
(if (i32.and (i32.ge_u (local.get $off) (i32.const 16))
|
|
833
1106
|
(i32.or (i32.eq (local.get $type) (i32.const ${PTR.TYPED}))
|
|
834
|
-
(i32.or (i32.eq (local.get $type) (i32.const ${PTR.
|
|
835
|
-
|
|
836
|
-
(i32.eq (local.get $type) (i32.const ${PTR.MAP}))))))
|
|
1107
|
+
(i32.or (i32.eq (local.get $type) (i32.const ${PTR.SET}))
|
|
1108
|
+
(i32.eq (local.get $type) (i32.const ${PTR.MAP})))))
|
|
837
1109
|
(then
|
|
838
1110
|
(local.set $props (i64.load (i32.sub (local.get $off) (i32.const 16))))
|
|
839
1111
|
(br_if $dynDone (i64.eqz (local.get $props)))
|
|
@@ -856,14 +1128,17 @@ export default (ctx) => {
|
|
|
856
1128
|
(br $dynDone))
|
|
857
1129
|
(else
|
|
858
1130
|
(global.set $__dyn_get_cache_props (f64.reinterpret_i64 (local.get $props))))))))
|
|
859
|
-
(local.set $poff (
|
|
1131
|
+
(local.set $poff (call $__ptr_offset (local.get $props)))
|
|
860
1132
|
(local.set $pcap (i32.load (i32.sub (local.get $poff) (i32.const 4))))
|
|
861
1133
|
(local.set $pend (i32.add (local.get $poff) (i32.mul (local.get $pcap) (i32.const ${MAP_ENTRY}))))
|
|
862
1134
|
(local.set $slot (i32.add (local.get $poff) (i32.mul (i32.and (local.get $h) (i32.sub (local.get $pcap) (i32.const 1))) (i32.const ${MAP_ENTRY}))))
|
|
863
1135
|
(block $hdone (loop $hprobe
|
|
864
1136
|
(br_if $dynDone (i64.eqz (i64.load (local.get $slot))))
|
|
865
|
-
|
|
866
|
-
|
|
1137
|
+
;; hash-first: slot stores the key's hash in its low 32 bits — one i32
|
|
1138
|
+
;; compare rejects collision steps without walking key bytes.
|
|
1139
|
+
(if (i32.eq (i32.load (local.get $slot)) (local.get $h))
|
|
1140
|
+
(then (if (call $__str_eq (i64.load (i32.add (local.get $slot) (i32.const 8))) (local.get $key))
|
|
1141
|
+
(then (return (i64.load (i32.add (local.get $slot) (i32.const 16))))))))
|
|
867
1142
|
(local.set $slot (i32.add (local.get $slot) (i32.const ${MAP_ENTRY})))
|
|
868
1143
|
(if (i32.ge_u (local.get $slot) (local.get $pend)) (then (local.set $slot (local.get $poff))))
|
|
869
1144
|
(local.set $tries (i32.add (local.get $tries) (i32.const 1)))
|
|
@@ -884,6 +1159,10 @@ export default (ctx) => {
|
|
|
884
1159
|
|
|
885
1160
|
ctx.core.stdlib['__dyn_get_expr_t'] = `(func $__dyn_get_expr_t (param $obj i64) (param $key i64) (param $t i32) (result i64)
|
|
886
1161
|
(local $val i64)
|
|
1162
|
+
;; Real-number receiver → no props; its garbage tag could match HASH and hit the
|
|
1163
|
+
;; __hash_get_local fallback below (heap read at a bogus offset → OOB).
|
|
1164
|
+
(if (f64.eq (f64.reinterpret_i64 (local.get $obj)) (f64.reinterpret_i64 (local.get $obj)))
|
|
1165
|
+
(then (return (i64.const ${UNDEF_NAN}))))
|
|
887
1166
|
(local.set $val (call $__dyn_get_t (local.get $obj) (local.get $key) (local.get $t)))
|
|
888
1167
|
(if (result i64)
|
|
889
1168
|
(i64.ne (local.get $val) (i64.const ${UNDEF_NAN}))
|
|
@@ -891,12 +1170,15 @@ export default (ctx) => {
|
|
|
891
1170
|
(else
|
|
892
1171
|
(if (result i64) (i32.eq (local.get $t) (i32.const ${PTR.HASH}))
|
|
893
1172
|
(then (call $__hash_get_local (local.get $obj) (local.get $key)))
|
|
894
|
-
(else (i64.const ${
|
|
1173
|
+
(else (i64.const ${UNDEF_NAN}))))))`
|
|
895
1174
|
|
|
896
1175
|
// Prehashed variant of __dyn_get_expr_t for constant string keys: the FNV hash
|
|
897
1176
|
// is folded at compile time (strHashLiteral), so no __str_hash call at runtime.
|
|
898
1177
|
ctx.core.stdlib['__dyn_get_expr_t_h'] = `(func $__dyn_get_expr_t_h (param $obj i64) (param $key i64) (param $t i32) (param $h i32) (result i64)
|
|
899
1178
|
(local $val i64)
|
|
1179
|
+
;; Real-number receiver → no props; guard the HASH fallback OOB (see __dyn_get_expr_t).
|
|
1180
|
+
(if (f64.eq (f64.reinterpret_i64 (local.get $obj)) (f64.reinterpret_i64 (local.get $obj)))
|
|
1181
|
+
(then (return (i64.const ${UNDEF_NAN}))))
|
|
900
1182
|
(local.set $val (call $__dyn_get_t_h (local.get $obj) (local.get $key) (local.get $t) (local.get $h)))
|
|
901
1183
|
(if (result i64)
|
|
902
1184
|
(i64.ne (local.get $val) (i64.const ${UNDEF_NAN}))
|
|
@@ -904,7 +1186,7 @@ export default (ctx) => {
|
|
|
904
1186
|
(else
|
|
905
1187
|
(if (result i64) (i32.eq (local.get $t) (i32.const ${PTR.HASH}))
|
|
906
1188
|
(then (call $__hash_get_local_h (local.get $obj) (local.get $key) (local.get $h)))
|
|
907
|
-
(else (i64.const ${
|
|
1189
|
+
(else (i64.const ${UNDEF_NAN}))))))`
|
|
908
1190
|
|
|
909
1191
|
// Like __dyn_get_expr but also resolves EXTERNAL host objects via __ext_prop.
|
|
910
1192
|
// Used at call sites where receiver type is statically unknown.
|
|
@@ -922,18 +1204,24 @@ export default (ctx) => {
|
|
|
922
1204
|
const extArm = ctx.features.external
|
|
923
1205
|
? `(if (result i64) (i32.eq (local.get $t) (i32.const ${PTR.EXTERNAL}))
|
|
924
1206
|
(then (call $__ext_prop (local.get $obj) (local.get $key)))
|
|
925
|
-
(else (i64.const ${
|
|
926
|
-
: `(i64.const ${
|
|
1207
|
+
(else (i64.const ${UNDEF_NAN})))`
|
|
1208
|
+
: `(i64.const ${UNDEF_NAN})`
|
|
927
1209
|
return `(func $__dyn_get_any_t (param $obj i64) (param $key i64) (param $t i32) (result i64)
|
|
928
1210
|
(local $val i64)
|
|
929
|
-
|
|
930
|
-
|
|
1211
|
+
;; A real number receiver (f===f — NaN-boxed pointers are NaN) has no dynamic
|
|
1212
|
+
;; props: \`(5).foo\` is undefined. Without this guard the bits are reinterpreted as
|
|
1213
|
+
;; a pointer and \`__dyn_get_t\` reads heap at a bogus offset → OOB for large values.
|
|
1214
|
+
(if (result i64) (f64.eq (f64.reinterpret_i64 (local.get $obj)) (f64.reinterpret_i64 (local.get $obj)))
|
|
1215
|
+
(then (i64.const ${UNDEF_NAN}))
|
|
931
1216
|
(else
|
|
932
|
-
(
|
|
933
|
-
|
|
934
|
-
(
|
|
935
|
-
|
|
936
|
-
|
|
1217
|
+
(if (result i64) (i32.eq (local.get $t) (i32.const ${PTR.HASH}))
|
|
1218
|
+
(then (call $__hash_get_local (local.get $obj) (local.get $key)))
|
|
1219
|
+
(else
|
|
1220
|
+
(local.set $val (call $__dyn_get_t (local.get $obj) (local.get $key) (local.get $t)))
|
|
1221
|
+
(if (result i64)
|
|
1222
|
+
(i64.ne (local.get $val) (i64.const ${UNDEF_NAN}))
|
|
1223
|
+
(then (local.get $val))
|
|
1224
|
+
(else ${extArm})))))))`
|
|
937
1225
|
}
|
|
938
1226
|
|
|
939
1227
|
// Prehashed variant of __dyn_get_any_t for constant string keys: the FNV hash
|
|
@@ -944,18 +1232,22 @@ export default (ctx) => {
|
|
|
944
1232
|
const extArm = ctx.features.external
|
|
945
1233
|
? `(if (result i64) (i32.eq (local.get $t) (i32.const ${PTR.EXTERNAL}))
|
|
946
1234
|
(then (call $__ext_prop (local.get $obj) (local.get $key)))
|
|
947
|
-
(else (i64.const ${
|
|
948
|
-
: `(i64.const ${
|
|
1235
|
+
(else (i64.const ${UNDEF_NAN})))`
|
|
1236
|
+
: `(i64.const ${UNDEF_NAN})`
|
|
949
1237
|
return `(func $__dyn_get_any_t_h (param $obj i64) (param $key i64) (param $t i32) (param $h i32) (result i64)
|
|
950
1238
|
(local $val i64)
|
|
951
|
-
|
|
952
|
-
|
|
1239
|
+
;; Real-number receiver → no dynamic props (see __dyn_get_any_t); guard the OOB.
|
|
1240
|
+
(if (result i64) (f64.eq (f64.reinterpret_i64 (local.get $obj)) (f64.reinterpret_i64 (local.get $obj)))
|
|
1241
|
+
(then (i64.const ${UNDEF_NAN}))
|
|
953
1242
|
(else
|
|
954
|
-
(
|
|
955
|
-
|
|
956
|
-
(
|
|
957
|
-
|
|
958
|
-
|
|
1243
|
+
(if (result i64) (i32.eq (local.get $t) (i32.const ${PTR.HASH}))
|
|
1244
|
+
(then (call $__hash_get_local_h (local.get $obj) (local.get $key) (local.get $h)))
|
|
1245
|
+
(else
|
|
1246
|
+
(local.set $val (call $__dyn_get_t_h (local.get $obj) (local.get $key) (local.get $t) (local.get $h)))
|
|
1247
|
+
(if (result i64)
|
|
1248
|
+
(i64.ne (local.get $val) (i64.const ${UNDEF_NAN}))
|
|
1249
|
+
(then (local.get $val))
|
|
1250
|
+
(else ${extArm})))))))`
|
|
959
1251
|
}
|
|
960
1252
|
|
|
961
1253
|
// Hot for `node.loc = pos` patterns (e.g. watr's parser tags every nested level).
|
|
@@ -1020,11 +1312,18 @@ export default (ctx) => {
|
|
|
1020
1312
|
(if (i64.ne (local.get $props) (local.get $oldProps))
|
|
1021
1313
|
(then (i64.store (i32.sub (local.get $off) (i32.const 16)) (local.get $props))))
|
|
1022
1314
|
(return (local.get $val))))
|
|
1315
|
+
;; HASH: a plain dict — its string keys ARE its own bucket entries (there is no
|
|
1316
|
+
;; off-16 sidecar; that belongs to TYPED/SET/MAP, which have a native shape PLUS
|
|
1317
|
+
;; ad-hoc props). Write directly into the receiver, mirroring __dyn_get's HASH arm.
|
|
1318
|
+
;; __hash_set_local forwards on grow, so the caller's boxed pointer stays valid.
|
|
1319
|
+
(if (i32.eq (local.get $type) (i32.const ${PTR.HASH}))
|
|
1320
|
+
(then
|
|
1321
|
+
(drop (call $__hash_set_local (local.get $obj) (local.get $key) (local.get $val)))
|
|
1322
|
+
(return (local.get $val))))
|
|
1023
1323
|
(if (i32.and (i32.ge_u (local.get $off) (i32.const 16))
|
|
1024
1324
|
(i32.or (i32.eq (local.get $type) (i32.const ${PTR.TYPED}))
|
|
1025
|
-
(i32.or (i32.eq (local.get $type) (i32.const ${PTR.
|
|
1026
|
-
|
|
1027
|
-
(i32.eq (local.get $type) (i32.const ${PTR.MAP}))))))
|
|
1325
|
+
(i32.or (i32.eq (local.get $type) (i32.const ${PTR.SET}))
|
|
1326
|
+
(i32.eq (local.get $type) (i32.const ${PTR.MAP})))))
|
|
1028
1327
|
(then
|
|
1029
1328
|
(local.set $oldProps (i64.load (i32.sub (local.get $off) (i32.const 16))))
|
|
1030
1329
|
(local.set $props
|
|
@@ -1148,9 +1447,9 @@ export default (ctx) => {
|
|
|
1148
1447
|
(global.set $__dyn_props (f64.reinterpret_i64 (local.get $root))))`
|
|
1149
1448
|
|
|
1150
1449
|
// Generated HASH probe functions
|
|
1151
|
-
ctx.core.stdlib['__hash_set'] = () => genUpsertGrow('__hash_set', MAP_ENTRY, '$__str_hash',
|
|
1152
|
-
ctx.core.stdlib['__hash_get'] = () => genLookup('__hash_get', MAP_ENTRY, '$__str_hash',
|
|
1153
|
-
ctx.core.stdlib['__hash_has'] = () => genLookup('__hash_has', MAP_ENTRY, '$__str_hash',
|
|
1450
|
+
ctx.core.stdlib['__hash_set'] = () => genUpsertGrow('__hash_set', MAP_ENTRY, '$__str_hash', strEqG, PTR.HASH, false, ctx.features.external, true)
|
|
1451
|
+
ctx.core.stdlib['__hash_get'] = () => genLookup('__hash_get', MAP_ENTRY, '$__str_hash', strEqG, PTR.HASH, true, ctx.features.external)
|
|
1452
|
+
ctx.core.stdlib['__hash_has'] = () => genLookup('__hash_has', MAP_ENTRY, '$__str_hash', strEqG, PTR.HASH, false, ctx.features.external)
|
|
1154
1453
|
|
|
1155
1454
|
// === `delete obj[k]`: lift from prepare for computed-key removal ===
|
|
1156
1455
|
// Static-key `delete obj.x` / `delete obj["x"]` is rejected in prepare (fixed schema);
|
|
@@ -1170,9 +1469,14 @@ export default (ctx) => {
|
|
|
1170
1469
|
if (prop === 'length' && (objType === VAL.ARRAY || objType === VAL.TYPED || objType === VAL.STRING || objType === VAL.SET || objType === VAL.MAP))
|
|
1171
1470
|
return typed(['i32.const', 1], 'i32')
|
|
1172
1471
|
|
|
1173
|
-
const schemaIdx = typeof obj === 'string' ? ctx.schema.
|
|
1472
|
+
const schemaIdx = typeof obj === 'string' ? ctx.schema.slotOf(obj, prop) : ctx.schema.slotOf(null, prop)
|
|
1174
1473
|
if (schemaIdx >= 0) return typed(['i32.const', 1], 'i32')
|
|
1175
|
-
|
|
1474
|
+
// A schema MISS does not prove absence: an OBJECT can carry off-schema
|
|
1475
|
+
// dynamic props (`o.z = …` → __dyn_set's propsPtr), and under the self-host
|
|
1476
|
+
// kernel schema.slotOf can under-resolve even an in-schema key. Don't fold to
|
|
1477
|
+
// a static 0 — fall through to the runtime probe below, which reads the
|
|
1478
|
+
// actual property via __dyn_get (OBJECT is in `hasDynProps`) and reports
|
|
1479
|
+
// presence by non-nullish, exactly as the `.`/`[]` READ path resolves it.
|
|
1176
1480
|
}
|
|
1177
1481
|
|
|
1178
1482
|
const keyTmp = temp()
|
|
@@ -1244,13 +1548,61 @@ export default (ctx) => {
|
|
|
1244
1548
|
['local.get', `$${outTmp}`]], 'i32')
|
|
1245
1549
|
}
|
|
1246
1550
|
|
|
1551
|
+
// === iterable normalization: Set/Map → dense Array ===
|
|
1552
|
+
|
|
1553
|
+
// `for (x of coll)` and `[...coll]` iterate in *value* order, but a Set/Map
|
|
1554
|
+
// stores entries in a sparse open-addressing table (live slots scattered among
|
|
1555
|
+
// empties). Index access `coll[i]` would read raw slot words. So normalize a
|
|
1556
|
+
// Set→keys-array / Map→[k,v]-entries-array once at loop/spread setup; an Array,
|
|
1557
|
+
// String, or TypedArray is already index-iterable and passes through untouched
|
|
1558
|
+
// (no copy). `valTypeOf(['()','__iter_arr',x])` (src/kind.js) mirrors this:
|
|
1559
|
+
// Set/Map → ARRAY, everything else → x's own type, so the downstream `arr[i]`
|
|
1560
|
+
// / `.length` dispatch stays statically typed.
|
|
1561
|
+
ctx.core.emit['__iter_arr'] = (src) => {
|
|
1562
|
+
const vt = valTypeOf(src)
|
|
1563
|
+
if (vt === VAL.ARRAY || vt === VAL.STRING || vt === VAL.TYPED || vt === VAL.BUFFER)
|
|
1564
|
+
return asF64(emit(src))
|
|
1565
|
+
const t = temp('iter')
|
|
1566
|
+
const bind = ['local.set', `$${t}`, asF64(emit(src))]
|
|
1567
|
+
if (vt === VAL.SET) return typed(['block', ['result', 'f64'], bind, collKeysFromTemp(t, SET_ENTRY)], 'f64')
|
|
1568
|
+
if (vt === VAL.MAP) return typed(['block', ['result', 'f64'], bind, collEntriesFromTemp(t, MAP_ENTRY)], 'f64')
|
|
1569
|
+
// Unknown receiver: resolve the kind once at runtime (loop-invariant).
|
|
1570
|
+
inc('__ptr_type')
|
|
1571
|
+
const ptrType = () => ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]
|
|
1572
|
+
return typed(['block', ['result', 'f64'], bind,
|
|
1573
|
+
['if', ['result', 'f64'], ['i32.eq', ptrType(), ['i32.const', PTR.SET]],
|
|
1574
|
+
['then', collKeysFromTemp(t, SET_ENTRY)],
|
|
1575
|
+
['else', ['if', ['result', 'f64'], ['i32.eq', ptrType(), ['i32.const', PTR.MAP]],
|
|
1576
|
+
['then', collEntriesFromTemp(t, MAP_ENTRY)],
|
|
1577
|
+
['else', ['local.get', `$${t}`]]]]]], 'f64')
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1247
1580
|
// === for...in on dynamic objects (HASH iteration) ===
|
|
1248
1581
|
|
|
1582
|
+
// Flatten a statement/block to void IR — a self-host-robust inline of
|
|
1583
|
+
// emitVoid+emitBlockBody (see the call site in for-in for why the bridge
|
|
1584
|
+
// emitVoid can't be used here). Recurses into `{}` blocks; emits each leaf
|
|
1585
|
+
// statement in void position and drops any leftover value.
|
|
1586
|
+
const emitFlatVoid = (node) => {
|
|
1587
|
+
if (isBlockBody(node)) {
|
|
1588
|
+
if (node.length === 1) return []
|
|
1589
|
+
const inner = node[1]
|
|
1590
|
+
const stmts = Array.isArray(inner) && inner[0] === ';' ? inner.slice(1) : [inner]
|
|
1591
|
+
const out = []
|
|
1592
|
+
for (const s of stmts) if (s != null && typeof s !== 'number') out.push(...emitFlatVoid(s))
|
|
1593
|
+
return out
|
|
1594
|
+
}
|
|
1595
|
+
const ir = emit(node, 'void')
|
|
1596
|
+
if (ir == null) return []
|
|
1597
|
+
const items = Array.isArray(ir) && (typeof ir[0] === 'string' || ir[0] == null) ? [ir] : ir
|
|
1598
|
+
return ir.type && ir.type !== 'void' ? [...items, 'drop'] : items
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1249
1601
|
// for-in: iterate HASH entries, binding key string to loop variable.
|
|
1250
1602
|
// Also handles OBJECT/ARRAY/etc whose dynamic props are stored at off-16
|
|
1251
1603
|
// as a HASH (see __dyn_set). Non-HASH receivers redirect to that props HASH.
|
|
1252
1604
|
ctx.core.emit['for-in'] = (varName, src, body) => {
|
|
1253
|
-
const off = tempI32('ho'), cap = tempI32('hc')
|
|
1605
|
+
const off = tempI32('ho'), cap = tempI32('hc'), n = tempI32('hn'), ord = tempI32('hr')
|
|
1254
1606
|
const i = tempI32('hi'), slot = tempI32('hs')
|
|
1255
1607
|
const ptrI64 = tempI64('hp'), srcOff = tempI32('hso'), srcType = tempI32('hst')
|
|
1256
1608
|
if (!ctx.func.locals.has(varName)) ctx.func.locals.set(varName, 'f64')
|
|
@@ -1260,10 +1612,17 @@ export default (ctx) => {
|
|
|
1260
1612
|
const needsCont = hasOwnContinue(body)
|
|
1261
1613
|
ctx.func.stack.push({ brk, loop: needsCont ? cont : loop })
|
|
1262
1614
|
let bodyFlat
|
|
1263
|
-
|
|
1615
|
+
// NOTE: `flat(body)` (the bridge-dispatched emitVoid) miscompiles in this
|
|
1616
|
+
// self-host call context — it returns [] for a void-postfix body
|
|
1617
|
+
// (`for (k in o) n++`, lowered to `(++n)-1`), silently dropping the loop
|
|
1618
|
+
// body so the kernel-compiled for-in iterates but does nothing. The exact
|
|
1619
|
+
// same emit+flatten logic inlined here compiles correctly. emitFlatVoid
|
|
1620
|
+
// mirrors emitVoid/emitBlockBody (minus early-return refinement narrowing,
|
|
1621
|
+
// which a loop body does not need).
|
|
1622
|
+
try { bodyFlat = emitFlatVoid(body) }
|
|
1264
1623
|
finally { ctx.func.stack.pop() }
|
|
1265
1624
|
const bodyBlock = needsCont ? [['block', cont, ...bodyFlat]] : bodyFlat
|
|
1266
|
-
inc('__ptr_type')
|
|
1625
|
+
inc('__ptr_type', '__len', '__coll_order')
|
|
1267
1626
|
return [
|
|
1268
1627
|
// Save source ptr as i64
|
|
1269
1628
|
['local.set', `$${ptrI64}`, ['i64.reinterpret_f64', va]],
|
|
@@ -1282,11 +1641,16 @@ export default (ctx) => {
|
|
|
1282
1641
|
['then',
|
|
1283
1642
|
['local.set', `$${off}`, ['call', '$__ptr_offset', ['local.get', `$${ptrI64}`]]],
|
|
1284
1643
|
['local.set', `$${cap}`, ['call', '$__cap', ['local.get', `$${ptrI64}`]]],
|
|
1644
|
+
['local.set', `$${n}`, ['call', '$__len', ['local.get', `$${ptrI64}`]]],
|
|
1645
|
+
// Snapshot live slots in insertion order (JS for-in spec order). Walk
|
|
1646
|
+
// the snapshot; re-check occupancy so a key the body deletes before it
|
|
1647
|
+
// is reached is skipped rather than re-bound from an emptied slot.
|
|
1648
|
+
['local.set', `$${ord}`, ['call', '$__coll_order', ['local.get', `$${off}`], ['local.get', `$${cap}`], ['i32.const', MAP_ENTRY]]],
|
|
1285
1649
|
['local.set', `$${i}`, ['i32.const', 0]],
|
|
1286
1650
|
['block', brk, ['loop', loop,
|
|
1287
|
-
['br_if', brk, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${
|
|
1288
|
-
['local.set', `$${slot}`, ['i32.add', ['local.get', `$${
|
|
1289
|
-
['i32.
|
|
1651
|
+
['br_if', brk, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${n}`]]],
|
|
1652
|
+
['local.set', `$${slot}`, ['i32.load', ['i32.add', ['local.get', `$${ord}`],
|
|
1653
|
+
['i32.shl', ['local.get', `$${i}`], ['i32.const', 2]]]]],
|
|
1290
1654
|
['if', ['i64.ne', ['i64.load', ['local.get', `$${slot}`]], ['i64.const', 0]],
|
|
1291
1655
|
['then',
|
|
1292
1656
|
['local.set', `$${varName}`, ['f64.reinterpret_i64', ['i64.load', ['i32.add', ['local.get', `$${slot}`], ['i32.const', 8]]]]],
|
|
@@ -1296,3 +1660,133 @@ export default (ctx) => {
|
|
|
1296
1660
|
]
|
|
1297
1661
|
}
|
|
1298
1662
|
}
|
|
1663
|
+
|
|
1664
|
+
// Walk a Set/Map backing table (bound f64 local `t`), copying one column of each
|
|
1665
|
+
// live entry into a fresh dense Array sized to the live count. `stride` is the
|
|
1666
|
+
// entry size (Set 16, Map 24); the first f64 word of each slot is the stored
|
|
1667
|
+
// hash — 0 marks an empty slot (no tombstones: delete back-shifts). `fieldOff`
|
|
1668
|
+
// picks the column: 8 = key (Set element / Map key), 16 = Map value. Mirrors
|
|
1669
|
+
// object.js's hash*FromTemp. Used by `__iter_arr` and `.keys()`/`.values()`.
|
|
1670
|
+
function collKeysFromTemp(t, stride, fieldOff = 8) {
|
|
1671
|
+
inc('__ptr_offset', '__cap', '__len', '__coll_order')
|
|
1672
|
+
const off = tempI32('cko'), cap = tempI32('ckc'), n = tempI32('ckn')
|
|
1673
|
+
const i = tempI32('cki'), ord = tempI32('ckr'), slot = tempI32('cks')
|
|
1674
|
+
const out = allocPtr({ type: PTR.ARRAY, len: ['local.get', `$${n}`], tag: 'cka' })
|
|
1675
|
+
const id = ctx.func.uniq++
|
|
1676
|
+
return ['block', ['result', 'f64'],
|
|
1677
|
+
['local.set', `$${n}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
|
|
1678
|
+
out.init,
|
|
1679
|
+
['local.set', `$${off}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
|
|
1680
|
+
['local.set', `$${cap}`, ['call', '$__cap', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
|
|
1681
|
+
['local.set', `$${ord}`, ['call', '$__coll_order', ['local.get', `$${off}`], ['local.get', `$${cap}`], ['i32.const', stride]]],
|
|
1682
|
+
['local.set', `$${i}`, ['i32.const', 0]],
|
|
1683
|
+
['block', `$ckbrk${id}`, ['loop', `$ckloop${id}`,
|
|
1684
|
+
['br_if', `$ckbrk${id}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${n}`]]],
|
|
1685
|
+
['local.set', `$${slot}`, ['i32.load', ['i32.add', ['local.get', `$${ord}`],
|
|
1686
|
+
['i32.shl', ['local.get', `$${i}`], ['i32.const', 2]]]]],
|
|
1687
|
+
elemStore(out.local, i,
|
|
1688
|
+
['f64.load', ['i32.add', ['local.get', `$${slot}`], ['i32.const', fieldOff]]]),
|
|
1689
|
+
['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
|
|
1690
|
+
['br', `$ckloop${id}`]]],
|
|
1691
|
+
out.ptr]
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
// Like collKeysFromTemp but builds 2-element pair arrays — Map/Set `entries()`
|
|
1695
|
+
// and `[...map]` yield pairs. Each live slot contributes a fresh 2-element Array
|
|
1696
|
+
// [slot+aOff, slot+bOff] boxed into the output: Map entries use (8,16) → [k,v];
|
|
1697
|
+
// Set entries use (8,8) → [v,v].
|
|
1698
|
+
function collEntriesFromTemp(t, stride, aOff = 8, bOff = 16) {
|
|
1699
|
+
inc('__ptr_offset', '__cap', '__len', '__alloc_hdr', '__coll_order')
|
|
1700
|
+
const off = tempI32('ceo'), cap = tempI32('cec'), n = tempI32('cen')
|
|
1701
|
+
const i = tempI32('cei'), ord = tempI32('cer'), slot = tempI32('ces'), pair = tempI32('cep')
|
|
1702
|
+
const out = allocPtr({ type: PTR.ARRAY, len: ['local.get', `$${n}`], tag: 'cea' })
|
|
1703
|
+
const id = ctx.func.uniq++
|
|
1704
|
+
return ['block', ['result', 'f64'],
|
|
1705
|
+
['local.set', `$${n}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
|
|
1706
|
+
out.init,
|
|
1707
|
+
['local.set', `$${off}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
|
|
1708
|
+
['local.set', `$${cap}`, ['call', '$__cap', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
|
|
1709
|
+
['local.set', `$${ord}`, ['call', '$__coll_order', ['local.get', `$${off}`], ['local.get', `$${cap}`], ['i32.const', stride]]],
|
|
1710
|
+
['local.set', `$${i}`, ['i32.const', 0]],
|
|
1711
|
+
['block', `$cebrk${id}`, ['loop', `$celoop${id}`,
|
|
1712
|
+
['br_if', `$cebrk${id}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${n}`]]],
|
|
1713
|
+
['local.set', `$${slot}`, ['i32.load', ['i32.add', ['local.get', `$${ord}`],
|
|
1714
|
+
['i32.shl', ['local.get', `$${i}`], ['i32.const', 2]]]]],
|
|
1715
|
+
['local.set', `$${pair}`, ['call', '$__alloc_hdr', ['i32.const', 2], ['i32.const', 2]]],
|
|
1716
|
+
['f64.store', ['local.get', `$${pair}`],
|
|
1717
|
+
['f64.load', ['i32.add', ['local.get', `$${slot}`], ['i32.const', aOff]]]],
|
|
1718
|
+
['f64.store', ['i32.add', ['local.get', `$${pair}`], ['i32.const', 8]],
|
|
1719
|
+
['f64.load', ['i32.add', ['local.get', `$${slot}`], ['i32.const', bOff]]]],
|
|
1720
|
+
elemStore(out.local, i, mkPtrIR(PTR.ARRAY, 0, ['local.get', `$${pair}`])),
|
|
1721
|
+
['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
|
|
1722
|
+
['br', `$celoop${id}`]]],
|
|
1723
|
+
out.ptr]
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
// Array.prototype.keys() → dense Array of indices [0, 1, …, len-1] as numbers.
|
|
1727
|
+
function arrIdxFromTemp(t) {
|
|
1728
|
+
inc('__len')
|
|
1729
|
+
const n = tempI32('ain'), i = tempI32('aii')
|
|
1730
|
+
const out = allocPtr({ type: PTR.ARRAY, len: ['local.get', `$${n}`], tag: 'aia' })
|
|
1731
|
+
const id = ctx.func.uniq++
|
|
1732
|
+
return ['block', ['result', 'f64'],
|
|
1733
|
+
['local.set', `$${n}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
|
|
1734
|
+
out.init,
|
|
1735
|
+
['local.set', `$${i}`, ['i32.const', 0]],
|
|
1736
|
+
['block', `$aibrk${id}`, ['loop', `$ailoop${id}`,
|
|
1737
|
+
['br_if', `$aibrk${id}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${n}`]]],
|
|
1738
|
+
elemStore(out.local, i, ['f64.convert_i32_s', ['local.get', `$${i}`]]),
|
|
1739
|
+
['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
|
|
1740
|
+
['br', `$ailoop${id}`]]],
|
|
1741
|
+
out.ptr]
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
// Array.prototype.entries() → dense Array of [index, element] pair arrays.
|
|
1745
|
+
function arrEntriesFromTemp(t) {
|
|
1746
|
+
inc('__len', '__ptr_offset', '__alloc_hdr')
|
|
1747
|
+
const n = tempI32('aen'), i = tempI32('aei'), src = tempI32('aes'), pair = tempI32('aep')
|
|
1748
|
+
const out = allocPtr({ type: PTR.ARRAY, len: ['local.get', `$${n}`], tag: 'aea' })
|
|
1749
|
+
const id = ctx.func.uniq++
|
|
1750
|
+
return ['block', ['result', 'f64'],
|
|
1751
|
+
['local.set', `$${n}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
|
|
1752
|
+
out.init,
|
|
1753
|
+
['local.set', `$${src}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
|
|
1754
|
+
['local.set', `$${i}`, ['i32.const', 0]],
|
|
1755
|
+
['block', `$aebrk${id}`, ['loop', `$aeloop${id}`,
|
|
1756
|
+
['br_if', `$aebrk${id}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${n}`]]],
|
|
1757
|
+
['local.set', `$${pair}`, ['call', '$__alloc_hdr', ['i32.const', 2], ['i32.const', 2]]],
|
|
1758
|
+
['f64.store', ['local.get', `$${pair}`], ['f64.convert_i32_s', ['local.get', `$${i}`]]],
|
|
1759
|
+
['f64.store', ['i32.add', ['local.get', `$${pair}`], ['i32.const', 8]], elemLoad(src, i)],
|
|
1760
|
+
elemStore(out.local, i, mkPtrIR(PTR.ARRAY, 0, ['local.get', `$${pair}`])),
|
|
1761
|
+
['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
|
|
1762
|
+
['br', `$aeloop${id}`]]],
|
|
1763
|
+
out.ptr]
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
// TypedArray.prototype.entries() → dense Array of [index, element] pairs, reading each
|
|
1767
|
+
// element width/kind-aware via __typed_get_idx (the plain arrEntriesFromTemp uses a raw
|
|
1768
|
+
// f64.load, wrong for non-f64 typed arrays). Guarded on __typed_get_idx being registered:
|
|
1769
|
+
// it only is when the typedarray module is loaded, which is exactly when a PTR.TYPED
|
|
1770
|
+
// value can exist — so when absent, this branch is dead and falls back to the receiver.
|
|
1771
|
+
function arrEntriesFromTempTyped(t) {
|
|
1772
|
+
if (!ctx.core.stdlib['__typed_get_idx']) return ['local.get', `$${t}`]
|
|
1773
|
+
inc('__len', '__typed_get_idx', '__alloc_hdr')
|
|
1774
|
+
const n = tempI32('aetn'), i = tempI32('aeti'), pair = tempI32('aetp')
|
|
1775
|
+
const out = allocPtr({ type: PTR.ARRAY, len: ['local.get', `$${n}`], tag: 'aet' })
|
|
1776
|
+
const id = ctx.func.uniq++
|
|
1777
|
+
const P = () => ['i64.reinterpret_f64', ['local.get', `$${t}`]]
|
|
1778
|
+
return ['block', ['result', 'f64'],
|
|
1779
|
+
['local.set', `$${n}`, ['call', '$__len', P()]],
|
|
1780
|
+
out.init,
|
|
1781
|
+
['local.set', `$${i}`, ['i32.const', 0]],
|
|
1782
|
+
['block', `$aetbrk${id}`, ['loop', `$aetloop${id}`,
|
|
1783
|
+
['br_if', `$aetbrk${id}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${n}`]]],
|
|
1784
|
+
['local.set', `$${pair}`, ['call', '$__alloc_hdr', ['i32.const', 2], ['i32.const', 2]]],
|
|
1785
|
+
['f64.store', ['local.get', `$${pair}`], ['f64.convert_i32_s', ['local.get', `$${i}`]]],
|
|
1786
|
+
['f64.store', ['i32.add', ['local.get', `$${pair}`], ['i32.const', 8]],
|
|
1787
|
+
['call', '$__typed_get_idx', P(), ['local.get', `$${i}`]]],
|
|
1788
|
+
elemStore(out.local, i, mkPtrIR(PTR.ARRAY, 0, ['local.get', `$${pair}`])),
|
|
1789
|
+
['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
|
|
1790
|
+
['br', `$aetloop${id}`]]],
|
|
1791
|
+
out.ptr]
|
|
1792
|
+
}
|