jz 0.8.1 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -9
- package/bench/README.md +121 -50
- package/bench/bench.svg +23 -23
- package/cli.js +3 -1
- package/dist/interop.js +1 -1
- package/dist/jz.js +7541 -6271
- package/index.js +165 -74
- package/interop.js +189 -17
- package/jzify/arguments.js +32 -1
- package/jzify/async.js +409 -0
- package/jzify/classes.js +136 -18
- package/jzify/generators.js +639 -0
- package/jzify/hoist-vars.js +73 -1
- package/jzify/index.js +123 -4
- package/jzify/names.js +1 -0
- package/jzify/transform.js +239 -1
- package/layout.js +48 -3
- package/module/array.js +299 -44
- package/module/atomics.js +144 -0
- package/module/collection.js +1429 -155
- package/module/core.js +431 -40
- package/module/date.js +142 -120
- package/module/fs.js +144 -0
- package/module/function.js +6 -3
- package/module/index.js +4 -1
- package/module/json.js +270 -49
- package/module/math.js +892 -32
- package/module/number.js +532 -163
- package/module/object.js +353 -95
- package/module/regex.js +157 -7
- package/module/schema.js +100 -2
- package/module/string.js +301 -84
- package/module/typedarray.js +264 -72
- package/module/web.js +36 -0
- package/package.json +5 -5
- package/src/abi/string.js +71 -5
- package/src/ast.js +11 -5
- package/src/autoload.js +28 -1
- package/src/bridge.js +7 -2
- package/src/compile/analyze-scans.js +22 -7
- package/src/compile/analyze.js +136 -30
- package/src/compile/dyn-closure-tables.js +277 -0
- package/src/compile/emit-assign.js +281 -15
- package/src/compile/emit.js +979 -54
- package/src/compile/index.js +271 -29
- package/src/compile/infer.js +34 -3
- package/src/compile/inplace-store.js +329 -0
- package/src/compile/narrow.js +543 -15
- package/src/compile/peel-stencil.js +5 -0
- package/src/compile/plan/index.js +45 -3
- package/src/compile/plan/inline.js +8 -1
- package/src/compile/plan/literals.js +39 -0
- package/src/compile/plan/scope.js +430 -23
- package/src/compile/program-facts.js +732 -38
- package/src/ctx.js +64 -9
- package/src/helper-counters.js +7 -1
- package/src/ir.js +113 -5
- package/src/kind-traits.js +66 -3
- package/src/kind.js +84 -12
- package/src/op-policy.js +7 -4
- package/src/optimize/index.js +1060 -750
- package/src/optimize/recurse.js +2 -2
- package/src/optimize/vectorize.js +972 -67
- package/src/prepare/index.js +792 -63
- package/src/prepare/math-kernel.js +331 -0
- package/src/prepare/pre-eval.js +714 -0
- package/src/snapshot.js +194 -0
- package/src/type.js +1170 -56
- package/src/wat/assemble.js +403 -65
- package/src/wat/codegen.js +74 -14
- package/transform.js +113 -4
- package/wasi.js +3 -0
package/module/collection.js
CHANGED
|
@@ -8,22 +8,192 @@
|
|
|
8
8
|
* @module collection
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { typed, asF64, asI64, asI32, NULL_NAN, UNDEF_NAN, temp, tempI32, tempI64, allocPtr, undefExpr, mkPtrIR, ptrTypeEq, elemStore, elemLoad } from '../src/ir.js'
|
|
11
|
+
import { typed, asF64, asI64, asI32, NULL_NAN, UNDEF_NAN, TOMB_NAN, temp, tempI32, tempI64, allocPtr, undefExpr, mkPtrIR, ptrTypeEq, elemStore, elemLoad, carrierF64 } from '../src/ir.js'
|
|
12
12
|
import { emit, deps, call } from '../src/bridge.js'
|
|
13
13
|
import { valTypeOf } from '../src/kind.js'
|
|
14
14
|
import { VAL, lookupValType } from '../src/reps.js'
|
|
15
15
|
import { hasOwnContinue, isBlockBody, isLiteralStr } from '../src/ast.js'
|
|
16
16
|
import { ctx, inc, PTR, LAYOUT, registerGetter, declGlobal } from '../src/ctx.js'
|
|
17
|
-
import { STR_INTERN_BIT } from '../layout.js'
|
|
17
|
+
import { STR_INTERN_BIT, STR_HCACHE_BIT, ssoBitI64Hex, encodePtrHi, i64Hex } from '../layout.js'
|
|
18
|
+
import { ssoEncode } from './string.js'
|
|
19
|
+
|
|
20
|
+
const SSO_BIT_I64 = ssoBitI64Hex()
|
|
21
|
+
// NaN-box bits of the SSO string 'length' — computed once; see the STRING
|
|
22
|
+
// arm in __dyn_get_t_h and __length's property-fallback arm (module/core.js).
|
|
23
|
+
// ssoEncode('length') never returns null (6 ASCII).
|
|
24
|
+
export const LENGTH_SSO_I64 = (() => { const e = ssoEncode('length'); return i64Hex((BigInt(encodePtrHi(4, e.aux) >>> 0) << 32n) | BigInt(e.offset)) })()
|
|
18
25
|
|
|
19
26
|
const SET_ENTRY = 16 // hash + key
|
|
20
27
|
const MAP_ENTRY = 24 // hash + key + value
|
|
21
28
|
const INIT_CAP = 8 // initial capacity (must be power of 2)
|
|
22
29
|
|
|
23
|
-
|
|
30
|
+
// __dyn_props global-table membership filter (see __dyn_props_filter's declGlobal
|
|
31
|
+
// comment). offExpr is an i32 WAT expr for the offset key. Mix folds the offset's
|
|
32
|
+
// mid bits (allocations are 8-byte aligned, so raw low bits are useless) down to
|
|
33
|
+
// a 6-bit bucket in a 64-bit bitset — never-false-negative, no false-negative risk
|
|
34
|
+
// from collisions (a collision just makes the filter's "maybe present" wider).
|
|
35
|
+
const dynPropsFilterBitIR = (offExpr) =>
|
|
36
|
+
`(i64.shl (i64.const 1) (i64.extend_i32_u (i32.and (i32.xor (i32.shr_u ${offExpr} (i32.const 3)) (i32.shr_u ${offExpr} (i32.const 9))) (i32.const 63))))`
|
|
37
|
+
// Set the filter bit for an offset key that was just inserted into the global table.
|
|
38
|
+
export const dynPropsFilterSetIR = (offExpr) =>
|
|
39
|
+
`(global.set $__dyn_props_filter (i64.or (global.get $__dyn_props_filter) ${dynPropsFilterBitIR(offExpr)}))`
|
|
40
|
+
// True (i32) when the filter bit is clear — i.e. offExpr is PROVEN never inserted,
|
|
41
|
+
// safe to skip the __ihash_get_local probe entirely. False (bit set) means "maybe
|
|
42
|
+
// present, maybe a collision" — falls through to the real probe.
|
|
43
|
+
export const dynPropsFilterMissIR = (offExpr) =>
|
|
44
|
+
`(i64.eqz (i64.and (global.get $__dyn_props_filter) ${dynPropsFilterBitIR(offExpr)}))`
|
|
45
|
+
|
|
46
|
+
// The post-init high-water mark (see module/core.js's __heap_reset) as a WAT operand —
|
|
47
|
+
// everything at/above it is EPHEMERAL (this compile's own arena, wiped by `_clear`);
|
|
48
|
+
// everything below it is DURABLE (module-init state, survives `_clear` forever). Falls
|
|
49
|
+
// back to a literal 0 (every offset reads as "ephemeral") when the module has no
|
|
50
|
+
// `__heap_reset` global at all (no allocator, or shared memory, whose reset is a plain
|
|
51
|
+
// HEAP.START rewind with no high-water-mark concept — see core.js). Read at
|
|
52
|
+
// template-EXPANSION time (thunked callers only — see durableFwdLogIR/heapResetWat
|
|
53
|
+
// consumers), so it observes the FINAL declaration state, not whatever was true when
|
|
54
|
+
// collection.js's own module body first ran.
|
|
55
|
+
//
|
|
56
|
+
// DURABLE-RECEIVER POLICY (dyn-props twin of the above): a receiver allocated
|
|
57
|
+
// at/below __heap_reset outlives `_clear()`, but a sidecar installed for it at
|
|
58
|
+
// RUNTIME lives in the round's arena — the surviving header slot then dangles
|
|
59
|
+
// across `_clear()` and the next round corrupts reused memory. So runtime
|
|
60
|
+
// dyn-prop writes on a durable receiver (off < __heap_reset) route to the
|
|
61
|
+
// GLOBAL __dyn_props table instead — __clear resets it, so prop lifetime
|
|
62
|
+
// matches storage lifetime. Init-time writes still land in durable sidecars
|
|
63
|
+
// (__heap_reset is seeded to data-end until __start's tail captures the
|
|
64
|
+
// post-init top, so off >= __heap_reset holds throughout init). Every read/
|
|
65
|
+
// write/delete/enumerate site that consults a header sidecar gates on this —
|
|
66
|
+
// see module/object.js's emitEnumerateObject and module/json.js's __json_obj
|
|
67
|
+
// for the array-IR / WAT-string twins that merge in the global table for
|
|
68
|
+
// durable receivers.
|
|
69
|
+
export const heapResetWat = () => ctx.scope.globals.has('__heap_reset') ? '(global.get $__heap_reset)' : '(i32.const 0)'
|
|
70
|
+
|
|
71
|
+
// A growable ARRAY/HASH/SET/MAP relocates by leaving a forwarding header behind
|
|
72
|
+
// (cap=-1 sentinel at off-4, new offset at off-8 — see layout.js's followForwardingWat).
|
|
73
|
+
// That is only safe WITHIN one compile round: `_clear()` rewinds the arena but never
|
|
74
|
+
// zeroes memory, so a forward written into a DURABLE header (offOld < __heap_reset,
|
|
75
|
+
// i.e. the block predates this round) permanently points at an EPHEMERAL target that
|
|
76
|
+
// the next round's allocations silently overwrite — any later chase through the
|
|
77
|
+
// durable alias then lands on garbage or goes OOB (.work/todo.md groundtruth archive,
|
|
78
|
+
// "array-growth forwarding is not _clear-safe"). Growing an EPHEMERAL block needs no
|
|
79
|
+
// protection: everything reachable from it is ephemeral too, so the whole chain (old
|
|
80
|
+
// header, new header, and every durable-side reference to it — there are none, by
|
|
81
|
+
// construction) is reclaimed together at `_clear()`.
|
|
82
|
+
//
|
|
83
|
+
// Fix: at the grow/shift site, BEFORE writing the forward (while off/len/cap — the
|
|
84
|
+
// header's pre-relocation state — are still live locals), log the durable→ephemeral
|
|
85
|
+
// transition to a small resettable side-table (module/core.js's __durable_fwd_log/
|
|
86
|
+
// __durable_fwd_heal) instead of (or rather: in addition to, so the in-round chase
|
|
87
|
+
// still works) trusting the header alone. `_clear()` then HEALS each logged header
|
|
88
|
+
// back to its exact pre-relocation (len, cap) — undoing the forward mark, so the
|
|
89
|
+
// durable block reverts to self-contained, non-forwarding, and correct (its own
|
|
90
|
+
// element/entry cells were never touched by the relocation; only the header words
|
|
91
|
+
// were). This keeps followForwardingWat/__ptr_offset_fwd (the hot chase, ~25% of
|
|
92
|
+
// self-host compile ticks) completely UNTOUCHED — the check only runs on the already-
|
|
93
|
+
// cold relocation path, and the heal sweep only runs inside `_clear()`, bounded by
|
|
94
|
+
// however many durable relocations happened that round (0 in the overwhelmingly
|
|
95
|
+
// common case).
|
|
96
|
+
//
|
|
97
|
+
// Checks BOTH ends, not just "is off durable": a fresh `__alloc_hdr`/`__alloc_hdr_n`
|
|
98
|
+
// target (grow, genUpsert/genUpsertGrow) is unconditionally ephemeral whenever the
|
|
99
|
+
// source-durable check can even fire (any allocation live past `__start`'s tail-
|
|
100
|
+
// capture is by construction >= the now-final `__heap_reset`), so newOff's own check
|
|
101
|
+
// is redundant there — but `.shift()`'s "new" header is just `off + 8`, a position
|
|
102
|
+
// INSIDE the same block, not a fresh allocation: ordinarily still durable (shifting a
|
|
103
|
+
// durable array is legitimate, persistent state and must NOT be undone at `_clear`),
|
|
104
|
+
// and only crosses into ephemeral in the one-in-8-bytes edge case where `off` sits
|
|
105
|
+
// exactly at `__heap_reset - 8`. Requiring both conditions everywhere makes the
|
|
106
|
+
// invariant self-evidently correct at every call site instead of relying on a
|
|
107
|
+
// per-caller argument about what its "new" offset can be.
|
|
108
|
+
// Emits nothing at all (not even a call site) when there's no `__heap_reset` to compare
|
|
109
|
+
// against — shared memory's `__clear` is a plain rewind-to-HEAP.START with no high-water
|
|
110
|
+
// mark (core.js), so EVERYTHING resets uniformly there and no state is ever "durable" to
|
|
111
|
+
// begin with (a separate, pre-existing, documented gap — see core.js's shared-memory
|
|
112
|
+
// `__clear` comment). Testing `ctx.scope.globals.has('__heap_reset')` directly (not just
|
|
113
|
+
// deferring to heapResetWat()'s own `(i32.const 0)` fallback, which would still emit an
|
|
114
|
+
// always-false-but-present call) matters for self-host inclusion: array.js's/
|
|
115
|
+
// collection.js's deps() edges declare '__durable_fwd_log' unconditionally at every grow/
|
|
116
|
+
// shift site, so core.js must ALSO unconditionally register the function whenever those
|
|
117
|
+
// sites exist — but core.js only defines __durable_fwd_log/__durable_fwd_heal in the
|
|
118
|
+
// owned-memory branch (they need __heap/__heap_reset, which shared memory doesn't have).
|
|
119
|
+
// A shared-memory build reaching this function with the fallback would therefore
|
|
120
|
+
// reference a never-registered stdlib name, tripping assemble.js's `internal: stdlib
|
|
121
|
+
// '__durable_fwd_log' was requested but never registered` sanity check.
|
|
122
|
+
export const durableFwdLogIR = (off, newOff, len, cap) => {
|
|
123
|
+
if (!ctx.scope.globals.has('__heap_reset')) return ''
|
|
124
|
+
return `
|
|
125
|
+
(if (i32.and (i32.lt_u (local.get $${off}) ${heapResetWat()}) (i32.ge_u (local.get $${newOff}) ${heapResetWat()}))
|
|
126
|
+
(then (call $__durable_fwd_log (local.get $${off}) (local.get $${len}) (local.get $${cap}))))`
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Value-write sibling of durableFwdLogIR: an EPHEMERAL boxed value stored into a
|
|
130
|
+
// DURABLE collection slot dangles across `_clear` (the corpus-wide warm trap — a
|
|
131
|
+
// durable memo dict handing round-1 node arrays into round-2's tree). Log the slot
|
|
132
|
+
// so `__durable_slot_heal` (wired into `__clear`) overwrites it with `undefined` —
|
|
133
|
+
// the pointed-at data dies with the arena, so entry-death is the only sound
|
|
134
|
+
// semantics. `slotLocal`+`byteOff` name the value slot; `valLocal` holds the boxed
|
|
135
|
+
// bits (i64). Same shared-memory gate as durableFwdLogIR (no watermark, no sweep).
|
|
136
|
+
export const durableSlotLogIR = (slotLocal, byteOff, valLocal) => {
|
|
137
|
+
if (!ctx.scope.globals.has('__heap_reset')) return ''
|
|
138
|
+
const addr = byteOff ? `(i32.add (local.get $${slotLocal}) (i32.const ${byteOff}))` : `(local.get $${slotLocal})`
|
|
139
|
+
return `
|
|
140
|
+
(if (i32.and (i32.lt_u ${addr} ${heapResetWat()}) (call $__is_eph_bits (local.get $${valLocal})))
|
|
141
|
+
(then (call $__durable_slot_log ${addr} (i32.const 0))))`
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ENTRY-insert variant: a NEW entry inserted into DURABLE table storage is
|
|
145
|
+
// round state regardless of what the key/value are — a fresh instance would not
|
|
146
|
+
// have the entry at all, and an ephemeral KEY can't even be value-healed (probes
|
|
147
|
+
// and enumeration would hash/compare the dangling box; measured: warm round 2
|
|
148
|
+
// hashed 15.5 MB of garbage-length "strings" where round 1 hashed 415 KB — the
|
|
149
|
+
// whole 2× warm-vs-fresh gap). Log the ENTRY base with bit0 set plus the table
|
|
150
|
+
// storage base; the heal turns the entry into a zombie — key ← TOMB_NAN
|
|
151
|
+
// (unforgeable, deref-free in every eq family), value ← undefined, table len
|
|
152
|
+
// decremented — that probes pass over and __coll_order/len-sized iterations
|
|
153
|
+
// skip. The slot stays occupied until the table grows (zombies never resurrect:
|
|
154
|
+
// nothing eq-matches TOMB_NAN). Entry addresses are 8-aligned → bit0 is free.
|
|
155
|
+
export const durableEntryLogIR = (slotLocal, offLocal) => {
|
|
156
|
+
if (!ctx.scope.globals.has('__heap_reset')) return ''
|
|
157
|
+
return `
|
|
158
|
+
(if (i32.lt_u (local.get $${slotLocal}) ${heapResetWat()})
|
|
159
|
+
(then (call $__durable_slot_log (i32.or (local.get $${slotLocal}) (i32.const 1)) (local.get $${offLocal}))))`
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Clamp to the >=2 convention (0=empty slot, 1=tombstone) — shared by every hash
|
|
163
|
+
// producer (SSO mix, byte-FNV, __jp_str, buildInternTable) so they all clamp identically.
|
|
164
|
+
const clampHash = (h) => (h <= 1 ? (h + 2) | 0 : h)
|
|
165
|
+
|
|
166
|
+
// SSO mix: 7 ops over the packed NaN-box lo/hi (see __str_hash's SSO branch,
|
|
167
|
+
// module/collection.js below, for the WAT twin — both MUST compute the same value).
|
|
168
|
+
// lo = offset (payload bits 0-31), hi = aux masked to bits 0-12 (length + char 4-5
|
|
169
|
+
// tail — SSO_BIT itself is excluded by the mask, so hi only carries discriminating
|
|
170
|
+
// content). Replaces the old 6-iteration per-char FNV loop with a fixed-cost mix.
|
|
171
|
+
const ssoMix = (lo, hi) => {
|
|
172
|
+
let h = Math.imul(hi ^ 0x9E3779B9, 0x85EBCA6B)
|
|
173
|
+
h = Math.imul(lo ^ h, 0xC2B2AE35)
|
|
174
|
+
h = (h ^ (h >>> 15)) | 0
|
|
175
|
+
return clampHash(h) >>> 0
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Byte-FNV-1a over UTF-8-ish bytes (charCodeAt & 0xFF — ASCII-only callers guarantee
|
|
179
|
+
// codepoint < 0x80, so this equals the byte value). Heap strings (>6 bytes or non-ASCII)
|
|
180
|
+
// keep this; __str_hash's heap branch and buildInternTable's static-intern prehash both
|
|
181
|
+
// compute the identical function — see module/string.js bind('str') and internProbeWat.
|
|
182
|
+
const byteFnv = (str) => {
|
|
24
183
|
let h = 0x811c9dc5 | 0
|
|
25
184
|
for (let i = 0; i < str.length; i++) h = Math.imul(h ^ (str.charCodeAt(i) & 0xFF), 0x01000193) | 0
|
|
26
|
-
return
|
|
185
|
+
return clampHash(h)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Compile-time hash for an ASCII string LITERAL — must equal __str_hash's runtime
|
|
189
|
+
// result for the same content: ≤6-ASCII strings are ALWAYS SSO (module/string.js header
|
|
190
|
+
// invariant), so they use the new ssoMix; longer/non-ASCII strings stay on heap and use
|
|
191
|
+
// byte-FNV. Callers (litKeyHash below, module/core.js, module/array.js, module/json.js)
|
|
192
|
+
// pass ASCII content — non-ASCII goes through the runtime __str_hash path instead.
|
|
193
|
+
export function strHashLiteral(str) {
|
|
194
|
+
const sso = ssoEncode(str)
|
|
195
|
+
if (sso) return ssoMix(sso.offset | 0, sso.aux & 0x1FFF)
|
|
196
|
+
return byteFnv(str)
|
|
27
197
|
}
|
|
28
198
|
|
|
29
199
|
const HASH_BUF = new ArrayBuffer(8)
|
|
@@ -45,10 +215,12 @@ function numConstLiteral(expr) {
|
|
|
45
215
|
|
|
46
216
|
// Compile-time probe hash for a LITERAL collection/property key, else null. A numeric
|
|
47
217
|
// constant → numHashLiteral; an ASCII string literal → strHashLiteral. The string case is
|
|
48
|
-
// ASCII-only on purpose: strHashLiteral folds `charCodeAt(i) & 0xFF`,
|
|
49
|
-
// __map_hash (FNV-1a over the UTF-8 bytes) ONLY for code points
|
|
50
|
-
// would fold a different hash than its stored key and silently
|
|
51
|
-
// runtime hash.
|
|
218
|
+
// ASCII-only on purpose: strHashLiteral's byte-FNV branch folds `charCodeAt(i) & 0xFF`,
|
|
219
|
+
// which equals __str_hash / __map_hash (FNV-1a over the UTF-8 bytes) ONLY for code points
|
|
220
|
+
// < 0x80 — a non-ASCII literal would fold a different hash than its stored key and silently
|
|
221
|
+
// miss, so it falls back to the runtime hash. (The ≤6-ASCII branch uses the SSO mix instead —
|
|
222
|
+
// still ASCII-only, since ssoEncode itself rejects non-ASCII and returns null.) Lets
|
|
223
|
+
// `m.get("if")` / `m.has("x")` skip the per-access __map_hash call.
|
|
52
224
|
const ASCII_KEY = /^[\x00-\x7f]*$/
|
|
53
225
|
const litKeyHash = (key) => {
|
|
54
226
|
const num = numConstLiteral(key)
|
|
@@ -121,9 +293,10 @@ const seqStore = `(i64.store (local.get $slot)
|
|
|
121
293
|
* owner's propsPtr slot that genUpsertGrow can rewrite). */
|
|
122
294
|
function genUpsert(name, entrySize, hashFn, eqExpr, expectedType, hasVal, hasExt) {
|
|
123
295
|
const valParam = hasVal ? '(param $val i64) ' : ''
|
|
124
|
-
const
|
|
296
|
+
const slotLog = hasVal ? durableSlotLogIR('slot', 16, 'val') : ''
|
|
297
|
+
const storeVal = hasVal ? `\n (i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))${slotLog}` : ''
|
|
125
298
|
const onMatch = hasVal
|
|
126
|
-
? `(then\n (i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))\n (br $done))`
|
|
299
|
+
? `(then\n (i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))${slotLog}\n (br $done))`
|
|
127
300
|
: `(then (br $done))`
|
|
128
301
|
const rehashVal = hasVal
|
|
129
302
|
? `\n (i64.store (i32.add (local.get $newslot) (i32.const 16)) (i64.load (i32.add (local.get $oldslot) (i32.const 16))))`
|
|
@@ -139,10 +312,16 @@ function genUpsert(name, entrySize, hashFn, eqExpr, expectedType, hasVal, hasExt
|
|
|
139
312
|
return `(func $${name} (param $coll i64) (param $key i64) ${valParam}(result i64)
|
|
140
313
|
(local $off i32) (local $cap i32) (local $h i32) (local $end i32) (local $slot i32)
|
|
141
314
|
(local $size i32) (local $newptr i32) (local $newcap i32) (local $i i32)
|
|
142
|
-
(local $oldslot i32) (local $newidx i32) (local $newslot i32)
|
|
315
|
+
(local $oldslot i32) (local $newidx i32) (local $newslot i32) (local $zb i32) (local $ztr i32)
|
|
143
316
|
${typeGuard}
|
|
144
|
-
(local.set $off (
|
|
317
|
+
(local.set $off (i32.wrap_i64 (i64.and (local.get $coll) (i64.const ${LAYOUT.OFFSET_MASK}))))
|
|
145
318
|
(local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
|
|
319
|
+
;; the cap load IS the forward check: -1 sentinel hops via the cold helper,
|
|
320
|
+
;; the live path pays zero extra — the per-probe __ptr_offset call drops
|
|
321
|
+
(if (i32.eq (local.get $cap) (i32.const -1))
|
|
322
|
+
(then
|
|
323
|
+
(local.set $off (call $__ptr_offset_fwd (local.get $off)))
|
|
324
|
+
(local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))))
|
|
146
325
|
(local.set $size (i32.load (i32.sub (local.get $off) (i32.const 8))))
|
|
147
326
|
;; Grow at 75% load (size*4 >= cap*3): 2× table, rehash, forward-mark old header.
|
|
148
327
|
(if (i32.ge_s (i32.mul (local.get $size) (i32.const 4)) (i32.mul (local.get $cap) (i32.const 3)))
|
|
@@ -169,22 +348,38 @@ function genUpsert(name, entrySize, hashFn, eqExpr, expectedType, hasVal, hasExt
|
|
|
169
348
|
(i32.add (i32.load (i32.sub (local.get $newptr) (i32.const 8))) (i32.const 1)))))
|
|
170
349
|
(local.set $i (i32.add (local.get $i) (i32.const 1)))
|
|
171
350
|
(br $rl)))
|
|
351
|
+
${durableFwdLogIR('off', 'newptr', 'size', 'cap')}
|
|
172
352
|
(i32.store (i32.sub (local.get $off) (i32.const 8)) (local.get $newptr))
|
|
173
353
|
(i32.store (i32.sub (local.get $off) (i32.const 4)) (i32.const -1))
|
|
174
354
|
(local.set $off (local.get $newptr))
|
|
175
355
|
(local.set $cap (local.get $newcap))))
|
|
176
356
|
(local.set $h (call ${hashFn} (local.get $key)))
|
|
177
357
|
${probeStart(entrySize)}
|
|
358
|
+
;; zombie-aware probe (durable-slot heal, TOMB_NAN keys): remember the first
|
|
359
|
+
;; zombie, keep probing to empty/match, insert into the zombie when the chain
|
|
360
|
+
;; ends (or when cap tries exhaust — physically-full-of-zombies table).
|
|
178
361
|
(block $done (loop $probe
|
|
179
362
|
(if (i64.eqz (i64.load (local.get $slot)))
|
|
180
363
|
(then
|
|
364
|
+
(if (local.get $zb) (then (local.set $slot (local.get $zb))))
|
|
181
365
|
${seqStore}
|
|
182
|
-
(i64.store (i32.add (local.get $slot) (i32.const 8)) (local.get $key))${storeVal}
|
|
366
|
+
(i64.store (i32.add (local.get $slot) (i32.const 8)) (local.get $key))${durableEntryLogIR('slot', 'off')}${storeVal}
|
|
183
367
|
(i32.store (i32.sub (local.get $off) (i32.const 8))
|
|
184
368
|
(i32.add (i32.load (i32.sub (local.get $off) (i32.const 8))) (i32.const 1)))
|
|
185
369
|
(br $done)))
|
|
186
|
-
(if $
|
|
370
|
+
(if (i64.eq (i64.load (i32.add (local.get $slot) (i32.const 8))) (i64.const ${TOMB_NAN}))
|
|
371
|
+
(then (if (i32.eqz (local.get $zb)) (then (local.set $zb (local.get $slot)))))
|
|
372
|
+
(else (if ${eqExpr} ${onMatch})))
|
|
187
373
|
${probeNext(entrySize)}
|
|
374
|
+
(local.set $ztr (i32.add (local.get $ztr) (i32.const 1)))
|
|
375
|
+
(if (i32.ge_s (local.get $ztr) (local.get $cap))
|
|
376
|
+
(then
|
|
377
|
+
(local.set $slot (local.get $zb))
|
|
378
|
+
${seqStore}
|
|
379
|
+
(i64.store (i32.add (local.get $slot) (i32.const 8)) (local.get $key))${durableEntryLogIR('slot', 'off')}${storeVal}
|
|
380
|
+
(i32.store (i32.sub (local.get $off) (i32.const 8))
|
|
381
|
+
(i32.add (i32.load (i32.sub (local.get $off) (i32.const 8))) (i32.const 1)))
|
|
382
|
+
(br $done)))
|
|
188
383
|
(br $probe)))
|
|
189
384
|
(local.get $coll))`
|
|
190
385
|
}
|
|
@@ -240,12 +435,30 @@ function genLookup(name, entrySize, hashFn, eqExpr, expectedType, wantValue, has
|
|
|
240
435
|
* the key was present (and len decremented), 0 otherwise. Home slots are recomputed
|
|
241
436
|
* from the stored hash (low 32 bits), so no rehash of the key is needed during the shift. */
|
|
242
437
|
function genDelete(name, entrySize, hashFn, eqExpr, expectedType) {
|
|
438
|
+
// for-in enum cache invalidation (core.js __hash_keys_ro / object.js
|
|
439
|
+
// emitEnumerateObject): delete is the one key-set change the cache's
|
|
440
|
+
// (off, len) key can miss — a later insert restores the cached len with a
|
|
441
|
+
// different key set. Unconditional clear (not off-compare): the OBJECT-arm
|
|
442
|
+
// cache is keyed by SIDECAR off, but a durable receiver's runtime props live
|
|
443
|
+
// in per-object hashes under __dyn_props whose offs the cache never sees —
|
|
444
|
+
// a delete there must still invalidate. HASH deletes are cold; SET/MAP
|
|
445
|
+
// tables never feed enumeration, so only the HASH instance pays.
|
|
446
|
+
const enumcInval = expectedType === PTR.HASH
|
|
447
|
+
? `(global.set $__enumc_off (i32.const 0))
|
|
448
|
+
`
|
|
449
|
+
: ''
|
|
243
450
|
return `(func $${name} (param $coll i64) (param $key i64) (result i32)
|
|
244
451
|
(local $off i32) (local $cap i32) (local $h i32) (local $end i32) (local $slot i32) (local $tries i32)
|
|
245
452
|
(local $i i32) (local $j i32) (local $k i32) (local $n i32)
|
|
246
453
|
(if (i32.ne (call $__ptr_type (local.get $coll)) (i32.const ${expectedType})) (then (return (i32.const 0))))
|
|
247
|
-
(local.set $off (
|
|
454
|
+
(local.set $off (i32.wrap_i64 (i64.and (local.get $coll) (i64.const ${LAYOUT.OFFSET_MASK}))))
|
|
248
455
|
(local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
|
|
456
|
+
;; the cap load IS the forward check: -1 sentinel hops via the cold helper,
|
|
457
|
+
;; the live path pays zero extra — the per-probe __ptr_offset call drops
|
|
458
|
+
(if (i32.eq (local.get $cap) (i32.const -1))
|
|
459
|
+
(then
|
|
460
|
+
(local.set $off (call $__ptr_offset_fwd (local.get $off)))
|
|
461
|
+
(local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))))
|
|
249
462
|
(local.set $h (call ${hashFn} (local.get $key)))
|
|
250
463
|
${probeStart(entrySize)}
|
|
251
464
|
(block $found
|
|
@@ -280,7 +493,7 @@ function genDelete(name, entrySize, hashFn, eqExpr, expectedType) {
|
|
|
280
493
|
(br $shift)))
|
|
281
494
|
(i64.store (local.get $i) (i64.const 0))
|
|
282
495
|
(i64.store (i32.add (local.get $i) (i32.const 8)) (i64.const 0))
|
|
283
|
-
(i32.store (i32.sub (local.get $off) (i32.const 8))
|
|
496
|
+
${enumcInval}(i32.store (i32.sub (local.get $off) (i32.const 8))
|
|
284
497
|
(i32.sub (i32.load (i32.sub (local.get $off) (i32.const 8))) (i32.const 1)))
|
|
285
498
|
(i32.const 1))`
|
|
286
499
|
}
|
|
@@ -306,10 +519,16 @@ function genUpsertGrow(name, entrySize, hashFn, eqExpr, typeConst, strict = fals
|
|
|
306
519
|
return `(func $${name} (param $obj i64) (param $key i64) (param $val i64) (result i64)
|
|
307
520
|
(local $off i32) (local $cap i32) (local $h i32) (local $end i32) (local $slot i32)
|
|
308
521
|
(local $size i32) (local $newptr i32) (local $newcap i32) (local $i i32)
|
|
309
|
-
(local $oldslot i32) (local $newidx i32) (local $newslot i32)
|
|
522
|
+
(local $oldslot i32) (local $newidx i32) (local $newslot i32) (local $zb i32) (local $ztr i32)
|
|
310
523
|
${typeGuard}
|
|
311
|
-
(local.set $off (
|
|
524
|
+
(local.set $off (i32.wrap_i64 (i64.and (local.get $obj) (i64.const ${LAYOUT.OFFSET_MASK}))))
|
|
312
525
|
(local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
|
|
526
|
+
;; the cap load IS the forward check: -1 sentinel hops via the cold helper,
|
|
527
|
+
;; the live path pays zero extra — the per-probe __ptr_offset call drops
|
|
528
|
+
(if (i32.eq (local.get $cap) (i32.const -1))
|
|
529
|
+
(then
|
|
530
|
+
(local.set $off (call $__ptr_offset_fwd (local.get $off)))
|
|
531
|
+
(local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))))
|
|
313
532
|
(local.set $size (i32.load (i32.sub (local.get $off) (i32.const 8))))
|
|
314
533
|
;; Grow if load factor > 75%: size * 4 >= cap * 3
|
|
315
534
|
(if (i32.ge_s (i32.mul (local.get $size) (i32.const 4)) (i32.mul (local.get $cap) (i32.const 3)))
|
|
@@ -340,8 +559,12 @@ function genUpsertGrow(name, entrySize, hashFn, eqExpr, typeConst, strict = fals
|
|
|
340
559
|
// Forward-mark the old header (cap=-1 sentinel at -4, new offset at -8) and
|
|
341
560
|
// keep the boxed pointer the caller holds: any alias resolves through
|
|
342
561
|
// __ptr_offset. This preserves JS reference identity for a grown dict held in
|
|
343
|
-
// multiple places (e.g. ctx.core.emit), which remint cannot.
|
|
344
|
-
|
|
562
|
+
// multiple places (e.g. ctx.core.emit), which remint cannot. Log the pre-grow
|
|
563
|
+
// (off, size, cap) first (durableFwdLogIR — no-op unless $off predates this
|
|
564
|
+
// round) so `_clear` can heal a durable header instead of leaving it forwarded
|
|
565
|
+
// at an ephemeral target that the next round overwrites.
|
|
566
|
+
? `${durableFwdLogIR('off', 'newptr', 'size', 'cap')}
|
|
567
|
+
(i32.store (i32.sub (local.get $off) (i32.const 8)) (local.get $newptr))
|
|
345
568
|
(i32.store (i32.sub (local.get $off) (i32.const 4)) (i32.const -1))
|
|
346
569
|
(local.set $off (local.get $newptr))
|
|
347
570
|
(local.set $cap (local.get $newcap))`
|
|
@@ -353,24 +576,151 @@ function genUpsertGrow(name, entrySize, hashFn, eqExpr, typeConst, strict = fals
|
|
|
353
576
|
;; Insert/update
|
|
354
577
|
(local.set $h (call ${hashFn} (local.get $key)))
|
|
355
578
|
${probeStart(entrySize)}
|
|
579
|
+
;; zombie-aware probe (durable-slot heal, TOMB_NAN keys) — see genUpsert.
|
|
356
580
|
(block $done (loop $probe
|
|
357
581
|
(if (i64.eqz (i64.load (local.get $slot)))
|
|
358
582
|
(then
|
|
583
|
+
(if (local.get $zb) (then (local.set $slot (local.get $zb))))
|
|
359
584
|
${seqStore}
|
|
360
|
-
(i64.store (i32.add (local.get $slot) (i32.const 8)) (local.get $key))
|
|
361
|
-
(i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))
|
|
585
|
+
(i64.store (i32.add (local.get $slot) (i32.const 8)) (local.get $key))${durableEntryLogIR('slot', 'off')}
|
|
586
|
+
(i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))${durableSlotLogIR('slot', 16, 'val')}
|
|
362
587
|
(i32.store (i32.sub (local.get $off) (i32.const 8))
|
|
363
588
|
(i32.add (i32.load (i32.sub (local.get $off) (i32.const 8))) (i32.const 1)))
|
|
364
589
|
(br $done)))
|
|
365
|
-
(if ${
|
|
590
|
+
(if (i64.eq (i64.load (i32.add (local.get $slot) (i32.const 8))) (i64.const ${TOMB_NAN}))
|
|
591
|
+
(then (if (i32.eqz (local.get $zb)) (then (local.set $zb (local.get $slot)))))
|
|
592
|
+
(else (if ${eqExpr}
|
|
593
|
+
(then
|
|
594
|
+
(i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))${durableSlotLogIR('slot', 16, 'val')}
|
|
595
|
+
(br $done)))))
|
|
596
|
+
${probeNext(entrySize)}
|
|
597
|
+
(local.set $ztr (i32.add (local.get $ztr) (i32.const 1)))
|
|
598
|
+
(if (i32.ge_s (local.get $ztr) (local.get $cap))
|
|
366
599
|
(then
|
|
367
|
-
(
|
|
600
|
+
(local.set $slot (local.get $zb))
|
|
601
|
+
${seqStore}
|
|
602
|
+
(i64.store (i32.add (local.get $slot) (i32.const 8)) (local.get $key))${durableEntryLogIR('slot', 'off')}
|
|
603
|
+
(i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))${durableSlotLogIR('slot', 16, 'val')}
|
|
604
|
+
(i32.store (i32.sub (local.get $off) (i32.const 8))
|
|
605
|
+
(i32.add (i32.load (i32.sub (local.get $off) (i32.const 8))) (i32.const 1)))
|
|
368
606
|
(br $done)))
|
|
369
|
-
${probeNext(entrySize)}
|
|
370
607
|
(br $probe)))
|
|
371
608
|
(local.get $obj))`
|
|
372
609
|
}
|
|
373
610
|
|
|
611
|
+
/** RMW slot upsert — genUpsertGrow's exact machinery (grow + forward-mark +
|
|
612
|
+
* zombie-aware probe) returning the entry's VALUE SLOT ADDRESS instead of
|
|
613
|
+
* storing a value: `o[k] = f(o[k])` fusion (emit-assign.js) hashes and probes
|
|
614
|
+
* ONCE for the read-modify-write instead of a full get + set pair. On insert
|
|
615
|
+
* the value seeds `undefined` (what a plain read of a missing key yields) and
|
|
616
|
+
* the entry-log runs, so the caller's later __slot_write is an ordinary value
|
|
617
|
+
* update. Sound across growth because the caller's BOX never changes: the old
|
|
618
|
+
* header forward-marks and the returned address points into the new table.
|
|
619
|
+
* Returns 0 unless the receiver is a live HASH — caller falls back to the
|
|
620
|
+
* generic dyn read/write pair. */
|
|
621
|
+
function genSlotUpsert(name, entrySize, hashFn, eqExpr) {
|
|
622
|
+
return `(func $${name} (param $obj i64) (param $key i64) (result i32)
|
|
623
|
+
(local $off i32) (local $cap i32) (local $h i32) (local $end i32) (local $slot i32)
|
|
624
|
+
(local $size i32) (local $newptr i32) (local $newcap i32) (local $i i32)
|
|
625
|
+
(local $oldslot i32) (local $newidx i32) (local $newslot i32) (local $zb i32) (local $ztr i32)
|
|
626
|
+
(local $kaux i32) (local $koff i32)
|
|
627
|
+
(if (i32.ne (call $__ptr_type (local.get $obj)) (i32.const ${PTR.HASH}))
|
|
628
|
+
(then (return (i32.const 0))))
|
|
629
|
+
(local.set $off (i32.wrap_i64 (i64.and (local.get $obj) (i64.const ${LAYOUT.OFFSET_MASK}))))
|
|
630
|
+
(local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
|
|
631
|
+
;; the cap load IS the forward check: -1 sentinel hops via the cold helper,
|
|
632
|
+
;; the live path pays zero extra — the per-probe __ptr_offset call drops
|
|
633
|
+
(if (i32.eq (local.get $cap) (i32.const -1))
|
|
634
|
+
(then
|
|
635
|
+
(local.set $off (call $__ptr_offset_fwd (local.get $off)))
|
|
636
|
+
(local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))))
|
|
637
|
+
(local.set $size (i32.load (i32.sub (local.get $off) (i32.const 8))))
|
|
638
|
+
(if (i32.ge_s (i32.mul (local.get $size) (i32.const 4)) (i32.mul (local.get $cap) (i32.const 3)))
|
|
639
|
+
(then
|
|
640
|
+
(local.set $newcap (i32.shl (local.get $cap) (i32.const 1)))
|
|
641
|
+
(local.set $newptr (call $__alloc_hdr_n (i32.const 0) (local.get $newcap) (i32.const ${entrySize})))
|
|
642
|
+
(local.set $i (i32.const 0))
|
|
643
|
+
(block $rd (loop $rl
|
|
644
|
+
(br_if $rd (i32.ge_s (local.get $i) (local.get $cap)))
|
|
645
|
+
(local.set $oldslot (i32.add (local.get $off) (i32.mul (local.get $i) (i32.const ${entrySize}))))
|
|
646
|
+
(if (i64.ne (i64.load (local.get $oldslot)) (i64.const 0))
|
|
647
|
+
(then
|
|
648
|
+
(local.set $h (call ${hashFn} (i64.load (i32.add (local.get $oldslot) (i32.const 8)))))
|
|
649
|
+
(local.set $newidx (i32.and (local.get $h) (i32.sub (local.get $newcap) (i32.const 1))))
|
|
650
|
+
(block $ins (loop $probe2
|
|
651
|
+
(local.set $newslot (i32.add (local.get $newptr) (i32.mul (local.get $newidx) (i32.const ${entrySize}))))
|
|
652
|
+
(br_if $ins (i64.eqz (i64.load (local.get $newslot))))
|
|
653
|
+
(local.set $newidx (i32.and (i32.add (local.get $newidx) (i32.const 1)) (i32.sub (local.get $newcap) (i32.const 1))))
|
|
654
|
+
(br $probe2)))
|
|
655
|
+
(i64.store (local.get $newslot) (i64.load (local.get $oldslot)))
|
|
656
|
+
(i64.store (i32.add (local.get $newslot) (i32.const 8)) (i64.load (i32.add (local.get $oldslot) (i32.const 8))))
|
|
657
|
+
(i64.store (i32.add (local.get $newslot) (i32.const 16)) (i64.load (i32.add (local.get $oldslot) (i32.const 16))))
|
|
658
|
+
(i32.store (i32.sub (local.get $newptr) (i32.const 8))
|
|
659
|
+
(i32.add (i32.load (i32.sub (local.get $newptr) (i32.const 8))) (i32.const 1)))))
|
|
660
|
+
(local.set $i (i32.add (local.get $i) (i32.const 1)))
|
|
661
|
+
(br $rl)))
|
|
662
|
+
${durableFwdLogIR('off', 'newptr', 'size', 'cap')}
|
|
663
|
+
(i32.store (i32.sub (local.get $off) (i32.const 8)) (local.get $newptr))
|
|
664
|
+
(i32.store (i32.sub (local.get $off) (i32.const 4)) (i32.const -1))
|
|
665
|
+
(local.set $off (local.get $newptr))
|
|
666
|
+
(local.set $cap (local.get $newcap))))
|
|
667
|
+
${hashFn === '$__str_hash' ? `;; tiered $__str_hash: the two FAST arms inline — SSO arithmetic mix and
|
|
668
|
+
;; the heap lazy-hash-cell load, one of which the dictionary-count hot path
|
|
669
|
+
;; pays per probe. Cold shapes (interned statics, uncached walk — and the
|
|
670
|
+
;; one-in-4G SSO mix that hashes to 0) call the helper, which recomputes
|
|
671
|
+
;; identically. Gates mirror $__str_hash's own exactly.
|
|
672
|
+
(local.set $kaux (i32.wrap_i64 (i64.and (i64.shr_u (local.get $key) (i64.const ${LAYOUT.AUX_SHIFT})) (i64.const ${LAYOUT.AUX_MASK}))))
|
|
673
|
+
(local.set $h (i32.const 0))
|
|
674
|
+
(if (i32.eq (i32.wrap_i64 (i64.and (i64.shr_u (local.get $key) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))) (i32.const ${PTR.STRING}))
|
|
675
|
+
(then
|
|
676
|
+
(local.set $koff (i32.wrap_i64 (i64.and (local.get $key) (i64.const ${LAYOUT.OFFSET_MASK}))))
|
|
677
|
+
(if (i32.shr_u (local.get $kaux) (i32.const 14))
|
|
678
|
+
(then
|
|
679
|
+
(local.set $h (i32.mul
|
|
680
|
+
(i32.xor (local.get $koff) (i32.mul (i32.xor (i32.and (local.get $kaux) (i32.const 0x1FFF)) (i32.const 0x9E3779B9)) (i32.const 0x85EBCA6B)))
|
|
681
|
+
(i32.const 0xC2B2AE35)))
|
|
682
|
+
(local.set $h (i32.xor (local.get $h) (i32.shr_u (local.get $h) (i32.const 15))))
|
|
683
|
+
;; $__str_hash's post-mix clamp, replicated EXACTLY (i32.le_s — it
|
|
684
|
+
;; shifts every NEGATIVE-signed hash by 2, not just 0/1): the
|
|
685
|
+
;; tiered value must be bit-equal to the helper's return and to
|
|
686
|
+
;; the lazy hash cells (they cache post-clamp values).
|
|
687
|
+
(if (i32.le_s (local.get $h) (i32.const 1))
|
|
688
|
+
(then (local.set $h (i32.add (local.get $h) (i32.const 2))))))
|
|
689
|
+
(else
|
|
690
|
+
(if (i32.and (i32.ge_u (local.get $koff) (i32.const 8))
|
|
691
|
+
(i32.eq (i32.and (local.get $kaux) (i32.const ${LAYOUT.SLICE_BIT | STR_HCACHE_BIT})) (i32.const ${STR_HCACHE_BIT})))
|
|
692
|
+
(then (local.set $h (i32.load (i32.sub (local.get $koff) (i32.const 8))))))))))
|
|
693
|
+
(if (i32.eqz (local.get $h)) (then (local.set $h (call ${hashFn} (local.get $key)))))`
|
|
694
|
+
: `(local.set $h (call ${hashFn} (local.get $key)))`}
|
|
695
|
+
${probeStart(entrySize)}
|
|
696
|
+
(block $done (loop $probe
|
|
697
|
+
(if (i64.eqz (i64.load (local.get $slot)))
|
|
698
|
+
(then
|
|
699
|
+
(if (local.get $zb) (then (local.set $slot (local.get $zb))))
|
|
700
|
+
${seqStore}
|
|
701
|
+
(i64.store (i32.add (local.get $slot) (i32.const 8)) (local.get $key))${durableEntryLogIR('slot', 'off')}
|
|
702
|
+
(i64.store (i32.add (local.get $slot) (i32.const 16)) (i64.const ${UNDEF_NAN}))
|
|
703
|
+
(i32.store (i32.sub (local.get $off) (i32.const 8))
|
|
704
|
+
(i32.add (i32.load (i32.sub (local.get $off) (i32.const 8))) (i32.const 1)))
|
|
705
|
+
(br $done)))
|
|
706
|
+
(if (i64.eq (i64.load (i32.add (local.get $slot) (i32.const 8))) (i64.const ${TOMB_NAN}))
|
|
707
|
+
(then (if (i32.eqz (local.get $zb)) (then (local.set $zb (local.get $slot)))))
|
|
708
|
+
(else (if ${eqExpr} (then (br $done)))))
|
|
709
|
+
${probeNext(entrySize)}
|
|
710
|
+
(local.set $ztr (i32.add (local.get $ztr) (i32.const 1)))
|
|
711
|
+
(if (i32.ge_s (local.get $ztr) (local.get $cap))
|
|
712
|
+
(then
|
|
713
|
+
(local.set $slot (local.get $zb))
|
|
714
|
+
${seqStore}
|
|
715
|
+
(i64.store (i32.add (local.get $slot) (i32.const 8)) (local.get $key))${durableEntryLogIR('slot', 'off')}
|
|
716
|
+
(i64.store (i32.add (local.get $slot) (i32.const 16)) (i64.const ${UNDEF_NAN}))
|
|
717
|
+
(i32.store (i32.sub (local.get $off) (i32.const 8))
|
|
718
|
+
(i32.add (i32.load (i32.sub (local.get $off) (i32.const 8))) (i32.const 1)))
|
|
719
|
+
(br $done)))
|
|
720
|
+
(br $probe)))
|
|
721
|
+
(i32.add (local.get $slot) (i32.const 16)))`
|
|
722
|
+
}
|
|
723
|
+
|
|
374
724
|
function genLookupStrict(name, entrySize, hashFn, eqExpr, expectedType, missing = UNDEF_NAN) {
|
|
375
725
|
return `(func $${name} (param $coll i64) (param $key i64) (result i64)
|
|
376
726
|
(local $off i32) (local $cap i32) (local $h i32) (local $end i32) (local $slot i32) (local $tries i32)
|
|
@@ -378,8 +728,14 @@ function genLookupStrict(name, entrySize, hashFn, eqExpr, expectedType, missing
|
|
|
378
728
|
(i32.wrap_i64 (i64.and (i64.shr_u (local.get $coll) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK})))
|
|
379
729
|
(i32.const ${expectedType}))
|
|
380
730
|
(then (return (i64.const ${missing}))))
|
|
381
|
-
(local.set $off (
|
|
731
|
+
(local.set $off (i32.wrap_i64 (i64.and (local.get $coll) (i64.const ${LAYOUT.OFFSET_MASK}))))
|
|
382
732
|
(local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
|
|
733
|
+
;; the cap load IS the forward check: -1 sentinel hops via the cold helper,
|
|
734
|
+
;; the live path pays zero extra — the per-probe __ptr_offset call drops
|
|
735
|
+
(if (i32.eq (local.get $cap) (i32.const -1))
|
|
736
|
+
(then
|
|
737
|
+
(local.set $off (call $__ptr_offset_fwd (local.get $off)))
|
|
738
|
+
(local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))))
|
|
383
739
|
(local.set $h (call ${hashFn} (local.get $key)))
|
|
384
740
|
${probeStart(entrySize)}
|
|
385
741
|
(block $done (loop $probe
|
|
@@ -411,13 +767,17 @@ function genLookupStrictPrehashed(name, entrySize, eqExpr, expectedType, missing
|
|
|
411
767
|
(else ${onEmpty}))))`
|
|
412
768
|
: `(if (i32.ne ${tExpr} (i32.const ${expectedType}))
|
|
413
769
|
(then ${onEmpty}))`
|
|
414
|
-
// SET/MAP/HASH grow by forward-marking; a boxed pointer may be stale → follow the chain.
|
|
415
|
-
const offExpr = '(call $__ptr_offset (local.get $coll))'
|
|
416
770
|
return `(func $${name} (param $coll i64) (param $key i64) (param $h i32) (result ${rt})
|
|
417
771
|
(local $off i32) (local $cap i32) (local $end i32) (local $slot i32) (local $tries i32)
|
|
418
772
|
${typeGuard}
|
|
419
|
-
(local.set $off ${
|
|
773
|
+
(local.set $off (i32.wrap_i64 (i64.and (local.get $coll) (i64.const ${LAYOUT.OFFSET_MASK}))))
|
|
420
774
|
(local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
|
|
775
|
+
;; the cap load IS the forward check: -1 sentinel hops via the cold helper,
|
|
776
|
+
;; the live path pays zero extra — the per-probe __ptr_offset call drops
|
|
777
|
+
(if (i32.eq (local.get $cap) (i32.const -1))
|
|
778
|
+
(then
|
|
779
|
+
(local.set $off (call $__ptr_offset_fwd (local.get $off)))
|
|
780
|
+
(local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))))
|
|
421
781
|
${probeStart(entrySize)}
|
|
422
782
|
(block $done (loop $probe
|
|
423
783
|
(if (i64.eqz (i64.load (local.get $slot))) (then ${onEmpty}))
|
|
@@ -431,28 +791,48 @@ function genLookupStrictPrehashed(name, entrySize, eqExpr, expectedType, missing
|
|
|
431
791
|
|
|
432
792
|
function genUpsertStrictPrehashed(name, entrySize, eqExpr, expectedType) {
|
|
433
793
|
return `(func $${name} (param $obj i64) (param $key i64) (param $h i32) (param $val i64) (result i64)
|
|
434
|
-
(local $off i32) (local $cap i32) (local $end i32) (local $slot i32)
|
|
794
|
+
(local $off i32) (local $cap i32) (local $end i32) (local $slot i32) (local $zb i32) (local $ztr i32)
|
|
435
795
|
(if (i32.ne
|
|
436
796
|
(i32.wrap_i64 (i64.and (i64.shr_u (local.get $obj) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK})))
|
|
437
797
|
(i32.const ${expectedType}))
|
|
438
798
|
(then (return (local.get $obj))))
|
|
439
|
-
(local.set $off (
|
|
799
|
+
(local.set $off (i32.wrap_i64 (i64.and (local.get $obj) (i64.const ${LAYOUT.OFFSET_MASK}))))
|
|
440
800
|
(local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
|
|
801
|
+
;; the cap load IS the forward check: -1 sentinel hops via the cold helper,
|
|
802
|
+
;; the live path pays zero extra — the per-probe __ptr_offset call drops
|
|
803
|
+
(if (i32.eq (local.get $cap) (i32.const -1))
|
|
804
|
+
(then
|
|
805
|
+
(local.set $off (call $__ptr_offset_fwd (local.get $off)))
|
|
806
|
+
(local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))))
|
|
441
807
|
${probeStart(entrySize)}
|
|
808
|
+
;; zombie-aware probe (durable-slot heal, TOMB_NAN keys) — see genUpsert.
|
|
442
809
|
(block $done (loop $probe
|
|
443
810
|
(if (i64.eqz (i64.load (local.get $slot)))
|
|
444
811
|
(then
|
|
812
|
+
(if (local.get $zb) (then (local.set $slot (local.get $zb))))
|
|
445
813
|
${seqStore}
|
|
446
|
-
(i64.store (i32.add (local.get $slot) (i32.const 8)) (local.get $key))
|
|
447
|
-
(i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))
|
|
814
|
+
(i64.store (i32.add (local.get $slot) (i32.const 8)) (local.get $key))${durableEntryLogIR('slot', 'off')}
|
|
815
|
+
(i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))${durableSlotLogIR('slot', 16, 'val')}
|
|
448
816
|
(i32.store (i32.sub (local.get $off) (i32.const 8))
|
|
449
817
|
(i32.add (i32.load (i32.sub (local.get $off) (i32.const 8))) (i32.const 1)))
|
|
450
818
|
(br $done)))
|
|
451
|
-
(if ${
|
|
819
|
+
(if (i64.eq (i64.load (i32.add (local.get $slot) (i32.const 8))) (i64.const ${TOMB_NAN}))
|
|
820
|
+
(then (if (i32.eqz (local.get $zb)) (then (local.set $zb (local.get $slot)))))
|
|
821
|
+
(else (if ${eqExpr}
|
|
822
|
+
(then
|
|
823
|
+
(i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))${durableSlotLogIR('slot', 16, 'val')}
|
|
824
|
+
(br $done)))))
|
|
825
|
+
${probeNext(entrySize)}
|
|
826
|
+
(local.set $ztr (i32.add (local.get $ztr) (i32.const 1)))
|
|
827
|
+
(if (i32.ge_s (local.get $ztr) (local.get $cap))
|
|
452
828
|
(then
|
|
453
|
-
(
|
|
829
|
+
(local.set $slot (local.get $zb))
|
|
830
|
+
${seqStore}
|
|
831
|
+
(i64.store (i32.add (local.get $slot) (i32.const 8)) (local.get $key))${durableEntryLogIR('slot', 'off')}
|
|
832
|
+
(i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))${durableSlotLogIR('slot', 16, 'val')}
|
|
833
|
+
(i32.store (i32.sub (local.get $off) (i32.const 8))
|
|
834
|
+
(i32.add (i32.load (i32.sub (local.get $off) (i32.const 8))) (i32.const 1)))
|
|
454
835
|
(br $done)))
|
|
455
|
-
${probeNext(entrySize)}
|
|
456
836
|
(br $probe)))
|
|
457
837
|
(local.get $obj))`
|
|
458
838
|
}
|
|
@@ -462,24 +842,55 @@ export default (ctx) => {
|
|
|
462
842
|
// Feature-gated deps: EXTERNAL-dependent symbols are only pulled when features.external.
|
|
463
843
|
// Evaluated lazily at resolveIncludes() time — after emission has finalized ctx.features.
|
|
464
844
|
const ifExt = (name) => () => ctx.features.external ? [name] : []
|
|
845
|
+
// Whether durableFwdLogIR emits a real call (vs '' — see its own comment): only when
|
|
846
|
+
// __heap_reset exists (owned-memory builds; shared memory never declares it — core.js).
|
|
847
|
+
// Gates the deps() edges below the SAME way, so a shared-memory build (where core.js
|
|
848
|
+
// never registers __durable_fwd_log/__durable_fwd_heal) never requests a name nothing
|
|
849
|
+
// delivers.
|
|
850
|
+
const needsDurableFwdLog = () => ctx.scope.globals.has('__heap_reset')
|
|
851
|
+
// durableSlotLogIR's call pair, gated identically (see its comment): every helper
|
|
852
|
+
// that stores a VALUE into a collection slot may log a durable-slot write.
|
|
853
|
+
const slotLogDeps = () => needsDurableFwdLog() ? ['__durable_slot_log', '__is_eph_bits'] : []
|
|
465
854
|
deps({
|
|
466
855
|
__same_value_zero: ['__str_eq'],
|
|
467
856
|
__map_hash: ['__hash', '__str_hash'],
|
|
468
|
-
|
|
469
|
-
|
|
857
|
+
// '__durable_fwd_log' on __set_add/__map_set/__hash_set/__hash_set_local: an
|
|
858
|
+
// EXPLICIT edge, not left to the auto-dep scan — genUpsert/genUpsertGrow's
|
|
859
|
+
// `forward` branch always contains a durableFwdLogIR() call, but self-host's
|
|
860
|
+
// realize/regex-scan auto-deps path silently drops a helper reachable only that
|
|
861
|
+
// way (the "Unknown func $__clamp_idx" shape documented in
|
|
862
|
+
// test/selfhost-includes.js) — that test would fail (and the kernel would trap)
|
|
863
|
+
// without these edges.
|
|
864
|
+
// slotLogDeps: hasVal=false skips the VALUE slot-log, but the ENTRY-insert
|
|
865
|
+
// log (durableEntryLogIR) still calls $__durable_slot_log — without the
|
|
866
|
+
// explicit edge the kernel leg drops the helper (auto-scan divergence, the
|
|
867
|
+
// selfhost-includes class) and every `new Set(...)` fails to compile there.
|
|
868
|
+
__set_add: () => [...(ctx.features.external ? ['__map_hash', '__same_value_zero', '__ptr_offset', '__ptr_offset_fwd', '__alloc_hdr_n', '__ext_set'] : ['__map_hash', '__same_value_zero', '__ptr_offset', '__ptr_offset_fwd', '__alloc_hdr_n']), ...(needsDurableFwdLog() ? ['__durable_fwd_log'] : []), ...slotLogDeps()],
|
|
869
|
+
__set_has: () => ctx.features.external ? ['__map_hash', '__same_value_zero', '__ptr_offset', '__ptr_offset_fwd', '__ext_has'] : ['__map_hash', '__same_value_zero', '__ptr_offset', '__ptr_offset_fwd'],
|
|
470
870
|
__set_delete: ['__map_hash', '__same_value_zero'],
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
871
|
+
__set_add_all: ['__ptr_offset', '__ptr_offset_fwd', '__cap', '__len', '__coll_order', '__set_add'],
|
|
872
|
+
__set_filter: ['__ptr_offset', '__ptr_offset_fwd', '__cap', '__len', '__coll_order', '__set_add', '__set_has', '__map_has'],
|
|
873
|
+
__set_all: ['__ptr_offset', '__ptr_offset_fwd', '__cap', '__len', '__coll_order', '__set_has', '__map_has'],
|
|
874
|
+
__sclone: ['__sclone_rec', '__mkptr', '__alloc_hdr_n'],
|
|
875
|
+
__sclone_rec: ['__ptr_type', '__ptr_offset', '__ptr_offset_fwd', '__ptr_aux', '__is_nullish', '__len', '__alloc', '__alloc_hdr_n', '__mkptr', '__map_get', '__map_set', '__set_add', '__coll_order', '__arr_from', '__obj_clone', '__sclone_hash_vals'],
|
|
876
|
+
__sclone_hash_vals: ['__sclone_rec'],
|
|
877
|
+
__map_set: () => [...(ctx.features.external ? ['__map_hash', '__same_value_zero', '__ptr_offset', '__ptr_offset_fwd', '__alloc_hdr_n', '__ext_set'] : ['__map_hash', '__same_value_zero', '__ptr_offset', '__ptr_offset_fwd', '__alloc_hdr_n']), ...(needsDurableFwdLog() ? ['__durable_fwd_log'] : []), ...slotLogDeps()],
|
|
878
|
+
__map_get: () => ctx.features.external ? ['__ext_prop', '__map_set', '__ptr_offset', '__ptr_offset_fwd'] : ['__map_set', '__ptr_offset', '__ptr_offset_fwd'],
|
|
879
|
+
__map_get_h: () => ctx.features.external ? ['__ext_prop', '__same_value_zero', '__ptr_offset', '__ptr_offset_fwd'] : ['__same_value_zero', '__ptr_offset', '__ptr_offset_fwd'],
|
|
880
|
+
__map_has: () => ctx.features.external ? ['__map_hash', '__same_value_zero', '__ptr_offset', '__ptr_offset_fwd', '__ext_has'] : ['__map_hash', '__same_value_zero', '__ptr_offset', '__ptr_offset_fwd'],
|
|
475
881
|
// Prehashed has-probes: caller folds the hash, so no __map_hash dependency.
|
|
476
|
-
__map_has_h: () => ctx.features.external ? ['__same_value_zero', '__ptr_offset', '__ext_has'] : ['__same_value_zero', '__ptr_offset'],
|
|
477
|
-
__set_has_h: () => ctx.features.external ? ['__same_value_zero', '__ptr_offset', '__ext_has'] : ['__same_value_zero', '__ptr_offset'],
|
|
882
|
+
__map_has_h: () => ctx.features.external ? ['__same_value_zero', '__ptr_offset', '__ptr_offset_fwd', '__ext_has'] : ['__same_value_zero', '__ptr_offset', '__ptr_offset_fwd'],
|
|
883
|
+
__set_has_h: () => ctx.features.external ? ['__same_value_zero', '__ptr_offset', '__ptr_offset_fwd', '__ext_has'] : ['__same_value_zero', '__ptr_offset', '__ptr_offset_fwd'],
|
|
478
884
|
__map_delete: ['__map_hash', '__same_value_zero'],
|
|
479
|
-
__map_from: ['__ptr_type', '__ptr_offset', '__len', '__typed_idx', '__map_set', '__mkptr', '__alloc_hdr_n', '__coll_order'],
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
885
|
+
__map_from: ['__ptr_type', '__ptr_offset', '__ptr_offset_fwd', '__len', '__typed_idx', '__map_set', '__mkptr', '__alloc_hdr_n', '__coll_order'],
|
|
886
|
+
// own edge: __map_new's body calls $__alloc_hdr_n — auto-scan-only
|
|
887
|
+
// reachability vanishes under self-host (test/selfhost-includes.js)
|
|
888
|
+
__map_new: ['__alloc_hdr_n'],
|
|
889
|
+
__hash_set: () => [
|
|
890
|
+
...(ctx.features.external ? ['__str_hash', '__str_eq', '__ptr_type', '__ext_set', '__dyn_set'] : ['__str_hash', '__str_eq', '__ptr_type', '__dyn_set']),
|
|
891
|
+
...(needsDurableFwdLog() ? ['__durable_fwd_log'] : []),
|
|
892
|
+
...slotLogDeps(),
|
|
893
|
+
],
|
|
483
894
|
__hash_get: () => ctx.features.external
|
|
484
895
|
? ['__str_hash', '__str_eq', '__ptr_type', '__ext_prop']
|
|
485
896
|
: ['__str_hash', '__str_eq', '__ptr_type'],
|
|
@@ -490,32 +901,35 @@ export default (ctx) => {
|
|
|
490
901
|
__hash_new_small: ['__alloc_hdr_n', '__mkptr'],
|
|
491
902
|
__hash_get_local: ['__str_hash', '__str_eq'],
|
|
492
903
|
__hash_get_local_h: ['__str_eq'],
|
|
493
|
-
__hash_set_local_h: ['__str_eq'],
|
|
494
|
-
__hash_set_local: ['__str_hash', '__str_eq', '__alloc_hdr_n', '__mkptr'],
|
|
904
|
+
__hash_set_local_h: () => ['__str_eq', ...slotLogDeps()],
|
|
905
|
+
__hash_set_local: () => ['__str_hash', '__str_eq', '__alloc_hdr_n', '__mkptr', ...(needsDurableFwdLog() ? ['__durable_fwd_log'] : []), ...slotLogDeps()],
|
|
906
|
+
__hash_slot: () => ['__str_hash', '__str_eq', '__alloc_hdr_n', '__ptr_type', '__ptr_offset', '__ptr_offset_fwd', ...(needsDurableFwdLog() ? ['__durable_fwd_log'] : []), ...slotLogDeps()],
|
|
907
|
+
__slot_write: () => slotLogDeps(),
|
|
495
908
|
__ihash_get_local: ['__map_hash'],
|
|
496
|
-
__ihash_set_local: ['__map_hash', '__alloc_hdr_n', '__mkptr'],
|
|
497
|
-
__dyn_get_t: ['__dyn_get_t_h', '__str_hash'],
|
|
498
|
-
__dyn_get_t_h: ['__ihash_get_local', '__str_eq', '__is_nullish'],
|
|
909
|
+
__ihash_set_local: () => ['__map_hash', '__alloc_hdr_n', '__mkptr', ...slotLogDeps()],
|
|
910
|
+
__dyn_get_t: ['__dyn_get_t_h', '__str_hash', '__is_str_key', '__to_str'],
|
|
911
|
+
__dyn_get_t_h: ['__ihash_get_local', '__str_eq', '__is_nullish', '__hash_get_local_h', '__str_arr_idx', '__str_byteLen'],
|
|
499
912
|
__dyn_get: ['__dyn_get_t', '__ptr_type'],
|
|
500
|
-
__dyn_get_expr_t: ['__dyn_get_t', '__hash_get_local'],
|
|
913
|
+
__dyn_get_expr_t: ['__dyn_get_t', '__hash_get_local', '__is_str_key', '__to_str', '__ptr_offset', '__ptr_offset_fwd'],
|
|
501
914
|
__dyn_get_expr_t_h: ['__dyn_get_t_h', '__hash_get_local_h'],
|
|
502
915
|
__dyn_get_expr: ['__dyn_get_expr_t', '__ptr_type'],
|
|
503
916
|
__dyn_get_any: ['__dyn_get_any_t', '__ptr_type'],
|
|
504
917
|
__dyn_get_any_t: () => ctx.features.external
|
|
505
|
-
? ['__dyn_get_t', '__hash_get_local', '__ext_prop']
|
|
506
|
-
: ['__dyn_get_t', '__hash_get_local'],
|
|
918
|
+
? ['__dyn_get_t', '__hash_get_local', '__ext_prop', '__is_str_key', '__to_str', '__ptr_offset', '__ptr_offset_fwd']
|
|
919
|
+
: ['__dyn_get_t', '__hash_get_local', '__is_str_key', '__to_str', '__ptr_offset', '__ptr_offset_fwd'],
|
|
507
920
|
__dyn_get_any_t_h: () => ctx.features.external
|
|
508
921
|
? ['__dyn_get_t_h', '__hash_get_local_h', '__ext_prop']
|
|
509
922
|
: ['__dyn_get_t_h', '__hash_get_local_h'],
|
|
510
923
|
__dyn_get_or: ['__dyn_get'],
|
|
511
|
-
__dyn_set: ['__hash_new', '__hash_new_small', '__ihash_get_local', '__ihash_set_local', '__hash_set_local', '__ptr_offset', '__is_nullish', '__str_eq'],
|
|
924
|
+
__dyn_set: ['__hash_new', '__hash_new_small', '__ihash_get_local', '__ihash_set_local', '__hash_set_local', '__ptr_offset', '__ptr_offset_fwd', '__is_nullish', '__str_eq', '__is_str_key', '__to_str', '__arr_set_idx_ptr', '__str_arr_idx'],
|
|
512
925
|
__dyn_move: ['__ihash_get_local', '__ihash_set_local', '__is_nullish'],
|
|
513
926
|
__hash_del_local: ['__str_hash', '__str_eq', '__ptr_type'],
|
|
514
|
-
__dyn_del: ['__hash_del_local', '__ihash_get_local', '__is_nullish'],
|
|
515
|
-
|
|
927
|
+
__dyn_del: ['__hash_del_local', '__ihash_get_local', '__is_nullish', '__is_str_key', '__to_str', '__str_arr_idx'],
|
|
928
|
+
__str_arr_idx: ['__str_byteLen', '__char_at'],
|
|
929
|
+
__coll_clear: ['__ptr_type', '__ptr_offset', '__ptr_offset_fwd'],
|
|
516
930
|
})
|
|
517
931
|
|
|
518
|
-
inc('__ptr_offset', '__cap')
|
|
932
|
+
inc('__ptr_offset', '__ptr_offset_fwd', '__cap')
|
|
519
933
|
|
|
520
934
|
// Monotonic insertion counter packed into each entry's hash-word high 32 bits
|
|
521
935
|
// (see seqStore). Restores JS insertion order at iteration without growing
|
|
@@ -524,8 +938,31 @@ export default (ctx) => {
|
|
|
524
938
|
if (!ctx.scope.globals.has('__seq'))
|
|
525
939
|
declGlobal('__seq', 'i32')
|
|
526
940
|
|
|
941
|
+
// for-in enum cache (core.js __hash_keys_ro): cached boxed key array keyed by
|
|
942
|
+
// (table off, live len). Declared here unconditionally — genDelete's HASH
|
|
943
|
+
// invalidation hook references $__enumc_off in its static WAT text, so the
|
|
944
|
+
// global must exist in any build that reaches __hash_del_local, for-in or not
|
|
945
|
+
// (same pattern as __seq/__dyn_props above; watr treeshakes them when unused).
|
|
946
|
+
if (!ctx.scope.globals.has('__enumc_off')) {
|
|
947
|
+
declGlobal('__enumc_off', 'i32')
|
|
948
|
+
declGlobal('__enumc_len', 'i32')
|
|
949
|
+
declGlobal('__enumc_arr', 'f64')
|
|
950
|
+
}
|
|
951
|
+
|
|
527
952
|
if (!ctx.scope.globals.has('__dyn_props'))
|
|
528
953
|
declGlobal('__dyn_props', 'f64')
|
|
954
|
+
// Never-false-negative membership filter over offsets ever inserted into the
|
|
955
|
+
// global __dyn_props table: a 64-bit bitset, bit = mix(off) & 63 for every
|
|
956
|
+
// offset key ever written there (see dynPropsFilterSetIR — all 3 insert
|
|
957
|
+
// sites: __dyn_set's global-table fallback, __dyn_move's own rekey, and
|
|
958
|
+
// array.js's headerPropsToGlobalIR). Probe sites (__dyn_move, __dyn_del's
|
|
959
|
+
// global fallback) test the bit first and skip the __ihash_get_local probe
|
|
960
|
+
// on a clear bit — sound because misses are never possible (bits are only
|
|
961
|
+
// ever set, never cleared; a false positive just falls through to the real
|
|
962
|
+
// probe). __dyn_props itself is never reset by __clear (see core.js), so
|
|
963
|
+
// this filter doesn't need resetting either.
|
|
964
|
+
if (!ctx.scope.globals.has('__dyn_props_filter'))
|
|
965
|
+
declGlobal('__dyn_props_filter', 'i64')
|
|
529
966
|
// 1-slot inline cache for the global __dyn_props lookup. Hot path for
|
|
530
967
|
// metacircular workloads (watr WAT parser): ~96% of execution sits in
|
|
531
968
|
// __dyn_get_t / __ihash_get_local. Caches last-seen (off → propsPtr) at
|
|
@@ -716,8 +1153,39 @@ export default (ctx) => {
|
|
|
716
1153
|
(i32.store (i32.sub (local.get $off) (i32.const 8)) (i32.const 0))))
|
|
717
1154
|
(f64.reinterpret_i64 (i64.const ${UNDEF_NAN})))`
|
|
718
1155
|
|
|
719
|
-
|
|
1156
|
+
// `.size` on a PROVEN Set/Map: entry count at off-8, direct __len.
|
|
1157
|
+
const sizeLen = (expr) => {
|
|
1158
|
+
inc('__len')
|
|
720
1159
|
return typed(['f64.convert_i32_s', ['call', '$__len', ['i64.reinterpret_f64', asF64(emit(expr))]]], 'f64')
|
|
1160
|
+
}
|
|
1161
|
+
registerGetter(`.${VAL.SET}:size`, sizeLen)
|
|
1162
|
+
registerGetter(`.${VAL.MAP}:size`, sizeLen)
|
|
1163
|
+
// `.size` on an unproven receiver: in JS only Set/Map expose an entry count —
|
|
1164
|
+
// on everything else `size` is an ordinary own property (or undefined). The
|
|
1165
|
+
// old bare-__len form read the length HEADER of whatever arrived, silently
|
|
1166
|
+
// returning 0 for `{size: 4}` — which broke the self-host kernel reading
|
|
1167
|
+
// `ctx.runtime.internTable.size` (the last L2 byte-parity divergence).
|
|
1168
|
+
// NaN-check guards real numbers whose bit pattern could false-match the tag
|
|
1169
|
+
// compare; the dyn dispatcher covers OBJECT schema slots, HASH keys,
|
|
1170
|
+
// sidecars, and primitives (→ undefined).
|
|
1171
|
+
registerGetter('.size', (expr) => {
|
|
1172
|
+
inc('__len', '__ptr_type', '__dyn_get_expr_t_h')
|
|
1173
|
+
const o = temp('szv')
|
|
1174
|
+
const t = tempI32('szt')
|
|
1175
|
+
const og = ['local.get', `$${o}`]
|
|
1176
|
+
return typed(['block', ['result', 'f64'],
|
|
1177
|
+
['local.set', `$${o}`, asF64(emit(expr))],
|
|
1178
|
+
['local.set', `$${t}`, ['call', '$__ptr_type', ['i64.reinterpret_f64', og]]],
|
|
1179
|
+
['if', ['result', 'f64'],
|
|
1180
|
+
['i32.and',
|
|
1181
|
+
['f64.ne', og, og],
|
|
1182
|
+
['i32.or',
|
|
1183
|
+
['i32.eq', ['local.get', `$${t}`], ['i32.const', PTR.SET]],
|
|
1184
|
+
['i32.eq', ['local.get', `$${t}`], ['i32.const', PTR.MAP]]]],
|
|
1185
|
+
['then', ['f64.convert_i32_s', ['call', '$__len', ['i64.reinterpret_f64', og]]]],
|
|
1186
|
+
['else', ['f64.reinterpret_i64', ['call', '$__dyn_get_expr_t_h',
|
|
1187
|
+
['i64.reinterpret_f64', og], asI64(emit(['str', 'size'])), ['local.get', `$${t}`],
|
|
1188
|
+
['i32.const', strHashLiteral('size')]]]]]], 'f64')
|
|
721
1189
|
})
|
|
722
1190
|
|
|
723
1191
|
// x instanceof Map / Set — typed-pointer predicates emitted by jzify. NaN-check
|
|
@@ -744,6 +1212,60 @@ export default (ctx) => {
|
|
|
744
1212
|
ctx.core.stdlib['__set_has_h'] = () => genLookupStrictPrehashed('__set_has_h', SET_ENTRY, sameValueZeroEqG, PTR.SET, UNDEF_NAN, ctx.features.external, false)
|
|
745
1213
|
ctx.core.stdlib['__set_delete'] = genDelete('__set_delete', SET_ENTRY, '$__map_hash', sameValueZeroEqG, PTR.SET)
|
|
746
1214
|
|
|
1215
|
+
// ES2025 Set algebra (union/intersection/difference/symmetricDifference +
|
|
1216
|
+
// isSubsetOf/isSupersetOf/isDisjointFrom). Three shared walkers over the
|
|
1217
|
+
// receiver's insertion order (__coll_order — result order is spec-exact); an
|
|
1218
|
+
// `other` that is not a real Set/Map is treated as empty (__set_has/__map_has
|
|
1219
|
+
// type-guard a wrong receiver to 0), the native-litmus line (a proven Set or
|
|
1220
|
+
// Map is in-model; an arbitrary set-like is not, no .has/.keys dispatch).
|
|
1221
|
+
// __set_add's SameValueZero dedup + insertion-seq stamping make add-order the
|
|
1222
|
+
// result order for free.
|
|
1223
|
+
// $stride is the src collection's entry stride (SET_ENTRY for a Set, MAP_ENTRY
|
|
1224
|
+
// for a Map) — a Map's keys sit at slot+8 too, so a Map `other` iterates as a
|
|
1225
|
+
// key set. The key is always at slot+8 in both layouts.
|
|
1226
|
+
const setWalkPreamble = `(local $off i32) (local $cap i32) (local $n i32) (local $ord i32) (local $i i32) (local $slot i32) (local $key i64) (local $has i32)
|
|
1227
|
+
(local.set $off (call $__ptr_offset (local.get $src)))
|
|
1228
|
+
(local.set $cap (call $__cap (local.get $src)))
|
|
1229
|
+
(local.set $n (call $__len (local.get $src)))
|
|
1230
|
+
(local.set $ord (call $__coll_order (local.get $off) (local.get $cap) (local.get $stride)))`
|
|
1231
|
+
const setWalkKey = `(local.set $slot (i32.load (i32.add (local.get $ord) (i32.shl (local.get $i) (i32.const 2)))))
|
|
1232
|
+
(local.set $key (i64.load (i32.add (local.get $slot) (i32.const 8))))`
|
|
1233
|
+
const otherHas = `(if (result i32) (local.get $otherIsMap)
|
|
1234
|
+
(then (call $__map_has (local.get $other) (local.get $key)))
|
|
1235
|
+
(else (call $__set_has (local.get $other) (local.get $key))))`
|
|
1236
|
+
// dst ← dst ∪ src (add every src key in insertion order).
|
|
1237
|
+
ctx.core.stdlib['__set_add_all'] = `(func $__set_add_all (param $dst i64) (param $src i64) (param $stride i32) (result i64)
|
|
1238
|
+
${setWalkPreamble}
|
|
1239
|
+
(block $d (loop $l
|
|
1240
|
+
(br_if $d (i32.ge_s (local.get $i) (local.get $n)))
|
|
1241
|
+
${setWalkKey}
|
|
1242
|
+
(local.set $dst (call $__set_add (local.get $dst) (local.get $key)))
|
|
1243
|
+
(local.set $i (i32.add (local.get $i) (i32.const 1))) (br $l)))
|
|
1244
|
+
(local.get $dst))`
|
|
1245
|
+
// For each src key, add to dst iff (other has key) == keep. keep=1 → intersection;
|
|
1246
|
+
// keep=0 → difference / symmetricDifference pass.
|
|
1247
|
+
ctx.core.stdlib['__set_filter'] = `(func $__set_filter (param $dst i64) (param $src i64) (param $stride i32) (param $other i64) (param $otherIsMap i32) (param $keep i32) (result i64)
|
|
1248
|
+
${setWalkPreamble}
|
|
1249
|
+
(block $d (loop $l
|
|
1250
|
+
(br_if $d (i32.ge_s (local.get $i) (local.get $n)))
|
|
1251
|
+
${setWalkKey}
|
|
1252
|
+
(local.set $has ${otherHas})
|
|
1253
|
+
(if (i32.eq (local.get $has) (local.get $keep))
|
|
1254
|
+
(then (local.set $dst (call $__set_add (local.get $dst) (local.get $key)))))
|
|
1255
|
+
(local.set $i (i32.add (local.get $i) (i32.const 1))) (br $l)))
|
|
1256
|
+
(local.get $dst))`
|
|
1257
|
+
// Predicate: return 0 as soon as any src key's presence in other != want; else 1.
|
|
1258
|
+
// isSubsetOf(A,B) = all(A, B, want=1); isDisjointFrom(A,B) = all(A, B, want=0).
|
|
1259
|
+
ctx.core.stdlib['__set_all'] = `(func $__set_all (param $src i64) (param $stride i32) (param $other i64) (param $otherIsMap i32) (param $want i32) (result i32)
|
|
1260
|
+
${setWalkPreamble}
|
|
1261
|
+
(block $d (loop $l
|
|
1262
|
+
(br_if $d (i32.ge_s (local.get $i) (local.get $n)))
|
|
1263
|
+
${setWalkKey}
|
|
1264
|
+
(local.set $has ${otherHas})
|
|
1265
|
+
(if (i32.ne (local.get $has) (local.get $want)) (then (return (i32.const 0))))
|
|
1266
|
+
(local.set $i (i32.add (local.get $i) (i32.const 1))) (br $l)))
|
|
1267
|
+
(i32.const 1))`
|
|
1268
|
+
|
|
747
1269
|
// === Map ===
|
|
748
1270
|
|
|
749
1271
|
ctx.core.emit['new.Map'] = (iterExpr) => {
|
|
@@ -762,8 +1284,10 @@ export default (ctx) => {
|
|
|
762
1284
|
|
|
763
1285
|
ctx.core.emit['.set'] = (mapExpr, key, val) => {
|
|
764
1286
|
inc('__map_set')
|
|
765
|
-
|
|
766
|
-
|
|
1287
|
+
// Keys and values are boxed-value slots — booleans cross as their atom so
|
|
1288
|
+
// set(true, …)/get(true) agree on bits and stored values keep identity.
|
|
1289
|
+
const value = val === undefined ? asI64(undefExpr()) : asI64(carrierF64(val, emit(val)))
|
|
1290
|
+
return typed(['f64.reinterpret_i64', ['call', '$__map_set', asI64(emit(mapExpr)), asI64(carrierF64(key, emit(key))), value]], 'f64')
|
|
767
1291
|
}
|
|
768
1292
|
ctx.core.emit[`.${VAL.MAP}:set`] = ctx.core.emit['.set']
|
|
769
1293
|
|
|
@@ -774,7 +1298,8 @@ export default (ctx) => {
|
|
|
774
1298
|
return typed(['f64.reinterpret_i64', ['call', '$__map_get_h', asI64(emit(mapExpr)), asI64(emit(key)), ['i32.const', h]]], 'f64')
|
|
775
1299
|
}
|
|
776
1300
|
inc('__map_get')
|
|
777
|
-
|
|
1301
|
+
// Key is a boxed-value slot — a bool key probes with the same atom bits .set stored.
|
|
1302
|
+
return typed(['f64.reinterpret_i64', ['call', '$__map_get', asI64(emit(mapExpr)), asI64(carrierF64(key, emit(key)))]], 'f64')
|
|
778
1303
|
}
|
|
779
1304
|
|
|
780
1305
|
ctx.core.emit['.get'] = emitMapGet
|
|
@@ -841,7 +1366,7 @@ export default (ctx) => {
|
|
|
841
1366
|
// the uniform closure width (forEach autoloads array → closure floor 2). Uses
|
|
842
1367
|
// the closure-call path like typedarray:forEach — forEach isn't a hot path.
|
|
843
1368
|
const collForEach = (stride, valOff, keyOff) => (expr, fn) => {
|
|
844
|
-
inc('__ptr_offset', '__cap', '__len', '__coll_order')
|
|
1369
|
+
inc('__ptr_offset', '__ptr_offset_fwd', '__cap', '__len', '__coll_order')
|
|
845
1370
|
const t = temp('fe'), cb = temp('fecb')
|
|
846
1371
|
const off = tempI32('feo'), cap = tempI32('fec'), n = tempI32('fen')
|
|
847
1372
|
const i = tempI32('fei'), ord = tempI32('fer'), slot = tempI32('fes')
|
|
@@ -868,6 +1393,324 @@ export default (ctx) => {
|
|
|
868
1393
|
ctx.core.emit[`.${VAL.MAP}:forEach`] = collForEach(MAP_ENTRY, 16, 8)
|
|
869
1394
|
ctx.core.emit[`.${VAL.SET}:forEach`] = collForEach(SET_ENTRY, 8, 8)
|
|
870
1395
|
|
|
1396
|
+
// === ES2025 Set algebra emitters ===
|
|
1397
|
+
// A = receiver (proven SET). `other` (B) may be a Set or Map at runtime; its
|
|
1398
|
+
// stride/probe-kind is resolved once via __ptr_type (a Map's keys are its key
|
|
1399
|
+
// set). Set-returning ops thread the fresh dst through the walker (forwarding
|
|
1400
|
+
// is transparent, but re-capture is defensively correct); predicates fold the
|
|
1401
|
+
// i32 result to a boolean carrier.
|
|
1402
|
+
const isMapRT = (f64) => ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', f64]], ['i32.const', PTR.MAP]]
|
|
1403
|
+
const strideRT = (f64) => ['select', ['i32.const', MAP_ENTRY], ['i32.const', SET_ENTRY], isMapRT(f64)]
|
|
1404
|
+
const isMapI32 = (f64) => isMapRT(f64) // 1 if Map, 0 otherwise
|
|
1405
|
+
// Evaluate A,B into temps; hand their f64 accessors to `body`, which returns
|
|
1406
|
+
// the block's final value node. resultType picks Set (f64) vs predicate (i32).
|
|
1407
|
+
const setBin = (a, b, resultType, body) => {
|
|
1408
|
+
inc('__ptr_type')
|
|
1409
|
+
const aT = temp('sopa'), bT = temp('sopb')
|
|
1410
|
+
const aF = typed(['local.get', `$${aT}`], 'f64'), bF = typed(['local.get', `$${bT}`], 'f64')
|
|
1411
|
+
return typed(['block', ['result', resultType],
|
|
1412
|
+
['local.set', `$${aT}`, asF64(emit(a))],
|
|
1413
|
+
['local.set', `$${bT}`, asF64(emit(b))],
|
|
1414
|
+
body(aF, bF)], resultType)
|
|
1415
|
+
}
|
|
1416
|
+
// Build the fresh-dst + threaded-walker-call sequence, returning the dst temp.
|
|
1417
|
+
const buildSet = (steps, tag) => {
|
|
1418
|
+
const dst = allocPtr({ type: PTR.SET, len: 0, cap: INIT_CAP, stride: SET_ENTRY, tag })
|
|
1419
|
+
const dstT = temp('sopd')
|
|
1420
|
+
const dI = () => ['i64.reinterpret_f64', ['local.get', `$${dstT}`]]
|
|
1421
|
+
const seq = ['block', ['result', 'f64'], dst.init, ['local.set', `$${dstT}`, dst.ptr]]
|
|
1422
|
+
for (const call of steps(dstT, dI)) seq.push(['local.set', `$${dstT}`, ['f64.reinterpret_i64', call]])
|
|
1423
|
+
seq.push(['local.get', `$${dstT}`])
|
|
1424
|
+
return seq
|
|
1425
|
+
}
|
|
1426
|
+
const addAll = (dI, srcF, strideIR) => ['call', '$__set_add_all', dI(), ['i64.reinterpret_f64', srcF], strideIR]
|
|
1427
|
+
const filterInto = (dI, srcF, strideIR, otherF, otherIsMap, keep) =>
|
|
1428
|
+
['call', '$__set_filter', dI(), ['i64.reinterpret_f64', srcF], strideIR, ['i64.reinterpret_f64', otherF], otherIsMap, ['i32.const', keep]]
|
|
1429
|
+
const allMatch = (srcF, strideIR, otherF, otherIsMap, want) =>
|
|
1430
|
+
['call', '$__set_all', ['i64.reinterpret_f64', srcF], strideIR, ['i64.reinterpret_f64', otherF], otherIsMap, ['i32.const', want]]
|
|
1431
|
+
|
|
1432
|
+
ctx.core.emit['.set:union'] = (a, b) => { inc('__set_add_all')
|
|
1433
|
+
return setBin(a, b, 'f64', (aF, bF) => buildSet((dstT, dI) => [
|
|
1434
|
+
addAll(dI, aF, ['i32.const', SET_ENTRY]),
|
|
1435
|
+
addAll(dI, bF, strideRT(bF)),
|
|
1436
|
+
], 'setu')) }
|
|
1437
|
+
|
|
1438
|
+
ctx.core.emit['.set:intersection'] = (a, b) => { inc('__set_filter', '__len')
|
|
1439
|
+
// Walk the SMALLER operand (ties → `this`, A) for spec-exact result order.
|
|
1440
|
+
return setBin(a, b, 'f64', (aF, bF) => {
|
|
1441
|
+
const aLen = ['call', '$__len', ['i64.reinterpret_f64', aF]]
|
|
1442
|
+
const bLen = ['call', '$__len', ['i64.reinterpret_f64', bF]]
|
|
1443
|
+
const walkA = buildSet((dstT, dI) => [filterInto(dI, aF, ['i32.const', SET_ENTRY], bF, isMapI32(bF), 1)], 'seti')
|
|
1444
|
+
const walkB = buildSet((dstT, dI) => [filterInto(dI, bF, strideRT(bF), aF, ['i32.const', 0], 1)], 'seti')
|
|
1445
|
+
return ['if', ['result', 'f64'], ['i32.le_s', aLen, bLen], ['then', walkA], ['else', walkB]]
|
|
1446
|
+
}) }
|
|
1447
|
+
|
|
1448
|
+
ctx.core.emit['.set:difference'] = (a, b) => { inc('__set_filter')
|
|
1449
|
+
// Always A's order (spec: iterate `this`, keep those NOT in other).
|
|
1450
|
+
return setBin(a, b, 'f64', (aF, bF) => buildSet((dstT, dI) => [
|
|
1451
|
+
filterInto(dI, aF, ['i32.const', SET_ENTRY], bF, isMapI32(bF), 0),
|
|
1452
|
+
], 'setd')) }
|
|
1453
|
+
|
|
1454
|
+
ctx.core.emit['.set:symmetricDifference'] = (a, b) => { inc('__set_filter')
|
|
1455
|
+
// (A not in B, A's order) then (B not in A, B's order).
|
|
1456
|
+
return setBin(a, b, 'f64', (aF, bF) => buildSet((dstT, dI) => [
|
|
1457
|
+
filterInto(dI, aF, ['i32.const', SET_ENTRY], bF, isMapI32(bF), 0),
|
|
1458
|
+
filterInto(dI, bF, strideRT(bF), aF, ['i32.const', 0], 0),
|
|
1459
|
+
], 'setx')) }
|
|
1460
|
+
|
|
1461
|
+
ctx.core.emit['.set:isSubsetOf'] = (a, b) => { inc('__set_all')
|
|
1462
|
+
// every key of A is in B
|
|
1463
|
+
return setBin(a, b, 'i32', (aF, bF) => allMatch(aF, ['i32.const', SET_ENTRY], bF, isMapI32(bF), 1)) }
|
|
1464
|
+
|
|
1465
|
+
ctx.core.emit['.set:isSupersetOf'] = (a, b) => { inc('__set_all')
|
|
1466
|
+
// every key of B is in A (walk B; A is always a Set → otherIsMap=0)
|
|
1467
|
+
return setBin(a, b, 'i32', (aF, bF) => allMatch(bF, strideRT(bF), aF, ['i32.const', 0], 1)) }
|
|
1468
|
+
|
|
1469
|
+
ctx.core.emit['.set:isDisjointFrom'] = (a, b) => { inc('__set_all')
|
|
1470
|
+
// no key of A is in B
|
|
1471
|
+
return setBin(a, b, 'i32', (aF, bF) => allMatch(aF, ['i32.const', SET_ENTRY], bF, isMapI32(bF), 0)) }
|
|
1472
|
+
|
|
1473
|
+
// === ES2024 Object.groupBy / Map.groupBy ===
|
|
1474
|
+
// Both bucket items by cb(item, i): Object.groupBy keys a dictionary (HASH)
|
|
1475
|
+
// by ToPropertyKey → __to_str; Map.groupBy keys a Map by SameValueZero (raw
|
|
1476
|
+
// boxed value). Buckets are plain arrays appended in iteration order. The
|
|
1477
|
+
// source normalizes through __iter_arr (Array/String/TypedArray pass through,
|
|
1478
|
+
// Set→keys, Map→entries) and reads elements via the polymorphic __typed_idx.
|
|
1479
|
+
const emitGroupBy = (isMap) => (items, fn) => {
|
|
1480
|
+
inc('__iter_arr', '__len', '__typed_idx', '__arr_push1')
|
|
1481
|
+
inc(...(isMap ? ['__map_set', '__map_get'] : ['__hash_new', '__hash_set', '__hash_get', '__to_str']))
|
|
1482
|
+
const recv = temp('gbs'), cb = temp('gbc'), result = temp('gbr')
|
|
1483
|
+
const len = tempI32('gbl'), i = tempI32('gbi')
|
|
1484
|
+
const item = temp('gbv'), key = tempI64('gbk'), bucket = temp('gbb')
|
|
1485
|
+
const id = ctx.func.uniq++
|
|
1486
|
+
const resI64 = ['i64.reinterpret_f64', ['local.get', `$${result}`]]
|
|
1487
|
+
const nb = allocPtr({ type: PTR.ARRAY, len: 0, cap: 0, tag: 'gbn' })
|
|
1488
|
+
const initResult = isMap
|
|
1489
|
+
? (() => { const out = allocPtr({ type: PTR.MAP, len: 0, cap: INIT_CAP, stride: MAP_ENTRY, tag: 'gbm' })
|
|
1490
|
+
return ['block', ['result', 'f64'], out.init, out.ptr] })()
|
|
1491
|
+
: ['call', '$__hash_new']
|
|
1492
|
+
const keyOf = (cbResult) => isMap ? asI64(cbResult) : ['call', '$__to_str', asI64(cbResult)]
|
|
1493
|
+
const get = isMap ? '$__map_get' : '$__hash_get'
|
|
1494
|
+
const set = isMap ? '$__map_set' : '$__hash_set'
|
|
1495
|
+
ctx.runtime.throws = true
|
|
1496
|
+
return typed(['block', ['result', 'f64'],
|
|
1497
|
+
['local.set', `$${recv}`, asF64(emit(['()', '__iter_arr', items]))],
|
|
1498
|
+
['local.set', `$${cb}`, asF64(emit(fn))],
|
|
1499
|
+
// spec GroupBy step 2: IsCallable(callbackfn) — throw before iterating,
|
|
1500
|
+
// not an indirect-call trap mid-loop
|
|
1501
|
+
['if', ['i32.eqz', ptrTypeEq(typed(['local.get', `$${cb}`], 'f64'), PTR.CLOSURE)],
|
|
1502
|
+
['then', ['throw', '$__jz_err', ['f64.const', 0]]]],
|
|
1503
|
+
['local.set', `$${result}`, initResult],
|
|
1504
|
+
['local.set', `$${len}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${recv}`]]]],
|
|
1505
|
+
['local.set', `$${i}`, ['i32.const', 0]],
|
|
1506
|
+
['block', `$gbrk${id}`, ['loop', `$gloop${id}`,
|
|
1507
|
+
['br_if', `$gbrk${id}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${len}`]]],
|
|
1508
|
+
['local.set', `$${item}`, ['call', '$__typed_idx', ['i64.reinterpret_f64', ['local.get', `$${recv}`]], ['local.get', `$${i}`]]],
|
|
1509
|
+
['local.set', `$${key}`, keyOf(ctx.closure.call(typed(['local.get', `$${cb}`], 'f64'),
|
|
1510
|
+
[typed(['local.get', `$${item}`], 'f64'), typed(['f64.convert_i32_s', ['local.get', `$${i}`]], 'f64')]))],
|
|
1511
|
+
['local.set', `$${bucket}`, ['f64.reinterpret_i64', ['call', get, resI64, ['local.get', `$${key}`]]]],
|
|
1512
|
+
// miss → fresh bucket, insert it (each distinct key allocates once)
|
|
1513
|
+
['if', ['i64.eq', ['i64.reinterpret_f64', ['local.get', `$${bucket}`]], ['i64.const', UNDEF_NAN]],
|
|
1514
|
+
['then',
|
|
1515
|
+
nb.init,
|
|
1516
|
+
['local.set', `$${bucket}`, nb.ptr],
|
|
1517
|
+
['local.set', `$${result}`, ['f64.reinterpret_i64',
|
|
1518
|
+
['call', set, resI64, ['local.get', `$${key}`], ['i64.reinterpret_f64', ['local.get', `$${bucket}`]]]]]]],
|
|
1519
|
+
// push may relocate the bucket — re-store the (possibly moved) pointer
|
|
1520
|
+
['local.set', `$${bucket}`, ['call', '$__arr_push1', ['i64.reinterpret_f64', ['local.get', `$${bucket}`]], typed(['local.get', `$${item}`], 'f64')]],
|
|
1521
|
+
['local.set', `$${result}`, ['f64.reinterpret_i64',
|
|
1522
|
+
['call', set, resI64, ['local.get', `$${key}`], ['i64.reinterpret_f64', ['local.get', `$${bucket}`]]]]],
|
|
1523
|
+
['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
|
|
1524
|
+
['br', `$gloop${id}`]]],
|
|
1525
|
+
['local.get', `$${result}`]], 'f64')
|
|
1526
|
+
}
|
|
1527
|
+
ctx.core.emit['Object.groupBy'] = emitGroupBy(false)
|
|
1528
|
+
ctx.core.emit['Map.groupBy'] = emitGroupBy(true)
|
|
1529
|
+
|
|
1530
|
+
// === structuredClone — deep arena clone ===
|
|
1531
|
+
// Walks the value graph copying every mutable container: arrays, schema
|
|
1532
|
+
// objects (incl. branded Dates), dictionary HASHes, Set/Map (insertion order
|
|
1533
|
+
// kept; Map keys AND values cloned, like the host), typed arrays, DataViews
|
|
1534
|
+
// and ArrayBuffers (a buffer shared by several views stays shared in the
|
|
1535
|
+
// clone). Numbers/atoms are immediate and strings immutable — passed through.
|
|
1536
|
+
// Closures and host handles raise (the host's DataCloneError). The `transfer`
|
|
1537
|
+
// option is out of model (arena memory has nothing to detach) and ignored.
|
|
1538
|
+
//
|
|
1539
|
+
// Identity memo: a real MAP keyed by boxed-pointer bits (SameValueZero on
|
|
1540
|
+
// non-string pointers ≡ bit identity) — cycles terminate, diamond sharing
|
|
1541
|
+
// dedupes. Every clone target is allocated at its final capacity, so no fill
|
|
1542
|
+
// can trigger a grow: memo'd pointers stay canonical and `===` identity
|
|
1543
|
+
// inside the cloned graph holds bit-exactly. The memo itself may grow, but
|
|
1544
|
+
// __map_set forward-marks, so a stale $memo in an outer frame still resolves.
|
|
1545
|
+
|
|
1546
|
+
ctx.core.stdlib['__sclone'] = `(func $__sclone (param $v f64) (result f64)
|
|
1547
|
+
(call $__sclone_rec (local.get $v)
|
|
1548
|
+
(i64.reinterpret_f64 (call $__mkptr (i32.const ${PTR.MAP}) (i32.const 0)
|
|
1549
|
+
(call $__alloc_hdr_n (i32.const 0) (i32.const ${INIT_CAP}) (i32.const ${MAP_ENTRY}))))))`
|
|
1550
|
+
|
|
1551
|
+
// Deep-clone the values of a freshly copied HASH table, in place.
|
|
1552
|
+
ctx.core.stdlib['__sclone_hash_vals'] = `(func $__sclone_hash_vals (param $off i32) (param $memo i64)
|
|
1553
|
+
(local $cap i32) (local $i i32) (local $slot i32)
|
|
1554
|
+
(local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
|
|
1555
|
+
(block $d (loop $l
|
|
1556
|
+
(br_if $d (i32.ge_s (local.get $i) (local.get $cap)))
|
|
1557
|
+
(local.set $slot (i32.add (local.get $off) (i32.mul (local.get $i) (i32.const ${MAP_ENTRY}))))
|
|
1558
|
+
(if (i32.and
|
|
1559
|
+
(i64.ne (i64.load (local.get $slot)) (i64.const 0))
|
|
1560
|
+
(i64.ne (i64.load (i32.add (local.get $slot) (i32.const 8))) (i64.const ${TOMB_NAN})))
|
|
1561
|
+
(then (i64.store (i32.add (local.get $slot) (i32.const 16))
|
|
1562
|
+
(i64.reinterpret_f64 (call $__sclone_rec
|
|
1563
|
+
(f64.reinterpret_i64 (i64.load (i32.add (local.get $slot) (i32.const 16))))
|
|
1564
|
+
(local.get $memo))))))
|
|
1565
|
+
(local.set $i (i32.add (local.get $i) (i32.const 1)))
|
|
1566
|
+
(br $l))))`
|
|
1567
|
+
|
|
1568
|
+
ctx.core.stdlib['__sclone_rec'] = () => {
|
|
1569
|
+
// Guarded: template expansion (pullStdlib) runs AFTER assemble's schema-table
|
|
1570
|
+
// build — an unconditional declGlobal would reset the freshly-baked init offset
|
|
1571
|
+
// back to 0 and every schema consumer would see an empty table.
|
|
1572
|
+
if (!ctx.scope.globals.has('__schema_tbl')) declGlobal('__schema_tbl', 'i32')
|
|
1573
|
+
return `(func $__sclone_rec (param $v f64) (param $memo i64) (result f64)
|
|
1574
|
+
(local $bits i64) (local $t i32) (local $hit i64) (local $out f64) (local $side i64)
|
|
1575
|
+
(local $src i32) (local $dst i32) (local $n i32) (local $cap i32) (local $i i32)
|
|
1576
|
+
(local $slot i32) (local $ord i32) (local $stride i32) (local $aux i32) (local $root i32) (local $newroot i32)
|
|
1577
|
+
;; ordinary numbers (incl. ±Infinity) are immediate
|
|
1578
|
+
(if (f64.eq (local.get $v) (local.get $v)) (then (return (local.get $v))))
|
|
1579
|
+
(local.set $bits (i64.reinterpret_f64 (local.get $v)))
|
|
1580
|
+
;; negative-NaN bit patterns are numeric NaN, never boxes
|
|
1581
|
+
(if (i64.eq (i64.and (local.get $bits) (i64.const 0xFFF0000000000000)) (i64.const 0xFFF0000000000000))
|
|
1582
|
+
(then (return (local.get $v))))
|
|
1583
|
+
(local.set $t (call $__ptr_type (local.get $bits)))
|
|
1584
|
+
;; atoms (canonical NaN / undefined / null / booleans) + immutable strings: share
|
|
1585
|
+
(if (i32.or (i32.eq (local.get $t) (i32.const ${PTR.ATOM})) (i32.eq (local.get $t) (i32.const ${PTR.STRING})))
|
|
1586
|
+
(then (return (local.get $v))))
|
|
1587
|
+
;; functions / host handles: DataCloneError
|
|
1588
|
+
(if (i32.or (i32.eq (local.get $t) (i32.const ${PTR.CLOSURE})) (i32.eq (local.get $t) (i32.const ${PTR.EXTERNAL})))
|
|
1589
|
+
(then (throw $__jz_err (f64.const 0))))
|
|
1590
|
+
;; already cloned? (cycle / diamond sharing) — __map_get yields raw i64 bits
|
|
1591
|
+
(local.set $hit (call $__map_get (local.get $memo) (local.get $bits)))
|
|
1592
|
+
(if (i32.eqz (call $__is_nullish (local.get $hit))) (then (return (f64.reinterpret_i64 (local.get $hit)))))
|
|
1593
|
+
|
|
1594
|
+
(if (i32.eq (local.get $t) (i32.const ${PTR.ARRAY}))
|
|
1595
|
+
(then
|
|
1596
|
+
(local.set $out (call $__arr_from (local.get $bits)))
|
|
1597
|
+
(drop (call $__map_set (local.get $memo) (local.get $bits) (i64.reinterpret_f64 (local.get $out))))
|
|
1598
|
+
(local.set $dst (call $__ptr_offset (i64.reinterpret_f64 (local.get $out))))
|
|
1599
|
+
(local.set $n (call $__len (local.get $bits)))
|
|
1600
|
+
(block $ad (loop $al
|
|
1601
|
+
(br_if $ad (i32.ge_s (local.get $i) (local.get $n)))
|
|
1602
|
+
(local.set $slot (i32.add (local.get $dst) (i32.shl (local.get $i) (i32.const 3))))
|
|
1603
|
+
(f64.store (local.get $slot) (call $__sclone_rec (f64.load (local.get $slot)) (local.get $memo)))
|
|
1604
|
+
(local.set $i (i32.add (local.get $i) (i32.const 1)))
|
|
1605
|
+
(br $al)))
|
|
1606
|
+
(return (local.get $out))))
|
|
1607
|
+
|
|
1608
|
+
(if (i32.eq (local.get $t) (i32.const ${PTR.OBJECT}))
|
|
1609
|
+
(then
|
|
1610
|
+
(local.set $out (call $__obj_clone (local.get $v)))
|
|
1611
|
+
(drop (call $__map_set (local.get $memo) (local.get $bits) (i64.reinterpret_f64 (local.get $out))))
|
|
1612
|
+
;; deep the schema slots — slot count via aux → schema table, like __obj_clone
|
|
1613
|
+
(if (i32.ne (global.get $__schema_tbl) (i32.const 0))
|
|
1614
|
+
(then (local.set $n (call $__len
|
|
1615
|
+
(i64.load (i32.add (global.get $__schema_tbl) (i32.shl (call $__ptr_aux (local.get $bits)) (i32.const 3))))))))
|
|
1616
|
+
(local.set $dst (call $__ptr_offset (i64.reinterpret_f64 (local.get $out))))
|
|
1617
|
+
(block $od (loop $ol
|
|
1618
|
+
(br_if $od (i32.ge_s (local.get $i) (local.get $n)))
|
|
1619
|
+
(local.set $slot (i32.add (local.get $dst) (i32.shl (local.get $i) (i32.const 3))))
|
|
1620
|
+
(f64.store (local.get $slot) (call $__sclone_rec (f64.load (local.get $slot)) (local.get $memo)))
|
|
1621
|
+
(local.set $i (i32.add (local.get $i) (i32.const 1)))
|
|
1622
|
+
(br $ol)))
|
|
1623
|
+
;; deep the dyn-props sidecar's values (__obj_clone already re-tabled it)
|
|
1624
|
+
(local.set $side (i64.load (i32.sub (local.get $dst) (i32.const 16))))
|
|
1625
|
+
(if (i32.eq (call $__ptr_type (local.get $side)) (i32.const ${PTR.HASH}))
|
|
1626
|
+
(then (call $__sclone_hash_vals (call $__ptr_offset (local.get $side)) (local.get $memo))))
|
|
1627
|
+
(return (local.get $out))))
|
|
1628
|
+
|
|
1629
|
+
(if (i32.eq (local.get $t) (i32.const ${PTR.HASH}))
|
|
1630
|
+
(then
|
|
1631
|
+
(local.set $out (call $__obj_clone (local.get $v)))
|
|
1632
|
+
(drop (call $__map_set (local.get $memo) (local.get $bits) (i64.reinterpret_f64 (local.get $out))))
|
|
1633
|
+
(call $__sclone_hash_vals (call $__ptr_offset (i64.reinterpret_f64 (local.get $out))) (local.get $memo))
|
|
1634
|
+
(return (local.get $out))))
|
|
1635
|
+
|
|
1636
|
+
(if (i32.or (i32.eq (local.get $t) (i32.const ${PTR.SET})) (i32.eq (local.get $t) (i32.const ${PTR.MAP})))
|
|
1637
|
+
(then
|
|
1638
|
+
(local.set $stride (select (i32.const ${MAP_ENTRY}) (i32.const ${SET_ENTRY}) (i32.eq (local.get $t) (i32.const ${PTR.MAP}))))
|
|
1639
|
+
(local.set $src (call $__ptr_offset (local.get $bits)))
|
|
1640
|
+
(local.set $cap (i32.load (i32.sub (local.get $src) (i32.const 4))))
|
|
1641
|
+
(local.set $out (call $__mkptr (local.get $t) (i32.const 0)
|
|
1642
|
+
(call $__alloc_hdr_n (i32.const 0) (local.get $cap) (local.get $stride))))
|
|
1643
|
+
(drop (call $__map_set (local.get $memo) (local.get $bits) (i64.reinterpret_f64 (local.get $out))))
|
|
1644
|
+
;; walk the source in insertion order; ≤len inserts into cap slots never grow,
|
|
1645
|
+
;; so $out's bits stay canonical (the memo entry above remains the pointer)
|
|
1646
|
+
(local.set $n (i32.load (i32.sub (local.get $src) (i32.const 8))))
|
|
1647
|
+
(local.set $ord (call $__coll_order (local.get $src) (local.get $cap) (local.get $stride)))
|
|
1648
|
+
(block $cd (loop $cl
|
|
1649
|
+
(br_if $cd (i32.ge_s (local.get $i) (local.get $n)))
|
|
1650
|
+
(local.set $slot (i32.load (i32.add (local.get $ord) (i32.shl (local.get $i) (i32.const 2)))))
|
|
1651
|
+
(if (i32.eq (local.get $t) (i32.const ${PTR.MAP}))
|
|
1652
|
+
(then (drop (call $__map_set (i64.reinterpret_f64 (local.get $out))
|
|
1653
|
+
(i64.reinterpret_f64 (call $__sclone_rec (f64.reinterpret_i64 (i64.load (i32.add (local.get $slot) (i32.const 8)))) (local.get $memo)))
|
|
1654
|
+
(i64.reinterpret_f64 (call $__sclone_rec (f64.reinterpret_i64 (i64.load (i32.add (local.get $slot) (i32.const 16)))) (local.get $memo))))))
|
|
1655
|
+
(else (drop (call $__set_add (i64.reinterpret_f64 (local.get $out))
|
|
1656
|
+
(i64.reinterpret_f64 (call $__sclone_rec (f64.reinterpret_i64 (i64.load (i32.add (local.get $slot) (i32.const 8)))) (local.get $memo)))))))
|
|
1657
|
+
(local.set $i (i32.add (local.get $i) (i32.const 1)))
|
|
1658
|
+
(br $cl)))
|
|
1659
|
+
(return (local.get $out))))
|
|
1660
|
+
|
|
1661
|
+
(if (i32.eq (local.get $t) (i32.const ${PTR.TYPED}))
|
|
1662
|
+
(then
|
|
1663
|
+
(local.set $aux (call $__ptr_aux (local.get $bits)))
|
|
1664
|
+
(local.set $src (call $__ptr_offset (local.get $bits)))
|
|
1665
|
+
(if (i32.and (local.get $aux) (i32.const 8))
|
|
1666
|
+
(then
|
|
1667
|
+
;; view: clone the root buffer once — memo'd as its boxed BUFFER value,
|
|
1668
|
+
;; the exact key .buffer reconstructs — and rebase the descriptor
|
|
1669
|
+
(local.set $root (i32.load (i32.add (local.get $src) (i32.const 8))))
|
|
1670
|
+
(local.set $newroot (call $__ptr_offset (i64.reinterpret_f64
|
|
1671
|
+
(call $__sclone_rec (call $__mkptr (i32.const ${PTR.BUFFER}) (i32.const 0) (local.get $root)) (local.get $memo)))))
|
|
1672
|
+
(local.set $dst (call $__alloc (i32.const 16)))
|
|
1673
|
+
(i32.store (local.get $dst) (i32.load (local.get $src)))
|
|
1674
|
+
(i32.store (i32.add (local.get $dst) (i32.const 4))
|
|
1675
|
+
(i32.add (local.get $newroot) (i32.sub (i32.load (i32.add (local.get $src) (i32.const 4))) (local.get $root))))
|
|
1676
|
+
(i32.store (i32.add (local.get $dst) (i32.const 8)) (local.get $newroot))
|
|
1677
|
+
(i32.store (i32.add (local.get $dst) (i32.const 12)) (i32.const 0)))
|
|
1678
|
+
(else
|
|
1679
|
+
;; owned storage: raw byte copy (header len is in bytes)
|
|
1680
|
+
(local.set $n (i32.load (i32.sub (local.get $src) (i32.const 8))))
|
|
1681
|
+
(local.set $dst (call $__alloc_hdr_n (local.get $n) (local.get $n) (i32.const 1)))
|
|
1682
|
+
(memory.copy (local.get $dst) (local.get $src) (local.get $n))))
|
|
1683
|
+
(local.set $out (call $__mkptr (i32.const ${PTR.TYPED}) (local.get $aux) (local.get $dst)))
|
|
1684
|
+
(drop (call $__map_set (local.get $memo) (local.get $bits) (i64.reinterpret_f64 (local.get $out))))
|
|
1685
|
+
(return (local.get $out))))
|
|
1686
|
+
|
|
1687
|
+
(if (i32.eq (local.get $t) (i32.const ${PTR.BUFFER}))
|
|
1688
|
+
(then
|
|
1689
|
+
(local.set $src (call $__ptr_offset (local.get $bits)))
|
|
1690
|
+
(local.set $n (i32.load (i32.sub (local.get $src) (i32.const 8))))
|
|
1691
|
+
(local.set $dst (call $__alloc_hdr_n (local.get $n) (local.get $n) (i32.const 1)))
|
|
1692
|
+
(memory.copy (local.get $dst) (local.get $src) (local.get $n))
|
|
1693
|
+
(local.set $out (call $__mkptr (i32.const ${PTR.BUFFER}) (i32.const 0) (local.get $dst)))
|
|
1694
|
+
(drop (call $__map_set (local.get $memo) (local.get $bits) (i64.reinterpret_f64 (local.get $out))))
|
|
1695
|
+
(return (local.get $out))))
|
|
1696
|
+
|
|
1697
|
+
;; unrecognized tag — pass through
|
|
1698
|
+
(local.get $v))`
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
// structuredClone(value[, options]) — options.transfer ignored (see above).
|
|
1702
|
+
ctx.core.emit['structuredClone'] = (val, _opts) => {
|
|
1703
|
+
inc('__sclone')
|
|
1704
|
+
// __obj_clone/__sclone_rec read $__schema_tbl but only join includes via the
|
|
1705
|
+
// pullStdlib dep-closure — after assemble has already decided whether to build
|
|
1706
|
+
// the table. Flag the consumption explicitly (same hook as the enumeration
|
|
1707
|
+
// scaffolds), or every OBJECT clones as zero slots.
|
|
1708
|
+
ctx.runtime.schemaTblConsumed = true
|
|
1709
|
+
// carrierF64: a bare boolean arg rides as a 0/1 carrier — box it to the
|
|
1710
|
+
// TRUE/FALSE atom so the clone round-trips `true`, not the number 1.
|
|
1711
|
+
return typed(['call', '$__sclone', carrierF64(val, emit(val))], 'f64')
|
|
1712
|
+
}
|
|
1713
|
+
|
|
871
1714
|
// Generated Map probe functions
|
|
872
1715
|
ctx.core.stdlib['__map_set'] = () => genUpsert('__map_set', MAP_ENTRY, '$__map_hash', sameValueZeroEqG, PTR.MAP, true, ctx.features.external)
|
|
873
1716
|
ctx.core.stdlib['__map_get'] = () => genLookup('__map_get', MAP_ENTRY, '$__map_hash', sameValueZeroEqG, PTR.MAP, true, ctx.features.external)
|
|
@@ -923,33 +1766,46 @@ export default (ctx) => {
|
|
|
923
1766
|
|
|
924
1767
|
// === HASH — dynamic string-keyed object (type=7) ===
|
|
925
1768
|
|
|
926
|
-
//
|
|
927
|
-
//
|
|
928
|
-
//
|
|
929
|
-
//
|
|
1769
|
+
// SSO branch: fixed 7-op avalanche mix over the packed NaN-box lo/hi, replacing the
|
|
1770
|
+
// old 6-iteration per-char FNV loop (~36 ops worst case). lo = payload bits 0-31
|
|
1771
|
+
// (the $off field, which for an SSO ptr IS the packed chars 0-3 + low nibble of
|
|
1772
|
+
// char 4 — no memory, no per-char extraction), hi = aux masked to 13 bits (length
|
|
1773
|
+
// + char 4-5 tail; SSO_BIT itself sits at aux bit 14, outside the mask, so it never
|
|
1774
|
+
// pollutes the mix). Same lo/hi pair strHashLiteral (module/collection.js, JS side)
|
|
1775
|
+
// builds from ssoEncode's {offset, aux} — the two MUST compute the identical
|
|
1776
|
+
// constant-for-constant mix or literal-prehashed probes silently miss. Heap strings
|
|
1777
|
+
// (>6 bytes or non-ASCII) are unaffected: they keep byte-FNV-1a below.
|
|
1778
|
+
// ~95M calls in watr self-host; SSO is the overwhelming majority post-invariant
|
|
1779
|
+
// (ec6a229: any ≤6-byte ASCII string IS SSO).
|
|
930
1780
|
ctx.core.stdlib['__str_hash'] = `(func $__str_hash (param $s i64) (result i32)
|
|
931
|
-
(local $h i32) (local $len i32) (local $lenA i32) (local $i i32) (local $t i32) (local $off i32) (local $aux i32) (local $w i32)
|
|
932
|
-
(local.set $h (i32.const 0x811c9dc5))
|
|
1781
|
+
(local $h i32) (local $len i32) (local $lenA i32) (local $i i32) (local $t i32) (local $off i32) (local $aux i32) (local $w i32) (local $hi i32) (local $cs i32)
|
|
933
1782
|
(local.set $t (i32.wrap_i64 (i64.and (i64.shr_u (local.get $s) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
|
|
934
1783
|
(local.set $off (i32.wrap_i64 (i64.and (local.get $s) (i64.const ${LAYOUT.OFFSET_MASK}))))
|
|
935
1784
|
(local.set $aux (i32.wrap_i64 (i64.and (i64.shr_u (local.get $s) (i64.const ${LAYOUT.AUX_SHIFT})) (i64.const ${LAYOUT.AUX_MASK}))))
|
|
936
1785
|
(if (i32.and (i32.eq (local.get $t) (i32.const ${PTR.STRING})) (i32.shr_u (local.get $aux) (i32.const 14)))
|
|
937
1786
|
(then
|
|
938
|
-
(local.set $
|
|
939
|
-
(
|
|
940
|
-
(
|
|
941
|
-
(
|
|
942
|
-
|
|
943
|
-
(i32.wrap_i64 (i64.and (i64.shr_u (local.get $s) (i64.mul (i64.extend_i32_u (local.get $i)) (i64.const 7))) (i64.const 0x7f))))
|
|
944
|
-
(i32.const 0x01000193)))
|
|
945
|
-
(local.set $i (i32.add (local.get $i) (i32.const 1)))
|
|
946
|
-
(br $ls))))
|
|
1787
|
+
(local.set $hi (i32.and (local.get $aux) (i32.const 0x1FFF)))
|
|
1788
|
+
(local.set $h (i32.mul
|
|
1789
|
+
(i32.xor (local.get $off) (i32.mul (i32.xor (local.get $hi) (i32.const 0x9E3779B9)) (i32.const 0x85EBCA6B)))
|
|
1790
|
+
(i32.const 0xC2B2AE35)))
|
|
1791
|
+
(local.set $h (i32.xor (local.get $h) (i32.shr_u (local.get $h) (i32.const 15)))))
|
|
947
1792
|
(else
|
|
1793
|
+
(local.set $h (i32.const 0x811c9dc5))
|
|
948
1794
|
;; canonical interned static: cached post-clamp FNV at -8 (see layout.js)
|
|
949
1795
|
(if (i32.and (i32.eq (local.get $t) (i32.const ${PTR.STRING}))
|
|
950
1796
|
(i32.and (i32.ge_u (local.get $off) (i32.const 8))
|
|
951
1797
|
(i32.eq (i32.and (local.get $aux) (i32.const ${LAYOUT.SSO_BIT | LAYOUT.SLICE_BIT | STR_INTERN_BIT})) (i32.const ${STR_INTERN_BIT}))))
|
|
952
1798
|
(then (return (i32.load (i32.sub (local.get $off) (i32.const 8))))))
|
|
1799
|
+
;; runtime-built heap string with a lazy hash cell at -8 (STR_HCACHE_BIT,
|
|
1800
|
+
;; layout.js): 0 = uncomputed — fall through to the FNV walk and fill it
|
|
1801
|
+
;; below. Mask excludes SLICE (its aux[12:0] is a length, bit 1 incidental).
|
|
1802
|
+
(if (i32.and (i32.eq (local.get $t) (i32.const ${PTR.STRING}))
|
|
1803
|
+
(i32.eq (i32.and (local.get $aux) (i32.const ${LAYOUT.SLICE_BIT | STR_HCACHE_BIT})) (i32.const ${STR_HCACHE_BIT})))
|
|
1804
|
+
(then
|
|
1805
|
+
(local.set $cs (i32.sub (local.get $off) (i32.const 8)))
|
|
1806
|
+
(local.set $h (i32.load (local.get $cs)))
|
|
1807
|
+
(if (local.get $h) (then (return (local.get $h))))
|
|
1808
|
+
(local.set $h (i32.const 0x811c9dc5))))
|
|
953
1809
|
(if (i32.and (i32.eq (local.get $t) (i32.const ${PTR.STRING})) (i32.ge_u (local.get $off) (i32.const 4)))
|
|
954
1810
|
(then (local.set $len (i32.load (i32.sub (local.get $off) (i32.const 4))))))
|
|
955
1811
|
;; 4-byte unrolled FNV-1a: each iter loads i32, mixes 4 bytes (little-endian) sequentially.
|
|
@@ -972,8 +1828,11 @@ export default (ctx) => {
|
|
|
972
1828
|
(local.set $i (i32.add (local.get $i) (i32.const 1)))
|
|
973
1829
|
(br $lh)))))
|
|
974
1830
|
;; Ensure >= 2 (0=empty, 1=tombstone)
|
|
975
|
-
(if (
|
|
976
|
-
(then (i32.add (local.get $h) (i32.const 2)))
|
|
1831
|
+
(if (i32.le_s (local.get $h) (i32.const 1))
|
|
1832
|
+
(then (local.set $h (i32.add (local.get $h) (i32.const 2)))))
|
|
1833
|
+
;; fill the lazy hash cell (post-clamp, so 0 stays unambiguous "uncomputed")
|
|
1834
|
+
(if (local.get $cs) (then (i32.store (local.get $cs) (local.get $h))))
|
|
1835
|
+
(local.get $h))`
|
|
977
1836
|
|
|
978
1837
|
ctx.core.stdlib['__hash_new'] = `(func $__hash_new (result f64)
|
|
979
1838
|
(call $__mkptr (i32.const ${PTR.HASH}) (i32.const 0)
|
|
@@ -991,15 +1850,29 @@ export default (ctx) => {
|
|
|
991
1850
|
|
|
992
1851
|
ctx.core.stdlib['__hash_get_local'] = genLookupStrict('__hash_get_local', MAP_ENTRY, '$__str_hash', strEqG, PTR.HASH)
|
|
993
1852
|
ctx.core.stdlib['__hash_get_local_h'] = genLookupStrictPrehashed('__hash_get_local_h', MAP_ENTRY, strEqG, PTR.HASH)
|
|
994
|
-
ctx.core.stdlib['__hash_set_local_h'] = genUpsertStrictPrehashed('__hash_set_local_h', MAP_ENTRY, strEqG, PTR.HASH)
|
|
995
|
-
|
|
1853
|
+
ctx.core.stdlib['__hash_set_local_h'] = () => genUpsertStrictPrehashed('__hash_set_local_h', MAP_ENTRY, strEqG, PTR.HASH)
|
|
1854
|
+
// Thunked (not called eagerly) so genUpsertGrow's durableFwdLogIR reads
|
|
1855
|
+
// heapResetWat()'s FINAL declaration state — see collection.js's heapResetWat
|
|
1856
|
+
// comment; module load order isn't otherwise settled at the time this string
|
|
1857
|
+
// would eagerly evaluate (same reasoning as module/core.js's __obj_clone).
|
|
1858
|
+
ctx.core.stdlib['__hash_set_local'] = () => genUpsertGrow('__hash_set_local', MAP_ENTRY, '$__str_hash', strEqG, PTR.HASH, true, false, true)
|
|
1859
|
+
ctx.core.stdlib['__hash_slot'] = () => genSlotUpsert('__hash_slot', MAP_ENTRY, '$__str_hash', strEqG)
|
|
1860
|
+
// The RMW fusion's value update — the store plus the durable-heal protocol
|
|
1861
|
+
// (mirrors durableSlotLogIR exactly; kept in the kernel so heal logic has one home).
|
|
1862
|
+
ctx.core.stdlib['__slot_write'] = () => ctx.scope.globals.has('__heap_reset')
|
|
1863
|
+
? `(func $__slot_write (param $a i32) (param $v i64)
|
|
1864
|
+
(i64.store (local.get $a) (local.get $v))
|
|
1865
|
+
(if (i32.and (i32.lt_u (local.get $a) ${heapResetWat()}) (call $__is_eph_bits (local.get $v)))
|
|
1866
|
+
(then (call $__durable_slot_log (local.get $a) (i32.const 0)))))`
|
|
1867
|
+
: `(func $__slot_write (param $a i32) (param $v i64)
|
|
1868
|
+
(i64.store (local.get $a) (local.get $v)))`
|
|
996
1869
|
// Tombstones an entry in a HASH (string keys). Returns 1 if found+deleted, 0 otherwise.
|
|
997
1870
|
// Used as the bucket-level primitive for __dyn_del.
|
|
998
1871
|
ctx.core.stdlib['__hash_del_local'] = genDelete('__hash_del_local', MAP_ENTRY, '$__str_hash', strEqG, PTR.HASH)
|
|
999
1872
|
// Outer __dyn_props hash: keyed by object offset (i32 as f64 bits), value is per-object props hash.
|
|
1000
1873
|
// Uses bit-hash + i64.eq — no string allocation for the unique integer key.
|
|
1001
1874
|
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)
|
|
1002
|
-
ctx.core.stdlib['__ihash_set_local'] = genUpsertGrow('__ihash_set_local', MAP_ENTRY, '$__map_hash', '(i64.eq (i64.load (i32.add (local.get $slot) (i32.const 8))) (local.get $key))', PTR.HASH, true)
|
|
1875
|
+
ctx.core.stdlib['__ihash_set_local'] = () => genUpsertGrow('__ihash_set_local', MAP_ENTRY, '$__map_hash', '(i64.eq (i64.load (i32.add (local.get $slot) (i32.const 8))) (local.get $key))', PTR.HASH, true)
|
|
1003
1876
|
|
|
1004
1877
|
// Inline __ptr_offset (forwarding-aware) and __hash_get_local body — dyn_get is the
|
|
1005
1878
|
// single hottest stdlib symbol in watr self-host (~95M calls). props returned by
|
|
@@ -1031,10 +1904,38 @@ export default (ctx) => {
|
|
|
1031
1904
|
// when the bit-eq already decided — that bare call per schema-key step was
|
|
1032
1905
|
// the hottest __str_eq producer in the self-host (the kernel includes __jp
|
|
1033
1906
|
// for optJSON, so the fallback arm is always compiled in).
|
|
1907
|
+
// The __str_eq fallback (JSON-parsed heap keys) is prefixed by an inline
|
|
1908
|
+
// one-SSO⇒ne test when SSO is on: any SSO operand with unequal bits cannot
|
|
1909
|
+
// content-match (≤6-ASCII⇒SSO invariant, module/string.js), so the call —
|
|
1910
|
+
// the hottest __str_eq producer in the self-host — is skipped for every
|
|
1911
|
+
// SSO-keyed miss step; only heap-vs-heap candidates still pay it.
|
|
1912
|
+
// Types allocated via __alloc_hdr/__alloc_hdr_n (see core.js) reserve a 16-byte
|
|
1913
|
+
// header with a propsPtr slot at off-16: ARRAY, OBJECT, TYPED, SET, MAP. HASH is
|
|
1914
|
+
// its own storage (no sidecar — handled by its own dedicated arm). Every OTHER
|
|
1915
|
+
// type (STRING, CLOSURE, ATOM, BUFFER, REGEX, DATE, EXTERNAL, …) has NO such
|
|
1916
|
+
// slot — reading off-16 for one of them walks into whatever memory precedes it
|
|
1917
|
+
// (for a static/interned STRING literal, unrelated PRECEDING STATIC DATA) and
|
|
1918
|
+
// misreads it as a props pointer, occasionally matching the HASH tag by chance
|
|
1919
|
+
// and handing a garbage pointer to __hash_get_local_h → OOB trap. The ORIGINAL
|
|
1920
|
+
// (pre-durable-policy) code never had this gap: each header-type arm below
|
|
1921
|
+
// individually gated on its OWN type tag before ever touching off-16. The
|
|
1922
|
+
// durable-receiver policy's sidecar-check (__dyn_get_t_h, __dyn_del) must gate
|
|
1923
|
+
// on this SAME explicit set, not the broader "not HASH" the global-table probe
|
|
1924
|
+
// uses (that probe is a safe opaque off-keyed hash lookup for any type).
|
|
1925
|
+
const hasPropsSidecarWat = (typeExpr) =>
|
|
1926
|
+
`(i32.or (i32.eq ${typeExpr} (i32.const ${PTR.ARRAY}))
|
|
1927
|
+
(i32.or (i32.eq ${typeExpr} (i32.const ${PTR.OBJECT}))
|
|
1928
|
+
(i32.or (i32.eq ${typeExpr} (i32.const ${PTR.TYPED}))
|
|
1929
|
+
(i32.or (i32.eq ${typeExpr} (i32.const ${PTR.SET}))
|
|
1930
|
+
(i32.eq ${typeExpr} (i32.const ${PTR.MAP}))))))`
|
|
1034
1931
|
const schemaKeyEq = (storedKey, userKey) => ctx.core.includes.has('__jp_obj') || ctx.core.includes.has('__jp')
|
|
1035
1932
|
? `(if (result i32) (i64.eq ${storedKey} ${userKey})
|
|
1036
1933
|
(then (i32.const 1))
|
|
1037
|
-
(else
|
|
1934
|
+
(else ${ctx.features.sso
|
|
1935
|
+
? `(if (result i32) (i64.ne (i64.and (i64.or ${storedKey} ${userKey}) (i64.const ${SSO_BIT_I64})) (i64.const 0))
|
|
1936
|
+
(then (i32.const 0))
|
|
1937
|
+
(else (call $__str_eq ${storedKey} ${userKey})))`
|
|
1938
|
+
: `(call $__str_eq ${storedKey} ${userKey})`}))`
|
|
1038
1939
|
: `(i64.eq ${storedKey} ${userKey})`
|
|
1039
1940
|
const buildObjectSchemaArm = () => (ctx.schema.list.length > 0 || ctx.core.includes.has('__jp_obj')) ? `
|
|
1040
1941
|
(if (i32.eq (local.get $type) (i32.const ${PTR.OBJECT}))
|
|
@@ -1086,22 +1987,79 @@ export default (ctx) => {
|
|
|
1086
1987
|
(local.set $idx (i32.add (local.get $idx) (i32.const 1)))
|
|
1087
1988
|
(br $schemaSetLoop)))))` : ''
|
|
1088
1989
|
|
|
1990
|
+
// Canonical array-index parse of a string key: '0' | [1-9][0-9]{0,9} within
|
|
1991
|
+
// i32 range → the index, else -1. JS property semantics: a canonical numeric
|
|
1992
|
+
// string on an ARRAY receiver addresses the ELEMENT ('1' ≡ 1), so every
|
|
1993
|
+
// string-keyed dyn entry must classify before probing the props sidecar.
|
|
1994
|
+
// __char_at returns the true byte (0 only past the REAL length, which
|
|
1995
|
+
// $__str_byteLen bounds first), so embedded-NUL keys can't false-match.
|
|
1996
|
+
ctx.core.stdlib['__str_arr_idx'] = `(func $__str_arr_idx (param $key i64) (result i32)
|
|
1997
|
+
(local $len i32) (local $i i32) (local $c i32) (local $n i64)
|
|
1998
|
+
(local.set $len (call $__str_byteLen (local.get $key)))
|
|
1999
|
+
(if (i32.or (i32.eqz (local.get $len)) (i32.gt_u (local.get $len) (i32.const 10)))
|
|
2000
|
+
(then (return (i32.const -1))))
|
|
2001
|
+
(if (i32.and (i32.eq (call $__char_at (local.get $key) (i32.const 0)) (i32.const 48))
|
|
2002
|
+
(i32.gt_u (local.get $len) (i32.const 1)))
|
|
2003
|
+
(then (return (i32.const -1))))
|
|
2004
|
+
(block $bad
|
|
2005
|
+
(loop $l
|
|
2006
|
+
(if (i32.ge_u (local.get $i) (local.get $len))
|
|
2007
|
+
(then
|
|
2008
|
+
(if (i64.gt_u (local.get $n) (i64.const 2147483646)) (then (return (i32.const -1))))
|
|
2009
|
+
(return (i32.wrap_i64 (local.get $n)))))
|
|
2010
|
+
(local.set $c (i32.sub (call $__char_at (local.get $key) (local.get $i)) (i32.const 48)))
|
|
2011
|
+
(br_if $bad (i32.gt_u (local.get $c) (i32.const 9)))
|
|
2012
|
+
(local.set $n (i64.add (i64.mul (local.get $n) (i64.const 10)) (i64.extend_i32_u (local.get $c))))
|
|
2013
|
+
(local.set $i (i32.add (local.get $i) (i32.const 1)))
|
|
2014
|
+
(br $l)))
|
|
2015
|
+
(i32.const -1))`
|
|
2016
|
+
|
|
1089
2017
|
ctx.core.stdlib['__dyn_get'] = `(func $__dyn_get (param $obj i64) (param $key i64) (result i64)
|
|
1090
2018
|
(call $__dyn_get_t (local.get $obj) (local.get $key) (call $__ptr_type (local.get $obj))))`
|
|
1091
2019
|
|
|
1092
|
-
// Thin wrapper:
|
|
1093
|
-
// call sites bypass this and call $__dyn_get_t_h
|
|
2020
|
+
// Thin wrapper: ToPropertyKey the runtime key, hash it once, delegate to the
|
|
2021
|
+
// prehashed body. Constant-key call sites bypass this and call $__dyn_get_t_h
|
|
2022
|
+
// directly with strHashLiteral() (compile-time keys are already strings).
|
|
2023
|
+
// ToPropertyKey: a non-string key (number/bool/null/undefined) addresses the
|
|
2024
|
+
// same slot as its string form — o[97] ≡ o['97'] (JS spec). Writes stringify
|
|
2025
|
+
// (emit-assign's staticPropertyKey fold, __dyn_set below), so reads must too.
|
|
1094
2026
|
ctx.core.stdlib['__dyn_get_t'] = `(func $__dyn_get_t (param $obj i64) (param $key i64) (param $type i32) (result i64)
|
|
2027
|
+
(if (i32.eqz (call $__is_str_key (local.get $key)))
|
|
2028
|
+
(then (local.set $key (call $__to_str (local.get $key)))))
|
|
1095
2029
|
(call $__dyn_get_t_h (local.get $obj) (local.get $key) (local.get $type) (call $__str_hash (local.get $key))))`
|
|
1096
2030
|
|
|
1097
2031
|
ctx.core.stdlib['__dyn_get_t_h'] = () => `(func $__dyn_get_t_h (param $obj i64) (param $key i64) (param $type i32) (param $h i32) (result i64)
|
|
1098
|
-
(local $props i64) (local $off i32)
|
|
2032
|
+
(local $props i64) (local $off i32) (local $val i64)
|
|
1099
2033
|
(local $poff i32) (local $pcap i32) (local $pend i32) (local $idx i32) (local $slot i32) (local $tries i32)
|
|
1100
2034
|
${buildObjectSchemaLocals()}
|
|
1101
2035
|
;; Real-number receiver (f===f — pointers are NaN-boxed) has no props: bail before
|
|
1102
2036
|
;; treating its bits as a heap offset (\`(5).foo\`/\`(5)[k]\` → undefined, not OOB).
|
|
1103
2037
|
(if (f64.eq (f64.reinterpret_i64 (local.get $obj)) (f64.reinterpret_i64 (local.get $obj)))
|
|
1104
2038
|
(then (return (i64.const ${UNDEF_NAN}))))
|
|
2039
|
+
;; STRING receiver + 'length' key → aux/byte length directly. Strings are
|
|
2040
|
+
;; primitives — they can never carry dyn props, yet an SSO string's packed
|
|
2041
|
+
;; chars LOOK like a tiny durable heap offset, so \`op.length\` in a parser
|
|
2042
|
+
;; loop (jessie: 1.96M reads/run, ~30% of runtime, causally measured by
|
|
2043
|
+
;; probe-doubling) took the durable global-probe arm for nothing. One i64
|
|
2044
|
+
;; compare: a runtime 'length' key is ALWAYS SSO (≤6 ASCII invariant), so
|
|
2045
|
+
;; constant-vs-key bit equality is exact. Other keys fall through unchanged.
|
|
2046
|
+
;; Form-proof: the const key at _h call sites may be DATA-INTERNED rather
|
|
2047
|
+
;; than SSO, so bit-equality alone goes dead — gate on the prehashed key
|
|
2048
|
+
;; hash (compile-time constant, zero cost) and confirm content via
|
|
2049
|
+
;; __str_eq (SSO/heap-form agnostic).
|
|
2050
|
+
(if (i32.and (i32.eq (local.get $type) (i32.const ${PTR.STRING}))
|
|
2051
|
+
(i32.eq (local.get $h) (i32.const ${strHashLiteral('length')})))
|
|
2052
|
+
(then
|
|
2053
|
+
(if (call $__str_eq (local.get $key) (i64.const ${LENGTH_SSO_I64}))
|
|
2054
|
+
(then (return (i64.reinterpret_f64 (f64.convert_i32_s (call $__str_byteLen (local.get $obj)))))))))
|
|
2055
|
+
;; STRING receivers END here: strings are primitives — no dyn props, no
|
|
2056
|
+
;; sidecar, no global-table entries (writes below drop, JS semantics), so
|
|
2057
|
+
;; the probe chain can never produce a value. Method-name lookups on
|
|
2058
|
+
;; unproven string receivers (cur.charCodeAt in a parser loop — jessie:
|
|
2059
|
+
;; 1.38M reads/run, every one a guaranteed miss) made this the largest
|
|
2060
|
+
;; single dyn sink after the array-index fix.
|
|
2061
|
+
(if (i32.eq (local.get $type) (i32.const ${PTR.STRING}))
|
|
2062
|
+
(then (return (i64.const ${UNDEF_NAN}))))
|
|
1105
2063
|
(local.set $off (i32.wrap_i64 (i64.and (local.get $obj) (i64.const ${LAYOUT.OFFSET_MASK}))))
|
|
1106
2064
|
;; CLOSURE with no env (offset 0): many function refs share offset 0, so key the
|
|
1107
2065
|
;; global __dyn_props hash on the function table index (negative — can't collide
|
|
@@ -1114,20 +2072,125 @@ export default (ctx) => {
|
|
|
1114
2072
|
(block $done
|
|
1115
2073
|
(loop $follow
|
|
1116
2074
|
(br_if $done (i32.lt_u (local.get $off) (i32.const 16)))
|
|
1117
|
-
|
|
2075
|
+
;; $__heap_end64 (i64), not i32.shl (memory.size) 16: at the wasm32 ceiling
|
|
2076
|
+
;; (memory.size()==65536 pages), the i32 form overflows to exactly 0 —
|
|
2077
|
+
;; see layout.js's followForwardingWat comment for the full account.
|
|
2078
|
+
(br_if $done (i64.gt_u (i64.extend_i32_u (local.get $off)) (global.get $__heap_end64)))
|
|
1118
2079
|
(br_if $done (i32.ne (i32.load (i32.sub (local.get $off) (i32.const 4))) (i32.const -1)))
|
|
1119
2080
|
(local.set $off (i32.load (i32.sub (local.get $off) (i32.const 8))))
|
|
1120
|
-
(br $follow)))
|
|
2081
|
+
(br $follow)))
|
|
2082
|
+
;; Canonical-index string key ('1' ≡ 1, JS array-index semantics) →
|
|
2083
|
+
;; ELEMENT, not sidecar. This is the single string-keyed net covering
|
|
2084
|
+
;; every read entry (dot, expr slow path, any, prehashed const keys):
|
|
2085
|
+
;; __dyn_set routes such keys to elements, so the sidecar can never
|
|
2086
|
+
;; hold them and an in-range miss is definitively undefined.
|
|
2087
|
+
;; Inline first-char digit reject: identifier keys ('loc', 'length' —
|
|
2088
|
+
;; the kernel-hot shape on array AST nodes) skip the parse call.
|
|
2089
|
+
(if (i32.and (i32.ge_u (local.get $off) (i32.const 16))
|
|
2090
|
+
(i32.lt_u (i32.sub (if (result i32) (i64.ne (i64.and (local.get $key) (i64.const ${SSO_BIT_I64})) (i64.const 0))
|
|
2091
|
+
(then (i32.and (i32.wrap_i64 (local.get $key)) (i32.const 127)))
|
|
2092
|
+
(else (i32.load8_u (i32.wrap_i64 (i64.and (local.get $key) (i64.const ${LAYOUT.OFFSET_MASK})))))) (i32.const 48)) (i32.const 10)))
|
|
2093
|
+
(then
|
|
2094
|
+
(local.set $idx (call $__str_arr_idx (local.get $key)))
|
|
2095
|
+
(if (i32.ge_s (local.get $idx) (i32.const 0))
|
|
2096
|
+
(then
|
|
2097
|
+
(if (i32.lt_u (local.get $idx) (i32.load (i32.sub (local.get $off) (i32.const 8))))
|
|
2098
|
+
(then (return (i64.load (i32.add (local.get $off) (i32.shl (local.get $idx) (i32.const 3)))))))
|
|
2099
|
+
(return (i64.const ${UNDEF_NAN}))))))))
|
|
2100
|
+
;; DURABLE-RECEIVER POLICY: a receiver allocated at/below the post-init
|
|
2101
|
+
;; high-water mark (__heap_reset) outlives _clear, but a sidecar CREATED
|
|
2102
|
+
;; FOR IT AT RUNTIME lives in the round's arena — the receiver's header
|
|
2103
|
+
;; slot survives _clear while the sidecar behind it doesn't, so __dyn_set
|
|
2104
|
+
;; routes runtime (post-init) writes on a durable receiver to the GLOBAL
|
|
2105
|
+
;; __dyn_props table instead (which __clear resets — prop lifetime
|
|
2106
|
+
;; matches storage). BUT a durable receiver's off-16 sidecar can ALSO
|
|
2107
|
+
;; hold real data: __heap_reset is only seeded to its final value at the
|
|
2108
|
+
;; TAIL of __start, so a prop written *during* init (e.g. a module-level
|
|
2109
|
+
;; IIFE mutating an init-built table — jz's own compile.js population of
|
|
2110
|
+
;; watr's opcode dict) sees off >= __heap_reset (still low) at write time
|
|
2111
|
+
;; and lands in the sidecar — which is itself durable (allocated before
|
|
2112
|
+
;; the same tail-capture), so nothing dangles. The same receiver can thus
|
|
2113
|
+
;; carry keys in BOTH places: untouched init-time keys in the sidecar,
|
|
2114
|
+
;; keys added or reassigned at runtime in the global table. Check global
|
|
2115
|
+
;; first (a key present in both was necessarily reassigned at runtime,
|
|
2116
|
+
;; so the newer global entry wins), then the sidecar. HASH is exempted —
|
|
2117
|
+
;; it is its own storage, no sidecar/global split applies to it.
|
|
2118
|
+
(if (i32.and (i32.ne (local.get $type) (i32.const ${PTR.HASH}))
|
|
2119
|
+
(i32.lt_u (local.get $off) ${heapResetWat()}))
|
|
2120
|
+
(then
|
|
2121
|
+
;; Header-carrying durable receiver: read its off-16 word ONCE. bit0 =
|
|
2122
|
+
;; RUNTIME-SHADOWED (set by __dyn_set's global route): only then can a
|
|
2123
|
+
;; global-table entry exist, so unmarked receivers skip the probe
|
|
2124
|
+
;; entirely — 60% of jessie's 1.07M durable probes were such
|
|
2125
|
+
;; always-miss reads (causally measured via probe-doubling, ~18% of
|
|
2126
|
+
;; runtime). Root≠0 guards the post-_clear stale-marker case (the
|
|
2127
|
+
;; wiped table must not be probed through a null root). Receivers
|
|
2128
|
+
;; without a header slot keep the unconditional bloom+probe path.
|
|
2129
|
+
(if (i32.and ${hasPropsSidecarWat('(local.get $type)')} (i32.ge_u (local.get $off) (i32.const 16)))
|
|
2130
|
+
(then
|
|
2131
|
+
(local.set $props (i64.load (i32.sub (local.get $off) (i32.const 16))))
|
|
2132
|
+
;; a shifted ARRAY's word holds forwarding bytes (not 0, not
|
|
2133
|
+
;; HASH-tagged): __arr_shift migrated its props to the global
|
|
2134
|
+
;; table and CANNOT mark — such receivers keep the unconditional
|
|
2135
|
+
;; probe (jump to the no-header path below via $probe).
|
|
2136
|
+
(if (i32.eqz (i32.or (i64.eqz (local.get $props))
|
|
2137
|
+
(i32.eq (i32.wrap_i64 (i64.and (i64.shr_u (local.get $props) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))) (i32.const ${PTR.HASH}))))
|
|
2138
|
+
(then (local.set $props (i64.const 0)) (local.set $tries (i32.const -1))))
|
|
2139
|
+
(if (i32.and (i32.eq (local.get $tries) (i32.const 0))
|
|
2140
|
+
(i32.and (i32.and (i32.wrap_i64 (local.get $props)) (i32.const 1))
|
|
2141
|
+
(f64.ne (global.get $__dyn_props) (f64.const 0))))
|
|
2142
|
+
(then
|
|
2143
|
+
(if (i32.eqz ${dynPropsFilterMissIR('(local.get $off)')})
|
|
2144
|
+
(then
|
|
2145
|
+
(local.set $val (call $__ihash_get_local (i64.reinterpret_f64 (global.get $__dyn_props))
|
|
2146
|
+
(i64.reinterpret_f64 (f64.convert_i32_s (local.get $off)))))
|
|
2147
|
+
(if (i32.eqz (call $__is_nullish (local.get $val)))
|
|
2148
|
+
(then
|
|
2149
|
+
(local.set $val (call $__hash_get_local_h (local.get $val) (local.get $key) (local.get $h)))
|
|
2150
|
+
(if (i64.ne (local.get $val) (i64.const ${UNDEF_NAN})) (then (return (local.get $val))))))))))
|
|
2151
|
+
(local.set $props (i64.and (local.get $props) (i64.const -2)))
|
|
2152
|
+
(if (i32.eq
|
|
2153
|
+
(i32.wrap_i64 (i64.and (i64.shr_u (local.get $props) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK})))
|
|
2154
|
+
(i32.const ${PTR.HASH}))
|
|
2155
|
+
(then (return (call $__hash_get_local_h (local.get $props) (local.get $key) (local.get $h))))))
|
|
2156
|
+
(else (local.set $tries (i32.const -1))))
|
|
2157
|
+
(if (i32.eq (local.get $tries) (i32.const -1))
|
|
2158
|
+
(then
|
|
2159
|
+
(local.set $tries (i32.const 0))
|
|
2160
|
+
(if (i32.eqz ${dynPropsFilterMissIR('(local.get $off)')})
|
|
2161
|
+
(then
|
|
2162
|
+
(local.set $props (call $__ihash_get_local (i64.reinterpret_f64 (global.get $__dyn_props))
|
|
2163
|
+
(i64.reinterpret_f64 (f64.convert_i32_s (local.get $off)))))
|
|
2164
|
+
(if (i32.eqz (call $__is_nullish (local.get $props)))
|
|
2165
|
+
(then
|
|
2166
|
+
(local.set $val (call $__hash_get_local_h (local.get $props) (local.get $key) (local.get $h)))
|
|
2167
|
+
(if (i64.ne (local.get $val) (i64.const ${UNDEF_NAN})) (then (return (local.get $val))))))))))
|
|
2168
|
+
;; Miss on both global and sidecar: an OBJECT still needs the
|
|
2169
|
+
;; schema-slot arm before giving up — a schema-poisoned variable
|
|
2170
|
+
;; (one variable bound to two different object shapes) resolves its
|
|
2171
|
+
;; field via the runtime schemaId lookup, not via any dyn-props
|
|
2172
|
+
;; path. A durable such object has no dyn props at all, so both
|
|
2173
|
+
;; checks above always miss for it and this must still run before
|
|
2174
|
+
;; concluding UNDEF. Self-contained here (not a fallthrough into the
|
|
2175
|
+
;; block below) so this arm's control flow never depends on the
|
|
2176
|
+
;; ephemeral-only header arms or their shared global-fallback code.
|
|
2177
|
+
${buildObjectSchemaArm()}
|
|
2178
|
+
(return (i64.const ${UNDEF_NAN}))))
|
|
1121
2179
|
(block $dynDone
|
|
1122
2180
|
(block $haveProps
|
|
2181
|
+
;; Ephemeral-only from here down (durable receivers already
|
|
2182
|
+
;; returned above) — the off >= __heap_reset conjuncts below are
|
|
2183
|
+
;; therefore always true, kept for defensive clarity and a single
|
|
2184
|
+
;; source of truth with __dyn_set/__dyn_del's mirrored gates.
|
|
1123
2185
|
;; ARRAY: header propsPtr at $off-16 is valid only when shift hasn't
|
|
1124
2186
|
;; rewritten the slot with forwarding bytes. Validate via HASH tag —
|
|
1125
2187
|
;; rejects 0 (no props) and forwarding garbage. Misses fall through to
|
|
1126
2188
|
;; the global hash, where __arr_shift migrates props on first .shift().
|
|
1127
2189
|
(if (i32.and (i32.eq (local.get $type) (i32.const ${PTR.ARRAY}))
|
|
1128
|
-
(i32.ge_u (local.get $off) (i32.const 16))
|
|
2190
|
+
(i32.and (i32.ge_u (local.get $off) (i32.const 16))
|
|
2191
|
+
(i32.ge_u (local.get $off) ${heapResetWat()})))
|
|
1129
2192
|
(then
|
|
1130
|
-
(local.set $props (i64.load (i32.sub (local.get $off) (i32.const 16))))
|
|
2193
|
+
(local.set $props (i64.and (i64.load (i32.sub (local.get $off) (i32.const 16))) (i64.const -2)))
|
|
1131
2194
|
(br_if $haveProps (i32.eq
|
|
1132
2195
|
(i32.wrap_i64 (i64.and (i64.shr_u (local.get $props) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK})))
|
|
1133
2196
|
(i32.const ${PTR.HASH})))
|
|
@@ -1138,13 +2201,15 @@ export default (ctx) => {
|
|
|
1138
2201
|
(br_if $dynDone (i64.eqz (local.get $props)))
|
|
1139
2202
|
(local.set $props (i64.const 0))))
|
|
1140
2203
|
;; OBJECT: heap-allocated (off >= __heap_start) carries propsPtr at
|
|
1141
|
-
;; off-16 from __alloc_hdr
|
|
2204
|
+
;; off-16 from __alloc_hdr — but only when also ephemeral (durable-
|
|
2205
|
+
;; receiver policy above). The slot is either 0 (no dyn props yet) or
|
|
1142
2206
|
;; a HASH — no forwarding-garbage case like ARRAY, so a bit-zero test
|
|
1143
|
-
;; is enough. Static-segment objects fall through to
|
|
2207
|
+
;; is enough. Static-segment and durable-heap objects fall through to
|
|
2208
|
+
;; the global hash.
|
|
1144
2209
|
(if (i32.and (i32.eq (local.get $type) (i32.const ${PTR.OBJECT}))
|
|
1145
|
-
(i32.ge_u (local.get $off) (
|
|
2210
|
+
(i32.ge_u (local.get $off) ${heapResetWat()}))
|
|
1146
2211
|
(then
|
|
1147
|
-
(local.set $props (i64.load (i32.sub (local.get $off) (i32.const 16))))
|
|
2212
|
+
(local.set $props (i64.and (i64.load (i32.sub (local.get $off) (i32.const 16))) (i64.const -2)))
|
|
1148
2213
|
(br_if $dynDone (i64.eqz (local.get $props)))
|
|
1149
2214
|
(br $haveProps)))
|
|
1150
2215
|
;; HASH: a plain dict whose string keys ARE its own bucket entries — the
|
|
@@ -1153,19 +2218,22 @@ export default (ctx) => {
|
|
|
1153
2218
|
;; __hash_get; this path serves receivers whose HASH type is only known at
|
|
1154
2219
|
;; runtime — e.g. a value read back through a function return, as in
|
|
1155
2220
|
;; derive(emitter)[op]. Without it, dyn-get reads the (absent) sidecar and
|
|
1156
|
-
;; reports every key missing.
|
|
2221
|
+
;; reports every key missing. HASH is its own storage (no sidecar, no
|
|
2222
|
+
;; global split) — durability is irrelevant here.
|
|
1157
2223
|
(if (i32.eq (local.get $type) (i32.const ${PTR.HASH}))
|
|
1158
2224
|
(then
|
|
1159
2225
|
(local.set $props (local.get $obj))
|
|
1160
2226
|
(br $haveProps)))
|
|
1161
2227
|
;; Other header types (TYPED/SET/MAP) carry propsPtr at off-16
|
|
1162
|
-
;; directly, bypassing the global __dyn_props hash
|
|
1163
|
-
|
|
2228
|
+
;; directly, bypassing the global __dyn_props hash — again only when
|
|
2229
|
+
;; ephemeral.
|
|
2230
|
+
(if (i32.and (i32.and (i32.ge_u (local.get $off) (i32.const 16))
|
|
2231
|
+
(i32.ge_u (local.get $off) ${heapResetWat()}))
|
|
1164
2232
|
(i32.or (i32.eq (local.get $type) (i32.const ${PTR.TYPED}))
|
|
1165
2233
|
(i32.or (i32.eq (local.get $type) (i32.const ${PTR.SET}))
|
|
1166
2234
|
(i32.eq (local.get $type) (i32.const ${PTR.MAP})))))
|
|
1167
2235
|
(then
|
|
1168
|
-
(local.set $props (i64.load (i32.sub (local.get $off) (i32.const 16))))
|
|
2236
|
+
(local.set $props (i64.and (i64.load (i32.sub (local.get $off) (i32.const 16))) (i64.const -2)))
|
|
1169
2237
|
(br_if $dynDone (i64.eqz (local.get $props)))
|
|
1170
2238
|
(br $haveProps)))
|
|
1171
2239
|
;; Fall back to the global __dyn_props hash (CLOSURE, shifted ARRAY,
|
|
@@ -1216,35 +2284,52 @@ export default (ctx) => {
|
|
|
1216
2284
|
(call $__dyn_get_expr_t (local.get $obj) (local.get $key) (call $__ptr_type (local.get $obj))))`
|
|
1217
2285
|
|
|
1218
2286
|
ctx.core.stdlib['__dyn_get_expr_t'] = `(func $__dyn_get_expr_t (param $obj i64) (param $key i64) (param $t i32) (result i64)
|
|
1219
|
-
(local $
|
|
2287
|
+
(local $f f64) (local $idx i32) (local $base i32)
|
|
1220
2288
|
;; Real-number receiver → no props; its garbage tag could match HASH and hit the
|
|
1221
|
-
;; __hash_get_local
|
|
2289
|
+
;; __hash_get_local arm below (heap read at a bogus offset → OOB).
|
|
1222
2290
|
(if (f64.eq (f64.reinterpret_i64 (local.get $obj)) (f64.reinterpret_i64 (local.get $obj)))
|
|
1223
2291
|
(then (return (i64.const ${UNDEF_NAN}))))
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
2292
|
+
;; ARRAY + raw integer key → ELEMENT read (JS array-index semantics), BEFORE
|
|
2293
|
+
;; ToPropertyKey. This is the generic \`a[i]\` fallback: the former
|
|
2294
|
+
;; stringify+str_hash+durable-double-probe chain taxed every numeric read
|
|
2295
|
+
;; on an unproven array (jessie's charcode tables: 3.7M reads/run). An
|
|
2296
|
+
;; in-range integer key can only live in the elements (__dyn_set routes it
|
|
2297
|
+
;; there), so OOB is definitively undefined. Fractional/negative/huge keys
|
|
2298
|
+
;; fall through to the string path (they are sidecar keys, '1.5'/'-1').
|
|
2299
|
+
(if (i32.eq (local.get $t) (i32.const ${PTR.ARRAY}))
|
|
2300
|
+
(then
|
|
2301
|
+
(local.set $f (f64.reinterpret_i64 (local.get $key)))
|
|
2302
|
+
(if (f64.eq (local.get $f) (local.get $f))
|
|
2303
|
+
(then
|
|
2304
|
+
(local.set $idx (i32.trunc_sat_f64_s (local.get $f)))
|
|
2305
|
+
(if (i32.and (f64.eq (f64.convert_i32_s (local.get $idx)) (local.get $f))
|
|
2306
|
+
(i32.ge_s (local.get $idx) (i32.const 0)))
|
|
2307
|
+
(then
|
|
2308
|
+
(local.set $base (call $__ptr_offset (local.get $obj)))
|
|
2309
|
+
(if (i32.lt_u (local.get $idx) (i32.load (i32.sub (local.get $base) (i32.const 8))))
|
|
2310
|
+
(then (return (i64.load (i32.add (local.get $base) (i32.shl (local.get $idx) (i32.const 3)))))))
|
|
2311
|
+
(return (i64.const ${UNDEF_NAN}))))))))
|
|
2312
|
+
;; ToPropertyKey — see __dyn_get_t; normalized here so the HASH arm reads string-keyed.
|
|
2313
|
+
(if (i32.eqz (call $__is_str_key (local.get $key)))
|
|
2314
|
+
(then (local.set $key (call $__to_str (local.get $key)))))
|
|
2315
|
+
;; HASH receivers FIRST: hashes never carry dyn_props (those attach to
|
|
2316
|
+
;; OBJECT/ARRAY only — the invariant __dyn_get_any already exploits), so
|
|
2317
|
+
;; the former dyn_get_t-then-fallback order paid the full str_hash +
|
|
2318
|
+
;; durable double-probe chain per read just to miss.
|
|
2319
|
+
(if (i32.eq (local.get $t) (i32.const ${PTR.HASH}))
|
|
2320
|
+
(then (return (call $__hash_get_local (local.get $obj) (local.get $key)))))
|
|
2321
|
+
(call $__dyn_get_t (local.get $obj) (local.get $key) (local.get $t)))`
|
|
1232
2322
|
|
|
1233
2323
|
// Prehashed variant of __dyn_get_expr_t for constant string keys: the FNV hash
|
|
1234
2324
|
// is folded at compile time (strHashLiteral), so no __str_hash call at runtime.
|
|
1235
2325
|
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)
|
|
1236
|
-
(
|
|
1237
|
-
;; Real-number receiver → no props; guard the HASH fallback OOB (see __dyn_get_expr_t).
|
|
2326
|
+
;; Real-number receiver → no props; guard the HASH arm OOB (see __dyn_get_expr_t).
|
|
1238
2327
|
(if (f64.eq (f64.reinterpret_i64 (local.get $obj)) (f64.reinterpret_i64 (local.get $obj)))
|
|
1239
2328
|
(then (return (i64.const ${UNDEF_NAN}))))
|
|
1240
|
-
|
|
1241
|
-
(if (
|
|
1242
|
-
(
|
|
1243
|
-
|
|
1244
|
-
(else
|
|
1245
|
-
(if (result i64) (i32.eq (local.get $t) (i32.const ${PTR.HASH}))
|
|
1246
|
-
(then (call $__hash_get_local_h (local.get $obj) (local.get $key) (local.get $h)))
|
|
1247
|
-
(else (i64.const ${UNDEF_NAN}))))))`
|
|
2329
|
+
;; HASH receivers first — same wasted-chain argument as __dyn_get_expr_t.
|
|
2330
|
+
(if (i32.eq (local.get $t) (i32.const ${PTR.HASH}))
|
|
2331
|
+
(then (return (call $__hash_get_local_h (local.get $obj) (local.get $key) (local.get $h)))))
|
|
2332
|
+
(call $__dyn_get_t_h (local.get $obj) (local.get $key) (local.get $t) (local.get $h)))`
|
|
1248
2333
|
|
|
1249
2334
|
// Like __dyn_get_expr but also resolves EXTERNAL host objects via __ext_prop.
|
|
1250
2335
|
// Used at call sites where receiver type is statically unknown.
|
|
@@ -1265,7 +2350,29 @@ export default (ctx) => {
|
|
|
1265
2350
|
(else (i64.const ${UNDEF_NAN})))`
|
|
1266
2351
|
: `(i64.const ${UNDEF_NAN})`
|
|
1267
2352
|
return `(func $__dyn_get_any_t (param $obj i64) (param $key i64) (param $t i32) (result i64)
|
|
1268
|
-
(local $val i64)
|
|
2353
|
+
(local $val i64) (local $f f64) (local $idx i32) (local $base i32)
|
|
2354
|
+
;; Real-number receiver → no props, and its garbage tag could match ARRAY
|
|
2355
|
+
;; below (bogus base → OOB). Same guard the expression tail repeats.
|
|
2356
|
+
(if (f64.eq (f64.reinterpret_i64 (local.get $obj)) (f64.reinterpret_i64 (local.get $obj)))
|
|
2357
|
+
(then (return (i64.const ${UNDEF_NAN}))))
|
|
2358
|
+
;; ARRAY + raw integer key → element read, before ToPropertyKey — the same
|
|
2359
|
+
;; generic array-index arm as __dyn_get_expr_t (see there for the analysis).
|
|
2360
|
+
(if (i32.eq (local.get $t) (i32.const ${PTR.ARRAY}))
|
|
2361
|
+
(then
|
|
2362
|
+
(local.set $f (f64.reinterpret_i64 (local.get $key)))
|
|
2363
|
+
(if (f64.eq (local.get $f) (local.get $f))
|
|
2364
|
+
(then
|
|
2365
|
+
(local.set $idx (i32.trunc_sat_f64_s (local.get $f)))
|
|
2366
|
+
(if (i32.and (f64.eq (f64.convert_i32_s (local.get $idx)) (local.get $f))
|
|
2367
|
+
(i32.ge_s (local.get $idx) (i32.const 0)))
|
|
2368
|
+
(then
|
|
2369
|
+
(local.set $base (call $__ptr_offset (local.get $obj)))
|
|
2370
|
+
(if (i32.lt_u (local.get $idx) (i32.load (i32.sub (local.get $base) (i32.const 8))))
|
|
2371
|
+
(then (return (i64.load (i32.add (local.get $base) (i32.shl (local.get $idx) (i32.const 3)))))))
|
|
2372
|
+
(return (i64.const ${UNDEF_NAN}))))))))
|
|
2373
|
+
;; ToPropertyKey — see __dyn_get_t; normalized here so the HASH arm reads string-keyed.
|
|
2374
|
+
(if (i32.eqz (call $__is_str_key (local.get $key)))
|
|
2375
|
+
(then (local.set $key (call $__to_str (local.get $key)))))
|
|
1269
2376
|
;; A real number receiver (f===f — NaN-boxed pointers are NaN) has no dynamic
|
|
1270
2377
|
;; props: \`(5).foo\` is undefined. Without this guard the bits are reinterpreted as
|
|
1271
2378
|
;; a pointer and \`__dyn_get_t\` reads heap at a bogus offset → OOB for large values.
|
|
@@ -1314,9 +2421,54 @@ export default (ctx) => {
|
|
|
1314
2421
|
// __ptr_offset inlined (forwarding-aware) — only ARRAY ever has forwarding.
|
|
1315
2422
|
ctx.core.stdlib['__dyn_set'] = () => `(func $__dyn_set (param $obj i64) (param $key i64) (param $val i64) (result i64)
|
|
1316
2423
|
(local $root i64) (local $props i64) (local $oldProps i64) (local $objKey i64)
|
|
1317
|
-
(local $off i32) (local $type i32) ${buildObjectSchemaSetLocals()}
|
|
2424
|
+
(local $off i32) (local $type i32) (local $kf f64) (local $kidx i32) ${buildObjectSchemaSetLocals()}
|
|
1318
2425
|
(local.set $off (i32.wrap_i64 (i64.and (local.get $obj) (i64.const ${LAYOUT.OFFSET_MASK}))))
|
|
1319
2426
|
(local.set $type (i32.wrap_i64 (i64.and (i64.shr_u (local.get $obj) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
|
|
2427
|
+
;; STRING receiver: primitives drop property writes (JS non-strict
|
|
2428
|
+
;; semantics) — the read path above guarantees UNDEF for them, so
|
|
2429
|
+
;; storing would only create unreadable entries. NaN guard: a real
|
|
2430
|
+
;; number's garbage tag may alias STRING; those keep today's path.
|
|
2431
|
+
(if (i32.and (i32.eq (local.get $type) (i32.const ${PTR.STRING}))
|
|
2432
|
+
(f64.ne (f64.reinterpret_i64 (local.get $obj)) (f64.reinterpret_i64 (local.get $obj))))
|
|
2433
|
+
(then (return (local.get $val))))
|
|
2434
|
+
;; ARRAY + integer key → ELEMENT store (grow + hole-fill via the same
|
|
2435
|
+
;; helper the statically-proven \`a[i]=v\` path uses), matching JS index
|
|
2436
|
+
;; semantics and the element arms in the dyn read entries. Guard real-
|
|
2437
|
+
;; number receivers first — their garbage tag could match ARRAY and the
|
|
2438
|
+
;; store would land OOB. Raw numeric arm before ToPropertyKey (hot);
|
|
2439
|
+
;; canonical numeric STRING arm after it ('1' ≡ 1). __arr_set_idx_ptr
|
|
2440
|
+
;; leaves a forwarding header on grow, which every dyn/static reader
|
|
2441
|
+
;; already follows — binding-unaware callers stay correct.
|
|
2442
|
+
(if (i32.and (i32.eq (local.get $type) (i32.const ${PTR.ARRAY}))
|
|
2443
|
+
(f64.ne (f64.reinterpret_i64 (local.get $obj)) (f64.reinterpret_i64 (local.get $obj))))
|
|
2444
|
+
(then
|
|
2445
|
+
(local.set $kf (f64.reinterpret_i64 (local.get $key)))
|
|
2446
|
+
(if (f64.eq (local.get $kf) (local.get $kf))
|
|
2447
|
+
(then
|
|
2448
|
+
(local.set $kidx (i32.trunc_sat_f64_s (local.get $kf)))
|
|
2449
|
+
(if (i32.and (f64.eq (f64.convert_i32_s (local.get $kidx)) (local.get $kf))
|
|
2450
|
+
(i32.ge_s (local.get $kidx) (i32.const 0)))
|
|
2451
|
+
(then
|
|
2452
|
+
(drop (call $__arr_set_idx_ptr (local.get $obj) (local.get $kidx) (f64.reinterpret_i64 (local.get $val))))
|
|
2453
|
+
(return (local.get $val)))))
|
|
2454
|
+
(else
|
|
2455
|
+
;; key is non-number here (NaN-boxed) — a bare tag test IS the
|
|
2456
|
+
;; string test, no __is_str_key call (its NaN guard is redundant).
|
|
2457
|
+
(if (i32.eq (i32.wrap_i64 (i64.and (i64.shr_u (local.get $key) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))) (i32.const ${PTR.STRING}))
|
|
2458
|
+
(then
|
|
2459
|
+
;; first-char digit reject — see __dyn_get_t_h's net
|
|
2460
|
+
(if (i32.lt_u (i32.sub (if (result i32) (i64.ne (i64.and (local.get $key) (i64.const ${SSO_BIT_I64})) (i64.const 0))
|
|
2461
|
+
(then (i32.and (i32.wrap_i64 (local.get $key)) (i32.const 127)))
|
|
2462
|
+
(else (i32.load8_u (i32.wrap_i64 (i64.and (local.get $key) (i64.const ${LAYOUT.OFFSET_MASK})))))) (i32.const 48)) (i32.const 10))
|
|
2463
|
+
(then
|
|
2464
|
+
(local.set $kidx (call $__str_arr_idx (local.get $key)))
|
|
2465
|
+
(if (i32.ge_s (local.get $kidx) (i32.const 0))
|
|
2466
|
+
(then
|
|
2467
|
+
(drop (call $__arr_set_idx_ptr (local.get $obj) (local.get $kidx) (f64.reinterpret_i64 (local.get $val))))
|
|
2468
|
+
(return (local.get $val))))))))))))
|
|
2469
|
+
;; ToPropertyKey — see __dyn_get_t. Stored keys are always strings.
|
|
2470
|
+
(if (i32.eqz (call $__is_str_key (local.get $key)))
|
|
2471
|
+
(then (local.set $key (call $__to_str (local.get $key)))))
|
|
1320
2472
|
;; CLOSURE with no env (offset 0): key __dyn_props on the function table index — see __dyn_get_t.
|
|
1321
2473
|
(if (i32.and (i32.eq (local.get $type) (i32.const ${PTR.CLOSURE})) (i32.eqz (local.get $off)))
|
|
1322
2474
|
(then (local.set $off (i32.sub (i32.const -1)
|
|
@@ -1326,7 +2478,10 @@ export default (ctx) => {
|
|
|
1326
2478
|
(block $done
|
|
1327
2479
|
(loop $follow
|
|
1328
2480
|
(br_if $done (i32.lt_u (local.get $off) (i32.const 16)))
|
|
1329
|
-
|
|
2481
|
+
;; $__heap_end64 (i64), not i32.shl (memory.size) 16: at the wasm32 ceiling
|
|
2482
|
+
;; (memory.size()==65536 pages), the i32 form overflows to exactly 0 —
|
|
2483
|
+
;; see layout.js's followForwardingWat comment for the full account.
|
|
2484
|
+
(br_if $done (i64.gt_u (i64.extend_i32_u (local.get $off)) (global.get $__heap_end64)))
|
|
1330
2485
|
(br_if $done (i32.ne (i32.load (i32.sub (local.get $off) (i32.const 4))) (i32.const -1)))
|
|
1331
2486
|
(local.set $off (i32.load (i32.sub (local.get $off) (i32.const 8))))
|
|
1332
2487
|
(br $follow)))))
|
|
@@ -1335,12 +2490,16 @@ export default (ctx) => {
|
|
|
1335
2490
|
;; skip the global __dyn_props hash entirely. ARRAY also uses this slot, but
|
|
1336
2491
|
;; only when shift hasn't overwritten it with forwarding bytes (HASH-tagged
|
|
1337
2492
|
;; check rejects 0 + forwarding garbage). Shifted ARRAYs fall back to the
|
|
1338
|
-
;; global __dyn_props where __arr_shift has migrated their props.
|
|
2493
|
+
;; global __dyn_props where __arr_shift has migrated their props. DURABLE-
|
|
2494
|
+
;; RECEIVER POLICY (see heapResetWat): a durable receiver (off < the
|
|
2495
|
+
;; post-init high-water mark) always falls through to the global table too,
|
|
2496
|
+
;; regardless of shift state — this mirrors __dyn_get_t_h's ARRAY arm.
|
|
1339
2497
|
(if (i32.eq (local.get $type) (i32.const ${PTR.ARRAY}))
|
|
1340
2498
|
(then
|
|
1341
|
-
(if (i32.ge_u (local.get $off) (i32.const 16))
|
|
2499
|
+
(if (i32.and (i32.ge_u (local.get $off) (i32.const 16))
|
|
2500
|
+
(i32.ge_u (local.get $off) ${heapResetWat()}))
|
|
1342
2501
|
(then
|
|
1343
|
-
(local.set $oldProps (i64.load (i32.sub (local.get $off) (i32.const 16))))
|
|
2502
|
+
(local.set $oldProps (i64.and (i64.load (i32.sub (local.get $off) (i32.const 16))) (i64.const -2)))
|
|
1344
2503
|
(if (i32.or
|
|
1345
2504
|
(i64.eqz (local.get $oldProps))
|
|
1346
2505
|
(i32.eq
|
|
@@ -1355,13 +2514,14 @@ export default (ctx) => {
|
|
|
1355
2514
|
(if (i64.ne (local.get $props) (local.get $oldProps))
|
|
1356
2515
|
(then (i64.store (i32.sub (local.get $off) (i32.const 16)) (local.get $props))))
|
|
1357
2516
|
(return (local.get $val))))))))
|
|
1358
|
-
;; OBJECT: heap-allocated (
|
|
1359
|
-
;; off-16. The slot is 0 (init) or HASH — no
|
|
1360
|
-
;;
|
|
2517
|
+
;; OBJECT: heap-allocated AND ephemeral (durable-receiver policy) writes
|
|
2518
|
+
;; propsPtr directly at off-16. The slot is 0 (init) or HASH — no
|
|
2519
|
+
;; forwarding-garbage like ARRAY. Static-segment and durable-heap OBJECTs
|
|
2520
|
+
;; fall through to the global __dyn_props.
|
|
1361
2521
|
(if (i32.and (i32.eq (local.get $type) (i32.const ${PTR.OBJECT}))
|
|
1362
|
-
(i32.ge_u (local.get $off) (
|
|
2522
|
+
(i32.ge_u (local.get $off) ${heapResetWat()}))
|
|
1363
2523
|
(then
|
|
1364
|
-
(local.set $oldProps (i64.load (i32.sub (local.get $off) (i32.const 16))))
|
|
2524
|
+
(local.set $oldProps (i64.and (i64.load (i32.sub (local.get $off) (i32.const 16))) (i64.const -2)))
|
|
1365
2525
|
(local.set $props
|
|
1366
2526
|
(if (result i64) (i64.eqz (local.get $oldProps))
|
|
1367
2527
|
(then (i64.reinterpret_f64 (call $__hash_new_small)))
|
|
@@ -1378,12 +2538,13 @@ export default (ctx) => {
|
|
|
1378
2538
|
(then
|
|
1379
2539
|
(drop (call $__hash_set_local (local.get $obj) (local.get $key) (local.get $val)))
|
|
1380
2540
|
(return (local.get $val))))
|
|
1381
|
-
|
|
2541
|
+
;; TYPED/SET/MAP header sidecar — ephemeral only (durable-receiver policy).
|
|
2542
|
+
(if (i32.and (i32.and (i32.ge_u (local.get $off) (i32.const 16)) (i32.ge_u (local.get $off) ${heapResetWat()}))
|
|
1382
2543
|
(i32.or (i32.eq (local.get $type) (i32.const ${PTR.TYPED}))
|
|
1383
2544
|
(i32.or (i32.eq (local.get $type) (i32.const ${PTR.SET}))
|
|
1384
2545
|
(i32.eq (local.get $type) (i32.const ${PTR.MAP})))))
|
|
1385
2546
|
(then
|
|
1386
|
-
(local.set $oldProps (i64.load (i32.sub (local.get $off) (i32.const 16))))
|
|
2547
|
+
(local.set $oldProps (i64.and (i64.load (i32.sub (local.get $off) (i32.const 16))) (i64.const -2)))
|
|
1387
2548
|
(local.set $props
|
|
1388
2549
|
(if (result i64) (i64.eqz (local.get $oldProps))
|
|
1389
2550
|
(then (i64.reinterpret_f64 (call $__hash_new_small)))
|
|
@@ -1397,18 +2558,50 @@ export default (ctx) => {
|
|
|
1397
2558
|
(if (i64.eqz (local.get $root))
|
|
1398
2559
|
(then (local.set $root (i64.reinterpret_f64 (call $__hash_new)))))
|
|
1399
2560
|
(local.set $objKey (i64.reinterpret_f64 (f64.convert_i32_s (local.get $off))))
|
|
1400
|
-
|
|
2561
|
+
;; Filter: a clear bit proves this offset was never inserted — the probe
|
|
2562
|
+
;; would miss, so skip straight to "no existing props" without calling
|
|
2563
|
+
;; __ihash_get_local. A set bit (maybe-present, or a collision) falls
|
|
2564
|
+
;; through to the real probe. Never a false negative — see filter's decl.
|
|
2565
|
+
(local.set $oldProps
|
|
2566
|
+
(if (result i64) ${dynPropsFilterMissIR('(local.get $off)')}
|
|
2567
|
+
(then (i64.const ${UNDEF_NAN}))
|
|
2568
|
+
(else (call $__ihash_get_local (local.get $root) (local.get $objKey)))))
|
|
1401
2569
|
(local.set $props
|
|
1402
2570
|
(if (result i64) (call $__is_nullish (local.get $oldProps))
|
|
1403
2571
|
(then (i64.reinterpret_f64 (call $__hash_new_small)))
|
|
1404
2572
|
(else (local.get $oldProps))))
|
|
1405
2573
|
(local.set $props (call $__hash_set_local (local.get $props) (local.get $key) (local.get $val)))
|
|
2574
|
+
;; for-in enum cache: a global-side prop insert changes a durable receiver's
|
|
2575
|
+
;; enumeration without touching the (sidecar-keyed) cache key — clear it.
|
|
2576
|
+
;; Unconditional: an insert into an EXISTING per-object hash skips the
|
|
2577
|
+
;; props≠oldProps rekey below, so this can't ride that guard. Cold path.
|
|
2578
|
+
(global.set $__enumc_off (i32.const 0))
|
|
1406
2579
|
(if (i64.ne (local.get $props) (local.get $oldProps))
|
|
1407
2580
|
(then
|
|
1408
2581
|
(local.set $root (call $__ihash_set_local (local.get $root) (local.get $objKey) (local.get $props)))
|
|
1409
2582
|
(global.set $__dyn_props (f64.reinterpret_i64 (local.get $root)))
|
|
2583
|
+
${dynPropsFilterSetIR('(local.get $off)')}
|
|
1410
2584
|
(if (i32.eq (local.get $off) (global.get $__dyn_get_cache_off))
|
|
1411
2585
|
(then (global.set $__dyn_get_cache_props (f64.reinterpret_i64 (local.get $props)))))))
|
|
2586
|
+
;; RUNTIME-SHADOWED MARKER: this durable receiver now has (at least one)
|
|
2587
|
+
;; runtime-written prop living in the global table — set bit0 of its own
|
|
2588
|
+
;; off-16 props word so the read path probes the global table ONLY for
|
|
2589
|
+
;; shadowed receivers (unshadowed durable reads skip straight to the
|
|
2590
|
+
;; init-time sidecar; measured: 60% of jessie's 1.07M probes are such
|
|
2591
|
+
;; always-miss reads). Only header-carrying receivers whose word is 0 or
|
|
2592
|
+
;; a real HASH ptr are marked — a shifted ARRAY's forwarding bytes (tag
|
|
2593
|
+
;; 0xF) must never be touched, and CLOSURE pseudo-offsets have no header.
|
|
2594
|
+
;; HASH ptr offsets are 8-aligned, so bit0 is free; every consumer of a
|
|
2595
|
+
;; DURABLE off-16 word masks it back out (i64.and -2).
|
|
2596
|
+
(if (i32.and (i32.and (i32.ge_u (local.get $off) (i32.const 16))
|
|
2597
|
+
(i32.lt_u (local.get $off) ${heapResetWat()}))
|
|
2598
|
+
${hasPropsSidecarWat('(local.get $type)')})
|
|
2599
|
+
(then
|
|
2600
|
+
(local.set $oldProps (i64.and (i64.load (i32.sub (local.get $off) (i32.const 16))) (i64.const -2)))
|
|
2601
|
+
(if (i32.or (i64.eqz (local.get $oldProps))
|
|
2602
|
+
(i32.eq (i32.wrap_i64 (i64.and (i64.shr_u (local.get $oldProps) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))) (i32.const ${PTR.HASH})))
|
|
2603
|
+
(then (i64.store (i32.sub (local.get $off) (i32.const 16))
|
|
2604
|
+
(i64.or (local.get $oldProps) (i64.const 1)))))))
|
|
1412
2605
|
(local.get $val))`
|
|
1413
2606
|
|
|
1414
2607
|
// Tag-dispatched delete (mirrors __dyn_set's dispatch). Returns 1 if a slot was
|
|
@@ -1445,9 +2638,18 @@ export default (ctx) => {
|
|
|
1445
2638
|
|
|
1446
2639
|
ctx.core.stdlib['__dyn_del'] = () => `(func $__dyn_del (param $obj i64) (param $key i64) (result i32)
|
|
1447
2640
|
(local $root i64) (local $props i64) (local $oldProps i64)
|
|
1448
|
-
(local $off i32) (local $type i32) (local $hit i32) ${buildObjectSchemaSetLocals()}
|
|
2641
|
+
(local $off i32) (local $type i32) (local $hit i32) (local $delidx i32) ${buildObjectSchemaSetLocals()}
|
|
2642
|
+
;; ToPropertyKey — see __dyn_get_t. Stored keys are always strings.
|
|
2643
|
+
(if (i32.eqz (call $__is_str_key (local.get $key)))
|
|
2644
|
+
(then (local.set $key (call $__to_str (local.get $key)))))
|
|
1449
2645
|
(local.set $off (i32.wrap_i64 (i64.and (local.get $obj) (i64.const ${LAYOUT.OFFSET_MASK}))))
|
|
1450
2646
|
(local.set $type (i32.wrap_i64 (i64.and (i64.shr_u (local.get $obj) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
|
|
2647
|
+
;; HASH receiver is ITS OWN storage (dictionary-mode {} — __dyn_set/__dyn_get
|
|
2648
|
+
;; write/read its entry table directly): delete the entry there. Every arm
|
|
2649
|
+
;; below only probes the props SIDECAR, which a dictionary doesn't use for
|
|
2650
|
+
;; its own keys — without this arm, delete d[k] silently no-ops.
|
|
2651
|
+
(if (i32.eq (local.get $type) (i32.const ${PTR.HASH}))
|
|
2652
|
+
(then (return (call $__hash_del_local (local.get $obj) (local.get $key)))))
|
|
1451
2653
|
${buildObjectSchemaDelArm()}
|
|
1452
2654
|
;; CLOSURE with no env: rekey to function table index (parallels __dyn_set / __dyn_get_t_h).
|
|
1453
2655
|
(if (i32.and (i32.eq (local.get $type) (i32.const ${PTR.CLOSURE})) (i32.eqz (local.get $off)))
|
|
@@ -1459,50 +2661,114 @@ export default (ctx) => {
|
|
|
1459
2661
|
(block $done
|
|
1460
2662
|
(loop $follow
|
|
1461
2663
|
(br_if $done (i32.lt_u (local.get $off) (i32.const 16)))
|
|
1462
|
-
|
|
2664
|
+
;; $__heap_end64 (i64), not i32.shl (memory.size) 16: at the wasm32 ceiling
|
|
2665
|
+
;; (memory.size()==65536 pages), the i32 form overflows to exactly 0 —
|
|
2666
|
+
;; see layout.js's followForwardingWat comment for the full account.
|
|
2667
|
+
(br_if $done (i64.gt_u (i64.extend_i32_u (local.get $off)) (global.get $__heap_end64)))
|
|
1463
2668
|
(br_if $done (i32.ne (i32.load (i32.sub (local.get $off) (i32.const 4))) (i32.const -1)))
|
|
1464
2669
|
(local.set $off (i32.load (i32.sub (local.get $off) (i32.const 8))))
|
|
1465
|
-
(br $follow)))
|
|
1466
|
-
|
|
2670
|
+
(br $follow)))
|
|
2671
|
+
;; Canonical-index key → element home (mirrors __dyn_set/__dyn_get):
|
|
2672
|
+
;; delete arr[i] leaves a hole (undefined), length unchanged — JS
|
|
2673
|
+
;; semantics. OOB delete is a no-op that still reports success.
|
|
2674
|
+
(if (i32.ge_u (local.get $off) (i32.const 16))
|
|
2675
|
+
(then
|
|
2676
|
+
(local.set $delidx (i32.const -1))
|
|
2677
|
+
(if (i32.lt_u (i32.sub (if (result i32) (i64.ne (i64.and (local.get $key) (i64.const ${SSO_BIT_I64})) (i64.const 0))
|
|
2678
|
+
(then (i32.and (i32.wrap_i64 (local.get $key)) (i32.const 127)))
|
|
2679
|
+
(else (i32.load8_u (i32.wrap_i64 (i64.and (local.get $key) (i64.const ${LAYOUT.OFFSET_MASK})))))) (i32.const 48)) (i32.const 10))
|
|
2680
|
+
(then (local.set $delidx (call $__str_arr_idx (local.get $key)))))
|
|
2681
|
+
(if (i32.ge_s (local.get $delidx) (i32.const 0))
|
|
2682
|
+
(then
|
|
2683
|
+
(if (i32.lt_u (local.get $delidx) (i32.load (i32.sub (local.get $off) (i32.const 8))))
|
|
2684
|
+
(then (i64.store (i32.add (local.get $off) (i32.shl (local.get $delidx) (i32.const 3))) (i64.const ${UNDEF_NAN}))))
|
|
2685
|
+
(return (i32.const 1))))))))
|
|
2686
|
+
;; DURABLE-RECEIVER POLICY (see __dyn_get_t_h's declaration comment for the
|
|
2687
|
+
;; full rationale): a durable receiver's key can live in EITHER the global
|
|
2688
|
+
;; table (runtime-written) or its off-16 sidecar (init-time-written) — try
|
|
2689
|
+
;; BOTH and OR the hit bits, not just the first that resolves. This is the
|
|
2690
|
+
;; delete-specific twist: a key set at init then reassigned at runtime
|
|
2691
|
+
;; exists in BOTH places (global shadows it for reads), so deleting only
|
|
2692
|
+
;; the global copy would leave the stale sidecar entry to resurface on the
|
|
2693
|
+
;; next get once the (now correctly empty) global lookup falls through to
|
|
2694
|
+
;; it. HASH is exempted — it is its own storage.
|
|
2695
|
+
(if (i32.and (i32.ne (local.get $type) (i32.const ${PTR.HASH}))
|
|
2696
|
+
(i32.lt_u (local.get $off) ${heapResetWat()}))
|
|
2697
|
+
(then
|
|
2698
|
+
(if (i32.eqz ${dynPropsFilterMissIR('(local.get $off)')})
|
|
2699
|
+
(then
|
|
2700
|
+
(local.set $root (i64.reinterpret_f64 (global.get $__dyn_props)))
|
|
2701
|
+
(if (i64.ne (local.get $root) (i64.const 0))
|
|
2702
|
+
(then
|
|
2703
|
+
(local.set $props (call $__ihash_get_local (local.get $root) (i64.reinterpret_f64 (f64.convert_i32_s (local.get $off)))))
|
|
2704
|
+
(if (i32.eqz (call $__is_nullish (local.get $props)))
|
|
2705
|
+
(then (local.set $hit (i32.or (local.get $hit) (call $__hash_del_local (local.get $props) (local.get $key))))))))))
|
|
2706
|
+
(if (i32.and ${hasPropsSidecarWat('(local.get $type)')} (i32.ge_u (local.get $off) (i32.const 16)))
|
|
2707
|
+
(then
|
|
2708
|
+
(local.set $oldProps (i64.and (i64.load (i32.sub (local.get $off) (i32.const 16))) (i64.const -2)))
|
|
2709
|
+
(if (i32.eq
|
|
2710
|
+
(i32.wrap_i64 (i64.and (i64.shr_u (local.get $oldProps) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK})))
|
|
2711
|
+
(i32.const ${PTR.HASH}))
|
|
2712
|
+
(then (local.set $hit (i32.or (local.get $hit) (call $__hash_del_local (local.get $oldProps) (local.get $key))))))))
|
|
2713
|
+
(return (local.get $hit))))
|
|
2714
|
+
;; ARRAY landed propsPtr (HASH-tagged means real sidecar; else fall through
|
|
2715
|
+
;; to global). Ephemeral only — durable receivers already returned above.
|
|
1467
2716
|
(if (i32.and (i32.eq (local.get $type) (i32.const ${PTR.ARRAY}))
|
|
1468
|
-
(i32.ge_u (local.get $off) (i32.const 16))
|
|
2717
|
+
(i32.and (i32.ge_u (local.get $off) (i32.const 16))
|
|
2718
|
+
(i32.ge_u (local.get $off) ${heapResetWat()})))
|
|
1469
2719
|
(then
|
|
1470
|
-
(local.set $oldProps (i64.load (i32.sub (local.get $off) (i32.const 16))))
|
|
2720
|
+
(local.set $oldProps (i64.and (i64.load (i32.sub (local.get $off) (i32.const 16))) (i64.const -2)))
|
|
1471
2721
|
(if (i32.eq
|
|
1472
2722
|
(i32.wrap_i64 (i64.and (i64.shr_u (local.get $oldProps) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK})))
|
|
1473
2723
|
(i32.const ${PTR.HASH}))
|
|
1474
2724
|
(then (return (i32.or (local.get $hit) (call $__hash_del_local (local.get $oldProps) (local.get $key))))))))
|
|
1475
|
-
;; OBJECT heap: propsPtr directly at off-16.
|
|
2725
|
+
;; OBJECT heap: propsPtr directly at off-16 — ephemeral only.
|
|
1476
2726
|
(if (i32.and (i32.eq (local.get $type) (i32.const ${PTR.OBJECT}))
|
|
1477
|
-
(i32.ge_u (local.get $off) (
|
|
2727
|
+
(i32.ge_u (local.get $off) ${heapResetWat()}))
|
|
1478
2728
|
(then
|
|
1479
|
-
(local.set $oldProps (i64.load (i32.sub (local.get $off) (i32.const 16))))
|
|
2729
|
+
(local.set $oldProps (i64.and (i64.load (i32.sub (local.get $off) (i32.const 16))) (i64.const -2)))
|
|
1480
2730
|
(if (i64.eqz (local.get $oldProps)) (then (return (local.get $hit))))
|
|
1481
2731
|
(return (i32.or (local.get $hit) (call $__hash_del_local (local.get $oldProps) (local.get $key))))))
|
|
1482
|
-
;; Other header types (TYPED/HASH/SET/MAP).
|
|
1483
|
-
(if (i32.and (i32.ge_u (local.get $off) (i32.const 16))
|
|
2732
|
+
;; Other header types (TYPED/HASH/SET/MAP) — ephemeral only.
|
|
2733
|
+
(if (i32.and (i32.and (i32.ge_u (local.get $off) (i32.const 16)) (i32.ge_u (local.get $off) ${heapResetWat()}))
|
|
1484
2734
|
(i32.or (i32.eq (local.get $type) (i32.const ${PTR.TYPED}))
|
|
1485
2735
|
(i32.or (i32.eq (local.get $type) (i32.const ${PTR.HASH}))
|
|
1486
2736
|
(i32.or (i32.eq (local.get $type) (i32.const ${PTR.SET}))
|
|
1487
2737
|
(i32.eq (local.get $type) (i32.const ${PTR.MAP}))))))
|
|
1488
2738
|
(then
|
|
1489
|
-
(local.set $oldProps (i64.load (i32.sub (local.get $off) (i32.const 16))))
|
|
2739
|
+
(local.set $oldProps (i64.and (i64.load (i32.sub (local.get $off) (i32.const 16))) (i64.const -2)))
|
|
1490
2740
|
(if (i64.eqz (local.get $oldProps)) (then (return (local.get $hit))))
|
|
1491
2741
|
(return (i32.or (local.get $hit) (call $__hash_del_local (local.get $oldProps) (local.get $key))))))
|
|
1492
2742
|
;; Fallback: global __dyn_props keyed by offset.
|
|
1493
2743
|
(local.set $root (i64.reinterpret_f64 (global.get $__dyn_props)))
|
|
1494
2744
|
(if (i64.eqz (local.get $root)) (then (return (local.get $hit))))
|
|
2745
|
+
;; Filter-proven absent: this offset was never inserted into __dyn_props,
|
|
2746
|
+
;; so there's nothing to delete — skip the __ihash_get_local probe.
|
|
2747
|
+
(if ${dynPropsFilterMissIR('(local.get $off)')} (then (return (local.get $hit))))
|
|
1495
2748
|
(local.set $props (call $__ihash_get_local (local.get $root) (i64.reinterpret_f64 (f64.convert_i32_s (local.get $off)))))
|
|
1496
2749
|
(if (call $__is_nullish (local.get $props)) (then (return (local.get $hit))))
|
|
1497
2750
|
(i32.or (local.get $hit) (call $__hash_del_local (local.get $props) (local.get $key))))`
|
|
1498
2751
|
|
|
1499
|
-
|
|
2752
|
+
// Called on EVERY array shift/grow once __dyn_set is included (module has any
|
|
2753
|
+
// dynamic-prop write) — almost always a miss (arrays with dyn props are rare).
|
|
2754
|
+
// The filter bit lets a miss return in O(1) globals-only work instead of an
|
|
2755
|
+
// __ihash_get_local probe (hash + bucket walk). Sound: a clear bit proves
|
|
2756
|
+
// $oldOff was never inserted, so the probe below would find nothing anyway.
|
|
2757
|
+
// Returns i32 1 if an entry was found+rekeyed, 0 on a no-op — callers use this
|
|
2758
|
+
// to know whether to mark the relocated array's header (see DYN_PROPS_GLOBAL_SENTINEL).
|
|
2759
|
+
ctx.core.stdlib['__dyn_move'] = `(func $__dyn_move (param $oldOff i32) (param $newOff i32) (result i32)
|
|
1500
2760
|
(local $props i64) (local $root i64)
|
|
1501
|
-
(if (f64.eq (global.get $__dyn_props) (f64.const 0)) (then (return)))
|
|
2761
|
+
(if (f64.eq (global.get $__dyn_props) (f64.const 0)) (then (return (i32.const 0))))
|
|
2762
|
+
(if ${dynPropsFilterMissIR('(local.get $oldOff)')} (then (return (i32.const 0))))
|
|
1502
2763
|
(local.set $props (call $__ihash_get_local (i64.reinterpret_f64 (global.get $__dyn_props)) (i64.reinterpret_f64 (f64.convert_i32_s (local.get $oldOff)))))
|
|
1503
|
-
(if (call $__is_nullish (local.get $props)) (then (return)))
|
|
2764
|
+
(if (call $__is_nullish (local.get $props)) (then (return (i32.const 0))))
|
|
1504
2765
|
(local.set $root (call $__ihash_set_local (i64.reinterpret_f64 (global.get $__dyn_props)) (i64.reinterpret_f64 (f64.convert_i32_s (local.get $newOff))) (local.get $props)))
|
|
1505
|
-
(global.set $__dyn_props (f64.reinterpret_i64 (local.get $root)))
|
|
2766
|
+
(global.set $__dyn_props (f64.reinterpret_i64 (local.get $root)))
|
|
2767
|
+
${dynPropsFilterSetIR('(local.get $newOff)')}
|
|
2768
|
+
;; for-in enum cache: props re-keyed to a relocated receiver — global-side
|
|
2769
|
+
;; enumeration state changed without touching the sidecar-keyed cache. Clear.
|
|
2770
|
+
(global.set $__enumc_off (i32.const 0))
|
|
2771
|
+
(i32.const 1))`
|
|
1506
2772
|
|
|
1507
2773
|
// Generated HASH probe functions
|
|
1508
2774
|
ctx.core.stdlib['__hash_set'] = () => genUpsertGrow('__hash_set', MAP_ENTRY, '$__str_hash', strEqG, PTR.HASH, false, ctx.features.external, true)
|
|
@@ -1625,9 +2891,17 @@ export default (ctx) => {
|
|
|
1625
2891
|
if (vt === VAL.SET) return typed(['block', ['result', 'f64'], bind, collKeysFromTemp(t, SET_ENTRY)], 'f64')
|
|
1626
2892
|
if (vt === VAL.MAP) return typed(['block', ['result', 'f64'], bind, collEntriesFromTemp(t, MAP_ENTRY)], 'f64')
|
|
1627
2893
|
// Unknown receiver: resolve the kind once at runtime (loop-invariant).
|
|
1628
|
-
|
|
2894
|
+
// ES: for-of / spread over null/undefined is a TypeError ("x is not
|
|
2895
|
+
// iterable") — throw, per spec. The silent zero-iteration this replaces
|
|
2896
|
+
// masked two real self-host miscompiles (a folded undefined-guard and a
|
|
2897
|
+
// never-armed matchAll swallowed their wrong undefineds into empty loops)
|
|
2898
|
+
// before they were caught. Known-vt receivers skip the check entirely.
|
|
2899
|
+
ctx.runtime.throws = true
|
|
2900
|
+
inc('__ptr_type', '__is_nullish')
|
|
1629
2901
|
const ptrType = () => ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]
|
|
1630
2902
|
return typed(['block', ['result', 'f64'], bind,
|
|
2903
|
+
['if', ['call', '$__is_nullish', ['i64.reinterpret_f64', ['local.get', `$${t}`]]],
|
|
2904
|
+
['then', ['throw', '$__jz_err', ['f64.const', 0]]]],
|
|
1631
2905
|
['if', ['result', 'f64'], ['i32.eq', ptrType(), ['i32.const', PTR.SET]],
|
|
1632
2906
|
['then', collKeysFromTemp(t, SET_ENTRY)],
|
|
1633
2907
|
['else', ['if', ['result', 'f64'], ['i32.eq', ptrType(), ['i32.const', PTR.MAP]],
|
|
@@ -1726,7 +3000,7 @@ export default (ctx) => {
|
|
|
1726
3000
|
// picks the column: 8 = key (Set element / Map key), 16 = Map value. Mirrors
|
|
1727
3001
|
// object.js's hash*FromTemp. Used by `__iter_arr` and `.keys()`/`.values()`.
|
|
1728
3002
|
function collKeysFromTemp(t, stride, fieldOff = 8) {
|
|
1729
|
-
inc('__ptr_offset', '__cap', '__len', '__coll_order')
|
|
3003
|
+
inc('__ptr_offset', '__ptr_offset_fwd', '__cap', '__len', '__coll_order')
|
|
1730
3004
|
const off = tempI32('cko'), cap = tempI32('ckc'), n = tempI32('ckn')
|
|
1731
3005
|
const i = tempI32('cki'), ord = tempI32('ckr'), slot = tempI32('cks')
|
|
1732
3006
|
const out = allocPtr({ type: PTR.ARRAY, len: ['local.get', `$${n}`], tag: 'cka' })
|
|
@@ -1754,7 +3028,7 @@ function collKeysFromTemp(t, stride, fieldOff = 8) {
|
|
|
1754
3028
|
// [slot+aOff, slot+bOff] boxed into the output: Map entries use (8,16) → [k,v];
|
|
1755
3029
|
// Set entries use (8,8) → [v,v].
|
|
1756
3030
|
function collEntriesFromTemp(t, stride, aOff = 8, bOff = 16) {
|
|
1757
|
-
inc('__ptr_offset', '__cap', '__len', '__alloc_hdr', '__coll_order')
|
|
3031
|
+
inc('__ptr_offset', '__ptr_offset_fwd', '__cap', '__len', '__alloc_hdr', '__coll_order')
|
|
1758
3032
|
const off = tempI32('ceo'), cap = tempI32('cec'), n = tempI32('cen')
|
|
1759
3033
|
const i = tempI32('cei'), ord = tempI32('cer'), slot = tempI32('ces'), pair = tempI32('cep')
|
|
1760
3034
|
const out = allocPtr({ type: PTR.ARRAY, len: ['local.get', `$${n}`], tag: 'cea' })
|
|
@@ -1801,7 +3075,7 @@ function arrIdxFromTemp(t) {
|
|
|
1801
3075
|
|
|
1802
3076
|
// Array.prototype.entries() → dense Array of [index, element] pair arrays.
|
|
1803
3077
|
function arrEntriesFromTemp(t) {
|
|
1804
|
-
inc('__len', '__ptr_offset', '__alloc_hdr')
|
|
3078
|
+
inc('__len', '__ptr_offset', '__ptr_offset_fwd', '__alloc_hdr')
|
|
1805
3079
|
const n = tempI32('aen'), i = tempI32('aei'), src = tempI32('aes'), pair = tempI32('aep')
|
|
1806
3080
|
const out = allocPtr({ type: PTR.ARRAY, len: ['local.get', `$${n}`], tag: 'aea' })
|
|
1807
3081
|
const id = ctx.func.uniq++
|