jz 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +99 -72
- package/bench/README.md +253 -100
- package/bench/bench.svg +58 -81
- package/cli.js +85 -12
- package/dist/interop.js +1 -0
- package/dist/jz.js +6196 -0
- package/index.d.ts +126 -0
- package/index.js +222 -35
- package/interop.js +240 -141
- package/layout.js +34 -18
- package/module/array.js +99 -111
- package/module/collection.js +124 -30
- package/module/console.js +1 -1
- package/module/core.js +163 -19
- package/module/json.js +3 -3
- package/module/math.js +162 -3
- package/module/number.js +268 -13
- package/module/object.js +37 -5
- package/module/regex.js +8 -7
- package/module/simd.js +37 -5
- package/module/string.js +203 -168
- package/module/typedarray.js +565 -112
- package/package.json +26 -7
- package/src/abi/string.js +29 -29
- package/src/ast.js +19 -2
- package/src/autoload.js +3 -0
- package/src/compile/analyze-scans.js +174 -11
- package/src/compile/analyze.js +101 -4
- package/src/compile/cse-load.js +200 -0
- package/src/compile/emit-assign.js +82 -20
- package/src/compile/emit.js +592 -51
- package/src/compile/index.js +504 -89
- package/src/compile/infer.js +36 -1
- package/src/compile/loop-divmod.js +109 -0
- package/src/compile/loop-model.js +91 -0
- package/src/compile/loop-square.js +102 -0
- package/src/compile/narrow.js +275 -41
- package/src/compile/peel-stencil.js +215 -0
- package/src/compile/plan/advise.js +55 -1
- package/src/compile/plan/common.js +29 -0
- package/src/compile/plan/index.js +8 -1
- package/src/compile/plan/inline.js +180 -22
- package/src/compile/plan/literals.js +313 -24
- package/src/compile/plan/scope.js +115 -39
- package/src/compile/program-facts.js +21 -2
- package/src/ctx.js +96 -19
- package/src/helper-counters.js +137 -0
- package/src/ir.js +157 -20
- package/src/kind-traits.js +34 -3
- package/src/kind.js +79 -4
- package/src/op-policy.js +5 -2
- package/src/ops.js +119 -0
- package/src/optimize/index.js +1274 -151
- package/src/optimize/recurse.js +182 -0
- package/src/optimize/vectorize.js +4187 -253
- package/src/prepare/index.js +75 -14
- package/src/prepare/lift-iife.js +149 -0
- package/src/reps.js +6 -2
- package/src/static.js +9 -0
- package/src/type.js +63 -51
- package/src/wat/assemble.js +286 -21
- package/src/widen.js +21 -0
- package/src/wat/optimize.js +0 -3760
package/layout.js
CHANGED
|
@@ -41,6 +41,29 @@ export const PTR = {
|
|
|
41
41
|
/** Reserved atom aux ids (PTR.ATOM). */
|
|
42
42
|
export const ATOM = { NULL: 1, UNDEF: 2, FALSE: 4, TRUE: 5 }
|
|
43
43
|
|
|
44
|
+
/** Tags whose heap block can relocate on growth (ARRAY/HASH/SET/MAP) — leaving a
|
|
45
|
+
* forwarding header that `__ptr_offset` must follow. `(1 << tag) & FORWARDING_MASK`
|
|
46
|
+
* tests membership in one shl+and, replacing a 4-way tag-equality OR. */
|
|
47
|
+
export const FORWARDING_MASK = (1 << PTR.ARRAY) | (1 << PTR.HASH) | (1 << PTR.SET) | (1 << PTR.MAP)
|
|
48
|
+
|
|
49
|
+
// BigInt views of the NaN-box fields — the carrier is 64-bit, JS bit-ops are 32-bit.
|
|
50
|
+
const TAG_SHIFT = BigInt(LAYOUT.TAG_SHIFT), TAG_MASK = BigInt(LAYOUT.TAG_MASK)
|
|
51
|
+
const AUX_SHIFT = BigInt(LAYOUT.AUX_SHIFT), AUX_MASK = BigInt(LAYOUT.AUX_MASK)
|
|
52
|
+
const OFFSET_MASK = BigInt(LAYOUT.OFFSET_MASK)
|
|
53
|
+
|
|
54
|
+
/** Format an i64 BigInt as a zero-padded `0x…` hex literal for WAT/IR templates. */
|
|
55
|
+
export const i64Hex = bits => '0x' + bits.toString(16).toUpperCase().padStart(16, '0')
|
|
56
|
+
|
|
57
|
+
/** Pack (type, aux, offset) into the i64 NaN-box carrier — the single source of
|
|
58
|
+
* truth for pointer bit layout. Compiler IR (`packPtrBits`/`mkPtrIR`), the
|
|
59
|
+
* `$__mkptr` inline specializer, and the interop high-word encoder all derive
|
|
60
|
+
* from this. `offset` defaults to 0 to yield the box prefix (offset OR'd later). */
|
|
61
|
+
export const ptrBits = (type, aux = 0, offset = 0) =>
|
|
62
|
+
LAYOUT.NAN_PREFIX_BITS
|
|
63
|
+
| ((BigInt(type) & TAG_MASK) << TAG_SHIFT)
|
|
64
|
+
| ((BigInt(aux) & AUX_MASK) << AUX_SHIFT)
|
|
65
|
+
| (BigInt(offset >>> 0) & OFFSET_MASK)
|
|
66
|
+
|
|
44
67
|
// =============================================================================
|
|
45
68
|
// PTR.TYPED element-type aux codec — which typed-array flavor lives in the aux
|
|
46
69
|
// field of a PTR.TYPED box. Pure (no compiler state) → lives with the NaN-box
|
|
@@ -89,20 +112,19 @@ export function ctorFromElemAux(aux) {
|
|
|
89
112
|
return isView ? `new.${name}.view` : `new.${name}`
|
|
90
113
|
}
|
|
91
114
|
|
|
92
|
-
/** Host-side high u32 word for NaN-boxed f64 pointer encoding (interop)
|
|
115
|
+
/** Host-side high u32 word for NaN-boxed f64 pointer encoding (interop) — the
|
|
116
|
+
* top 32 bits of `ptrBits(type, aux)` (offset lives in the low word). */
|
|
93
117
|
export const encodePtrHi = (type, aux) =>
|
|
94
|
-
(
|
|
118
|
+
((LAYOUT.NAN_PREFIX << 16) | ((type & LAYOUT.TAG_MASK) << (LAYOUT.TAG_SHIFT - 32)) | (aux & LAYOUT.AUX_MASK)) >>> 0
|
|
95
119
|
|
|
96
|
-
export const decodePtrType = hi => (hi >>>
|
|
97
|
-
export const decodePtrAux = hi => hi &
|
|
120
|
+
export const decodePtrType = hi => (hi >>> (LAYOUT.TAG_SHIFT - 32)) & LAYOUT.TAG_MASK
|
|
121
|
+
export const decodePtrAux = hi => hi & LAYOUT.AUX_MASK
|
|
98
122
|
|
|
99
123
|
/** i64 NaN-prefix OR-mask for WAT `(i64.const …)` templates. */
|
|
100
|
-
export const nanPrefixHex = () =>
|
|
101
|
-
'0x' + LAYOUT.NAN_PREFIX_BITS.toString(16).toUpperCase().padStart(16, '0')
|
|
124
|
+
export const nanPrefixHex = () => i64Hex(LAYOUT.NAN_PREFIX_BITS)
|
|
102
125
|
|
|
103
126
|
/** Atom sentinel as i64 hex (compiler WAT templates). */
|
|
104
|
-
export const atomNanHex = atomId =>
|
|
105
|
-
'0x' + (LAYOUT.NAN_PREFIX_BITS | (BigInt(atomId) << BigInt(LAYOUT.AUX_SHIFT))).toString(16).toUpperCase().padStart(16, '0')
|
|
127
|
+
export const atomNanHex = atomId => i64Hex(LAYOUT.NAN_PREFIX_BITS | (BigInt(atomId) << AUX_SHIFT))
|
|
106
128
|
|
|
107
129
|
/** STRING aux bit 0 on a PLAIN-HEAP string (SSO and SLICE clear): this is a
|
|
108
130
|
* CANONICAL interned string — the static-pool copy (or an intern-table hit
|
|
@@ -115,22 +137,16 @@ export const atomNanHex = atomId =>
|
|
|
115
137
|
export const STR_INTERN_BIT = 0x1
|
|
116
138
|
|
|
117
139
|
/** Pre-shifted STRING SSO aux bit as i64 hex. */
|
|
118
|
-
export const ssoBitI64Hex = () =>
|
|
119
|
-
'0x' + (BigInt(LAYOUT.SSO_BIT) << BigInt(LAYOUT.AUX_SHIFT)).toString(16).toUpperCase().padStart(16, '0')
|
|
140
|
+
export const ssoBitI64Hex = () => i64Hex(BigInt(LAYOUT.SSO_BIT) << AUX_SHIFT)
|
|
120
141
|
|
|
121
142
|
/** Pre-shifted STRING slice/view aux bit as i64 hex. */
|
|
122
|
-
export const sliceBitI64Hex = () =>
|
|
123
|
-
'0x' + (BigInt(LAYOUT.SLICE_BIT) << BigInt(LAYOUT.AUX_SHIFT)).toString(16).toUpperCase().padStart(16, '0')
|
|
143
|
+
export const sliceBitI64Hex = () => i64Hex(BigInt(LAYOUT.SLICE_BIT) << AUX_SHIFT)
|
|
124
144
|
|
|
125
145
|
/** Full i64 NaN-box hex for `(i64.const …)` — ptr type + aux, offset OR'd separately. */
|
|
126
|
-
export const ptrNanHex = (ptrType, aux = 0) =>
|
|
127
|
-
'0x' + ptrBoxPrefixBigInt(ptrType, aux).toString(16).toUpperCase().padStart(16, '0')
|
|
146
|
+
export const ptrNanHex = (ptrType, aux = 0) => i64Hex(ptrBits(ptrType, aux))
|
|
128
147
|
|
|
129
148
|
/** Compile-time i64 prefix for mkPtrIR (before offset OR). */
|
|
130
|
-
export const ptrBoxPrefixBigInt = (ptrType, aux = 0) =>
|
|
131
|
-
(0x7FF8n << 48n)
|
|
132
|
-
| ((BigInt(ptrType) & 0xFn) << 47n)
|
|
133
|
-
| ((BigInt(aux) & 0x7FFFn) << 32n)
|
|
149
|
+
export const ptrBoxPrefixBigInt = (ptrType, aux = 0) => ptrBits(ptrType, aux)
|
|
134
150
|
|
|
135
151
|
/** Host-side atom sentinel high-u32 values (interop f64 decode). */
|
|
136
152
|
export const ATOM_HI = {
|
package/module/array.js
CHANGED
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
|
|
11
11
|
import { typed, asF64, asI64, asI32, NULL_NAN, UNDEF_NAN, temp, tempI32, allocPtr, multiCount, arrayLoop, elemLoad, elemStore, truthyIR, extractF64Bits, appendStaticSlots, mkPtrIR, slotAddr, isLiteralStr, resolveValType, undefExpr, ptrTypeEq } from '../src/ir.js'
|
|
12
12
|
import { inBoundsArrIdx } from '../src/type.js'
|
|
13
|
-
import { emit, spread, deps } from '../src/bridge.js'
|
|
13
|
+
import { emit, spread, deps, idx as emitIndex } from '../src/bridge.js'
|
|
14
14
|
import { valTypeOf } from '../src/kind.js'
|
|
15
15
|
import { extractParams, classifyParam, ASSIGN_OPS, refsName, REFS_IN_EXPR } from '../src/ast.js'
|
|
16
|
-
import { staticPropertyKey, staticObjectProps, inlineArraySid } from '../src/static.js'
|
|
16
|
+
import { staticPropertyKey, staticObjectProps, inlineArraySid, staticIndexKey, intLiteralValue } from '../src/static.js'
|
|
17
17
|
import { VAL, lookupValType, lookupNotString, updateRep } from '../src/reps.js'
|
|
18
18
|
import { structInline } from '../src/abi/index.js'
|
|
19
|
-
import { ctx, inc, err, PTR, LAYOUT, followForwardingWat } from '../src/ctx.js'
|
|
19
|
+
import { ctx, inc, err, warnDeopt, PTR, LAYOUT, followForwardingWat } from '../src/ctx.js'
|
|
20
20
|
import { strHashLiteral } from './collection.js'
|
|
21
21
|
|
|
22
22
|
|
|
@@ -33,7 +33,7 @@ function allocArray(len, cap) {
|
|
|
33
33
|
* only an 8-byte header left off-16 pointing at adjacent data-segment bytes, so
|
|
34
34
|
* for-in / named-prop lookup (which read off-16 as the props-sidecar pointer)
|
|
35
35
|
* walked garbage → OOB (test262 built-ins/Object/keys sparse-array). */
|
|
36
|
-
function staticArrayPtr(slots) {
|
|
36
|
+
export function staticArrayPtr(slots) {
|
|
37
37
|
if (!ctx.runtime.data) ctx.runtime.data = ''
|
|
38
38
|
while (ctx.runtime.data.length % 8 !== 0) ctx.runtime.data += '\0'
|
|
39
39
|
const headerOff = ctx.runtime.data.length
|
|
@@ -249,9 +249,9 @@ export default (ctx) => {
|
|
|
249
249
|
'__ptr_offset',
|
|
250
250
|
...(needsArrayDynMove() ? ['__dyn_move', '__hash_new', '__ihash_set_local'] : []),
|
|
251
251
|
],
|
|
252
|
-
__arr_fill: ['__ptr_offset'],
|
|
252
|
+
__arr_fill: ['__ptr_offset', '__clamp_idx'], // body-calls __clamp_idx; declare it (self-host auto-scan can't be relied on — see test/selfhost-includes.js)
|
|
253
253
|
__arr_set_idx_ptr: ['__arr_grow', '__ptr_offset'],
|
|
254
|
-
__arr_push1: ['__arr_grow_known', '
|
|
254
|
+
__arr_push1: ['__arr_grow_known', '__ptr_offset_fwd'],
|
|
255
255
|
__arr_set_length: ['__arr_grow_known', '__ptr_offset', '__ptr_type'],
|
|
256
256
|
__arr_unshift: ['__arr_grow', '__len', '__ptr_offset'],
|
|
257
257
|
__arr_splice: ['__arr_grow', '__len', '__ptr_offset', '__alloc_hdr', '__mkptr'],
|
|
@@ -333,67 +333,6 @@ export default (ctx) => {
|
|
|
333
333
|
// Full body handles TYPED element types and view indirection since external host can
|
|
334
334
|
// pass typed arrays even when typedarray module isn't loaded. When features.typedarray
|
|
335
335
|
// and features.external are both off, collapses to ARRAY-only f64 indexing.
|
|
336
|
-
ctx.core.stdlib['__typed_idx'] = () => {
|
|
337
|
-
if (!ctx.features.typedarray && !ctx.features.external) {
|
|
338
|
-
return `(func $__typed_idx (param $ptr i64) (param $i i32) (result f64)
|
|
339
|
-
(local $len i32)
|
|
340
|
-
(local.set $len (call $__len (local.get $ptr)))
|
|
341
|
-
(if (result f64)
|
|
342
|
-
(i32.or
|
|
343
|
-
(i32.lt_s (local.get $i) (i32.const 0))
|
|
344
|
-
(i32.ge_u (local.get $i) (local.get $len)))
|
|
345
|
-
(then (f64.const nan:${UNDEF_NAN}))
|
|
346
|
-
(else (f64.load (i32.add (call $__ptr_offset (local.get $ptr)) (i32.shl (local.get $i) (i32.const 3)))))))`
|
|
347
|
-
}
|
|
348
|
-
// Hot (~37M calls in watr self-host). Type/aux/offset extracted once from $ptr.
|
|
349
|
-
return `(func $__typed_idx (param $ptr i64) (param $i i32) (result f64)
|
|
350
|
-
(local $t i32) (local $off i32) (local $et i32) (local $len i32) (local $aux i32)
|
|
351
|
-
(local.set $t (i32.wrap_i64 (i64.and (i64.shr_u (local.get $ptr) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
|
|
352
|
-
(local.set $off (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ${LAYOUT.OFFSET_MASK}))))
|
|
353
|
-
;; ARRAY fast path: follow forwarding inline, bounds-check against header len, f64.load — no $__len call.
|
|
354
|
-
(if (i32.and (i32.eq (local.get $t) (i32.const ${PTR.ARRAY})) (i32.ge_u (local.get $off) (i32.const 8)))
|
|
355
|
-
(then
|
|
356
|
-
${followForwardingWat('$off', { lowGuard: false })}
|
|
357
|
-
(return (if (result f64)
|
|
358
|
-
(i32.and (i32.ge_s (local.get $i) (i32.const 0)) (i32.lt_u (local.get $i) (i32.load (i32.sub (local.get $off) (i32.const 8)))))
|
|
359
|
-
(then (f64.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3)))))
|
|
360
|
-
(else (f64.const nan:${UNDEF_NAN}))))))
|
|
361
|
-
(local.set $aux (i32.wrap_i64 (i64.and (i64.shr_u (local.get $ptr) (i64.const ${LAYOUT.AUX_SHIFT})) (i64.const ${LAYOUT.AUX_MASK}))))
|
|
362
|
-
(if
|
|
363
|
-
(i32.and
|
|
364
|
-
(i32.eq (local.get $t) (i32.const ${PTR.TYPED}))
|
|
365
|
-
(i32.ne (i32.and (local.get $aux) (i32.const 8)) (i32.const 0)))
|
|
366
|
-
(then (local.set $off (i32.load (i32.add (local.get $off) (i32.const 4))))))
|
|
367
|
-
(local.set $len (call $__len (local.get $ptr)))
|
|
368
|
-
(if (result f64)
|
|
369
|
-
(i32.or
|
|
370
|
-
(i32.lt_s (local.get $i) (i32.const 0))
|
|
371
|
-
(i32.ge_u (local.get $i) (local.get $len)))
|
|
372
|
-
(then (f64.const nan:${UNDEF_NAN}))
|
|
373
|
-
(else
|
|
374
|
-
(if (result f64) (i32.eq (local.get $t) (i32.const ${PTR.TYPED}))
|
|
375
|
-
(then
|
|
376
|
-
(local.set $et (i32.and (local.get $aux) (i32.const 7)))
|
|
377
|
-
(if (result f64) (i32.ge_u (local.get $et) (i32.const 6))
|
|
378
|
-
(then (if (result f64) (i32.eq (local.get $et) (i32.const 7))
|
|
379
|
-
(then (if (result f64) (i32.and (local.get $aux) (i32.const 16))
|
|
380
|
-
(then (f64.reinterpret_i64 (i64.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3))))))
|
|
381
|
-
(else (f64.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3)))))))
|
|
382
|
-
(else (f64.promote_f32 (f32.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 2))))))))
|
|
383
|
-
(else (if (result f64) (i32.ge_u (local.get $et) (i32.const 4))
|
|
384
|
-
(then (if (result f64) (i32.and (local.get $et) (i32.const 1))
|
|
385
|
-
(then (f64.convert_i32_u (i32.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 2))))))
|
|
386
|
-
(else (f64.convert_i32_s (i32.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 2))))))))
|
|
387
|
-
(else (if (result f64) (i32.ge_u (local.get $et) (i32.const 2))
|
|
388
|
-
(then (if (result f64) (i32.and (local.get $et) (i32.const 1))
|
|
389
|
-
(then (f64.convert_i32_u (i32.load16_u (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 1))))))
|
|
390
|
-
(else (f64.convert_i32_s (i32.load16_s (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 1))))))))
|
|
391
|
-
(else (if (result f64) (i32.and (local.get $et) (i32.const 1))
|
|
392
|
-
(then (f64.convert_i32_u (i32.load8_u (i32.add (local.get $off) (local.get $i)))))
|
|
393
|
-
(else (f64.convert_i32_s (i32.load8_s (i32.add (local.get $off) (local.get $i)))))))))))))
|
|
394
|
-
(else (f64.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3)))))))))`
|
|
395
|
-
}
|
|
396
|
-
|
|
397
336
|
// Array.from(src) — shallow copy of array (memory.copy of f64 elements)
|
|
398
337
|
ctx.core.stdlib['__arr_from'] = `(func $__arr_from (param $src i64) (result f64)
|
|
399
338
|
(local $len i32) (local $dst i32)
|
|
@@ -576,12 +515,13 @@ export default (ctx) => {
|
|
|
576
515
|
ctx.core.stdlib['__arr_push1'] = `(func $__arr_push1 (param $ptr i64) (param $val f64) (result f64)
|
|
577
516
|
(local $p f64) (local $base i32) (local $len i32)
|
|
578
517
|
(local.set $p (f64.reinterpret_i64 (local.get $ptr)))
|
|
579
|
-
(local.set $base (
|
|
518
|
+
(local.set $base (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ${LAYOUT.OFFSET_MASK}))))
|
|
519
|
+
${followForwardingWat('$base', { lowGuard: true })}
|
|
580
520
|
(local.set $len (i32.load (i32.sub (local.get $base) (i32.const 8))))
|
|
581
521
|
(if (i32.lt_s (i32.load (i32.sub (local.get $base) (i32.const 4))) (i32.add (local.get $len) (i32.const 1)))
|
|
582
522
|
(then
|
|
583
523
|
(local.set $p (call $__arr_grow_known (local.get $ptr) (i32.add (local.get $len) (i32.const 1))))
|
|
584
|
-
(local.set $base (
|
|
524
|
+
(local.set $base (i32.wrap_i64 (i64.and (i64.reinterpret_f64 (local.get $p)) (i64.const ${LAYOUT.OFFSET_MASK}))))))
|
|
585
525
|
(f64.store (i32.add (local.get $base) (i32.shl (local.get $len) (i32.const 3))) (local.get $val))
|
|
586
526
|
(i32.store (i32.sub (local.get $base) (i32.const 8)) (i32.add (local.get $len) (i32.const 1)))
|
|
587
527
|
(local.get $p))`
|
|
@@ -625,8 +565,14 @@ export default (ctx) => {
|
|
|
625
565
|
if (!hasSpread) {
|
|
626
566
|
const len = elems.length
|
|
627
567
|
// R: Static data segment for arrays of pure-literal elements (own-memory only).
|
|
628
|
-
//
|
|
629
|
-
|
|
568
|
+
// Raw f64 bits embedded directly — a constant array becomes a const pointer with no
|
|
569
|
+
// alloc and no per-element store. A static array aliases ONE shared data-segment
|
|
570
|
+
// region, so this is sound only when no caller expects a fresh instance per
|
|
571
|
+
// evaluation: at module scope the literal runs exactly once. A function-local
|
|
572
|
+
// literal (which would leak in-place mutations across calls — a latent bug the old
|
|
573
|
+
// len≥4 gate also had) allocs fresh instead. Module scope lifts the size floor too,
|
|
574
|
+
// so `const x = [1, 2, 3]` is a data segment, not an alloc.
|
|
575
|
+
if (ctx.func.atModuleScope && len >= 1 && !ctx.memory.shared) {
|
|
630
576
|
// asF64 folds i32.const → f64.const literally, so int-literal arrays also qualify.
|
|
631
577
|
const slots = elems.map(e => extractF64Bits(asF64(emit(e))))
|
|
632
578
|
if (slots.every(b => b !== null)) return staticArrayPtr(slots)
|
|
@@ -651,6 +597,14 @@ export default (ctx) => {
|
|
|
651
597
|
// === Index read ===
|
|
652
598
|
|
|
653
599
|
ctx.core.emit['[]'] = (arr, idx) => {
|
|
600
|
+
// A literal NEGATIVE index is always out of range → undefined (JS semantics), never a
|
|
601
|
+
// raw `payload + (-1)*8` load that reads heap before the allocation. A side-effecting
|
|
602
|
+
// receiver still evaluates. Mirrors VT['[]'] returning null for the same case.
|
|
603
|
+
{ const li = intLiteralValue(idx)
|
|
604
|
+
if (li != null && li < 0)
|
|
605
|
+
return typeof arr === 'string'
|
|
606
|
+
? undefExpr()
|
|
607
|
+
: typed(['block', ['result', 'f64'], ['drop', asF64(emit(arr))], undefExpr()], 'f64') }
|
|
654
608
|
// Hoist non-identifier arr so side-effecting sources (e.g. `foo.shift()[i]`) execute once.
|
|
655
609
|
// The rest of the handler inlines `emit(arr)` into multiple IR positions, which would
|
|
656
610
|
// otherwise re-execute the source expression per use at runtime.
|
|
@@ -676,10 +630,13 @@ export default (ctx) => {
|
|
|
676
630
|
const litKey = isLiteralStr(idx) ? idx[1]
|
|
677
631
|
: typeof arr === 'string' && lookupValType(arr) === VAL.OBJECT ? staticPropertyKey(idx)
|
|
678
632
|
: null
|
|
679
|
-
// SRoA flat object: `o['k']` → `local.get $o#i` (
|
|
680
|
-
|
|
633
|
+
// SRoA flat object/array: `o['k']` / `a[2]` → `local.get $o#i` (scanFlatObjects).
|
|
634
|
+
// A bare integer index resolves its slot key here (not via `litKey`, which stays
|
|
635
|
+
// null for arrays so the heap-array / schema paths below are untouched).
|
|
636
|
+
if (typeof arr === 'string' && ctx.func.flatObjects?.has(arr)) {
|
|
681
637
|
const fo = ctx.func.flatObjects.get(arr)
|
|
682
|
-
const
|
|
638
|
+
const flatKey = litKey != null ? litKey : staticIndexKey(idx)
|
|
639
|
+
const fi = flatKey != null ? fo.names.indexOf(flatKey) : -1
|
|
683
640
|
if (fi >= 0) return typed(['local.get', `$${arr}#${fi}`], 'f64')
|
|
684
641
|
}
|
|
685
642
|
if (litKey != null && typeof arr === 'string' && ctx.schema.slotOf) {
|
|
@@ -700,14 +657,34 @@ export default (ctx) => {
|
|
|
700
657
|
// Multi-value calls are materialized at call site (see '()' handler), so
|
|
701
658
|
// func()[i] works naturally — func() returns a heap array pointer, [i] indexes it.
|
|
702
659
|
const vt = typeof arr === 'string' ? lookupValType(arr) : valTypeOf(arr)
|
|
703
|
-
|
|
660
|
+
// Literal string key on a receiver that isn't a known array/typed/string:
|
|
661
|
+
// `x['a']` IS `x.a` — delegate to the dot emitter so an untyped/OBJECT/external
|
|
662
|
+
// receiver gets the same polymorphic dispatch (__dyn_get_any_t_h, host-external
|
|
663
|
+
// aware). The local `dynLoad` fallback below calls __dyn_get, which only probes
|
|
664
|
+
// the internal HASH layout — so `x['a']` on a host object silently returned
|
|
665
|
+
// undefined while `x.a` worked. (Known ARRAY/TYPED/STRING fall through unchanged:
|
|
666
|
+
// their string-key semantics differ from a HASH/OBJECT property read.)
|
|
667
|
+
if (litKey != null && vt !== VAL.ARRAY && vt !== VAL.TYPED && vt !== VAL.STRING)
|
|
668
|
+
return emit(['.', arr, litKey])
|
|
669
|
+
// emitIndex (not bare asI32(emit)) narrows integer index arithmetic — incl. a
|
|
670
|
+
// literal term like the `+1` of `a[i*W + x + 1]` — to i32 ops instead of the
|
|
671
|
+
// f64 convert/trunc round-trip. Non-i32 keys (string dispatch) fall back to
|
|
672
|
+
// asI32(emit) inside emitIndex, so this is a strict improvement for every branch.
|
|
673
|
+
const va = emit(arr), vi = emitIndex(idx)
|
|
704
674
|
const ptrExpr = asF64(va)
|
|
705
675
|
const dynLoad = (objExpr, keyExpr) => {
|
|
706
676
|
if (ctx.transform.strict) err(`strict mode: dynamic property access \`${typeof arr === 'string' ? arr : '<expr>'}[<expr>]\` falls back to __dyn_get. Use a literal key or known typed-array receiver, or pass { strict: false }.`)
|
|
677
|
+
warnDeopt('deopt-dyn-read', `dynamic property read \`${typeof arr === 'string' ? arr : '<expr>'}[…]\` couldn't resolve a static type — it falls back to a runtime hash lookup (~1.5–2× slower than a typed/slot read, far worse in a hot loop). Use a literal key, a typed-array receiver, or a Map for genuinely dynamic keys.`)
|
|
707
678
|
inc('__dyn_get')
|
|
708
679
|
return ['f64.reinterpret_i64', ['call', '$__dyn_get', ['i64.reinterpret_f64', objExpr], ['i64.reinterpret_f64', keyExpr]]]
|
|
709
680
|
}
|
|
710
681
|
const stringLoad = () => (inc('__str_idx'), ['call', '$__str_idx', ['i64.reinterpret_f64', ptrExpr], vi])
|
|
682
|
+
// A numeric index on an unknown receiver is array/typed access by design — kept
|
|
683
|
+
// lean (no OBJECT/HASH dyn-get fork): an object with numeric keys is a degenerate
|
|
684
|
+
// pattern not worth a per-access string-coercion + hash probe in every hot loop.
|
|
685
|
+
// The WRITE path still routes a numeric `o[i]=v` on an OBJECT to __dyn_set for
|
|
686
|
+
// SAFETY (no schema-slot corruption / OOB), so such a read returns undefined
|
|
687
|
+
// rather than corrupting — matching JS for an out-of-range typed/array index.
|
|
711
688
|
const arrayLoad = (['call', '$__typed_idx', ['i64.reinterpret_f64', ptrExpr], vi])
|
|
712
689
|
const emitDynamicKeyDispatch = (objExpr, numericLoad) => {
|
|
713
690
|
const keyTmp = temp()
|
|
@@ -753,6 +730,16 @@ export default (ctx) => {
|
|
|
753
730
|
if (keyType === VAL.STRING)
|
|
754
731
|
return typed(dynLoad(ptrExpr, asF64(emit(idx))), 'f64')
|
|
755
732
|
if (vt === 'array') {
|
|
733
|
+
// Base offset of the array's data region. A binding proven never relocated
|
|
734
|
+
// (scanNeverGrown — a fresh array literal whose every use is a pure read, so no
|
|
735
|
+
// grow op can ever run) skips the realloc-forwarding follow: its base is the raw
|
|
736
|
+
// post-header offset `wrap(reinterpret(ptr) & OFFSET_MASK)`, no __ptr_offset call.
|
|
737
|
+
// Memory-safe ONLY under that proof — a relocated array read through this stale
|
|
738
|
+
// base would corrupt memory (see scanNeverGrown's default-deny rationale).
|
|
739
|
+
const neverGrown = typeof arr === 'string' && ctx.func.localReps?.get(arr)?.neverGrown === true
|
|
740
|
+
const arrBase = () => neverGrown
|
|
741
|
+
? ['i32.wrap_i64', ['i64.and', ['i64.reinterpret_f64', ptrExpr], ['i64.const', LAYOUT.OFFSET_MASK]]]
|
|
742
|
+
: (inc('__ptr_offset'), ['call', '$__ptr_offset', ['i64.reinterpret_f64', ptrExpr]])
|
|
756
743
|
// structInline Array<S>: element i is K consecutive inline f64 schema
|
|
757
744
|
// cells — no per-row heap object, no stored element pointer. `arr[i]` is
|
|
758
745
|
// the byte address of the element's first cell, returned as a first-class
|
|
@@ -761,11 +748,10 @@ export default (ctx) => {
|
|
|
761
748
|
// structInline handles (src/analyze.js analyzeStructInline).
|
|
762
749
|
const inlSid = inlineArraySid(arr)
|
|
763
750
|
if (inlSid != null) {
|
|
764
|
-
inc('__ptr_offset')
|
|
765
751
|
const baseI32 = tempI32('ab')
|
|
766
752
|
const K = ctx.schema.list[inlSid].length
|
|
767
753
|
const cell = typed(structInline(K).ops.elemAddr(
|
|
768
|
-
['local.tee', `$${baseI32}`,
|
|
754
|
+
['local.tee', `$${baseI32}`, arrBase()],
|
|
769
755
|
vi), 'i32')
|
|
770
756
|
cell.ptrKind = VAL.OBJECT
|
|
771
757
|
cell.ptrAux = inlSid
|
|
@@ -775,51 +761,51 @@ export default (ctx) => {
|
|
|
775
761
|
// not __typed_idx (which does __len + __ptr_offset = two forwarding follows
|
|
776
762
|
// plus type-dispatch overhead irrelevant for plain arrays).
|
|
777
763
|
const keyIsNum = keyType === VAL.NUMBER
|
|
778
|
-
// Inline fast path
|
|
779
|
-
//
|
|
780
|
-
//
|
|
781
|
-
//
|
|
782
|
-
//
|
|
783
|
-
//
|
|
784
|
-
//
|
|
785
|
-
//
|
|
786
|
-
//
|
|
787
|
-
//
|
|
788
|
-
// the
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
//
|
|
792
|
-
//
|
|
793
|
-
//
|
|
794
|
-
//
|
|
795
|
-
//
|
|
796
|
-
|
|
764
|
+
// Inline fast path for any known plain ARRAY + numeric key: the type-tag
|
|
765
|
+
// dispatch and bounds check inside __arr_idx(_known) are dead weight in hot
|
|
766
|
+
// kernels — most visibly AST walkers doing `node[i]` over heterogeneous
|
|
767
|
+
// arrays, where no element schema/valType is ever inferred. Emit the
|
|
768
|
+
// f64.load directly. base goes through __ptr_offset (still the forwarding
|
|
769
|
+
// follow), and hoistAddrBase CSEs the (base, i) pair across the iteration
|
|
770
|
+
// body. taggedLinear stores every element as one 8-byte f64 cell —
|
|
771
|
+
// Array<NUMBER>/<STRING>/<OBJECT> alike — so a direct f64.load is correct
|
|
772
|
+
// regardless of elem kind (raw f64 for NUMBER, NaN-boxed pointer for
|
|
773
|
+
// OBJECT/STRING; downstream typed() handles both). The load shape is fixed
|
|
774
|
+
// by the carrier, not by any rep fact, so we do NOT gate on a known element
|
|
775
|
+
// schema/valType: a bare `let a = [...]` walked by index gets the same
|
|
776
|
+
// inline load as an Array<{x,y,z}>. (structInline arrays returned above via
|
|
777
|
+
// inlineArraySid; typed arrays are VAL.TYPED, handled below.)
|
|
778
|
+
//
|
|
779
|
+
// Take the UNCHECKED inline load only when the index is proven in-bounds by
|
|
780
|
+
// an enclosing canonical loop `for (let i=C; i<arr.length; i++)` — a pure
|
|
781
|
+
// index<length structural proof (scanBoundedArrIdx, src/type.js), itself
|
|
782
|
+
// element-kind-independent. Skipping the bounds check on an arbitrary numeric
|
|
783
|
+
// index is unsound: `a[1]` on a length-1 array would read the raw cell instead
|
|
784
|
+
// of undefined; those fall through to the inline bounds-checked load below.
|
|
785
|
+
const idxProvenInBounds = keyIsNum
|
|
797
786
|
&& typeof arr === 'string' && typeof idx === 'string'
|
|
798
787
|
&& inBoundsArrIdx(ctx).has(arr + '\x00' + idx)
|
|
799
788
|
if (idxProvenInBounds) {
|
|
800
|
-
|
|
801
|
-
// __ptr_offset returns i32 — base local must be i32 (not the default
|
|
802
|
-
// f64 NaN-box temp). Flat tee form so downstream peepholes can fold
|
|
789
|
+
// base local must be i32. Flat tee form so downstream peepholes can fold
|
|
803
790
|
// `i32.wrap_i64 (i64.reinterpret_f64 (f64.load …))` → `i32.load …`
|
|
804
791
|
// when this load feeds a ptrUnboxed OBJECT field.
|
|
805
792
|
const baseI32 = tempI32('ab')
|
|
806
793
|
return typed(ctx.abi.array.ops.load(
|
|
807
|
-
['local.tee', `$${baseI32}`,
|
|
794
|
+
['local.tee', `$${baseI32}`, arrBase()],
|
|
808
795
|
vi), 'f64')
|
|
809
796
|
}
|
|
810
|
-
// Known
|
|
797
|
+
// Known plain array, numeric key, NOT proven in-bounds → inline bounds-checked
|
|
811
798
|
// load: `idx < len ? load : undefined`. Same semantics as __arr_idx_known but
|
|
812
799
|
// inline, so watr hoists the loop-invariant len load and CSEs the base — the
|
|
813
800
|
// residual cost is a single (predictable) compare per access, not a call. Skipping
|
|
814
801
|
// the check would read raw memory for OOB indices (e.g. `a[1]` on a length-1 array).
|
|
815
|
-
if (
|
|
816
|
-
inc('__ptr_offset')
|
|
802
|
+
if (keyIsNum) {
|
|
817
803
|
const baseI32 = tempI32('ab'), idxI32 = tempI32('ai')
|
|
818
804
|
return typed(['if', ['result', 'f64'],
|
|
819
805
|
['i32.lt_u',
|
|
820
806
|
['local.tee', `$${idxI32}`, vi],
|
|
821
807
|
['i32.load', ['i32.sub',
|
|
822
|
-
['local.tee', `$${baseI32}`,
|
|
808
|
+
['local.tee', `$${baseI32}`, arrBase()],
|
|
823
809
|
['i32.const', 8]]]],
|
|
824
810
|
['then', ctx.abi.array.ops.load(['local.get', `$${baseI32}`], ['local.get', `$${idxI32}`])],
|
|
825
811
|
['else', undefExpr()]], 'f64')
|
|
@@ -897,6 +883,10 @@ export default (ctx) => {
|
|
|
897
883
|
|
|
898
884
|
// .push(val) → append, increment len, return array (possibly reallocated pointer)
|
|
899
885
|
ctx.core.emit['.push'] = (arr, ...vals) => {
|
|
886
|
+
// `_expect` is overwritten by recursive emit() calls below. Capture the
|
|
887
|
+
// statement-position hint now so a dropped `xs.push(v)` can skip computing
|
|
888
|
+
// the JS return length while still performing the mutation/writeback.
|
|
889
|
+
const void_ = ctx.func._expect === 'void'
|
|
900
890
|
// structInline Array<S>: `.push({S})` writes the K schema fields as K
|
|
901
891
|
// consecutive f64 cells. Flatten the struct literal into K schema-ordered
|
|
902
892
|
// field-value nodes and fall through to the general multi-value store path
|
|
@@ -924,8 +914,10 @@ export default (ctx) => {
|
|
|
924
914
|
const readVar = box ? ['f64.load', ['local.get', `$${box}`]] : isGlobal ? ['global.get', `$${arr}`] : ['local.get', `$${arr}`]
|
|
925
915
|
const writeVar = v => box ? ['f64.store', ['local.get', `$${box}`], v] : isGlobal ? ['global.set', `$${arr}`, v] : ['local.set', `$${arr}`, v]
|
|
926
916
|
const vv = asF64(emit(vals[0]))
|
|
917
|
+
const pushed = ['call', '$__arr_push1', ['i64.reinterpret_f64', readVar], vv]
|
|
918
|
+
if (void_) return typed(['block', writeVar(pushed)], 'void')
|
|
927
919
|
return typed(['block', ['result', 'f64'],
|
|
928
|
-
writeVar(
|
|
920
|
+
writeVar(pushed),
|
|
929
921
|
['f64.convert_i32_s', ['i32.load', ['i32.sub',
|
|
930
922
|
['call', '$__ptr_offset', ['i64.reinterpret_f64', readVar]], ['i32.const', 8]]]]], 'f64')
|
|
931
923
|
}
|
|
@@ -1077,12 +1069,8 @@ export default (ctx) => {
|
|
|
1077
1069
|
(if (i32.ge_u (local.get $off) (i32.const 8))
|
|
1078
1070
|
(then
|
|
1079
1071
|
(local.set $len (i32.load (i32.sub (local.get $off) (i32.const 8))))
|
|
1080
|
-
(
|
|
1081
|
-
(
|
|
1082
|
-
(if (i32.gt_s (local.get $start) (local.get $len)) (then (local.set $start (local.get $len))))
|
|
1083
|
-
(if (i32.lt_s (local.get $end) (i32.const 0)) (then (local.set $end (i32.add (local.get $len) (local.get $end)))))
|
|
1084
|
-
(if (i32.lt_s (local.get $end) (i32.const 0)) (then (local.set $end (i32.const 0))))
|
|
1085
|
-
(if (i32.gt_s (local.get $end) (local.get $len)) (then (local.set $end (local.get $len))))
|
|
1072
|
+
(local.set $start (call $__clamp_idx (local.get $start) (local.get $len)))
|
|
1073
|
+
(local.set $end (call $__clamp_idx (local.get $end) (local.get $len)))
|
|
1086
1074
|
(local.set $i (local.get $start))
|
|
1087
1075
|
(block $done (loop $fill
|
|
1088
1076
|
(br_if $done (i32.ge_s (local.get $i) (local.get $end)))
|