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/core.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* @module core
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { typed, asF64, asI32, asI64, NULL_NAN, UNDEF_NAN, FALSE_NAN, TRUE_NAN, temp, usesDynProps, ptrOffsetIR, isNullish, valKindToPtr, sidecarOverride, undefExpr } from '../src/ir.js'
|
|
12
|
+
import { typed, asF64, asI32, asI64, NULL_NAN, UNDEF_NAN, TOMB_NAN, FALSE_NAN, TRUE_NAN, temp, usesDynProps, ptrOffsetIR, isNullish, valKindToPtr, sidecarOverride, undefExpr, cloneIR } from '../src/ir.js'
|
|
13
13
|
import { emit, spread, deps, wat } from '../src/bridge.js'
|
|
14
14
|
import { reconstructArgsWithSpreads } from '../src/ir.js'
|
|
15
15
|
import { valTypeOf, shapeOf } from '../src/kind.js'
|
|
@@ -18,9 +18,9 @@ import { inlineArraySid } from '../src/static.js'
|
|
|
18
18
|
import { VAL, lookupValType, lookupNotString, repOf, updateRep } from '../src/reps.js'
|
|
19
19
|
import { ctx, err, inc, PTR, LAYOUT, HEAP, FORWARDING_MASK, emitArity, followForwardingWat, declGlobal } from '../src/ctx.js'
|
|
20
20
|
import { ptrOffsetFwdWat, STR_INTERN_BIT } from '../layout.js'
|
|
21
|
-
import { nanPrefixHex } from '../layout.js'
|
|
21
|
+
import { nanPrefixHex, nanPrefixMaskHex, ssoBitI64Hex, encodePtrHi, i64Hex } from '../layout.js'
|
|
22
22
|
import { initSchema } from './schema.js'
|
|
23
|
-
import { strHashLiteral } from './collection.js'
|
|
23
|
+
import { strHashLiteral, heapResetWat, LENGTH_SSO_I64 } from './collection.js'
|
|
24
24
|
|
|
25
25
|
const NAN_BITS = nanPrefixHex()
|
|
26
26
|
|
|
@@ -36,12 +36,28 @@ export default (ctx) => {
|
|
|
36
36
|
__is_str_key: ['__ptr_type'],
|
|
37
37
|
__str_len: ['__ptr_type', '__ptr_offset', '__ptr_aux'],
|
|
38
38
|
__set_len: ['__ptr_offset_fwd'],
|
|
39
|
-
|
|
39
|
+
// Property-fallback arm (`.length` as an ordinary own key on OBJECT/HASH
|
|
40
|
+
// receivers) needs the dyn dispatcher — but only when the program can even
|
|
41
|
+
// HOLD such a property (a schema'd object or dyn/hash machinery exists);
|
|
42
|
+
// string/array-only programs keep the lean undefined arm.
|
|
43
|
+
__length: () => ['__ptr_type', '__str_len', '__len',
|
|
44
|
+
...(lengthNeedsDynArm() ? ['__dyn_get_expr_t_h'] : [])],
|
|
40
45
|
__alloc: ['__memgrow'],
|
|
41
46
|
__alloc_hdr: ['__alloc'],
|
|
42
47
|
__alloc_hdr_n: ['__alloc'],
|
|
43
48
|
__coll_order: ['__alloc'],
|
|
44
|
-
|
|
49
|
+
__hash_keys_ro: ['__ptr_offset', '__coll_order', '__alloc_hdr', '__mkptr'],
|
|
50
|
+
// Durable-receiver global-table merge (see __obj_clone's body) pulls in
|
|
51
|
+
// __ihash_get_local/__is_nullish only when collection.js's dyn-props
|
|
52
|
+
// machinery is actually part of this build (mirrors json.js's __json_obj
|
|
53
|
+
// and array.js's needsArrayDynMove-gated deps thunks).
|
|
54
|
+
__obj_clone: () => ['__ptr_type', '__ptr_aux', '__ptr_offset', '__len', '__cap', '__alloc_hdr', '__alloc_hdr_n', '__mkptr',
|
|
55
|
+
...(ctx.scope.globals.has('__dyn_props') ? ['__ihash_get_local', '__is_nullish'] : [])],
|
|
56
|
+
__durable_fwd_log: ['__alloc'],
|
|
57
|
+
__durable_fwd_heal: [],
|
|
58
|
+
__durable_slot_log: ['__alloc'],
|
|
59
|
+
__durable_slot_heal: [],
|
|
60
|
+
__is_eph_bits: [],
|
|
45
61
|
})
|
|
46
62
|
|
|
47
63
|
ctx.core.stdlib['__is_nullish'] = `(func $__is_nullish (param $v i64) (result i32)
|
|
@@ -127,9 +143,18 @@ export default (ctx) => {
|
|
|
127
143
|
(i64.const ${UNDEF_NAN}))`
|
|
128
144
|
ctx.core.stdlib['__dyn_get_or'] = `(func $__dyn_get_or (param $obj i64) (param $key i64) (param $fallback i64) (result i64)
|
|
129
145
|
(local.get $fallback))`
|
|
146
|
+
// Sidecar probe entry (sidecarOverride / the builtin-shadow method fork): with
|
|
147
|
+
// no dyn-props module there are no own props — the probe always misses and the
|
|
148
|
+
// builtin arm runs, which is exactly the stub-world semantics.
|
|
149
|
+
ctx.core.stdlib['__dyn_get_expr'] = `(func $__dyn_get_expr (param $obj i64) (param $key i64) (result i64)
|
|
150
|
+
(i64.const ${UNDEF_NAN}))`
|
|
130
151
|
ctx.core.stdlib['__dyn_set'] = `(func $__dyn_set (param $obj i64) (param $key i64) (param $val i64) (result i64)
|
|
131
152
|
(local.get $val))`
|
|
132
|
-
|
|
153
|
+
// Signature must match collection.js's real __dyn_move (i32 result: 1 = an
|
|
154
|
+
// entry was found+rekeyed, 0 = no-op) — array.js's grow/shift call sites are
|
|
155
|
+
// built once and call whichever version ends up registered.
|
|
156
|
+
ctx.core.stdlib['__dyn_move'] = `(func $__dyn_move (param $oldOff i32) (param $newOff i32) (result i32)
|
|
157
|
+
(i32.const 0))`
|
|
133
158
|
|
|
134
159
|
// Memory section auto-enabled: compile.js checks ctx.module.modules.ptr
|
|
135
160
|
|
|
@@ -287,6 +312,23 @@ export default (ctx) => {
|
|
|
287
312
|
// OBJECT slots (offsets < heap base) don't misread arbitrary memory at off-16.
|
|
288
313
|
// Updated by optimizeModule() when data segment exceeds HEAP.START bytes.
|
|
289
314
|
declGlobal('__heap_start', 'i32', HEAP.START)
|
|
315
|
+
// Current memory limit in BYTES — __alloc's inline fast-path check
|
|
316
|
+
// (`next > __heap_end` → slow __memgrow call) replaces a per-alloc call whose
|
|
317
|
+
// page math always concluded "no grow" (jessie: 1.1M entries/run). __memgrow
|
|
318
|
+
// updates it after any growth; stale-LOW is safe (one extra slow call) and
|
|
319
|
+
// wasm memory never shrinks. 65536-page (4 GiB) memories wrap the shl to 0 —
|
|
320
|
+
// every alloc slow-paths there, still correct. Seeded 0: first alloc pays once.
|
|
321
|
+
declGlobal('__heap_end', 'i32', 0)
|
|
322
|
+
// i64 twin of __heap_end, for the CORRECTNESS-sensitive forwarding-chase bound
|
|
323
|
+
// (layout.js's followForwardingWat/ptrOffsetFwdWat, and their inline collection.js
|
|
324
|
+
// dyn-props copies): those checks gate whether a relocated ARRAY/SET/MAP/HASH's
|
|
325
|
+
// forwarding header gets followed at all, so the __heap_end wraparound-to-0 at the
|
|
326
|
+
// wasm32 4 GiB ceiling — benign for __alloc's slow-path retry — would there instead
|
|
327
|
+
// silently disable forwarding forever (every off > 0 reads as "out of bounds", so the
|
|
328
|
+
// cap=-1 sentinel of an abandoned block is never re-chased and gets misread as a real
|
|
329
|
+
// capacity). __memgrow updates this alongside __heap_end so every hot dereference site
|
|
330
|
+
// pays one $__heap_end64 global read instead of recomputing i64.shl(memory.size,16).
|
|
331
|
+
declGlobal('__heap_end64', 'i64', 0)
|
|
290
332
|
|
|
291
333
|
// Shared memory keeps the heap pointer in linear memory (memory[HEAP.PTR_ADDR]):
|
|
292
334
|
// wasm globals are per-instance, so threads sharing one memory must share one
|
|
@@ -317,16 +359,33 @@ export default (ctx) => {
|
|
|
317
359
|
(then (local.set $cur (i32.sub (i32.const 65536) (memory.size))))) ;; cap at wasm32 max
|
|
318
360
|
(if (i32.eq (memory.grow (local.get $cur)) (i32.const -1))
|
|
319
361
|
(then (if (i32.eq (memory.grow (i32.sub (local.get $need) (memory.size))) (i32.const -1))
|
|
320
|
-
(then (unreachable)))))))
|
|
362
|
+
(then (unreachable)))))))
|
|
363
|
+
(global.set $__heap_end (i32.shl (memory.size) (i32.const 16)))
|
|
364
|
+
(global.set $__heap_end64 (i64.shl (i64.extend_i32_u (memory.size)) (i64.const 16))))`
|
|
321
365
|
|
|
322
366
|
if (ctx.memory.shared) {
|
|
323
367
|
// Heap offset stored at memory[HEAP.PTR_ADDR] (i32), just before heap start at
|
|
324
368
|
// HEAP.START. Threads sharing one memory must share one pointer cell.
|
|
325
|
-
|
|
369
|
+
// TRULY-shared memory (opts.sharedMemory → ctx.memory.atomic): the bump is a
|
|
370
|
+
// CAS retry loop — a plain load/store pair would hand two racing threads the
|
|
371
|
+
// same block. Plain imported memory keeps the cheap non-atomic bump.
|
|
372
|
+
ctx.core.stdlib['__alloc'] = ctx.memory.atomic ? `(func $__alloc (param $bytes i32) (result i32)
|
|
373
|
+
(local $ptr i32) (local $next i32)
|
|
374
|
+
(block $done (loop $retry
|
|
375
|
+
(local.set $ptr (i32.atomic.load (i32.const ${HEAP.PTR_ADDR})))
|
|
376
|
+
(local.set $next (i32.and (i32.add (i32.add (local.get $ptr) (local.get $bytes)) (i32.const 7)) (i32.const -8)))
|
|
377
|
+
(if (i32.gt_u (local.get $next) (global.get $__heap_end))
|
|
378
|
+
(then (call $__memgrow (local.get $next))))
|
|
379
|
+
(br_if $done (i32.eq
|
|
380
|
+
(i32.atomic.rmw.cmpxchg (i32.const ${HEAP.PTR_ADDR}) (local.get $ptr) (local.get $next))
|
|
381
|
+
(local.get $ptr)))
|
|
382
|
+
(br $retry)))
|
|
383
|
+
(local.get $ptr))` : `(func $__alloc (param $bytes i32) (result i32)
|
|
326
384
|
(local $ptr i32) (local $next i32)
|
|
327
385
|
(local.set $ptr (i32.load (i32.const ${HEAP.PTR_ADDR})))
|
|
328
386
|
(local.set $next (i32.and (i32.add (i32.add (local.get $ptr) (local.get $bytes)) (i32.const 7)) (i32.const -8)))
|
|
329
|
-
(
|
|
387
|
+
(if (i32.gt_u (local.get $next) (global.get $__heap_end))
|
|
388
|
+
(then (call $__memgrow (local.get $next))))
|
|
330
389
|
(i32.store (i32.const ${HEAP.PTR_ADDR}) (local.get $next))
|
|
331
390
|
(local.get $ptr))`
|
|
332
391
|
// NOTE: shared memory rewinds to the raw HEAP.START, NOT a post-init high-water
|
|
@@ -337,7 +396,7 @@ export default (ctx) => {
|
|
|
337
396
|
// clock at 0, heap ptr at HEAP.PTR_ADDR). Owned memory (the self-host + default
|
|
338
397
|
// case) is the one fixed below; revisit shared if a thread-pooled reset hits it.
|
|
339
398
|
ctx.core.stdlib['__clear'] = `(func $__clear
|
|
340
|
-
(i32.store (i32.const ${HEAP.PTR_ADDR}) (i32.const ${HEAP.START})))`
|
|
399
|
+
(${ctx.memory.atomic ? 'i32.atomic.store' : 'i32.store'} (i32.const ${HEAP.PTR_ADDR}) (i32.const ${HEAP.START})))`
|
|
341
400
|
} else {
|
|
342
401
|
// Own memory: heap offset in a global, exported so the JS-side adapter
|
|
343
402
|
// (alloc:false, no `_alloc` export) shares the pointer.
|
|
@@ -356,11 +415,133 @@ export default (ctx) => {
|
|
|
356
415
|
(local $ptr i32) (local $next i32)
|
|
357
416
|
(local.set $ptr (global.get $__heap))
|
|
358
417
|
(local.set $next (i32.and (i32.add (i32.add (local.get $ptr) (local.get $bytes)) (i32.const 7)) (i32.const -8)))
|
|
359
|
-
(
|
|
418
|
+
(if (i32.gt_u (local.get $next) (global.get $__heap_end))
|
|
419
|
+
(then (call $__memgrow (local.get $next))))
|
|
360
420
|
(global.set $__heap (local.get $next))
|
|
361
421
|
(local.get $ptr))`
|
|
422
|
+
// __clear rewinds the bump arena, but __dyn_props/__dyn_get_cache_* (declared
|
|
423
|
+
// unconditionally whenever the collection module loads — module/collection.js)
|
|
424
|
+
// cache pointers/offsets INTO that arena across calls, so a warm compile-clear-
|
|
425
|
+
// compile loop needs them reset too — see the post-hoc patch in
|
|
426
|
+
// src/wat/assemble.js (search "__dyn_props reset") for WHY this can't gate on
|
|
427
|
+
// `ctx.scope.globals.has(...)` at declaration time: that's true whenever
|
|
428
|
+
// collection is loaded AT ALL, even for a program that never touches dynamic
|
|
429
|
+
// props, and __clear's own resolved text is scanned by reachableStdlib — an
|
|
430
|
+
// unconditional `global.set $__dyn_props` line here would leak that (dead,
|
|
431
|
+
// for this program) name into non-dyn-prop output, both wasting bytes and
|
|
432
|
+
// (worse) tripping WAT-substring test assertions like
|
|
433
|
+
// `!/__dyn_get/.test(wat)` (test/closures.js) since __dyn_get_cache_off/props
|
|
434
|
+
// contain that substring. The real gate — whether __dyn_set (the only writer
|
|
435
|
+
// of __dyn_props) is actually reachable — isn't known until AFTER
|
|
436
|
+
// reachableStdlib runs, so the reset is injected post-hoc once that's settled.
|
|
362
437
|
ctx.core.stdlib['__clear'] = `(func $__clear
|
|
363
438
|
(global.set $__heap (global.get $__heap_reset)))`
|
|
439
|
+
|
|
440
|
+
// Durable relocation log — see collection.js's durableFwdLogIR for the full
|
|
441
|
+
// rationale (array/hash/set/map growth forwards a DURABLE header into an
|
|
442
|
+
// EPHEMERAL new block; `_clear` must heal that back before rewinding the arena
|
|
443
|
+
// or the durable alias dangles forever). `__durable_fwd_buf` is allocated
|
|
444
|
+
// lazily (raw `__alloc`, no forwarding-capable header of its own — it must
|
|
445
|
+
// never recurse into the bug it exists to fix) on the first durable grow of a
|
|
446
|
+
// round; `__durable_fwd_heal` (wired into `__clear` post-hoc, see
|
|
447
|
+
// src/wat/assemble.js) restores every logged header to its pre-grow (len, cap)
|
|
448
|
+
// and resets both globals to 0 so the buffer is re-allocated fresh next round —
|
|
449
|
+
// it only needs to survive from "logged this round" to "healed at this round's
|
|
450
|
+
// `_clear`", never across a reset. 256 entries is a trap-on-overflow ceiling
|
|
451
|
+
// for a count that is 0 in the overwhelmingly common program (real durable-
|
|
452
|
+
// growth sites are a handful of compiler-internal structures, not user data).
|
|
453
|
+
declGlobal('__durable_fwd_buf', 'i32')
|
|
454
|
+
declGlobal('__durable_fwd_n', 'i32')
|
|
455
|
+
ctx.core.stdlib['__durable_fwd_log'] = `(func $__durable_fwd_log (param $off i32) (param $len i32) (param $cap i32)
|
|
456
|
+
(local $base i32) (local $n i32)
|
|
457
|
+
(if (i32.eqz (global.get $__durable_fwd_buf))
|
|
458
|
+
(then (global.set $__durable_fwd_buf (call $__alloc (i32.const 3072)))))
|
|
459
|
+
(local.set $n (global.get $__durable_fwd_n))
|
|
460
|
+
(if (i32.ge_s (local.get $n) (i32.const 256)) (then (unreachable)))
|
|
461
|
+
(local.set $base (i32.add (global.get $__durable_fwd_buf) (i32.mul (local.get $n) (i32.const 12))))
|
|
462
|
+
(i32.store (local.get $base) (local.get $off))
|
|
463
|
+
(i32.store (i32.add (local.get $base) (i32.const 4)) (local.get $len))
|
|
464
|
+
(i32.store (i32.add (local.get $base) (i32.const 8)) (local.get $cap))
|
|
465
|
+
(global.set $__durable_fwd_n (i32.add (local.get $n) (i32.const 1))))`
|
|
466
|
+
ctx.core.stdlib['__durable_fwd_heal'] = `(func $__durable_fwd_heal
|
|
467
|
+
(local $i i32) (local $n i32) (local $base i32) (local $off i32)
|
|
468
|
+
(local.set $n (global.get $__durable_fwd_n))
|
|
469
|
+
(block $done (loop $l
|
|
470
|
+
(br_if $done (i32.ge_s (local.get $i) (local.get $n)))
|
|
471
|
+
(local.set $base (i32.add (global.get $__durable_fwd_buf) (i32.mul (local.get $i) (i32.const 12))))
|
|
472
|
+
(local.set $off (i32.load (local.get $base)))
|
|
473
|
+
(i32.store (i32.sub (local.get $off) (i32.const 8)) (i32.load (i32.add (local.get $base) (i32.const 4))))
|
|
474
|
+
(i32.store (i32.sub (local.get $off) (i32.const 4)) (i32.load (i32.add (local.get $base) (i32.const 8))))
|
|
475
|
+
(local.set $i (i32.add (local.get $i) (i32.const 1)))
|
|
476
|
+
(br $l)))
|
|
477
|
+
(global.set $__durable_fwd_n (i32.const 0))
|
|
478
|
+
(global.set $__durable_fwd_buf (i32.const 0)))`
|
|
479
|
+
|
|
480
|
+
// Durable SLOT log — the value-write sibling of the relocation log above. A
|
|
481
|
+
// collection whose storage is DURABLE (init-created dict, off < __heap_reset)
|
|
482
|
+
// can receive an EPHEMERAL boxed value at runtime (a memo caching this round's
|
|
483
|
+
// parsed node, a registry entry) — the slot then dangles across \`_clear\` and
|
|
484
|
+
// the next round reads reused-arena garbage through it (the corpus-wide warm
|
|
485
|
+
// trap: a durable literal-text→node dict handing round-1 node arrays into
|
|
486
|
+
// round-2's tree). Writers call \`__durable_slot_log(addr)\` when storing an
|
|
487
|
+
// ephemeral value into a durable slot (see collection.js durableSlotLogIR);
|
|
488
|
+
// \`__durable_slot_heal\` (wired into \`__clear\` post-hoc, like the fwd heal)
|
|
489
|
+
// overwrites every logged slot with \`undefined\` — the pointed-at data dies
|
|
490
|
+
// with the arena, so entry-death is the only sound semantics. Same lazy-buffer
|
|
491
|
+
// + trap-ceiling design as the fwd log; slots are 4 bytes each so one page
|
|
492
|
+
// covers 1024 writes (durable-receiver writes are rare by construction).
|
|
493
|
+
declGlobal('__durable_slot_buf', 'i32')
|
|
494
|
+
declGlobal('__durable_slot_n', 'i32')
|
|
495
|
+
ctx.core.stdlib['__is_eph_bits'] = `(func $__is_eph_bits (param $b i64) (result i32)
|
|
496
|
+
(local $t i32)
|
|
497
|
+
;; boxed heap pointer: quiet-NaN prefix, heap-kind tag, non-SSO, offset past the durable watermark
|
|
498
|
+
(if (i64.ne (i64.and (local.get $b) (i64.const ${nanPrefixMaskHex()})) (i64.const ${nanPrefixHex()}))
|
|
499
|
+
(then (return (i32.const 0))))
|
|
500
|
+
(local.set $t (i32.wrap_i64 (i64.and (i64.shr_u (local.get $b) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
|
|
501
|
+
;; heap kinds {ARRAY,BUFFER,TYPED,STRING,OBJECT,HASH,SET,MAP,CLOSURE} = bits 1-4,6-10 → 0x7DE
|
|
502
|
+
(if (i32.eqz (i32.and (i32.shl (i32.const 1) (local.get $t)) (i32.const 0x7DE)))
|
|
503
|
+
(then (return (i32.const 0))))
|
|
504
|
+
(if (i32.and (i32.eq (local.get $t) (i32.const ${PTR.STRING}))
|
|
505
|
+
(i64.ne (i64.and (local.get $b) (i64.const ${ssoBitI64Hex()})) (i64.const 0)))
|
|
506
|
+
(then (return (i32.const 0))))
|
|
507
|
+
(i32.ge_u (i32.wrap_i64 (i64.and (local.get $b) (i64.const 0xFFFFFFFF))) (global.get $__heap_reset)))`
|
|
508
|
+
ctx.core.stdlib['__durable_slot_log'] = `(func $__durable_slot_log (param $addr i32) (param $tbl i32)
|
|
509
|
+
(local $n i32) (local $base i32)
|
|
510
|
+
(if (i32.eqz (global.get $__durable_slot_buf))
|
|
511
|
+
(then (global.set $__durable_slot_buf (call $__alloc (i32.const 8192)))))
|
|
512
|
+
(local.set $n (global.get $__durable_slot_n))
|
|
513
|
+
(if (i32.ge_s (local.get $n) (i32.const 1024)) (then (unreachable)))
|
|
514
|
+
(local.set $base (i32.add (global.get $__durable_slot_buf) (i32.shl (local.get $n) (i32.const 3))))
|
|
515
|
+
(i32.store (local.get $base) (local.get $addr))
|
|
516
|
+
(i32.store (i32.add (local.get $base) (i32.const 4)) (local.get $tbl))
|
|
517
|
+
(global.set $__durable_slot_n (i32.add (local.get $n) (i32.const 1))))`
|
|
518
|
+
ctx.core.stdlib['__durable_slot_heal'] = `(func $__durable_slot_heal
|
|
519
|
+
(local $i i32) (local $n i32) (local $a i32) (local $base i32) (local $tbl i32)
|
|
520
|
+
(local.set $n (global.get $__durable_slot_n))
|
|
521
|
+
(block $done (loop $l
|
|
522
|
+
(br_if $done (i32.ge_s (local.get $i) (local.get $n)))
|
|
523
|
+
(local.set $base (i32.add (global.get $__durable_slot_buf) (i32.shl (local.get $i) (i32.const 3))))
|
|
524
|
+
(local.set $a (i32.load (local.get $base)))
|
|
525
|
+
(local.set $tbl (i32.load (i32.add (local.get $base) (i32.const 4))))
|
|
526
|
+
(if (i32.and (local.get $a) (i32.const 1))
|
|
527
|
+
;; bit0: ENTRY heal — this round INSERTED the entry into durable storage; a
|
|
528
|
+
;; fresh instance would not have it. Zombie it (key TOMB, value undefined —
|
|
529
|
+
;; probes pass over, __coll_order skips) and decrement the table len so
|
|
530
|
+
;; len-sized iteration and .size agree. Runs AFTER __durable_fwd_heal, so a
|
|
531
|
+
;; grown-then-healed table's len is already its restored pre-grow value.
|
|
532
|
+
(then
|
|
533
|
+
(local.set $a (i32.and (local.get $a) (i32.const -2)))
|
|
534
|
+
(i64.store (i32.add (local.get $a) (i32.const 8)) (i64.const ${TOMB_NAN}))
|
|
535
|
+
(i64.store (i32.add (local.get $a) (i32.const 16)) (i64.const ${UNDEF_NAN}))
|
|
536
|
+
(i32.store (i32.sub (local.get $tbl) (i32.const 8))
|
|
537
|
+
(i32.sub (i32.load (i32.sub (local.get $tbl) (i32.const 8))) (i32.const 1))))
|
|
538
|
+
;; plain: VALUE heal — the entry pre-existed durably; its old value is
|
|
539
|
+
;; unrecoverable, undefined is the honest read.
|
|
540
|
+
(else (i64.store (local.get $a) (i64.const ${UNDEF_NAN}))))
|
|
541
|
+
(local.set $i (i32.add (local.get $i) (i32.const 1)))
|
|
542
|
+
(br $l)))
|
|
543
|
+
(global.set $__durable_slot_n (i32.const 0))
|
|
544
|
+
(global.set $__durable_slot_buf (i32.const 0)))`
|
|
364
545
|
}
|
|
365
546
|
|
|
366
547
|
// Build an insertion-ordered list of live slot offsets for a Set/Map/HASH
|
|
@@ -384,7 +565,10 @@ export default (ctx) => {
|
|
|
384
565
|
(block $gd (loop $gl
|
|
385
566
|
(br_if $gd (i32.ge_s (local.get $i) (local.get $cap)))
|
|
386
567
|
(local.set $slot (i32.add (local.get $off) (i32.mul (local.get $i) (local.get $stride))))
|
|
387
|
-
(if (
|
|
568
|
+
(if (i32.and
|
|
569
|
+
(i64.ne (i64.load (local.get $slot)) (i64.const 0))
|
|
570
|
+
;; skip healed zombie entries (durable-slot heal: key = TOMB sentinel)
|
|
571
|
+
(i64.ne (i64.load (i32.add (local.get $slot) (i32.const 8))) (i64.const ${TOMB_NAN})))
|
|
388
572
|
(then
|
|
389
573
|
(i32.store (i32.add (local.get $buf) (i32.shl (local.get $n) (i32.const 2))) (local.get $slot))
|
|
390
574
|
(local.set $n (i32.add (local.get $n) (i32.const 1)))))
|
|
@@ -411,6 +595,43 @@ export default (ctx) => {
|
|
|
411
595
|
(br $sl)))
|
|
412
596
|
(local.get $buf))`
|
|
413
597
|
|
|
598
|
+
// for-in's HASH key enumeration with a 1-slot enum cache (V8's EnumCache analog).
|
|
599
|
+
// A for-in over an unchanged dict re-derives the same key array every entry —
|
|
600
|
+
// __coll_order buffer + out-array alloc + cap scan + sort per loop entry (the
|
|
601
|
+
// dominant cost of any per-call `for (k in cfg)` pattern: jessie's comment
|
|
602
|
+
// wrapper paid this per TOKEN). Cache the boxed key array keyed by
|
|
603
|
+
// (table off, live len): an insert changes len, so it misses naturally with no
|
|
604
|
+
// insert-side hook; the only len-preserving key-set change is delete-then-insert,
|
|
605
|
+
// so the HASH delete (genDelete, collection.js) clears the cache unconditionally;
|
|
606
|
+
// `__clear` resets it (arena rewind can re-issue the cached off to a new table).
|
|
607
|
+
// Grow/remint relocation is safe hook-free: reads resolve forwarding to the new
|
|
608
|
+
// off (≠ cached), and a husk off is never re-issued within an arena epoch.
|
|
609
|
+
// ONLY sound for for-in (`__keys_ro`), whose result is read-only by construction —
|
|
610
|
+
// Object.keys must keep fresh-array semantics (callers may mutate the result).
|
|
611
|
+
ctx.core.stdlib['__hash_keys_ro'] = `(func $__hash_keys_ro (param $hbits i64) (result f64)
|
|
612
|
+
(local $off i32) (local $n i32) (local $ord i32) (local $i i32) (local $out i32)
|
|
613
|
+
(local.set $off (call $__ptr_offset (local.get $hbits)))
|
|
614
|
+
;; degenerate/null backing (off below heap base): empty result, uncached
|
|
615
|
+
(if (i32.lt_u (local.get $off) (i32.const ${HEAP.START}))
|
|
616
|
+
(then (return (call $__mkptr (i32.const ${PTR.ARRAY}) (i32.const 0) (call $__alloc_hdr (i32.const 0) (i32.const 0))))))
|
|
617
|
+
(local.set $n (i32.load (i32.sub (local.get $off) (i32.const 8))))
|
|
618
|
+
(if (i32.and (i32.eq (local.get $off) (global.get $__enumc_off))
|
|
619
|
+
(i32.eq (local.get $n) (global.get $__enumc_len)))
|
|
620
|
+
(then (return (global.get $__enumc_arr))))
|
|
621
|
+
(local.set $ord (call $__coll_order (local.get $off)
|
|
622
|
+
(i32.load (i32.sub (local.get $off) (i32.const 4))) (i32.const 24)))
|
|
623
|
+
(local.set $out (call $__alloc_hdr (local.get $n) (local.get $n)))
|
|
624
|
+
(block $brk (loop $l
|
|
625
|
+
(br_if $brk (i32.ge_s (local.get $i) (local.get $n)))
|
|
626
|
+
(i64.store (i32.add (local.get $out) (i32.shl (local.get $i) (i32.const 3)))
|
|
627
|
+
(i64.load (i32.add (i32.load (i32.add (local.get $ord) (i32.shl (local.get $i) (i32.const 2)))) (i32.const 8))))
|
|
628
|
+
(local.set $i (i32.add (local.get $i) (i32.const 1)))
|
|
629
|
+
(br $l)))
|
|
630
|
+
(global.set $__enumc_off (local.get $off))
|
|
631
|
+
(global.set $__enumc_len (local.get $n))
|
|
632
|
+
(global.set $__enumc_arr (call $__mkptr (i32.const ${PTR.ARRAY}) (i32.const 0) (local.get $out)))
|
|
633
|
+
(global.get $__enumc_arr))`
|
|
634
|
+
|
|
414
635
|
// === Memory-based length/cap helpers (C-style headers) ===
|
|
415
636
|
|
|
416
637
|
// Array/TypedArray/Buffer: [-8:len(i32)][-4:cap(i32)][data...]
|
|
@@ -582,7 +803,10 @@ export default (ctx) => {
|
|
|
582
803
|
// - HASH: copy header + every probe slot wholesale (entries hold immutable
|
|
583
804
|
// string keys + scalar/pointer values — a byte copy is an independent dict).
|
|
584
805
|
// - anything else (primitive): nothing to clone, return as-is.
|
|
585
|
-
|
|
806
|
+
// Thunked (not a plain template string) so heapResetWat()/the __dyn_props
|
|
807
|
+
// presence check below read the FINAL declaration state — see collection.js's
|
|
808
|
+
// heapResetWat comment for why.
|
|
809
|
+
ctx.core.stdlib['__obj_clone'] = () => `(func $__obj_clone (param $v f64) (result f64)
|
|
586
810
|
(local $bits i64) (local $t i32) (local $sid i32) (local $n i32) (local $cap i32)
|
|
587
811
|
(local $src i32) (local $dst i32) (local $props i64)
|
|
588
812
|
(local.set $bits (i64.reinterpret_f64 (local.get $v)))
|
|
@@ -598,12 +822,36 @@ export default (ctx) => {
|
|
|
598
822
|
(local.set $cap (i32.add (local.get $n) (i32.eqz (local.get $n))))
|
|
599
823
|
(local.set $dst (call $__alloc_hdr (i32.const 0) (local.get $cap)))
|
|
600
824
|
(memory.copy (local.get $dst) (local.get $src) (i32.shl (local.get $n) (i32.const 3)))
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
825
|
+
;; Dyn-props (off-schema keys added by o[k]=v): heap-allocated sources
|
|
826
|
+
;; (src >= __heap_start) carry them at src-16 as a HASH sidecar
|
|
827
|
+
;; (populated by an init-time write, or by any write at all on an
|
|
828
|
+
;; EPHEMERAL source) and/or in the global __dyn_props table (populated
|
|
829
|
+
;; by a RUNTIME/post-init write on a DURABLE source — see
|
|
830
|
+
;; collection.js's heapResetWat for the full policy). Static-segment
|
|
831
|
+
;; sources (src < __heap_start) have no header — both checks below
|
|
832
|
+
;; are gated on src >= __heap_start so neither reads neighbor static
|
|
833
|
+
;; data. Prefers the sidecar when present (authoritative for a
|
|
834
|
+
;; DURABLE source's untouched init-time keys, and the only source for
|
|
835
|
+
;; an ephemeral one); falls back to the global entry otherwise. A
|
|
836
|
+
;; source with keys split across BOTH (some at init, more added at
|
|
837
|
+
;; runtime) clones only the sidecar's — a known narrow gap versus
|
|
838
|
+
;; Object.keys/JSON.stringify's full merge, accepted here because a
|
|
839
|
+
;; spread of such a genuinely mixed durable dict is materially rarer.
|
|
840
|
+
(local.set $props (i64.load (i32.sub (local.get $src) (i32.const 16))))
|
|
841
|
+
(if (i32.and (i32.ge_u (local.get $src) (global.get $__heap_start))
|
|
842
|
+
(i32.eq (call $__ptr_type (local.get $props)) (i32.const ${PTR.HASH})))
|
|
843
|
+
(then (i64.store (i32.sub (local.get $dst) (i32.const 16))
|
|
844
|
+
(i64.reinterpret_f64 (call $__obj_clone (f64.reinterpret_i64 (local.get $props))))))${ctx.scope.globals.has('__dyn_props') ? `
|
|
845
|
+
(else
|
|
846
|
+
(if (i32.and (i32.ge_u (local.get $src) (global.get $__heap_start))
|
|
847
|
+
(i32.lt_u (local.get $src) ${heapResetWat()}))
|
|
848
|
+
(then
|
|
849
|
+
(if (f64.ne (global.get $__dyn_props) (f64.const 0))
|
|
850
|
+
(then
|
|
851
|
+
(local.set $props (call $__ihash_get_local (i64.reinterpret_f64 (global.get $__dyn_props)) (i64.reinterpret_f64 (f64.convert_i32_s (local.get $src)))))
|
|
852
|
+
(if (i32.eqz (call $__is_nullish (local.get $props)))
|
|
853
|
+
(then (i64.store (i32.sub (local.get $dst) (i32.const 16))
|
|
854
|
+
(i64.reinterpret_f64 (call $__obj_clone (f64.reinterpret_i64 (local.get $props)))))))))))` : ''}))
|
|
607
855
|
(return (call $__mkptr (i32.const ${PTR.OBJECT}) (local.get $sid) (local.get $dst)))))
|
|
608
856
|
(if (i32.eq (local.get $t) (i32.const ${PTR.HASH}))
|
|
609
857
|
(then
|
|
@@ -640,8 +888,8 @@ export default (ctx) => {
|
|
|
640
888
|
// === Shared dispatch helpers ===
|
|
641
889
|
|
|
642
890
|
/** Emit .length access for a WASM f64 node. Monomorphize by vt, or runtime dispatch.
|
|
643
|
-
* ARRAY
|
|
644
|
-
*
|
|
891
|
+
* ARRAY length is i32 at offset-8 — inline that load directly instead of calling
|
|
892
|
+
* __len which re-dispatches on type. ptrOffsetIR handles
|
|
645
893
|
* ARRAY forwarding (non-ARRAY skips the forwarding loop). TYPED has a variable-width
|
|
646
894
|
* layout depending on the aux typed-element shift, so it still routes through __len.
|
|
647
895
|
* `notString` (from rep.notString — write-shape evidence rules out primitive string)
|
|
@@ -657,10 +905,13 @@ export default (ctx) => {
|
|
|
657
905
|
ctx.core.jsstring.add('length')
|
|
658
906
|
return typed(['f64.convert_i32_s', ['call', '$__jss_length', va]], 'f64')
|
|
659
907
|
}
|
|
660
|
-
if (vt === VAL.ARRAY
|
|
908
|
+
if (vt === VAL.ARRAY) {
|
|
661
909
|
const off = ptrOffsetIR(va, vt)
|
|
662
910
|
return typed(['f64.convert_i32_s', ['i32.load', ['i32.sub', off, ['i32.const', 8]]]], 'f64')
|
|
663
911
|
}
|
|
912
|
+
// Set/Map have no .length in JS — their count is `.size`. (The former
|
|
913
|
+
// shared-layout __len shortcut returned the entry count here.)
|
|
914
|
+
if (vt === VAL.SET || vt === VAL.MAP) return undefExpr()
|
|
664
915
|
if (vt === VAL.TYPED)
|
|
665
916
|
return typed(['f64.convert_i32_s', ['call', '$__len', ['i64.reinterpret_f64', va]]], 'f64')
|
|
666
917
|
// Known string → byteLen via the active string rep. Pass the slot
|
|
@@ -691,7 +942,7 @@ export default (ctx) => {
|
|
|
691
942
|
// for ad-hoc props on pointer-backed values, so schema reads should bypass it.
|
|
692
943
|
// Slot val-types reach the emit-time consumer via valTypeOf → ctx.schema.slotVT
|
|
693
944
|
// (read on the AST `.prop` node), not via tagging this IR node.
|
|
694
|
-
function emitSchemaSlotRead(baseExpr, idx) {
|
|
945
|
+
function emitSchemaSlotRead(baseExpr, idx, i32Certain) {
|
|
695
946
|
// An unboxed proven-non-ARRAY pointer (a structInline element cell, a narrowed local)
|
|
696
947
|
// reaches ptrOffsetIR raw so it returns the offset directly — no `__ptr_offset` call.
|
|
697
948
|
// Pre-boxing via asF64 strips ptrKind and forces every field read onto the call path
|
|
@@ -700,7 +951,80 @@ export default (ctx) => {
|
|
|
700
951
|
const base = (baseExpr?.ptrKind != null && baseExpr.ptrKind !== VAL.ARRAY)
|
|
701
952
|
? baseExpr
|
|
702
953
|
: (baseExpr?.type === 'f64' ? baseExpr : asF64(baseExpr))
|
|
703
|
-
|
|
954
|
+
const load = ctx.abi.object.ops.load(ptrOffsetIR(base, VAL.OBJECT), idx)
|
|
955
|
+
// Strict-int32 slot (ctx.schema.slotI32CertainAt — every censused write is
|
|
956
|
+
// exactly-int32, never -0): land the value directly in i32. trunc_sat of
|
|
957
|
+
// such an f64 is a value-exact round-trip, and every int consumer skips
|
|
958
|
+
// the ToInt32 guard/convert battery the f64 route pays (the immutable
|
|
959
|
+
// kernel's per-field cost); f64 consumers convert back at one op.
|
|
960
|
+
if (i32Certain) return typed(['i32.trunc_sat_f64_s', load], 'i32')
|
|
961
|
+
return typed(load, 'f64')
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// Top 32 bits of the i64 NaN-box carrier: NAN_PREFIX | PTR tag (TAG_SHIFT=47)
|
|
965
|
+
// | schemaId aux (AUX_SHIFT=32) — layout.js packs all three above bit 31, so
|
|
966
|
+
// masking the whole high word and comparing to encodePtrHi(OBJECT, sid) proves
|
|
967
|
+
// "is an OBJECT" AND "is exactly this schema" in one i64 compare; the low
|
|
968
|
+
// word (this instance's heap offset) is irrelevant and stays unmasked.
|
|
969
|
+
const OBJECT_SCHEMA_HI_MASK = '0xFFFFFFFF00000000'
|
|
970
|
+
const objectSchemaGuardHex = (sid) => i64Hex(BigInt(encodePtrHi(PTR.OBJECT, sid)) << 32n)
|
|
971
|
+
|
|
972
|
+
/** Monomorphic schema-slot devirtualization for a receiver whose static type
|
|
973
|
+
* is fully unknown (emitPropAccess's `vt == null` case, the __dyn_get_any_t_h
|
|
974
|
+
* path). `guard` (from ctx.schema.guardedSlotOf) proves `prop` names a field
|
|
975
|
+
* on exactly one registered schema program-wide: the subscript dispatch-
|
|
976
|
+
* descriptor pattern (`d.op`/`d.l`/`d.word`) and jz's own emit-table/IR-node
|
|
977
|
+
* reads under self-host are both a hot dot-read whose receiver is ALWAYS
|
|
978
|
+
* that one schema in practice, even though it flows through a parameter or
|
|
979
|
+
* array element the static analysis never pins to VAL.OBJECT.
|
|
980
|
+
*
|
|
981
|
+
* Emits a single masked i64 compare (OBJECT_SCHEMA_HI_MASK) then a direct
|
|
982
|
+
* payload-slot load; any other receiver (a different schema, or not an
|
|
983
|
+
* OBJECT at all) falls to `slow()` — the exact call this site would have
|
|
984
|
+
* emitted with no guard at all, so this can only ever be as fast, never
|
|
985
|
+
* wrong. Soundness: collection.js's __dyn_set schema arm
|
|
986
|
+
* (buildObjectSchemaSetArm) mirrors every dynamic write to a schema-named
|
|
987
|
+
* key into the payload slot, so the slot stays authoritative even after an
|
|
988
|
+
* `obj[k] = v` write through the dyn-props sidecar/global table — schema
|
|
989
|
+
* fields are never shadowed by a dynamic write. */
|
|
990
|
+
function emitSchemaSlotGuarded(va, guard, slow, prop) {
|
|
991
|
+
// Clone: `slow` is a closure the CALLER built over the same `va` object
|
|
992
|
+
// (emitPropAccess's `const slow = () => …emitDynGetAnyTyped(va, …)`), so this
|
|
993
|
+
// function's own use of `va` below and the caller's use inside `slow()` would
|
|
994
|
+
// otherwise be the SAME node appearing twice in the final if/then/else tree —
|
|
995
|
+
// one logical read, but two tree positions. A later pass that walks the tree
|
|
996
|
+
// once per node identity (not per position) — e.g. local-slot lifetime/reuse
|
|
997
|
+
// analysis — sees only the FIRST occurrence's use and can free/reassign the
|
|
998
|
+
// local behind the second, producing whatever locals happen to be resident at
|
|
999
|
+
// that point (see cloneIR's doc; ir.js). va is a pure receiver read (a local/
|
|
1000
|
+
// cell load), safe to duplicate as long as each occurrence is its own object.
|
|
1001
|
+
va = cloneIR(va)
|
|
1002
|
+
const bits = asI64(va?.type ? va : typed(va, 'f64'))
|
|
1003
|
+
const cond = ['i64.eq',
|
|
1004
|
+
['i64.and', bits, ['i64.const', OBJECT_SCHEMA_HI_MASK]],
|
|
1005
|
+
['i64.const', objectSchemaGuardHex(guard.sid)]]
|
|
1006
|
+
// PTR.OBJECT never forwards (FORWARDING_MASK — ctx.js — only ARRAY/HASH/
|
|
1007
|
+
// SET/MAP headers relocate on growth), so once the guard above has proven
|
|
1008
|
+
// the tag, the payload offset is a bare mask: no __ptr_offset call needed.
|
|
1009
|
+
// ptrOffsetIR (src/ir.js) always emits that call for an untyped node — it
|
|
1010
|
+
// has no way to know the forwarding check is dead here — so this inlines
|
|
1011
|
+
// the same extraction __ptr_offset itself would perform for an OBJECT tag.
|
|
1012
|
+
const off = ['i32.wrap_i64', ['i64.and', bits, ['i64.const', LAYOUT.OFFSET_MASK]]]
|
|
1013
|
+
const fast = typed(ctx.abi.object.ops.load(off, guard.slot), 'f64')
|
|
1014
|
+
const ir = typed(['if', ['result', 'f64'],
|
|
1015
|
+
cond,
|
|
1016
|
+
['then', fast],
|
|
1017
|
+
['else', slow()]], 'f64')
|
|
1018
|
+
// Slot-kind stamp for ToNumber sinking (toNumF64): when the ONE schema
|
|
1019
|
+
// this guard proves censuses the slot as NUMBER — and the prop is never
|
|
1020
|
+
// written anywhere (a write could store any kind) — the guard-HIT arm's
|
|
1021
|
+
// raw load is already a plain number. toNumF64 then coerces only the
|
|
1022
|
+
// dyn-miss arm instead of wrapping the whole read in __to_num — the
|
|
1023
|
+
// shapes-dispatch pattern's per-field coercion collapses on the hot path.
|
|
1024
|
+
if (ctx.schema.slotTypes?.get(guard.sid)?.[guard.slot] === VAL.NUMBER
|
|
1025
|
+
&& ctx.types.writtenProps && !ctx.types.writtenProps.has(prop))
|
|
1026
|
+
ir.guardedNumSlot = true
|
|
1027
|
+
return ir
|
|
704
1028
|
}
|
|
705
1029
|
|
|
706
1030
|
function emitHashGetLocalConst(base, key, prop) {
|
|
@@ -709,11 +1033,15 @@ export default (ctx) => {
|
|
|
709
1033
|
return typed(['f64.reinterpret_i64', ['call', '$__hash_get_local_h', receiver, key, ['i32.const', strHashLiteral(prop)]]], 'f64')
|
|
710
1034
|
}
|
|
711
1035
|
|
|
1036
|
+
// Every call site embeds `receiver` a SECOND time directly (as the dyn-get
|
|
1037
|
+
// call's own receiver arg) alongside this function's result — clone so the two
|
|
1038
|
+
// occurrences are distinct node objects (see emitSchemaSlotGuarded's comment /
|
|
1039
|
+
// cloneIR's doc for why aliased IR nodes are unsound to leave standing).
|
|
712
1040
|
function emitTypeTag(receiver, vt) {
|
|
713
1041
|
const p = valKindToPtr(vt)
|
|
714
1042
|
if (p != null) return ['i32.const', p]
|
|
715
1043
|
inc('__ptr_type')
|
|
716
|
-
return ['call', '$__ptr_type', receiver]
|
|
1044
|
+
return ['call', '$__ptr_type', cloneIR(receiver)]
|
|
717
1045
|
}
|
|
718
1046
|
|
|
719
1047
|
function emitDynGetExprTyped(base, key, vt, prop) {
|
|
@@ -722,7 +1050,12 @@ export default (ctx) => {
|
|
|
722
1050
|
// prehashed body — no __str_hash on every access.
|
|
723
1051
|
if (typeof prop === 'string') {
|
|
724
1052
|
inc('__dyn_get_expr_t_h')
|
|
725
|
-
|
|
1053
|
+
const call = ['call', '$__dyn_get_expr_t_h', receiver, key, emitTypeTag(receiver, vt), ['i32.const', strHashLiteral(prop)]]
|
|
1054
|
+
// Schema-set devirt marker — same contract as emitDynGetAnyTyped below
|
|
1055
|
+
// (identical 4-arg layout); without it the wasi host (features.external
|
|
1056
|
+
// off routes reads here) never devirtualizes megamorphic prop reads.
|
|
1057
|
+
if (ctx.transform.optimize) call.dvProp = prop
|
|
1058
|
+
return typed(['f64.reinterpret_i64', call], 'f64')
|
|
726
1059
|
}
|
|
727
1060
|
inc('__dyn_get_expr_t')
|
|
728
1061
|
return typed(['f64.reinterpret_i64', ['call', '$__dyn_get_expr_t', receiver, key, emitTypeTag(receiver, vt)]], 'f64')
|
|
@@ -734,7 +1067,14 @@ export default (ctx) => {
|
|
|
734
1067
|
// prehashed body — no __str_hash on every access (hot for `parse.step` etc).
|
|
735
1068
|
if (typeof prop === 'string') {
|
|
736
1069
|
inc('__dyn_get_any_t_h')
|
|
737
|
-
|
|
1070
|
+
const call = ['call', '$__dyn_get_any_t_h', receiver, key, emitTypeTag(receiver, vt), ['i32.const', strHashLiteral(prop)]]
|
|
1071
|
+
// Schema-set devirt marker: the optimizer (devirtSchemaReads) rewrites this
|
|
1072
|
+
// megamorphic probe into a br_table over the module's registered schemas —
|
|
1073
|
+
// direct slot loads per schema, this call as the always-sound default arm.
|
|
1074
|
+
// Tagged here (not built) because schema.list is still growing while
|
|
1075
|
+
// function bodies emit; the pass runs after module init completes.
|
|
1076
|
+
if (ctx.transform.optimize) call.dvProp = prop
|
|
1077
|
+
return typed(['f64.reinterpret_i64', call], 'f64')
|
|
738
1078
|
}
|
|
739
1079
|
inc('__dyn_get_any_t')
|
|
740
1080
|
return typed(['f64.reinterpret_i64', ['call', '$__dyn_get_any_t', receiver, key, emitTypeTag(receiver, vt)]], 'f64')
|
|
@@ -800,7 +1140,7 @@ export default (ctx) => {
|
|
|
800
1140
|
if (va?.ptrKind === VAL.OBJECT && va.ptrAux != null && typeof prop === 'string') {
|
|
801
1141
|
const sch = ctx.schema.list[va.ptrAux]
|
|
802
1142
|
const si = sch ? sch.indexOf(prop) : -1
|
|
803
|
-
if (si >= 0) return emitSchemaSlotRead(va, si)
|
|
1143
|
+
if (si >= 0) return emitSchemaSlotRead(va, si, ctx.schema.slotI32CertainBySid?.(va.ptrAux, prop))
|
|
804
1144
|
}
|
|
805
1145
|
let schemaIdx = typeof obj === 'string' ? ctx.schema.slotOf(obj, prop) : ctx.schema.slotOf(null, prop)
|
|
806
1146
|
// Chain receiver (e.g. `o.meta.bias`): when the chain resolves to a known
|
|
@@ -815,7 +1155,8 @@ export default (ctx) => {
|
|
|
815
1155
|
}
|
|
816
1156
|
}
|
|
817
1157
|
const key = asI64(emit(['str', prop]))
|
|
818
|
-
if (schemaIdx >= 0) return emitSchemaSlotRead(va, schemaIdx
|
|
1158
|
+
if (schemaIdx >= 0) return emitSchemaSlotRead(va, schemaIdx,
|
|
1159
|
+
typeof obj === 'string' && ctx.schema.slotI32CertainAt?.(obj, prop))
|
|
819
1160
|
if (typeof obj === 'string') {
|
|
820
1161
|
const vt = lookupValType(obj)
|
|
821
1162
|
if (usesDynProps(vt)) {
|
|
@@ -833,12 +1174,18 @@ export default (ctx) => {
|
|
|
833
1174
|
if (vt == null) {
|
|
834
1175
|
// In WASI mode, values are always JSON-derived (never PTR.EXTERNAL host objects).
|
|
835
1176
|
// Skip the external branch and dispatch through the typed HASH/OBJECT path.
|
|
836
|
-
|
|
1177
|
+
const isWasi = ctx.transform.host === 'wasi'
|
|
837
1178
|
// `fromOptional` (a `?.prop` read) short-circuits on nullish, so its
|
|
838
1179
|
// PTR.EXTERNAL arm is dead unless host externals are already in play —
|
|
839
1180
|
// don't force the __ext_prop import just for an optional read.
|
|
840
|
-
if (!fromOptional) ctx.features.external = true
|
|
841
|
-
|
|
1181
|
+
if (!isWasi && !fromOptional) ctx.features.external = true
|
|
1182
|
+
const slow = () => isWasi ? emitDynGetExprTyped(va, key, vt, prop) : emitDynGetAnyTyped(va, key, vt, prop)
|
|
1183
|
+
// Monomorphic schema-slot devirtualization (see emitSchemaSlotGuarded):
|
|
1184
|
+
// `prop` uniquely identifies one registered schema program-wide, so
|
|
1185
|
+
// guard on it instead of always paying the full dynamic dispatch
|
|
1186
|
+
// (durable-receiver check + ihash probe + schema-table scan).
|
|
1187
|
+
const guard = ctx.schema.guardedSlotOf(prop)
|
|
1188
|
+
return guard ? emitSchemaSlotGuarded(va, guard, slow, prop) : slow()
|
|
842
1189
|
}
|
|
843
1190
|
// Primitive receiver (number/boolean/bigint): no dynamic props — `(5).foo` is
|
|
844
1191
|
// undefined. Without this the value falls to the __hash_get fallback, which
|
|
@@ -862,16 +1209,35 @@ export default (ctx) => {
|
|
|
862
1209
|
// this program (features.* + hash-stdlib presence). ARRAY is always live; STRING and
|
|
863
1210
|
// number are always dispatched. The __len disjunction collapses to whichever of
|
|
864
1211
|
// ARRAY/TYPED/HASH/SET/MAP are reachable. STRING covers both heap and SSO via __str_len.
|
|
1212
|
+
// Can `.length` on an unproven receiver resolve to an ORDINARY own property?
|
|
1213
|
+
// Only when the program can hold one: a registered object schema, or the
|
|
1214
|
+
// dyn-prop/hash machinery. Otherwise the fallback arm stays plain undefined
|
|
1215
|
+
// and __length pulls no dyn dispatcher. The dispatcher lives in
|
|
1216
|
+
// module/collection.js — gate on its registration too, so a build whose
|
|
1217
|
+
// autoload never pulled that module keeps the lean arm instead of
|
|
1218
|
+
// requesting an unregistered stdlib. Consulted at pull time (factory +
|
|
1219
|
+
// deps thunk), when schemas, includes, and module set are final — same
|
|
1220
|
+
// pattern as the old HASH-arm includes probe.
|
|
1221
|
+
const lengthNeedsDynArm = () =>
|
|
1222
|
+
ctx.core.stdlib['__dyn_get_expr_t_h'] != null &&
|
|
1223
|
+
(ctx.schema?.list?.length > 0 || ctx.core.includes.has('__hash_new') ||
|
|
1224
|
+
ctx.core.includes.has('__dyn_set') || ctx.core.includes.has('__hash_set'))
|
|
1225
|
+
|
|
865
1226
|
ctx.core.stdlib['__length'] = () => {
|
|
866
1227
|
const types = [PTR.ARRAY]
|
|
867
1228
|
if (ctx.features.typedarray) types.push(PTR.TYPED)
|
|
868
|
-
if (ctx.core.includes.has('__hash_new') || ctx.core.includes.has('__dyn_set') || ctx.core.includes.has('__hash_set'))
|
|
869
|
-
types.push(PTR.HASH)
|
|
870
|
-
if (ctx.features.set) types.push(PTR.SET)
|
|
871
|
-
if (ctx.features.map) types.push(PTR.MAP)
|
|
872
1229
|
const eqT = (n) => `(i32.eq (local.get $t) (i32.const ${n}))`
|
|
873
1230
|
let disj = eqT(types[0])
|
|
874
1231
|
for (let i = 1; i < types.length; i++) disj = `(i32.or ${disj} ${eqT(types[i])})`
|
|
1232
|
+
// Everything that is not a string/array/typed reads `length` as an ordinary
|
|
1233
|
+
// property: OBJECT schema slot, HASH key, sidecar — or undefined. (Set/Map
|
|
1234
|
+
// have NO .length in JS — their count is `.size`; the old HASH/SET/MAP
|
|
1235
|
+
// __len arms returned the entry count, which no JS engine does.) The dyn
|
|
1236
|
+
// dispatcher guards real-number receivers itself; gated to keep the lean
|
|
1237
|
+
// undefined arm in programs that can't hold such a property at all.
|
|
1238
|
+
const propArm = lengthNeedsDynArm()
|
|
1239
|
+
? `(f64.reinterpret_i64 (call $__dyn_get_expr_t_h (local.get $v) (i64.const ${LENGTH_SSO_I64}) (local.get $t) (i32.const ${strHashLiteral('length')})))`
|
|
1240
|
+
: `(f64.const nan:${UNDEF_NAN})`
|
|
875
1241
|
const lenArm = `(block (result f64)
|
|
876
1242
|
(local.set $off (i32.wrap_i64 (i64.and (local.get $v) (i64.const ${LAYOUT.OFFSET_MASK}))))
|
|
877
1243
|
(if (result f64) ${disj}
|
|
@@ -879,7 +1245,7 @@ export default (ctx) => {
|
|
|
879
1245
|
(if (result f64) (i32.ge_u (local.get $off) (i32.const 8))
|
|
880
1246
|
(then (f64.convert_i32_s (call $__len (local.get $v))))
|
|
881
1247
|
(else (f64.const nan:${UNDEF_NAN}))))
|
|
882
|
-
(else
|
|
1248
|
+
(else ${propArm})))`
|
|
883
1249
|
const stringArm = `(if (result f64) (i32.eq (local.get $t) (i32.const ${PTR.STRING}))
|
|
884
1250
|
(then (f64.convert_i32_s (call $__str_len (local.get $v))))
|
|
885
1251
|
(else ${lenArm}))`
|
|
@@ -910,7 +1276,7 @@ export default (ctx) => {
|
|
|
910
1276
|
return typed(['f64.convert_i32_s', ['call', '$__len', ['i64.reinterpret_f64', inner]]], 'f64')
|
|
911
1277
|
}
|
|
912
1278
|
const idx = ctx.schema.slotOf(obj, prop)
|
|
913
|
-
if (idx >= 0) return emitSchemaSlotRead(emit(obj), idx)
|
|
1279
|
+
if (idx >= 0) return emitSchemaSlotRead(emit(obj), idx, ctx.schema.slotI32CertainAt?.(obj, prop))
|
|
914
1280
|
}
|
|
915
1281
|
|
|
916
1282
|
if (prop === 'length') {
|
|
@@ -950,6 +1316,10 @@ export default (ctx) => {
|
|
|
950
1316
|
}
|
|
951
1317
|
const rep = typeof obj === 'string' ? repOf(obj) : null
|
|
952
1318
|
const vt = rep ? rep.val : valTypeOf(obj)
|
|
1319
|
+
// Proven OBJECT/HASH receiver: `.length` is an ordinary own property
|
|
1320
|
+
// (schema slot / hash key), never a builtin length — resolve statically
|
|
1321
|
+
// instead of paying __length's runtime dispatch.
|
|
1322
|
+
if (vt === VAL.OBJECT || vt === VAL.HASH) return emitPropAccess(emit(obj), obj, 'length')
|
|
953
1323
|
const notString = vt == null && typeof obj === 'string' && lookupNotString(obj)
|
|
954
1324
|
// jsstring carrier: keep the externref-typed IR so emitLengthAccess can
|
|
955
1325
|
// dispatch to `wasm:js-string.length` instead of forcing through f64.
|
|
@@ -993,9 +1363,13 @@ export default (ctx) => {
|
|
|
993
1363
|
// Module-registered property getter (.size, .byteLength, …). Methods sharing
|
|
994
1364
|
// the bare-`.prop` table (`.values`, `.pop`, date getters) are untagged and
|
|
995
1365
|
// fall through to a real property read — `m.values` reads the "values" field.
|
|
1366
|
+
// A PROVEN OBJECT/HASH receiver never has builtin accessors in JS — its
|
|
1367
|
+
// `.size`/`.byteLength` are ordinary own properties, so skip the getter and
|
|
1368
|
+
// fall through to the real property read below.
|
|
996
1369
|
const propKey = `.${prop}`
|
|
997
1370
|
const propEmitter = ctx.core.emit[propKey]
|
|
998
|
-
if (propEmitter && ctx.core.getters.has(propKey)
|
|
1371
|
+
if (propEmitter && ctx.core.getters.has(propKey) &&
|
|
1372
|
+
ptVt !== VAL.OBJECT && ptVt !== VAL.HASH) return propEmitter(obj)
|
|
999
1373
|
|
|
1000
1374
|
return emitPropAccess(emit(obj), obj, prop)
|
|
1001
1375
|
}
|
|
@@ -1006,12 +1380,15 @@ export default (ctx) => {
|
|
|
1006
1380
|
// notNullish inlines an isNullish check — (i32.or (i64.eq X NULL)
|
|
1007
1381
|
// (i64.eq X UNDEF)) — that duplicates its operand, so a tee'd
|
|
1008
1382
|
// side-effecting value would run twice.
|
|
1383
|
+
// asF64 on the taken arm: the dispatched access may come back i32-narrowed
|
|
1384
|
+
// (an int-certain slot read at O0 keeps its raw i32), and the f64-typed if
|
|
1385
|
+
// fails validation ("type error in fallthru: expected f64, got i32").
|
|
1009
1386
|
const optionalGuard = (t, va, thenIR) =>
|
|
1010
1387
|
typed(['block', ['result', 'f64'],
|
|
1011
1388
|
['local.set', `$${t}`, va],
|
|
1012
1389
|
['if', ['result', 'f64'],
|
|
1013
1390
|
notNullish(typed(['local.get', `$${t}`], 'f64')),
|
|
1014
|
-
['then', thenIR],
|
|
1391
|
+
['then', asF64(thenIR)],
|
|
1015
1392
|
['else', ['f64.const', `nan:${UNDEF_NAN}`]]]], 'f64')
|
|
1016
1393
|
|
|
1017
1394
|
// Receiver-evaluate-once: allocate a fresh hoist-temp `$t`, emit `value` into
|
|
@@ -1075,6 +1452,20 @@ export default (ctx) => {
|
|
|
1075
1452
|
|
|
1076
1453
|
// Optional call: fn?.(...args) → null if fn is null, else call fn
|
|
1077
1454
|
ctx.core.emit['?.()'] = (callee, ...args) => {
|
|
1455
|
+
// Statically-lifted func-prop callee: `p.step?.()` where prepare lifted
|
|
1456
|
+
// `p.step = arrow` into the named function `p$step`. Non-nullish by
|
|
1457
|
+
// construction, so the optional is moot — delegate to the full `()` dispatch
|
|
1458
|
+
// (direct call). Without this arm the dead-write-drop plan (which assumes
|
|
1459
|
+
// call sites lower to direct calls) drops the write while this emitter read
|
|
1460
|
+
// the never-written dyn table → undefined. multiProp (reassigned) slots stay
|
|
1461
|
+
// dynamic: their live value is the prop-global and may legitimately be nullish.
|
|
1462
|
+
if (Array.isArray(callee) && callee[0] === '.' && typeof callee[1] === 'string' && typeof callee[2] === 'string') {
|
|
1463
|
+
const base = ctx.scope.chain[callee[1]] || callee[1]
|
|
1464
|
+
if (ctx.func.names.has(`${base}$${callee[2]}`) && !ctx.func.multiProp.has(`${base}.${callee[2]}`)) {
|
|
1465
|
+
const callArgs = args.length === 0 ? null : args.length === 1 ? args[0] : [',', ...args]
|
|
1466
|
+
return asF64(ctx.core.emit['()'](callee, callArgs))
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1078
1469
|
// Method-reference callee: `recv.m(...)` or `recv?.m(...)` form. Methods are
|
|
1079
1470
|
// statically registered emitters and aren't real closure values, so route them
|
|
1080
1471
|
// as a direct method call. The outer optional short-circuits when the receiver
|