jz 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +315 -318
- package/bench/README.md +369 -0
- package/bench/bench.svg +102 -0
- package/cli.js +104 -30
- package/dist/interop.js +1 -0
- package/dist/jz.js +6024 -0
- package/index.d.ts +126 -0
- package/index.js +362 -84
- package/interop.js +159 -186
- package/jz.svg +5 -0
- package/jzify/arguments.js +97 -0
- package/jzify/bundler.js +382 -0
- package/jzify/classes.js +328 -0
- package/jzify/hoist-vars.js +177 -0
- package/jzify/index.js +51 -0
- package/jzify/names.js +37 -0
- package/jzify/switch.js +106 -0
- package/jzify/transform.js +349 -0
- package/layout.js +184 -0
- package/module/array.js +377 -176
- package/module/collection.js +639 -145
- package/module/console.js +55 -43
- package/module/core.js +264 -153
- package/module/date.js +15 -3
- package/module/function.js +73 -6
- package/module/index.js +2 -1
- package/module/json.js +226 -61
- package/module/math.js +551 -187
- package/module/number.js +327 -60
- package/module/object.js +474 -184
- package/module/regex.js +255 -25
- package/module/schema.js +24 -6
- package/module/simd.js +117 -0
- package/module/string.js +621 -226
- package/module/symbol.js +1 -1
- package/module/timer.js +9 -14
- package/module/typedarray.js +459 -73
- package/package.json +58 -14
- package/src/abi/index.js +39 -23
- package/src/abi/string.js +38 -41
- package/src/ast.js +460 -0
- package/src/autoload.js +29 -24
- package/src/bridge.js +111 -0
- package/src/compile/analyze-scans.js +824 -0
- package/src/compile/analyze.js +1600 -0
- package/src/compile/emit-assign.js +411 -0
- package/src/compile/emit.js +3512 -0
- package/src/compile/flow-types.js +103 -0
- package/src/{compile.js → compile/index.js} +778 -128
- package/src/{infer.js → compile/infer.js} +62 -98
- package/src/compile/loop-divmod.js +155 -0
- package/src/{narrow.js → compile/narrow.js} +409 -106
- package/src/compile/peel-stencil.js +261 -0
- package/src/compile/plan/advise.js +370 -0
- package/src/compile/plan/common.js +150 -0
- package/src/compile/plan/index.js +122 -0
- package/src/compile/plan/inline.js +682 -0
- package/src/compile/plan/literals.js +1199 -0
- package/src/compile/plan/loops.js +472 -0
- package/src/compile/plan/scope.js +649 -0
- package/src/compile/program-facts.js +423 -0
- package/src/ctx.js +220 -64
- package/src/ir.js +589 -172
- package/src/kind-traits.js +132 -0
- package/src/kind.js +524 -0
- package/src/op-policy.js +57 -0
- package/src/{optimize.js → optimize/index.js} +1432 -473
- package/src/optimize/vectorize.js +4660 -0
- package/src/param-reps.js +65 -0
- package/src/parse.js +44 -0
- package/src/{prepare.js → prepare/index.js} +649 -205
- package/src/prepare/lift-iife.js +149 -0
- package/src/reps.js +116 -0
- package/src/resolve.js +12 -3
- package/src/static.js +208 -0
- package/src/type.js +651 -0
- package/src/{assemble.js → wat/assemble.js} +275 -55
- package/src/wat/optimize.js +3938 -0
- package/transform.js +21 -0
- package/wasi.js +47 -5
- package/src/analyze.js +0 -3818
- package/src/emit.js +0 -3040
- package/src/jzify.js +0 -1580
- package/src/plan.js +0 -2132
- package/src/vectorize.js +0 -1088
- /package/src/{codegen.js → wat/codegen.js} +0 -0
package/module/core.js
CHANGED
|
@@ -9,32 +9,38 @@
|
|
|
9
9
|
* @module core
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { typed, asF64, asI32, asI64, NULL_NAN, UNDEF_NAN, temp, usesDynProps, ptrOffsetIR, isNullish, valKindToPtr } from '../src/ir.js'
|
|
13
|
-
import { emit,
|
|
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'
|
|
13
|
+
import { emit, spread, deps } from '../src/bridge.js'
|
|
14
14
|
import { reconstructArgsWithSpreads } from '../src/ir.js'
|
|
15
|
-
import { valTypeOf,
|
|
16
|
-
import {
|
|
15
|
+
import { valTypeOf, shapeOf } from '../src/kind.js'
|
|
16
|
+
import { T } from '../src/ast.js'
|
|
17
|
+
import { inlineArraySid } from '../src/static.js'
|
|
18
|
+
import { VAL, lookupValType, lookupNotString, repOf, updateRep } from '../src/reps.js'
|
|
19
|
+
import { ctx, err, inc, PTR, LAYOUT, HEAP, FORWARDING_MASK, emitArity, followForwardingWat, declGlobal } from '../src/ctx.js'
|
|
20
|
+
import { ptrOffsetFwdWat, STR_INTERN_BIT } from '../layout.js'
|
|
21
|
+
import { nanPrefixHex } from '../layout.js'
|
|
17
22
|
import { initSchema } from './schema.js'
|
|
18
23
|
import { strHashLiteral } from './collection.js'
|
|
19
24
|
|
|
20
|
-
|
|
21
|
-
const NAN_BITS = '0x' + LAYOUT.NAN_PREFIX_BITS.toString(16).toUpperCase().padStart(16, '0')
|
|
25
|
+
const NAN_BITS = nanPrefixHex()
|
|
22
26
|
|
|
23
27
|
export default (ctx) => {
|
|
24
|
-
|
|
28
|
+
deps({
|
|
25
29
|
__eq: ['__str_eq', '__ptr_type'],
|
|
26
30
|
__typeof: ['__ptr_type', '__is_nullish'],
|
|
27
|
-
__len: ['__typed_shift'],
|
|
31
|
+
__len: ['__typed_shift', '__ptr_offset', '__ptr_offset_fwd'],
|
|
28
32
|
__cap: ['__typed_shift', '__ptr_type', '__ptr_offset', '__ptr_aux'],
|
|
29
33
|
__typed_data: ['__ptr_offset', '__ptr_aux'],
|
|
30
|
-
__ptr_offset: [],
|
|
34
|
+
__ptr_offset: ['__ptr_offset_fwd'],
|
|
35
|
+
__ptr_offset_fwd: [],
|
|
31
36
|
__is_str_key: ['__ptr_type'],
|
|
32
37
|
__str_len: ['__ptr_type', '__ptr_offset', '__ptr_aux'],
|
|
33
|
-
__set_len: [],
|
|
38
|
+
__set_len: ['__ptr_offset_fwd'],
|
|
34
39
|
__length: ['__ptr_type', '__ptr_offset', '__str_len', '__len'],
|
|
35
|
-
|
|
40
|
+
__alloc: ['__memgrow'],
|
|
36
41
|
__alloc_hdr: ['__alloc'],
|
|
37
42
|
__alloc_hdr_n: ['__alloc'],
|
|
43
|
+
__coll_order: ['__alloc'],
|
|
38
44
|
})
|
|
39
45
|
|
|
40
46
|
ctx.core.stdlib['__is_nullish'] = `(func $__is_nullish (param $v i64) (result i32)
|
|
@@ -62,15 +68,26 @@ export default (ctx) => {
|
|
|
62
68
|
(f64.eq (local.get $fb) (local.get $fb)))
|
|
63
69
|
(then (f64.eq (local.get $fa) (local.get $fb)))
|
|
64
70
|
(else
|
|
65
|
-
;; At least one operand is a NaN-box
|
|
66
|
-
;;
|
|
71
|
+
;; At least one operand is a NaN-box (the && above failed). For both to
|
|
72
|
+
;; be strings BOTH must be NaN-boxed: tag bits are only meaningful on a
|
|
73
|
+
;; NaN-box, so a normal number whose exponent bits happen to alias the
|
|
74
|
+
;; STRING tag (e.g. ASCII content read as f64) must NOT route to __str_eq
|
|
75
|
+
;; — that would deref garbage. number-vs-string is simply false.
|
|
67
76
|
(local.set $ta (i32.wrap_i64 (i64.and (i64.shr_u (local.get $a) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
|
|
68
77
|
(local.set $tb (i32.wrap_i64 (i64.and (i64.shr_u (local.get $b) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
|
|
69
78
|
(if (result i32)
|
|
70
79
|
(i32.and
|
|
71
|
-
(i32.eq (local.get $ta) (i32.const ${PTR.STRING}))
|
|
72
|
-
(i32.eq (local.get $tb) (i32.const ${PTR.STRING})))
|
|
73
|
-
(then
|
|
80
|
+
(i32.and (f64.ne (local.get $fa) (local.get $fa)) (i32.eq (local.get $ta) (i32.const ${PTR.STRING})))
|
|
81
|
+
(i32.and (f64.ne (local.get $fb) (local.get $fb)) (i32.eq (local.get $tb) (i32.const ${PTR.STRING}))))
|
|
82
|
+
(then
|
|
83
|
+
;; both canonical interned (bit-ne already known) ⇒ unequal —
|
|
84
|
+
;; skip the __str_eq call entirely (see STR_INTERN_BIT, layout.js)
|
|
85
|
+
(if (result i32)
|
|
86
|
+
(i32.and
|
|
87
|
+
(i32.eq (i32.and (i32.wrap_i64 (i64.shr_u (local.get $a) (i64.const ${LAYOUT.AUX_SHIFT}))) (i32.const ${LAYOUT.SSO_BIT | LAYOUT.SLICE_BIT | STR_INTERN_BIT})) (i32.const ${STR_INTERN_BIT}))
|
|
88
|
+
(i32.eq (i32.and (i32.wrap_i64 (i64.shr_u (local.get $b) (i64.const ${LAYOUT.AUX_SHIFT}))) (i32.const ${LAYOUT.SSO_BIT | LAYOUT.SLICE_BIT | STR_INTERN_BIT})) (i32.const ${STR_INTERN_BIT})))
|
|
89
|
+
(then (i32.const 0))
|
|
90
|
+
(else (call $__str_eq (local.get $a) (local.get $b)))))
|
|
74
91
|
(else (i32.const 0))))))))`
|
|
75
92
|
|
|
76
93
|
ctx.core.stdlib['__is_null'] = `(func $__is_null (param $v i64) (result i32)
|
|
@@ -86,11 +103,13 @@ export default (ctx) => {
|
|
|
86
103
|
(else
|
|
87
104
|
(i32.and
|
|
88
105
|
(i32.and
|
|
89
|
-
(
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
(
|
|
93
|
-
|
|
106
|
+
(i32.and
|
|
107
|
+
(i64.ne (local.get $v) (i64.const ${NAN_BITS}))
|
|
108
|
+
(i64.ne (local.get $v) (i64.const ${NULL_NAN})))
|
|
109
|
+
(i32.and
|
|
110
|
+
(i64.ne (local.get $v) (i64.const ${UNDEF_NAN}))
|
|
111
|
+
(i64.ne (local.get $v) (i64.const 0x7FFA400000000000))))
|
|
112
|
+
(i64.ne (local.get $v) (i64.const ${FALSE_NAN}))))))`
|
|
94
113
|
|
|
95
114
|
ctx.core.stdlib['__is_str_key'] = `(func $__is_str_key (param $v i64) (result i32)
|
|
96
115
|
(local $f f64)
|
|
@@ -124,29 +143,54 @@ export default (ctx) => {
|
|
|
124
143
|
(i64.shl (i64.and (i64.extend_i32_u (local.get $aux)) (i64.const ${LAYOUT.AUX_MASK})) (i64.const ${LAYOUT.AUX_SHIFT}))
|
|
125
144
|
(i64.and (i64.extend_i32_u (local.get $offset)) (i64.const ${LAYOUT.OFFSET_MASK})))))))`
|
|
126
145
|
|
|
146
|
+
ctx.core.stdlib['__ptr_offset_fwd'] = ptrOffsetFwdWat()
|
|
147
|
+
|
|
127
148
|
ctx.core.stdlib['__ptr_offset'] = `(func $__ptr_offset (param $ptr i64) (result i32)
|
|
128
|
-
(local $bits i64) (local $off i32)
|
|
149
|
+
(local $bits i64) (local $off i32) (local $t i32)
|
|
129
150
|
(local.set $bits (local.get $ptr))
|
|
130
151
|
(local.set $off (i32.wrap_i64 (i64.and (local.get $bits) (i64.const ${LAYOUT.OFFSET_MASK}))))
|
|
131
|
-
;;
|
|
132
|
-
;;
|
|
133
|
-
;;
|
|
134
|
-
(
|
|
135
|
-
|
|
136
|
-
(i32.const ${PTR.ARRAY}))
|
|
152
|
+
;; ARRAY/SET/MAP/HASH can be reallocated on growth; follow the forwarding pointer
|
|
153
|
+
;; (cap=-1 sentinel at -4, new offset at -8). Other types never forward, so they skip
|
|
154
|
+
;; the loop; a well-formed ptr without forwarding pays one bounds + cap check per hop.
|
|
155
|
+
(local.set $t (i32.wrap_i64 (i64.and (i64.shr_u (local.get $bits) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
|
|
156
|
+
(if (i32.and (i32.shl (i32.const 1) (local.get $t)) (i32.const ${FORWARDING_MASK}))
|
|
137
157
|
(then
|
|
138
|
-
(
|
|
139
|
-
(loop $follow
|
|
140
|
-
(br_if $done (i32.lt_u (local.get $off) (i32.const 8)))
|
|
141
|
-
(br_if $done (i32.gt_u (local.get $off) (i32.shl (memory.size) (i32.const 16))))
|
|
142
|
-
(br_if $done (i32.ne (i32.load (i32.sub (local.get $off) (i32.const 4))) (i32.const -1)))
|
|
143
|
-
(local.set $off (i32.load (i32.sub (local.get $off) (i32.const 8))))
|
|
144
|
-
(br $follow)))))
|
|
158
|
+
${followForwardingWat('$off', { lowGuard: true })}))
|
|
145
159
|
(local.get $off))`
|
|
146
160
|
|
|
147
161
|
ctx.core.stdlib['__ptr_aux'] = `(func $__ptr_aux (param $ptr i64) (result i32)
|
|
148
162
|
(i32.wrap_i64 (i64.and (i64.shr_u (local.get $ptr) (i64.const ${LAYOUT.AUX_SHIFT})) (i64.const ${LAYOUT.AUX_MASK}))))`
|
|
149
163
|
|
|
164
|
+
// Exact JS `%` (fmod) for the f64 path. wasm has no f64 remainder, and the
|
|
165
|
+
// textbook `a - b*trunc(a/b)` is both INEXACT (rounding in trunc/mul/sub for
|
|
166
|
+
// large a/b) and WRONG on the IEEE edges. This does the spec exactly:
|
|
167
|
+
// NaN if a or b is NaN, a is ±Inf, or b is 0; a if b is ±Inf or |a|<|b|;
|
|
168
|
+
// otherwise binary long division — scale |b| up to ≤|a|, then subtract-and-
|
|
169
|
+
// halve back down to |b|. Every step (×2, ×0.5, aligned subtraction) is
|
|
170
|
+
// exact in f64, so the remainder is bit-identical to JS. Sign follows the
|
|
171
|
+
// dividend (copysign), matching `(-5)%3 === -2`, `5%(-3) === 2`, `-0%3 === -0`.
|
|
172
|
+
ctx.core.stdlib['__rem'] = `(func $__rem (param $a f64) (param $b f64) (result f64)
|
|
173
|
+
(local $x f64) (local $y f64)
|
|
174
|
+
(if (f64.ne (local.get $a) (local.get $a)) (then (return (local.get $a))))
|
|
175
|
+
(if (f64.ne (local.get $b) (local.get $b)) (then (return (local.get $b))))
|
|
176
|
+
(local.set $x (f64.abs (local.get $a)))
|
|
177
|
+
(local.set $y (f64.abs (local.get $b)))
|
|
178
|
+
(if (i32.or (f64.eq (local.get $x) (f64.const inf)) (f64.eq (local.get $y) (f64.const 0)))
|
|
179
|
+
(then (return (f64.div (f64.const 0) (f64.const 0)))))
|
|
180
|
+
(if (i32.or (f64.eq (local.get $y) (f64.const inf)) (f64.lt (local.get $x) (local.get $y)))
|
|
181
|
+
(then (return (local.get $a))))
|
|
182
|
+
(block $up (loop $ul
|
|
183
|
+
(br_if $up (f64.gt (f64.mul (local.get $y) (f64.const 2)) (local.get $x)))
|
|
184
|
+
(local.set $y (f64.mul (local.get $y) (f64.const 2)))
|
|
185
|
+
(br $ul)))
|
|
186
|
+
(block $dn (loop $dl
|
|
187
|
+
(br_if $dn (f64.lt (local.get $y) (f64.abs (local.get $b))))
|
|
188
|
+
(if (f64.ge (local.get $x) (local.get $y)) (then (local.set $x (f64.sub (local.get $x) (local.get $y)))))
|
|
189
|
+
(local.set $y (f64.mul (local.get $y) (f64.const 0.5)))
|
|
190
|
+
(br $dl)))
|
|
191
|
+
(f64.copysign (local.get $x) (local.get $a)))`
|
|
192
|
+
|
|
193
|
+
|
|
150
194
|
ctx.core.stdlib['__ptr_type'] = `(func $__ptr_type (param $ptr i64) (result i32)
|
|
151
195
|
(i32.wrap_i64 (i64.and (i64.shr_u (local.get $ptr) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))`
|
|
152
196
|
|
|
@@ -167,66 +211,115 @@ export default (ctx) => {
|
|
|
167
211
|
|
|
168
212
|
// Heap-base watermark: gates header-backed propsPtr fast paths so static-data
|
|
169
213
|
// OBJECT slots (offsets < heap base) don't misread arbitrary memory at off-16.
|
|
170
|
-
// Updated by optimizeModule() when data segment exceeds
|
|
171
|
-
|
|
214
|
+
// Updated by optimizeModule() when data segment exceeds HEAP.START bytes.
|
|
215
|
+
declGlobal('__heap_start', 'i32', HEAP.START)
|
|
172
216
|
|
|
173
|
-
// Shared memory keeps the heap pointer in linear memory (memory[
|
|
217
|
+
// Shared memory keeps the heap pointer in linear memory (memory[HEAP.PTR_ADDR]):
|
|
174
218
|
// wasm globals are per-instance, so threads sharing one memory must share one
|
|
175
219
|
// pointer cell. Non-shared memory (incl. alloc:false) uses the `$__heap`
|
|
176
220
|
// global — exported so the JS-side adapter (memory.String etc) bumps the same
|
|
177
221
|
// pointer. Storing it in memory would collide with the static data section
|
|
178
|
-
// whenever the data exceeds
|
|
222
|
+
// whenever the data exceeds HEAP.PTR_ADDR bytes.
|
|
223
|
+
// Geometric memory growth shared by `__alloc` and the in-place string
|
|
224
|
+
// bump-extend paths (string.js). Ensures linear memory covers byte offset
|
|
225
|
+
// `$next`, growing when short. Growing one page at a time turns a long-running
|
|
226
|
+
// embedding (watr called thousands of times) into O(n²) — each memory.grow may
|
|
227
|
+
// relocate and copy the whole heap — so we request at least the current size
|
|
228
|
+
// (≥2× total) in one shot; only on hitting the declared maximum do we fall back
|
|
229
|
+
// to the bare minimum. `$need` is the TOTAL pages required to cover $next; the
|
|
230
|
+
// byte size of memory ((memory.size)<<16) is computed in i64 because it
|
|
231
|
+
// overflows i32 at the wasm32 max of 65536 pages (4 GiB) — without that,
|
|
232
|
+
// capacity reads as 0 and every allocation spuriously tries to grow past the
|
|
233
|
+
// ceiling, trapping near 4 GiB.
|
|
234
|
+
ctx.core.stdlib['__memgrow'] = `(func $__memgrow (param $next i32)
|
|
235
|
+
(local $cur i32) (local $need i32)
|
|
236
|
+
(local.set $need (i32.wrap_i64 (i64.shr_u (i64.add (i64.extend_i32_u (local.get $next)) (i64.const 65535)) (i64.const 16))))
|
|
237
|
+
(if (i32.gt_u (local.get $need) (memory.size))
|
|
238
|
+
(then
|
|
239
|
+
(if (i64.gt_u (i64.extend_i32_u (local.get $need)) (i64.const 65536)) (then (unreachable)))
|
|
240
|
+
(local.set $cur (i32.sub (local.get $need) (memory.size))) ;; minimum delta
|
|
241
|
+
(if (i32.lt_u (local.get $cur) (memory.size)) (then (local.set $cur (memory.size)))) ;; geometric
|
|
242
|
+
(if (i32.gt_u (i32.add (local.get $cur) (memory.size)) (i32.const 65536))
|
|
243
|
+
(then (local.set $cur (i32.sub (i32.const 65536) (memory.size))))) ;; cap at wasm32 max
|
|
244
|
+
(if (i32.eq (memory.grow (local.get $cur)) (i32.const -1))
|
|
245
|
+
(then (if (i32.eq (memory.grow (i32.sub (local.get $need) (memory.size))) (i32.const -1))
|
|
246
|
+
(then (unreachable))))))))`
|
|
247
|
+
|
|
179
248
|
if (ctx.memory.shared) {
|
|
180
|
-
// Heap offset stored at memory[
|
|
181
|
-
//
|
|
182
|
-
// host-run binary traps on the first allocation that exceeds the initial
|
|
183
|
-
// 64 KB. Use the same geometric-growth strategy as the owned-memory variant.
|
|
249
|
+
// Heap offset stored at memory[HEAP.PTR_ADDR] (i32), just before heap start at
|
|
250
|
+
// HEAP.START. Threads sharing one memory must share one pointer cell.
|
|
184
251
|
ctx.core.stdlib['__alloc'] = `(func $__alloc (param $bytes i32) (result i32)
|
|
185
|
-
(local $ptr i32) (local $next i32)
|
|
186
|
-
(local.set $ptr (i32.load (i32.const
|
|
252
|
+
(local $ptr i32) (local $next i32)
|
|
253
|
+
(local.set $ptr (i32.load (i32.const ${HEAP.PTR_ADDR})))
|
|
187
254
|
(local.set $next (i32.and (i32.add (i32.add (local.get $ptr) (local.get $bytes)) (i32.const 7)) (i32.const -8)))
|
|
188
|
-
(
|
|
189
|
-
(
|
|
190
|
-
(then
|
|
191
|
-
(local.set $need (i32.shr_u (i32.add (i32.sub (local.get $next) (local.get $cur)) (i32.const 65535)) (i32.const 16)))
|
|
192
|
-
(if (i32.lt_u (local.get $need) (memory.size)) (then (local.set $need (memory.size))))
|
|
193
|
-
(if (i32.eq (memory.grow (local.get $need)) (i32.const -1))
|
|
194
|
-
(then (if (i32.eq (memory.grow
|
|
195
|
-
(i32.shr_u (i32.add (i32.sub (local.get $next) (local.get $cur)) (i32.const 65535)) (i32.const 16)))
|
|
196
|
-
(i32.const -1)) (then (unreachable)))))))
|
|
197
|
-
(i32.store (i32.const 1020) (local.get $next))
|
|
255
|
+
(call $__memgrow (local.get $next))
|
|
256
|
+
(i32.store (i32.const ${HEAP.PTR_ADDR}) (local.get $next))
|
|
198
257
|
(local.get $ptr))`
|
|
199
258
|
ctx.core.stdlib['__clear'] = `(func $__clear
|
|
200
|
-
(i32.store (i32.const
|
|
259
|
+
(i32.store (i32.const ${HEAP.PTR_ADDR}) (i32.const ${HEAP.START})))`
|
|
201
260
|
} else {
|
|
202
|
-
// Own memory: heap offset in a global,
|
|
203
|
-
//
|
|
204
|
-
|
|
205
|
-
// Bump allocator with geometric growth. Growing one page at a time turns a
|
|
206
|
-
// long-running embedding (e.g. watr called thousands of times) into O(n²) —
|
|
207
|
-
// each memory.grow may relocate and copy the whole heap. So when we must
|
|
208
|
-
// grow, request at least the current size (≥2× total) in one shot; only on
|
|
209
|
-
// hitting the declared maximum do we fall back to the bare minimum.
|
|
261
|
+
// Own memory: heap offset in a global, exported so the JS-side adapter
|
|
262
|
+
// (alloc:false, no `_alloc` export) shares the pointer.
|
|
263
|
+
declGlobal('__heap', 'i32', HEAP.START, { export: '__heap' })
|
|
210
264
|
ctx.core.stdlib['__alloc'] = `(func $__alloc (param $bytes i32) (result i32)
|
|
211
|
-
(local $ptr i32) (local $next i32)
|
|
265
|
+
(local $ptr i32) (local $next i32)
|
|
212
266
|
(local.set $ptr (global.get $__heap))
|
|
213
|
-
;; Align next allocation to 8 bytes
|
|
214
267
|
(local.set $next (i32.and (i32.add (i32.add (local.get $ptr) (local.get $bytes)) (i32.const 7)) (i32.const -8)))
|
|
215
|
-
(
|
|
216
|
-
(if (i32.gt_u (local.get $next) (local.get $cur))
|
|
217
|
-
(then
|
|
218
|
-
(local.set $need (i32.shr_u (i32.add (i32.sub (local.get $next) (local.get $cur)) (i32.const 65535)) (i32.const 16)))
|
|
219
|
-
(if (i32.lt_u (local.get $need) (memory.size)) (then (local.set $need (memory.size))))
|
|
220
|
-
(if (i32.eq (memory.grow (local.get $need)) (i32.const -1))
|
|
221
|
-
(then (if (i32.eq (memory.grow
|
|
222
|
-
(i32.shr_u (i32.add (i32.sub (local.get $next) (local.get $cur)) (i32.const 65535)) (i32.const 16)))
|
|
223
|
-
(i32.const -1)) (then (unreachable)))))))
|
|
268
|
+
(call $__memgrow (local.get $next))
|
|
224
269
|
(global.set $__heap (local.get $next))
|
|
225
270
|
(local.get $ptr))`
|
|
226
271
|
ctx.core.stdlib['__clear'] = `(func $__clear
|
|
227
|
-
(global.set $__heap (i32.const
|
|
272
|
+
(global.set $__heap (i32.const ${HEAP.START})))`
|
|
228
273
|
}
|
|
229
274
|
|
|
275
|
+
// Build an insertion-ordered list of live slot offsets for a Set/Map/HASH
|
|
276
|
+
// backing table at $off (cap slots of $stride bytes). Returns a fresh i32 array
|
|
277
|
+
// (live-count entries) of slot offsets sorted by packed sequence (the insertion
|
|
278
|
+
// counter rides in each entry's hash-word high 32 bits — see collection.js's
|
|
279
|
+
// seqStore). Every order-sensitive iteration (keys/values/entries, for-in,
|
|
280
|
+
// spread, JSON, Map copy) walks this instead of raw slot order, so jz matches
|
|
281
|
+
// the JS spec's insertion order. Lives in core (not collection) because object
|
|
282
|
+
// and json iterate HASH tables without pulling the collection module. Insertion
|
|
283
|
+
// sort: enumerated collections are small, and it stays branch-light when sorted.
|
|
284
|
+
ctx.core.stdlib['__coll_order'] = `(func $__coll_order (param $off i32) (param $cap i32) (param $stride i32) (result i32)
|
|
285
|
+
(local $i i32) (local $n i32) (local $slot i32) (local $buf i32)
|
|
286
|
+
(local $j i32) (local $k i32) (local $cur i32) (local $sq i32)
|
|
287
|
+
;; A null/empty backing pointer (off below the heap base) has no live slots —
|
|
288
|
+
;; ordering it yields the empty list. Guard before the $off-8 length read so a
|
|
289
|
+
;; degenerate receiver returns an empty buffer instead of faulting on load(-8).
|
|
290
|
+
(if (i32.lt_u (local.get $off) (i32.const ${HEAP.START})) (then (return (call $__alloc (i32.const 0)))))
|
|
291
|
+
(local.set $buf (call $__alloc (i32.shl (i32.load (i32.sub (local.get $off) (i32.const 8))) (i32.const 2))))
|
|
292
|
+
;; gather live slot offsets (occupied ⇔ hash word ≠ 0)
|
|
293
|
+
(block $gd (loop $gl
|
|
294
|
+
(br_if $gd (i32.ge_s (local.get $i) (local.get $cap)))
|
|
295
|
+
(local.set $slot (i32.add (local.get $off) (i32.mul (local.get $i) (local.get $stride))))
|
|
296
|
+
(if (i64.ne (i64.load (local.get $slot)) (i64.const 0))
|
|
297
|
+
(then
|
|
298
|
+
(i32.store (i32.add (local.get $buf) (i32.shl (local.get $n) (i32.const 2))) (local.get $slot))
|
|
299
|
+
(local.set $n (i32.add (local.get $n) (i32.const 1)))))
|
|
300
|
+
(local.set $i (i32.add (local.get $i) (i32.const 1)))
|
|
301
|
+
(br $gl)))
|
|
302
|
+
;; insertion-sort buf[0..n) ascending by sequence = hash-word high 32 bits
|
|
303
|
+
(local.set $j (i32.const 1))
|
|
304
|
+
(block $sd (loop $sl
|
|
305
|
+
(br_if $sd (i32.ge_s (local.get $j) (local.get $n)))
|
|
306
|
+
(local.set $cur (i32.load (i32.add (local.get $buf) (i32.shl (local.get $j) (i32.const 2)))))
|
|
307
|
+
(local.set $sq (i32.wrap_i64 (i64.shr_u (i64.load (local.get $cur)) (i64.const 32))))
|
|
308
|
+
(local.set $k (i32.sub (local.get $j) (i32.const 1)))
|
|
309
|
+
(block $id (loop $il
|
|
310
|
+
(br_if $id (i32.lt_s (local.get $k) (i32.const 0)))
|
|
311
|
+
(br_if $id (i32.le_u
|
|
312
|
+
(i32.wrap_i64 (i64.shr_u (i64.load (i32.load (i32.add (local.get $buf) (i32.shl (local.get $k) (i32.const 2))))) (i64.const 32)))
|
|
313
|
+
(local.get $sq)))
|
|
314
|
+
(i32.store (i32.add (local.get $buf) (i32.shl (i32.add (local.get $k) (i32.const 1)) (i32.const 2)))
|
|
315
|
+
(i32.load (i32.add (local.get $buf) (i32.shl (local.get $k) (i32.const 2)))))
|
|
316
|
+
(local.set $k (i32.sub (local.get $k) (i32.const 1)))
|
|
317
|
+
(br $il)))
|
|
318
|
+
(i32.store (i32.add (local.get $buf) (i32.shl (i32.add (local.get $k) (i32.const 1)) (i32.const 2))) (local.get $cur))
|
|
319
|
+
(local.set $j (i32.add (local.get $j) (i32.const 1)))
|
|
320
|
+
(br $sl)))
|
|
321
|
+
(local.get $buf))`
|
|
322
|
+
|
|
230
323
|
// === Memory-based length/cap helpers (C-style headers) ===
|
|
231
324
|
|
|
232
325
|
// Array/TypedArray/Buffer: [-8:len(i32)][-4:cap(i32)][data...]
|
|
@@ -263,12 +356,7 @@ export default (ctx) => {
|
|
|
263
356
|
(if (result i32)
|
|
264
357
|
(i32.and (i32.eq (local.get $t) (i32.const 1)) (i32.ge_u (local.get $off) (i32.const 8)))
|
|
265
358
|
(then
|
|
266
|
-
(
|
|
267
|
-
(loop $follow
|
|
268
|
-
(br_if $done (i32.gt_u (local.get $off) (i32.shl (memory.size) (i32.const 16))))
|
|
269
|
-
(br_if $done (i32.ne (i32.load (i32.sub (local.get $off) (i32.const 4))) (i32.const -1)))
|
|
270
|
-
(local.set $off (i32.load (i32.sub (local.get $off) (i32.const 8))))
|
|
271
|
-
(br $follow)))
|
|
359
|
+
${followForwardingWat('$off', { lowGuard: false })}
|
|
272
360
|
(i32.load (i32.sub (local.get $off) (i32.const 8))))
|
|
273
361
|
(else
|
|
274
362
|
(if (result i32)
|
|
@@ -288,7 +376,9 @@ export default (ctx) => {
|
|
|
288
376
|
(call $__typed_shift (i32.and (local.get $aux) (i32.const 7)))))
|
|
289
377
|
(else (i32.shr_u (i32.load (i32.sub (local.get $off) (i32.const 8)))
|
|
290
378
|
(call $__typed_shift (i32.and (local.get $aux) (i32.const 7)))))))
|
|
291
|
-
|
|
379
|
+
;; HASH/SET/MAP/BUFFER: re-resolve offset so grown SET/MAP follow the
|
|
380
|
+
;; forwarding chain (HASH/BUFFER never forward → same inline offset).
|
|
381
|
+
(else (i32.load (i32.sub (call $__ptr_offset (local.get $ptr)) (i32.const 8))))))
|
|
292
382
|
(else (i32.const 0))))))`
|
|
293
383
|
|
|
294
384
|
ctx.core.stdlib['__cap'] = `(func $__cap (param $ptr i64) (result i32)
|
|
@@ -349,13 +439,7 @@ export default (ctx) => {
|
|
|
349
439
|
(then
|
|
350
440
|
(if (i32.eq (local.get $t) (i32.const 1))
|
|
351
441
|
(then
|
|
352
|
-
(
|
|
353
|
-
(loop $follow
|
|
354
|
-
(br_if $done (i32.lt_u (local.get $off) (i32.const 8)))
|
|
355
|
-
(br_if $done (i32.gt_u (local.get $off) (i32.shl (memory.size) (i32.const 16))))
|
|
356
|
-
(br_if $done (i32.ne (i32.load (i32.sub (local.get $off) (i32.const 4))) (i32.const -1)))
|
|
357
|
-
(local.set $off (i32.load (i32.sub (local.get $off) (i32.const 8))))
|
|
358
|
-
(br $follow)))))
|
|
442
|
+
${followForwardingWat('$off', { lowGuard: true })}))
|
|
359
443
|
(i32.store (i32.sub (local.get $off) (i32.const 8)) (local.get $len)))))`
|
|
360
444
|
|
|
361
445
|
// Alloc header(16) + data(cap*stride). Layout: [propsPtr@-16(f64=0), len@-8, cap@-4],
|
|
@@ -377,12 +461,18 @@ export default (ctx) => {
|
|
|
377
461
|
|
|
378
462
|
// Generic header allocator for non-8 strides: Set (16), Map probe (24), TypedArray raw (1).
|
|
379
463
|
// Same 16-byte header layout as __alloc_hdr; per-entry stride is passed dynamically.
|
|
464
|
+
// Header (16B) + cap*stride slots. Collections (Set/Map/HASH) key "empty slot"
|
|
465
|
+
// off a zero hash word, so the slot region MUST start zeroed. The bump allocator
|
|
466
|
+
// reuses memory after a heap reset (__clear) without re-zeroing, so we cannot
|
|
467
|
+
// lean on fresh-page zeroing here — clear the slots explicitly. Also covers the
|
|
468
|
+
// grow path, which rehashes into a freshly-allocated table expecting empties.
|
|
380
469
|
ctx.core.stdlib['__alloc_hdr_n'] = `(func $__alloc_hdr_n (param $len i32) (param $cap i32) (param $stride i32) (result i32)
|
|
381
470
|
(local $ptr i32)
|
|
382
471
|
(local.set $ptr (call $__alloc (i32.add (i32.const 16) (i32.mul (local.get $cap) (local.get $stride)))))
|
|
383
472
|
(i64.store (local.get $ptr) (i64.const 0))
|
|
384
473
|
(i32.store (i32.add (local.get $ptr) (i32.const 8)) (local.get $len))
|
|
385
474
|
(i32.store (i32.add (local.get $ptr) (i32.const 12)) (local.get $cap))
|
|
475
|
+
(memory.fill (i32.add (local.get $ptr) (i32.const 16)) (i32.const 0) (i32.mul (local.get $cap) (local.get $stride)))
|
|
386
476
|
(i32.add (local.get $ptr) (i32.const 16)))`
|
|
387
477
|
|
|
388
478
|
// Allocator + exports are deferred: only included when memory is actually needed.
|
|
@@ -460,7 +550,14 @@ export default (ctx) => {
|
|
|
460
550
|
// Slot val-types reach the emit-time consumer via valTypeOf → ctx.schema.slotVT
|
|
461
551
|
// (read on the AST `.prop` node), not via tagging this IR node.
|
|
462
552
|
function emitSchemaSlotRead(baseExpr, idx) {
|
|
463
|
-
|
|
553
|
+
// An unboxed proven-non-ARRAY pointer (a structInline element cell, a narrowed local)
|
|
554
|
+
// reaches ptrOffsetIR raw so it returns the offset directly — no `__ptr_offset` call.
|
|
555
|
+
// Pre-boxing via asF64 strips ptrKind and forces every field read onto the call path
|
|
556
|
+
// (the dcbb433 perf cliff on object/struct kernels — `p.x,p.y,p.z` per loop iteration).
|
|
557
|
+
// A NaN-box or untyped value still routes through f64 for the reinterpret/forwarding path.
|
|
558
|
+
const base = (baseExpr?.ptrKind != null && baseExpr.ptrKind !== VAL.ARRAY)
|
|
559
|
+
? baseExpr
|
|
560
|
+
: (baseExpr?.type === 'f64' ? baseExpr : asF64(baseExpr))
|
|
464
561
|
return typed(ctx.abi.object.ops.load(ptrOffsetIR(base, VAL.OBJECT), idx), 'f64')
|
|
465
562
|
}
|
|
466
563
|
|
|
@@ -542,7 +639,7 @@ export default (ctx) => {
|
|
|
542
639
|
}
|
|
543
640
|
|
|
544
641
|
/** Emit .prop access for a WASM f64 node using schema or HASH fallback. */
|
|
545
|
-
function emitPropAccess(va, obj, prop) {
|
|
642
|
+
function emitPropAccess(va, obj, prop, fromOptional = false) {
|
|
546
643
|
// Anonymous-literal fast path: when `obj` resolves at compile time to an
|
|
547
644
|
// object literal `{...}` (either directly, or through a `.prop` chain
|
|
548
645
|
// walked back to one), use the literal's slot index instead of falling
|
|
@@ -553,21 +650,21 @@ export default (ctx) => {
|
|
|
553
650
|
// `({a:{b:1}}).a.b` where the receiver is anonymous. Spread sources
|
|
554
651
|
// (`{...x}`) shift slot ordering and would need their own resolution.
|
|
555
652
|
const slot = literalSlot(obj, prop)
|
|
556
|
-
if (slot >= 0) return emitSchemaSlotRead(
|
|
653
|
+
if (slot >= 0) return emitSchemaSlotRead(va, slot)
|
|
557
654
|
// Receiver IR is an unboxed OBJECT pointer carrying its own schema (a
|
|
558
655
|
// structInline element cell, a narrowed local): resolve the field's fixed
|
|
559
656
|
// slot directly from `ptrAux` — more precise than the structural
|
|
560
|
-
// `ctx.schema.
|
|
657
|
+
// `ctx.schema.slotOf(null, …)` and never falls to the dyn dispatcher.
|
|
561
658
|
if (va?.ptrKind === VAL.OBJECT && va.ptrAux != null && typeof prop === 'string') {
|
|
562
659
|
const sch = ctx.schema.list[va.ptrAux]
|
|
563
660
|
const si = sch ? sch.indexOf(prop) : -1
|
|
564
|
-
if (si >= 0) return emitSchemaSlotRead(
|
|
661
|
+
if (si >= 0) return emitSchemaSlotRead(va, si)
|
|
565
662
|
}
|
|
566
|
-
let schemaIdx = typeof obj === 'string' ? ctx.schema.
|
|
663
|
+
let schemaIdx = typeof obj === 'string' ? ctx.schema.slotOf(obj, prop) : ctx.schema.slotOf(null, prop)
|
|
567
664
|
// Chain receiver (e.g. `o.meta.bias`): when the chain resolves to a known
|
|
568
665
|
// OBJECT shape via JSON-shape propagation, the parent shape's `names`
|
|
569
666
|
// gives the slot directly. Avoids the structural ambiguity of
|
|
570
|
-
// ctx.schema.
|
|
667
|
+
// ctx.schema.slotOf(null, prop) when multiple registered schemas share a key.
|
|
571
668
|
if (schemaIdx < 0 && typeof obj !== 'string') {
|
|
572
669
|
const sh = shapeOf(obj)
|
|
573
670
|
if ((sh?.val === VAL.OBJECT || sh?.val === VAL.HASH) && sh.names) {
|
|
@@ -576,7 +673,7 @@ export default (ctx) => {
|
|
|
576
673
|
}
|
|
577
674
|
}
|
|
578
675
|
const key = asI64(emit(['str', prop]))
|
|
579
|
-
if (schemaIdx >= 0) return emitSchemaSlotRead(
|
|
676
|
+
if (schemaIdx >= 0) return emitSchemaSlotRead(va, schemaIdx)
|
|
580
677
|
if (typeof obj === 'string') {
|
|
581
678
|
const vt = lookupValType(obj)
|
|
582
679
|
if (usesDynProps(vt)) {
|
|
@@ -595,9 +692,16 @@ export default (ctx) => {
|
|
|
595
692
|
// In WASI mode, values are always JSON-derived (never PTR.EXTERNAL host objects).
|
|
596
693
|
// Skip the external branch and dispatch through the typed HASH/OBJECT path.
|
|
597
694
|
if (ctx.transform.host === 'wasi') return emitDynGetExprTyped(va, key, vt, prop)
|
|
598
|
-
|
|
695
|
+
// `fromOptional` (a `?.prop` read) short-circuits on nullish, so its
|
|
696
|
+
// PTR.EXTERNAL arm is dead unless host externals are already in play —
|
|
697
|
+
// don't force the __ext_prop import just for an optional read.
|
|
698
|
+
if (!fromOptional) ctx.features.external = true
|
|
599
699
|
return emitDynGetAnyTyped(va, key, vt, prop)
|
|
600
700
|
}
|
|
701
|
+
// Primitive receiver (number/boolean/bigint): no dynamic props — `(5).foo` is
|
|
702
|
+
// undefined. Without this the value falls to the __hash_get fallback, which
|
|
703
|
+
// reinterprets the primitive's bits as a HASH pointer and reads heap → OOB.
|
|
704
|
+
if (vt === VAL.NUMBER || vt === VAL.BOOL || vt === VAL.BIGINT) return undefExpr()
|
|
601
705
|
inc('__hash_get', '__str_hash', '__str_eq')
|
|
602
706
|
return typed(['f64.reinterpret_i64', ['call', '$__hash_get', asI64(va), key]], 'f64')
|
|
603
707
|
}
|
|
@@ -662,8 +766,8 @@ export default (ctx) => {
|
|
|
662
766
|
const inner = ctx.schema.emitInner(obj)
|
|
663
767
|
return typed(['f64.convert_i32_s', ['call', '$__len', ['i64.reinterpret_f64', inner]]], 'f64')
|
|
664
768
|
}
|
|
665
|
-
const idx = ctx.schema.
|
|
666
|
-
if (idx >= 0) return emitSchemaSlotRead(
|
|
769
|
+
const idx = ctx.schema.slotOf(obj, prop)
|
|
770
|
+
if (idx >= 0) return emitSchemaSlotRead(emit(obj), idx)
|
|
667
771
|
}
|
|
668
772
|
|
|
669
773
|
if (prop === 'length') {
|
|
@@ -691,7 +795,7 @@ export default (ctx) => {
|
|
|
691
795
|
// UTF-8 and __str_byteLen returns byte count, so this matches the runtime
|
|
692
796
|
// semantics. Skips the call + NaN-unbox round-trip entirely.
|
|
693
797
|
if (Array.isArray(obj) && (obj[0] === 'str' || obj[0] == null) && typeof obj[1] === 'string') {
|
|
694
|
-
return typed(['f64.const',
|
|
798
|
+
return typed(['f64.const', new TextEncoder().encode(obj[1]).length], 'f64')
|
|
695
799
|
}
|
|
696
800
|
// structInline Array<S>: the header `len` counts physical f64 cells (K
|
|
697
801
|
// per element), so the JS array length is `physicalLen / K`.
|
|
@@ -712,14 +816,15 @@ export default (ctx) => {
|
|
|
712
816
|
}
|
|
713
817
|
|
|
714
818
|
// Type-specific property emitter (`.regex:source`, …) — the property-read
|
|
715
|
-
// mirror of the `.vt:method` method-dispatch table.
|
|
716
|
-
//
|
|
717
|
-
//
|
|
819
|
+
// mirror of the `.vt:method` method-dispatch table. Only entries tagged as
|
|
820
|
+
// getters (via `getter()`) fire here: reading `re.source` yields a value,
|
|
821
|
+
// but reading `m.keys`/`re.test` is not a call and must not invoke the
|
|
822
|
+
// method (which would materialize a view / run the probe).
|
|
718
823
|
const ptRep = typeof obj === 'string' ? repOf(obj) : null
|
|
719
824
|
const ptVt = ptRep ? ptRep.val : valTypeOf(obj)
|
|
720
825
|
if (ptVt) {
|
|
721
826
|
const tpEmitter = ctx.core.emit[`.${ptVt}:${prop}`]
|
|
722
|
-
if (tpEmitter
|
|
827
|
+
if (tpEmitter?.getter) return tpEmitter(obj)
|
|
723
828
|
}
|
|
724
829
|
|
|
725
830
|
// valueOf/toString are ToPrimitive hooks (ES2024 7.1.1) that an own data
|
|
@@ -734,24 +839,19 @@ export default (ctx) => {
|
|
|
734
839
|
if ((prop === 'valueOf' || prop === 'toString') && ctx.closure.call &&
|
|
735
840
|
(ptVt === VAL.ARRAY || ptVt === VAL.TYPED || ptVt === VAL.OBJECT)) {
|
|
736
841
|
const builtin = ctx.core.emit[`.${ptVt}:${prop}`] || ctx.core.emit[`.${prop}`]
|
|
737
|
-
if (builtin && builtin
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
['local.set', `$${o}`, asF64(emit(obj))],
|
|
742
|
-
['local.set', `$${p}`, ['f64.reinterpret_i64',
|
|
743
|
-
['call', '$__dyn_get_expr', ['i64.reinterpret_f64', ['local.get', `$${o}`]], asI64(emit(['str', prop]))]]],
|
|
744
|
-
['if', ['result', 'f64'],
|
|
745
|
-
['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${p}`]]], ['i32.const', PTR.CLOSURE]],
|
|
746
|
-
['then', ['local.get', `$${p}`]],
|
|
747
|
-
['else', asF64(builtin(o))]]], 'f64')
|
|
842
|
+
if (builtin && emitArity(builtin) <= 1) {
|
|
843
|
+
return sidecarOverride(emit(obj), asI64(emit(['str', prop])),
|
|
844
|
+
(p) => ['local.get', `$${p}`], // READ: yield the override closure value
|
|
845
|
+
(o) => asF64(builtin(o))) // else the arity-≤1 builtin's value
|
|
748
846
|
}
|
|
749
847
|
}
|
|
750
848
|
|
|
751
|
-
// Module-registered property
|
|
849
|
+
// Module-registered property getter (.size, .byteLength, …). Methods sharing
|
|
850
|
+
// the bare-`.prop` table (`.values`, `.pop`, date getters) are untagged and
|
|
851
|
+
// fall through to a real property read — `m.values` reads the "values" field.
|
|
752
852
|
const propKey = `.${prop}`
|
|
753
853
|
const propEmitter = ctx.core.emit[propKey]
|
|
754
|
-
if (propEmitter
|
|
854
|
+
if (propEmitter?.getter) return propEmitter(obj)
|
|
755
855
|
|
|
756
856
|
return emitPropAccess(emit(obj), obj, prop)
|
|
757
857
|
}
|
|
@@ -783,7 +883,13 @@ export default (ctx) => {
|
|
|
783
883
|
return optionalGuard(t, va, useFn(t))
|
|
784
884
|
}
|
|
785
885
|
|
|
786
|
-
// Optional chaining: obj?.prop →
|
|
886
|
+
// Optional chaining: obj?.prop → undefined if obj is nullish, else obj.prop.
|
|
887
|
+
// Delegate the property read to emitPropAccess — the SAME resolution the plain
|
|
888
|
+
// `.` emitter uses (passing the hoisted temp's value for the load, but the
|
|
889
|
+
// original `obj` name for schema/valType lookup). The previous hand-rolled copy
|
|
890
|
+
// diverged: it lacked emitPropAccess's `VAL.OBJECT off-schema → __dyn_get_expr`
|
|
891
|
+
// branch and fell to `__hash_get`, which mis-reads fixed-shape OBJECT memory
|
|
892
|
+
// (a self-host miscompile — `o?.x` returned undefined under the kernel).
|
|
787
893
|
ctx.core.emit['?.'] = (obj, prop) => evalOnce(obj, (t) => {
|
|
788
894
|
const rep = typeof obj === 'string' ? repOf(obj) : null
|
|
789
895
|
const vt = rep ? rep.val : valTypeOf(obj)
|
|
@@ -791,27 +897,19 @@ export default (ctx) => {
|
|
|
791
897
|
const notString = vt == null && typeof obj === 'string' && lookupNotString(obj)
|
|
792
898
|
return emitLengthAccess(['local.get', `$${t}`], vt, notString)
|
|
793
899
|
}
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
if (
|
|
803
|
-
// Unknown receiver — in WASI mode use typed dispatch (no PTR.EXTERNAL values).
|
|
804
|
-
// In JS host mode use __dyn_get_any_t but don't force features.external here
|
|
805
|
-
// since ?.prop short-circuits on nullish (EXTERNAL arm is dead unless already on).
|
|
806
|
-
return ctx.transform.host === 'wasi'
|
|
807
|
-
? emitDynGetExprTyped(['local.get', `$${t}`], asI64(emit(['str', prop])), objType, prop)
|
|
808
|
-
: emitDynGetAnyTyped(['local.get', `$${t}`], asI64(emit(['str', prop])), objType, prop)
|
|
809
|
-
inc('__hash_get', '__str_hash', '__str_eq')
|
|
810
|
-
return ['f64.reinterpret_i64', ['call', '$__hash_get', ['i64.reinterpret_f64', ['local.get', `$${t}`]], asI64(emit(['str', prop]))]]
|
|
900
|
+
// Type-specific + module-registered property getters (`.size`, `.byteLength`,
|
|
901
|
+
// `.regex:source`, …) — the SAME getter dispatch the plain `.` emitter runs
|
|
902
|
+
// (only entries tagged via `getter()` fire; untagged `.values`/`.pop` stay a
|
|
903
|
+
// field read). Read the already-hoisted, null-guarded temp `t` rather than
|
|
904
|
+
// re-emitting `obj`. Without this `s?.size` fell straight to emitPropAccess (a
|
|
905
|
+
// plain field read) and returned undefined — a Set/Map size getter never ran.
|
|
906
|
+
if (vt) {
|
|
907
|
+
const tg = ctx.core.emit[`.${vt}:${prop}`]
|
|
908
|
+
if (tg?.getter) return tg(t)
|
|
811
909
|
}
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
return
|
|
910
|
+
const g = ctx.core.emit[`.${prop}`]
|
|
911
|
+
if (g?.getter) return g(t)
|
|
912
|
+
return emitPropAccess(typed(['local.get', `$${t}`], 'f64'), obj, prop, true)
|
|
815
913
|
})
|
|
816
914
|
|
|
817
915
|
// Optional index: arr?.[i] → null if arr is null, else arr[i]
|
|
@@ -836,14 +934,21 @@ export default (ctx) => {
|
|
|
836
934
|
// as a direct method call. The outer optional short-circuits when the receiver
|
|
837
935
|
// is nullish — the method itself is statically known to exist.
|
|
838
936
|
if (Array.isArray(callee) && (callee[0] === '.' || callee[0] === '?.') && typeof callee[2] === 'string') {
|
|
839
|
-
const
|
|
840
|
-
if (
|
|
937
|
+
const method = callee[2]
|
|
938
|
+
if (ctx.core.emit[`.${method}`]) {
|
|
841
939
|
const recv = callee[1]
|
|
842
940
|
return evalOnce(recv, (t) => {
|
|
843
|
-
// Emit-time rep seed on fresh `?.()` recv-temp so
|
|
941
|
+
// Emit-time rep seed on fresh `?.()` recv-temp so the dispatch fast-paths fire.
|
|
844
942
|
const vt = typeof recv === 'string' ? repOf(recv)?.val : valTypeOf(recv)
|
|
845
943
|
if (vt) updateRep(t, { val: vt })
|
|
846
|
-
|
|
944
|
+
// Re-enter the full `()` method dispatch (runtime string/array dispatch,
|
|
945
|
+
// charCodeAt, schema, …) rather than the bare generic `.${method}` emitter
|
|
946
|
+
// — that emitter is the *array* `includes`/`indexOf`/… and would mis-run on
|
|
947
|
+
// a string receiver. Mirrors `?.[]`'s re-entry into `[]`. The method is
|
|
948
|
+
// statically known to exist, so the inner optional is moot; `t` is already
|
|
949
|
+
// nullish-guarded by evalOnce. Args re-bundle into the `()` arg slot.
|
|
950
|
+
const callArgs = args.length === 0 ? null : args.length === 1 ? args[0] : [',', ...args]
|
|
951
|
+
return asF64(ctx.core.emit['()'](['.', t, method], callArgs))
|
|
847
952
|
})
|
|
848
953
|
}
|
|
849
954
|
}
|
|
@@ -862,7 +967,7 @@ export default (ctx) => {
|
|
|
862
967
|
else normal.push(a)
|
|
863
968
|
}
|
|
864
969
|
const combined = reconstructArgsWithSpreads(normal, spreads)
|
|
865
|
-
const arrayIR =
|
|
970
|
+
const arrayIR = spread(combined)
|
|
866
971
|
callResult = ctx.closure.call(typed(['local.get', `$${t}`], 'f64'), [arrayIR], true)
|
|
867
972
|
} else {
|
|
868
973
|
callResult = ctx.closure.call(typed(['local.get', `$${t}`], 'f64'), args)
|
|
@@ -891,9 +996,9 @@ export default (ctx) => {
|
|
|
891
996
|
// boolean; isBoolExpr additionally catches `Boolean(x)` and parenthesized forms.
|
|
892
997
|
if (valTypeOf(a) === VAL.BOOL || isBoolExpr(a)) return emit(['str', 'boolean'])
|
|
893
998
|
if (!ctx.runtime.typeofStrs) {
|
|
894
|
-
ctx.runtime.typeofStrs = ['number', 'undefined', 'string', 'function', 'symbol', 'object']
|
|
999
|
+
ctx.runtime.typeofStrs = ['number', 'undefined', 'string', 'function', 'symbol', 'object', 'boolean']
|
|
895
1000
|
for (const s of ctx.runtime.typeofStrs)
|
|
896
|
-
|
|
1001
|
+
declGlobal(`__tof_${s}`, 'f64')
|
|
897
1002
|
}
|
|
898
1003
|
inc('__typeof')
|
|
899
1004
|
// Receiver type unknown; enable branches that wouldn't otherwise be reachable.
|
|
@@ -916,7 +1021,7 @@ export default (ctx) => {
|
|
|
916
1021
|
;; That bit pattern is the math NaN value, not a tagged pointer — treat as "number".
|
|
917
1022
|
;; Negative-NaN bit patterns (sign bit set) don't match NAN_PREFIX so are uniquely numeric.
|
|
918
1023
|
(if (i32.or
|
|
919
|
-
(i64.eq (local.get $v) (i64.const
|
|
1024
|
+
(i64.eq (local.get $v) (i64.const ${NAN_BITS}))
|
|
920
1025
|
(i64.eq (i64.and (local.get $v) (i64.const 0xFFF0000000000000))
|
|
921
1026
|
(i64.const 0xFFF0000000000000)))
|
|
922
1027
|
(then (return (global.get $__tof_number))))
|
|
@@ -925,6 +1030,9 @@ export default (ctx) => {
|
|
|
925
1030
|
;; typeof null === "object" — the historical JS quirk, distinct from undefined.
|
|
926
1031
|
(if (i64.eq (local.get $v) (i64.const ${NULL_NAN}))
|
|
927
1032
|
(then (return (global.get $__tof_object))))
|
|
1033
|
+
;; Boolean atoms (FALSE_NAN / TRUE_NAN) — carry at the JS boundary.
|
|
1034
|
+
(if (i64.eq (i64.and (local.get $v) (i64.const 0xFFFFFFFEFFFFFFFF)) (i64.const ${FALSE_NAN}))
|
|
1035
|
+
(then (return (global.get $__tof_boolean))))
|
|
928
1036
|
(local.set $t (call $__ptr_type (local.get $v)))
|
|
929
1037
|
(if ${stringTest}
|
|
930
1038
|
(then (return (global.get $__tof_string))))
|
|
@@ -937,11 +1045,14 @@ export default (ctx) => {
|
|
|
937
1045
|
// === Schema helpers (centralized in module/schema.js) ===
|
|
938
1046
|
initSchema(ctx)
|
|
939
1047
|
|
|
940
|
-
// Low-level pointer helpers callable from jz code
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
ctx.core.emit['
|
|
1048
|
+
// Low-level pointer helpers callable from jz code. Each handler inc()'s its
|
|
1049
|
+
// stdlib func so the call resolves at every optimize level. At opt≥1 fusedRewrite
|
|
1050
|
+
// inlines `call $__ptr_*` to bit-ops (the func is then dead-code-eliminated), but
|
|
1051
|
+
// the inc() must fire first so pullStdlib has the body when watr assembles at opt0.
|
|
1052
|
+
ctx.core.emit['__mkptr'] = (t, a, o) => (inc('__mkptr'), typed(['call', '$__mkptr', asI32(emit(t)), asI32(emit(a)), asI32(emit(o))], 'f64'))
|
|
1053
|
+
ctx.core.emit['__ptr_type'] = (p) => (inc('__ptr_type'), typed(['f64.convert_i32_s', ['call', '$__ptr_type', asI64(emit(p))]], 'f64'))
|
|
1054
|
+
ctx.core.emit['__ptr_aux'] = (p) => (inc('__ptr_aux'), typed(['f64.convert_i32_s', ['call', '$__ptr_aux', asI64(emit(p))]], 'f64'))
|
|
1055
|
+
ctx.core.emit['__ptr_offset'] = (p) => (inc('__ptr_offset'), typed(['f64.convert_i32_s', ['call', '$__ptr_offset', asI64(emit(p))]], 'f64'))
|
|
945
1056
|
|
|
946
1057
|
// Error(msg) — passthrough (throw handles any value). Subclasses share the
|
|
947
1058
|
// same shape: jz doesn't model typed-error dispatch, so SyntaxError/TypeError/
|