jz 0.7.0 → 0.8.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 +37 -33
- package/bench/README.md +176 -73
- package/bench/bench.svg +58 -71
- package/cli.js +12 -5
- package/dist/interop.js +1 -1
- package/dist/jz.js +1366 -1101
- package/index.js +40 -9
- package/interop.js +193 -138
- package/layout.js +29 -18
- package/module/array.js +49 -73
- package/module/collection.js +83 -25
- package/module/console.js +1 -1
- package/module/core.js +161 -15
- package/module/json.js +3 -3
- package/module/math.js +167 -117
- package/module/number.js +247 -13
- package/module/object.js +11 -5
- package/module/regex.js +8 -7
- package/module/string.js +295 -171
- package/module/typedarray.js +169 -105
- package/package.json +7 -3
- package/src/abi/string.js +40 -35
- package/src/ast.js +19 -2
- package/src/compile/analyze.js +64 -2
- package/src/compile/cse-load.js +200 -0
- package/src/compile/emit-assign.js +73 -14
- package/src/compile/emit.js +324 -34
- package/src/compile/index.js +204 -61
- package/src/compile/infer.js +8 -1
- package/src/compile/loop-divmod.js +12 -58
- package/src/compile/loop-model.js +91 -0
- package/src/compile/loop-recurrence.js +167 -0
- package/src/compile/loop-square.js +102 -0
- package/src/compile/narrow.js +180 -34
- package/src/compile/peel-stencil.js +18 -64
- package/src/compile/plan/common.js +29 -0
- package/src/compile/plan/index.js +4 -1
- package/src/compile/plan/inline.js +176 -21
- package/src/compile/plan/literals.js +93 -19
- package/src/ctx.js +51 -12
- package/src/helper-counters.js +137 -0
- package/src/ir.js +102 -13
- package/src/kind-traits.js +7 -3
- package/src/kind.js +14 -1
- package/src/op-policy.js +5 -2
- package/src/ops.js +119 -0
- package/src/optimize/index.js +1125 -136
- package/src/optimize/recurse.js +182 -0
- package/src/optimize/vectorize.js +1302 -144
- package/src/prepare/index.js +29 -12
- package/src/reps.js +4 -1
- package/src/type.js +53 -45
- package/src/wat/assemble.js +92 -9
- package/src/widen.js +21 -0
- package/src/wat/optimize.js +0 -3938
package/module/typedarray.js
CHANGED
|
@@ -12,7 +12,7 @@ import { emit, idx, deps, call } from '../src/bridge.js'
|
|
|
12
12
|
import { valTypeOf } from '../src/kind.js'
|
|
13
13
|
import { VAL, lookupValType } from '../src/reps.js'
|
|
14
14
|
import { nanPrefixHex, TYPED_ELEM_NAMES, TYPED_ELEM_CODE, TYPED_ELEM_BIGINT_FLAG, encodeTypedElemAux } from '../layout.js'
|
|
15
|
-
import { inc, PTR, LAYOUT,
|
|
15
|
+
import { inc, PTR, LAYOUT, registerGetter } from '../src/ctx.js'
|
|
16
16
|
|
|
17
17
|
const _NAN_BITS = nanPrefixHex()
|
|
18
18
|
|
|
@@ -28,6 +28,12 @@ const STORE = [
|
|
|
28
28
|
'i32.store8', 'i32.store8', 'i32.store16', 'i32.store16',
|
|
29
29
|
'i32.store', 'i32.store', 'f32.store', 'f64.store',
|
|
30
30
|
]
|
|
31
|
+
// f64 value → this element's stored representation (paired with STORE). Signed
|
|
32
|
+
// kinds trunc_s, unsigned trunc_u, f32 demotes, f64 stores as-is (null = identity).
|
|
33
|
+
const FROM_F64 = [
|
|
34
|
+
'i32.trunc_f64_s', 'i32.trunc_f64_u', 'i32.trunc_f64_s', 'i32.trunc_f64_u',
|
|
35
|
+
'i32.trunc_f64_s', 'i32.trunc_f64_u', 'f32.demote_f64', null,
|
|
36
|
+
]
|
|
31
37
|
|
|
32
38
|
// SIMD: vector width per element type (elements per v128)
|
|
33
39
|
const VEC_WIDTH = [16, 16, 8, 8, 4, 4, 4, 2] // 128 bits / element bits
|
|
@@ -202,10 +208,17 @@ export default (ctx) => {
|
|
|
202
208
|
__to_buffer: ['__ptr_type', '__ptr_offset', '__ptr_aux', '__mkptr'],
|
|
203
209
|
__typed_set_idx: ['__ptr_aux', '__ptr_offset'],
|
|
204
210
|
__typed_get_idx: ['__ptr_aux', '__ptr_offset'],
|
|
205
|
-
|
|
211
|
+
// __clamp_idx is body-called by every range op (fill/copyWithin/subarray/slice). It has NO
|
|
212
|
+
// other manual-dep edge in the whole stdlib, so it's reachable ONLY via resolveIncludes'
|
|
213
|
+
// auto-scan — which diverges under self-host (jz.wasm), dropping it ("Unknown func
|
|
214
|
+
// $__clamp_idx" on typed .fill/.subarray in the kernel). Declare it manually here so the
|
|
215
|
+
// reliable dep path includes it. Pinned by test/selfhost-includes.js.
|
|
216
|
+
__typed_fill: ['__len', '__typed_set_idx', '__clamp_idx'],
|
|
206
217
|
__typed_reverse: ['__len', '__typed_get_idx', '__typed_set_idx'],
|
|
207
|
-
__typed_copyWithin: ['__len', '__typed_get_idx', '__typed_set_idx'],
|
|
218
|
+
__typed_copyWithin: ['__len', '__typed_get_idx', '__typed_set_idx', '__clamp_idx'],
|
|
208
219
|
__typed_sort: ['__len', '__typed_get_idx', '__typed_set_idx'],
|
|
220
|
+
__subarray: ['__ptr_aux', '__ptr_offset', '__typed_shift', '__typed_data', '__len', '__mkptr', '__alloc', '__clamp_idx'],
|
|
221
|
+
__typed_slice_rt: ['__ptr_aux', '__typed_shift', '__typed_data', '__len', '__mkptr', '__alloc_hdr_n', '__clamp_idx'],
|
|
209
222
|
// __str_join uses __typed_idx when typedarray is loaded (plain arrays promoted to
|
|
210
223
|
// Int32Array by promoteIntArrayLiterals can produce PTR.TYPED results via .map()).
|
|
211
224
|
__str_join: [...(ctx.core.stdlibDeps.__str_join ?? []), '__typed_idx'],
|
|
@@ -266,6 +279,60 @@ export default (ctx) => {
|
|
|
266
279
|
(i32.load (i32.add (local.get $off) (i32.const 8)))))
|
|
267
280
|
(else (call $__mkptr (i32.const ${PTR.BUFFER}) (i32.const 0) (local.get $off)))))))`
|
|
268
281
|
|
|
282
|
+
// __subarray(ptr, begin, end, useEnd) — runtime-dispatched `.subarray(...)` for
|
|
283
|
+
// when the receiver's elem type / view-ness isn't statically known (a binding
|
|
284
|
+
// reassigned owned→view: `let c = new T(d); c = c.subarray(n)`). Reads elem type
|
|
285
|
+
// and the view bit off the aux byte, so it is correct whether `ptr` is an owned
|
|
286
|
+
// typed array or an existing view. Mirrors the static `.typed:subarray` emitter:
|
|
287
|
+
// builds the 16-byte descriptor [byteLen][dataOff][rootOff] and tags TYPED|view.
|
|
288
|
+
// Cold path (slicing, not per-element) — correctness over speed.
|
|
289
|
+
ctx.core.stdlib['__subarray'] = `(func $__subarray (param $ptr i64) (param $begin i32) (param $end i32) (param $useEnd i32) (result f64)
|
|
290
|
+
(local $aux i32) (local $shift i32) (local $off i32) (local $data i32) (local $root i32)
|
|
291
|
+
(local $len i32) (local $lo i32) (local $hi i32) (local $n i32) (local $desc i32)
|
|
292
|
+
(local.set $aux (call $__ptr_aux (local.get $ptr)))
|
|
293
|
+
(local.set $shift (call $__typed_shift (i32.and (local.get $aux) (i32.const 7))))
|
|
294
|
+
(local.set $off (call $__ptr_offset (local.get $ptr)))
|
|
295
|
+
(local.set $data (call $__typed_data (local.get $ptr)))
|
|
296
|
+
(local.set $root
|
|
297
|
+
(if (result i32) (i32.and (local.get $aux) (i32.const 8))
|
|
298
|
+
(then (i32.load (i32.add (local.get $off) (i32.const 8))))
|
|
299
|
+
(else (local.get $off))))
|
|
300
|
+
(local.set $len (call $__len (local.get $ptr)))
|
|
301
|
+
(local.set $lo (call $__clamp_idx (local.get $begin) (local.get $len)))
|
|
302
|
+
(if (local.get $useEnd)
|
|
303
|
+
(then
|
|
304
|
+
(local.set $hi (call $__clamp_idx (local.get $end) (local.get $len))))
|
|
305
|
+
(else (local.set $hi (local.get $len))))
|
|
306
|
+
(local.set $n (select (i32.sub (local.get $hi) (local.get $lo)) (i32.const 0) (i32.gt_s (local.get $hi) (local.get $lo))))
|
|
307
|
+
(local.set $desc (call $__alloc (i32.const 16)))
|
|
308
|
+
(i32.store (local.get $desc) (i32.shl (local.get $n) (local.get $shift)))
|
|
309
|
+
(i32.store (i32.add (local.get $desc) (i32.const 4)) (i32.add (local.get $data) (i32.shl (local.get $lo) (local.get $shift))))
|
|
310
|
+
(i32.store (i32.add (local.get $desc) (i32.const 8)) (local.get $root))
|
|
311
|
+
(call $__mkptr (i32.const ${PTR.TYPED}) (i32.or (local.get $aux) (i32.const 8)) (local.get $desc)))`
|
|
312
|
+
|
|
313
|
+
// __typed_slice_rt(ptr, begin, end, useEnd) — runtime-dispatched `.slice(...)` for
|
|
314
|
+
// a receiver whose elem type / view-ness isn't statically known. Unlike subarray
|
|
315
|
+
// this returns a fresh OWNED copy (bit-exact memory.copy, so bigint-safe too). Reads
|
|
316
|
+
// elem type + data address off the aux byte. Cold path — correctness over speed.
|
|
317
|
+
ctx.core.stdlib['__typed_slice_rt'] = `(func $__typed_slice_rt (param $ptr i64) (param $begin i32) (param $end i32) (param $useEnd i32) (result f64)
|
|
318
|
+
(local $aux i32) (local $et i32) (local $shift i32) (local $src i32)
|
|
319
|
+
(local $len i32) (local $lo i32) (local $hi i32) (local $n i32) (local $byteLen i32) (local $dst i32)
|
|
320
|
+
(local.set $aux (call $__ptr_aux (local.get $ptr)))
|
|
321
|
+
(local.set $et (i32.and (local.get $aux) (i32.const 7)))
|
|
322
|
+
(local.set $shift (call $__typed_shift (local.get $et)))
|
|
323
|
+
(local.set $src (call $__typed_data (local.get $ptr)))
|
|
324
|
+
(local.set $len (call $__len (local.get $ptr)))
|
|
325
|
+
(local.set $lo (call $__clamp_idx (local.get $begin) (local.get $len)))
|
|
326
|
+
(if (local.get $useEnd)
|
|
327
|
+
(then
|
|
328
|
+
(local.set $hi (call $__clamp_idx (local.get $end) (local.get $len))))
|
|
329
|
+
(else (local.set $hi (local.get $len))))
|
|
330
|
+
(local.set $n (select (i32.sub (local.get $hi) (local.get $lo)) (i32.const 0) (i32.gt_s (local.get $hi) (local.get $lo))))
|
|
331
|
+
(local.set $byteLen (i32.shl (local.get $n) (local.get $shift)))
|
|
332
|
+
(local.set $dst (call $__alloc_hdr_n (local.get $byteLen) (local.get $byteLen) (i32.const 1)))
|
|
333
|
+
(memory.copy (local.get $dst) (i32.add (local.get $src) (i32.shl (local.get $lo) (local.get $shift))) (local.get $byteLen))
|
|
334
|
+
(call $__mkptr (i32.const ${PTR.TYPED}) (i32.and (local.get $aux) (i32.const 23)) (local.get $dst)))`
|
|
335
|
+
|
|
269
336
|
// Constructor: new Float64Array(len) | new F64Array(arr) | new F64Array(buf) | new F64Array(buf, off, len)
|
|
270
337
|
for (const [name, elemType] of Object.entries(TYPED_ELEM_CODE)) {
|
|
271
338
|
const aux = typedAux(name)
|
|
@@ -278,6 +345,7 @@ export default (ctx) => {
|
|
|
278
345
|
// and tags the TYPED ptr with aux=elemType|8. Reads/writes alias the parent,
|
|
279
346
|
// .buffer reconstructs the root BUFFER, .byteOffset = dataOff - parentOff.
|
|
280
347
|
if (offsetExpr != null && lenExpr2 != null) {
|
|
348
|
+
ctx.features.typedView = true // subview aliases the parent buffer — SLP must not assume disjoint bases
|
|
281
349
|
const src = temp('tvs')
|
|
282
350
|
const parentOff = tempI32('tvp')
|
|
283
351
|
const byteLen = tempI32('tvb')
|
|
@@ -303,9 +371,12 @@ export default (ctx) => {
|
|
|
303
371
|
// TYPED retagged at the same offset — the byteLen header is shared with the parent.
|
|
304
372
|
// __len(view) = byteLen >> shift computes elemCount for this view's elemType.
|
|
305
373
|
if (srcType === VAL.BUFFER || srcType === VAL.TYPED) {
|
|
374
|
+
ctx.features.typedView = true // zero-copy reinterpret aliases the source — SLP must not pack across it
|
|
306
375
|
return mkPtrIR(PTR.TYPED, aux, ['call', '$__ptr_offset', ['i64.reinterpret_f64', asF64(emit(lenExpr))]])
|
|
307
376
|
}
|
|
308
377
|
if (srcType == null && ctx.core.emit[`${name}.from`]) {
|
|
378
|
+
ctx.features.typedView = true // unknown arg: runtime may take the buffer/typed zero-copy-view branch
|
|
379
|
+
|
|
309
380
|
// Runtime dispatch: number → allocate; array → copy elements; buffer/typed → zero-copy view.
|
|
310
381
|
const src = temp('ts')
|
|
311
382
|
const len = tempI32('tl')
|
|
@@ -410,7 +481,7 @@ export default (ctx) => {
|
|
|
410
481
|
// .buffer — always aliased (zero-copy). BUFFER: passthrough.
|
|
411
482
|
// Owned TYPED: retag as BUFFER at same offset — the byteLen header is shared.
|
|
412
483
|
// TYPED view (incl. DataView): BUFFER at descriptor[8] (root parent data offset).
|
|
413
|
-
|
|
484
|
+
registerGetter('.buffer', (obj) => {
|
|
414
485
|
if (typeof obj === 'string') {
|
|
415
486
|
const ctor = ctx.types.typedElem?.get(obj)
|
|
416
487
|
if (ctor === 'new.ArrayBuffer') return asF64(emit(obj))
|
|
@@ -430,7 +501,7 @@ export default (ctx) => {
|
|
|
430
501
|
|
|
431
502
|
// .byteLength — BUFFER: raw __len. Owned TYPED: elemCount * stride.
|
|
432
503
|
// View TYPED (incl. DataView): descriptor[0], via the __byte_length fallback.
|
|
433
|
-
|
|
504
|
+
registerGetter('.byteLength', (obj) => {
|
|
434
505
|
if (typeof obj === 'string') {
|
|
435
506
|
const ctor = ctx.types.typedElem?.get(obj)
|
|
436
507
|
if (ctor === 'new.ArrayBuffer') {
|
|
@@ -454,7 +525,7 @@ export default (ctx) => {
|
|
|
454
525
|
})
|
|
455
526
|
|
|
456
527
|
// .byteOffset — owned: 0. View: descriptor[4] - descriptor[8].
|
|
457
|
-
|
|
528
|
+
registerGetter('.byteOffset', (obj) => {
|
|
458
529
|
if (typeof obj === 'string') {
|
|
459
530
|
const ctor = ctx.types.typedElem?.get(obj)
|
|
460
531
|
if (ctor?.endsWith('.view')) {
|
|
@@ -543,6 +614,10 @@ export default (ctx) => {
|
|
|
543
614
|
// We treat `undefined`/`null`/`0`/`''` as falsy (BE) per ToBoolean.
|
|
544
615
|
const staticLE = (node) => {
|
|
545
616
|
if (node === undefined) return false // arg omitted → BE
|
|
617
|
+
// Prepare lowers a literal `true`/`false` endianness flag to `['bool', 0|1]`;
|
|
618
|
+
// fold it so `dv.getInt16(p, true)` is one native load (no per-access LE branch
|
|
619
|
+
// + dead byte-swap arm — the dominant overhead in DataView codec loops).
|
|
620
|
+
if (Array.isArray(node) && node[0] === 'bool') return !!node[1]
|
|
546
621
|
if (Array.isArray(node) && node[0] == null) {
|
|
547
622
|
// [] → undefined, [null, x] → literal x
|
|
548
623
|
if (node.length === 1) return false
|
|
@@ -603,7 +678,18 @@ export default (ctx) => {
|
|
|
603
678
|
(f64.reinterpret_i64 (i64.const ${_NAN_BITS}))
|
|
604
679
|
(local.get $v)
|
|
605
680
|
(f64.ne (local.get $v) (local.get $v))))`
|
|
606
|
-
|
|
681
|
+
// Inline the NaN-canonicalization select rather than a call — a DataView float
|
|
682
|
+
// read runs this per sample in codec loops, and the inlined `(select nan v (v≠v))`
|
|
683
|
+
// is the exact idiom the auto-vectorizer recognizes (vs an opaque call).
|
|
684
|
+
const canonNaN = (vIR) => {
|
|
685
|
+
const t = temp('cn')
|
|
686
|
+
return typed(['block', ['result', 'f64'],
|
|
687
|
+
['local.set', `$${t}`, vIR],
|
|
688
|
+
['select',
|
|
689
|
+
['f64.reinterpret_i64', ['i64.const', _NAN_BITS]],
|
|
690
|
+
['local.get', `$${t}`],
|
|
691
|
+
['f64.ne', ['local.get', `$${t}`], ['local.get', `$${t}`]]]], 'f64')
|
|
692
|
+
}
|
|
607
693
|
|
|
608
694
|
// ToIndex for DataView byte offsets (spec 25.1.2.x getViewValue/setViewValue):
|
|
609
695
|
// ToIntegerOrInfinity then reject negatives and >2^53-1 with a RangeError. The
|
|
@@ -644,8 +730,25 @@ export default (ctx) => {
|
|
|
644
730
|
const dvIndexChecked = (offNode, size, viewSize) => {
|
|
645
731
|
ctx.runtime.throws = true
|
|
646
732
|
const idxT = tempI32('dvb')
|
|
733
|
+
const offIR = emit(offNode)
|
|
734
|
+
// Fast path: a proven-i32 byte offset (a loop-advanced `p`, `i*2`, a `|0` value)
|
|
735
|
+
// carries no fraction/NaN/negative-from-ToNumber surprise, so it skips the f64
|
|
736
|
+
// ToIndex round-trip and the `__dv_index` CALL — the dominant per-sample cost in
|
|
737
|
+
// DataView-based codec loops. An i32 range (`<0`) + bounds (`>viewSize-size`)
|
|
738
|
+
// check is all the spec needs here; this lowers `dv.getInt16(p,true)` to one
|
|
739
|
+
// `i32.load16_s`, matching the explicit-typed-view path.
|
|
740
|
+
if (offIR.type === 'i32') {
|
|
741
|
+
return typed(['block', ['result', 'i32'],
|
|
742
|
+
['local.set', `$${idxT}`, offIR],
|
|
743
|
+
['if', ['i32.or',
|
|
744
|
+
['i32.lt_s', ['local.get', `$${idxT}`], ['i32.const', 0]],
|
|
745
|
+
['i32.gt_s', ['local.get', `$${idxT}`], ['i32.sub', viewSize, ['i32.const', size]]]],
|
|
746
|
+
['then', ['throw', '$__jz_err', ['f64.const', 0]]]],
|
|
747
|
+
['local.get', `$${idxT}`]], 'i32')
|
|
748
|
+
}
|
|
749
|
+
inc('__dv_index')
|
|
647
750
|
return typed(['block', ['result', 'i32'],
|
|
648
|
-
['local.set', `$${idxT}`,
|
|
751
|
+
['local.set', `$${idxT}`, typed(['call', '$__dv_index', toNumF64(offNode, offIR)], 'i32')],
|
|
649
752
|
['if', ['i32.gt_s', ['local.get', `$${idxT}`], ['i32.sub', viewSize, ['i32.const', size]]],
|
|
650
753
|
['then', ['throw', '$__jz_err', ['f64.const', 0]]]],
|
|
651
754
|
['local.get', `$${idxT}`]], 'i32')
|
|
@@ -812,16 +915,11 @@ export default (ctx) => {
|
|
|
812
915
|
len: ['i32.mul', ['local.get', `$${len}`], ['i32.const', stride]], stride: 1, tag: 'tf' })
|
|
813
916
|
const t = out.local
|
|
814
917
|
const id = ctx.func.uniq++
|
|
815
|
-
const
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
['f32.demote_f64', ['f64.load', ['i32.add', ['local.get', `$${off}`], ['i32.shl', ['local.get', `$${i}`], ['i32.const', 3]]]]]]
|
|
821
|
-
: [store,
|
|
822
|
-
['i32.add', ['local.get', `$${t}`], ['i32.mul', ['local.get', `$${i}`], ['i32.const', stride]]],
|
|
823
|
-
[(elemType & 1) ? 'i32.trunc_f64_u' : 'i32.trunc_f64_s',
|
|
824
|
-
['f64.load', ['i32.add', ['local.get', `$${off}`], ['i32.shl', ['local.get', `$${i}`], ['i32.const', 3]]]]]]
|
|
918
|
+
const conv = FROM_F64[elemType]
|
|
919
|
+
const srcF64 = ['f64.load', ['i32.add', ['local.get', `$${off}`], ['i32.shl', ['local.get', `$${i}`], ['i32.const', 3]]]]
|
|
920
|
+
const storeExpr = [store,
|
|
921
|
+
['i32.add', ['local.get', `$${t}`], ['i32.mul', ['local.get', `$${i}`], ['i32.const', stride]]],
|
|
922
|
+
conv ? [conv, srcF64] : srcF64]
|
|
825
923
|
return typed(['block', ['result', 'f64'],
|
|
826
924
|
['local.set', `$${srcL}`, asF64(emit(src))],
|
|
827
925
|
['local.set', `$${off}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${srcL}`]]]],
|
|
@@ -859,7 +957,14 @@ export default (ctx) => {
|
|
|
859
957
|
receiver = receiver[1][1]
|
|
860
958
|
chainOutput = true
|
|
861
959
|
}
|
|
862
|
-
|
|
960
|
+
// Nested receiver `arr[i]` where `arr`'s elements are typed arrays of a known
|
|
961
|
+
// ctor (`Array.from(n, () => new Float32Array())` — codec channelData). The i-th
|
|
962
|
+
// element IS that owned typed array; emit(receiver) already loads its pointer, so
|
|
963
|
+
// the standard typedDataAddr path inlines `arr[i][j]` to a direct load/store.
|
|
964
|
+
const ctor = (Array.isArray(receiver) && receiver[0] === '[]' && receiver.length === 3 && typeof receiver[1] === 'string'
|
|
965
|
+
? ctx.func.localReps?.get(receiver[1])?.arrayElemTypedCtor
|
|
966
|
+
: null)
|
|
967
|
+
|| (typeof receiver === 'string' && ctx.types.typedElem?.get(receiver))
|
|
863
968
|
if (!ctor) return null
|
|
864
969
|
const isView = viewOutput || (!chainOutput && ctor.endsWith('.view'))
|
|
865
970
|
const name = ctor.endsWith('.view') ? ctor.slice(4, -5) : ctor.slice(4)
|
|
@@ -887,65 +992,6 @@ export default (ctx) => {
|
|
|
887
992
|
|
|
888
993
|
// Runtime-dispatch typed index: checks ptr_type + aux to load with correct stride.
|
|
889
994
|
// For TYPED views (aux bit 3), $off indirects through descriptor[4] to real data.
|
|
890
|
-
// Factory — collapses to ARRAY-only f64 indexing when no TYPED pointer can reach here.
|
|
891
|
-
// Identical factory in array.js; whichever module loads last wins the registration.
|
|
892
|
-
ctx.core.stdlib['__typed_idx'] = () => {
|
|
893
|
-
if (!ctx.features.typedarray && !ctx.features.external) {
|
|
894
|
-
return `(func $__typed_idx (param $ptr i64) (param $i i32) (result f64)
|
|
895
|
-
(local $len i32)
|
|
896
|
-
(local.set $len (call $__len (local.get $ptr)))
|
|
897
|
-
(if (result f64)
|
|
898
|
-
(i32.or
|
|
899
|
-
(i32.lt_s (local.get $i) (i32.const 0))
|
|
900
|
-
(i32.ge_u (local.get $i) (local.get $len)))
|
|
901
|
-
(then (f64.const nan:${UNDEF_NAN}))
|
|
902
|
-
(else (f64.load (i32.add (call $__ptr_offset (local.get $ptr)) (i32.shl (local.get $i) (i32.const 3)))))))`
|
|
903
|
-
}
|
|
904
|
-
return `(func $__typed_idx (param $ptr i64) (param $i i32) (result f64)
|
|
905
|
-
(local $off i32) (local $et i32) (local $len i32) (local $aux i32)
|
|
906
|
-
(local.set $off (call $__ptr_offset (local.get $ptr)))
|
|
907
|
-
;; ARRAY fast path: __ptr_offset already followed any forwarding — read header len + f64.load, no $__len call.
|
|
908
|
-
(if (i32.and (i32.eq (call $__ptr_type (local.get $ptr)) (i32.const ${PTR.ARRAY})) (i32.ge_u (local.get $off) (i32.const 8)))
|
|
909
|
-
(then (return (if (result f64)
|
|
910
|
-
(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)))))
|
|
911
|
-
(then (f64.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3)))))
|
|
912
|
-
(else (f64.const nan:${UNDEF_NAN}))))))
|
|
913
|
-
(local.set $aux (call $__ptr_aux (local.get $ptr)))
|
|
914
|
-
(if
|
|
915
|
-
(i32.and
|
|
916
|
-
(i32.eq (call $__ptr_type (local.get $ptr)) (i32.const ${PTR.TYPED}))
|
|
917
|
-
(i32.ne (i32.and (local.get $aux) (i32.const 8)) (i32.const 0)))
|
|
918
|
-
(then (local.set $off (i32.load (i32.add (local.get $off) (i32.const 4))))))
|
|
919
|
-
(local.set $len (call $__len (local.get $ptr)))
|
|
920
|
-
(if (result f64)
|
|
921
|
-
(i32.or
|
|
922
|
-
(i32.lt_s (local.get $i) (i32.const 0))
|
|
923
|
-
(i32.ge_u (local.get $i) (local.get $len)))
|
|
924
|
-
(then (f64.const nan:${UNDEF_NAN}))
|
|
925
|
-
(else
|
|
926
|
-
(if (result f64) (i32.eq (call $__ptr_type (local.get $ptr)) (i32.const ${PTR.TYPED}))
|
|
927
|
-
(then
|
|
928
|
-
(local.set $et (i32.and (local.get $aux) (i32.const 7)))
|
|
929
|
-
(if (result f64) (i32.ge_u (local.get $et) (i32.const 6))
|
|
930
|
-
(then (if (result f64) (i32.eq (local.get $et) (i32.const 7))
|
|
931
|
-
(then (if (result f64) (i32.and (local.get $aux) (i32.const ${TYPED_ELEM_BIGINT_FLAG}))
|
|
932
|
-
(then (f64.reinterpret_i64 (i64.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3))))))
|
|
933
|
-
(else (f64.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3)))))))
|
|
934
|
-
(else (f64.promote_f32 (f32.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 2))))))))
|
|
935
|
-
(else (if (result f64) (i32.ge_u (local.get $et) (i32.const 4))
|
|
936
|
-
(then (if (result f64) (i32.and (local.get $et) (i32.const 1))
|
|
937
|
-
(then (f64.convert_i32_u (i32.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 2))))))
|
|
938
|
-
(else (f64.convert_i32_s (i32.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 2))))))))
|
|
939
|
-
(else (if (result f64) (i32.ge_u (local.get $et) (i32.const 2))
|
|
940
|
-
(then (if (result f64) (i32.and (local.get $et) (i32.const 1))
|
|
941
|
-
(then (f64.convert_i32_u (i32.load16_u (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 1))))))
|
|
942
|
-
(else (f64.convert_i32_s (i32.load16_s (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 1))))))))
|
|
943
|
-
(else (if (result f64) (i32.and (local.get $et) (i32.const 1))
|
|
944
|
-
(then (f64.convert_i32_u (i32.load8_u (i32.add (local.get $off) (local.get $i)))))
|
|
945
|
-
(else (f64.convert_i32_s (i32.load8_s (i32.add (local.get $off) (local.get $i)))))))))))))
|
|
946
|
-
(else (f64.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3)))))))))`
|
|
947
|
-
}
|
|
948
|
-
|
|
949
995
|
ctx.core.stdlib['__typed_set_idx'] = `(func $__typed_set_idx (param $ptr i64) (param $i i32) (param $v f64) (result f64)
|
|
950
996
|
(local $off i32) (local $aux i32) (local $et i32) (local $bits i32)
|
|
951
997
|
(local.set $aux (call $__ptr_aux (local.get $ptr)))
|
|
@@ -982,12 +1028,8 @@ export default (ctx) => {
|
|
|
982
1028
|
ctx.core.stdlib['__typed_fill'] = `(func $__typed_fill (param $ptr i64) (param $val f64) (param $start i32) (param $end i32) (result f64)
|
|
983
1029
|
(local $len i32) (local $i i32)
|
|
984
1030
|
(local.set $len (call $__len (local.get $ptr)))
|
|
985
|
-
(
|
|
986
|
-
(
|
|
987
|
-
(if (i32.gt_s (local.get $start) (local.get $len)) (then (local.set $start (local.get $len))))
|
|
988
|
-
(if (i32.lt_s (local.get $end) (i32.const 0)) (then (local.set $end (i32.add (local.get $len) (local.get $end)))))
|
|
989
|
-
(if (i32.lt_s (local.get $end) (i32.const 0)) (then (local.set $end (i32.const 0))))
|
|
990
|
-
(if (i32.gt_s (local.get $end) (local.get $len)) (then (local.set $end (local.get $len))))
|
|
1031
|
+
(local.set $start (call $__clamp_idx (local.get $start) (local.get $len)))
|
|
1032
|
+
(local.set $end (call $__clamp_idx (local.get $end) (local.get $len)))
|
|
991
1033
|
(local.set $i (local.get $start))
|
|
992
1034
|
(block $done (loop $fill
|
|
993
1035
|
(br_if $done (i32.ge_s (local.get $i) (local.get $end)))
|
|
@@ -1047,15 +1089,9 @@ export default (ctx) => {
|
|
|
1047
1089
|
ctx.core.stdlib['__typed_copyWithin'] = `(func $__typed_copyWithin (param $ptr i64) (param $target i32) (param $start i32) (param $end i32) (result f64)
|
|
1048
1090
|
(local $len i32) (local $count i32) (local $k i32)
|
|
1049
1091
|
(local.set $len (call $__len (local.get $ptr)))
|
|
1050
|
-
(
|
|
1051
|
-
(
|
|
1052
|
-
(
|
|
1053
|
-
(if (i32.lt_s (local.get $start) (i32.const 0)) (then (local.set $start (i32.add (local.get $len) (local.get $start)))))
|
|
1054
|
-
(if (i32.lt_s (local.get $start) (i32.const 0)) (then (local.set $start (i32.const 0))))
|
|
1055
|
-
(if (i32.gt_s (local.get $start) (local.get $len)) (then (local.set $start (local.get $len))))
|
|
1056
|
-
(if (i32.lt_s (local.get $end) (i32.const 0)) (then (local.set $end (i32.add (local.get $len) (local.get $end)))))
|
|
1057
|
-
(if (i32.lt_s (local.get $end) (i32.const 0)) (then (local.set $end (i32.const 0))))
|
|
1058
|
-
(if (i32.gt_s (local.get $end) (local.get $len)) (then (local.set $end (local.get $len))))
|
|
1092
|
+
(local.set $target (call $__clamp_idx (local.get $target) (local.get $len)))
|
|
1093
|
+
(local.set $start (call $__clamp_idx (local.get $start) (local.get $len)))
|
|
1094
|
+
(local.set $end (call $__clamp_idx (local.get $end) (local.get $len)))
|
|
1059
1095
|
(local.set $count (i32.sub (local.get $end) (local.get $start)))
|
|
1060
1096
|
(if (i32.gt_s (local.get $count) (i32.sub (local.get $len) (local.get $target)))
|
|
1061
1097
|
(then (local.set $count (i32.sub (local.get $len) (local.get $target)))))
|
|
@@ -1238,20 +1274,28 @@ export default (ctx) => {
|
|
|
1238
1274
|
['f32.store', off, ['f32.demote_f64', ['local.get', `$${vt}`]]],
|
|
1239
1275
|
['local.get', `$${vt}`]], void_ ? 'void' : 'f64') // Float32Array
|
|
1240
1276
|
}
|
|
1241
|
-
// Integer store: when the source is already i32-typed (bitwise ops, |0,
|
|
1242
|
-
//
|
|
1243
|
-
//
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1277
|
+
// Integer store: when the source is already i32-typed (bitwise ops, |0, known-i32 var) —
|
|
1278
|
+
// OR an `f64.convert_i32_*` that peels back to i32 (an Int8/Uint8/Int16/… element READ
|
|
1279
|
+
// materialized as f64 by the universal value model) — store the i32 low bits directly,
|
|
1280
|
+
// skipping the f64 detour that costs a sign branch + i64 trunc + i32 wrap on every write.
|
|
1281
|
+
// This eradicates the f64 round-trip on byte/typed-array TRANSFORMS — `out[i] = table[in[j]]`
|
|
1282
|
+
// and `dst[i] = src[j]` (base64, qoi, wav, blur) — where both sides are integer elements.
|
|
1283
|
+
// `store8/16` mask the low bits, so storing the convert's i32 source is bit-identical; the
|
|
1284
|
+
// non-void result reboxes that i32 to f64 (the assignment's RHS value, in element range here).
|
|
1285
|
+
const i32Backed = valIR.type === 'i32' ||
|
|
1286
|
+
(Array.isArray(valIR) && (valIR[0] === 'f64.convert_i32_s' || valIR[0] === 'f64.convert_i32_u'))
|
|
1287
|
+
if (i32Backed) {
|
|
1288
|
+
const vi = asI32(valIR)
|
|
1289
|
+
const cheap = Array.isArray(vi) &&
|
|
1290
|
+
((vi[0] === 'local.get' && typeof vi[1] === 'string') ||
|
|
1291
|
+
(vi[0] === 'i32.const' && (typeof vi[1] === 'number' || typeof vi[1] === 'string')))
|
|
1292
|
+
if (void_ && cheap) return typed([STORE[et], off, vi], 'void')
|
|
1249
1293
|
const v32 = tempI32('tw')
|
|
1250
1294
|
return typed(void_ ? ['block',
|
|
1251
|
-
['local.set', `$${v32}`,
|
|
1295
|
+
['local.set', `$${v32}`, vi],
|
|
1252
1296
|
[STORE[et], off, ['local.get', `$${v32}`]]]
|
|
1253
1297
|
: ['block', ['result', 'f64'],
|
|
1254
|
-
['local.set', `$${v32}`,
|
|
1298
|
+
['local.set', `$${v32}`, vi],
|
|
1255
1299
|
[STORE[et], off, ['local.get', `$${v32}`]],
|
|
1256
1300
|
[(et & 1) ? 'f64.convert_i32_u' : 'f64.convert_i32_s', ['local.get', `$${v32}`]]], void_ ? 'void' : 'f64')
|
|
1257
1301
|
}
|
|
@@ -1714,7 +1758,17 @@ export default (ctx) => {
|
|
|
1714
1758
|
// Bulk copy via `memory.copy`.
|
|
1715
1759
|
ctx.core.emit['.typed:slice'] = (arr, start, end) => {
|
|
1716
1760
|
const r = resolveElem(arr)
|
|
1717
|
-
if (!r
|
|
1761
|
+
if (!r) {
|
|
1762
|
+
// Elem type / view-ness not statically known (owned→view reassigned binding).
|
|
1763
|
+
// Dispatch off the runtime aux byte instead of crashing on empty IR.
|
|
1764
|
+
inc('__typed_slice_rt')
|
|
1765
|
+
return typed(['call', '$__typed_slice_rt',
|
|
1766
|
+
['i64.reinterpret_f64', asF64(emit(arr))],
|
|
1767
|
+
start == null ? ['i32.const', 0] : asI32(emit(start)),
|
|
1768
|
+
end == null ? ['i32.const', 0] : asI32(emit(end)),
|
|
1769
|
+
['i32.const', end == null ? 0 : 1]], 'f64')
|
|
1770
|
+
}
|
|
1771
|
+
if (r.isBigInt) return null
|
|
1718
1772
|
const { et, isView } = r
|
|
1719
1773
|
const arrLoc = temp('tsa'), srcPtr = tempI32('tssp'), srcLen = tempI32('tssl')
|
|
1720
1774
|
const lo = tempI32('tslo'), hi = tempI32('tshi'), n = tempI32('tsn')
|
|
@@ -1809,8 +1863,18 @@ export default (ctx) => {
|
|
|
1809
1863
|
// NOT a copy). Builds the 16-byte descriptor [byteLen][dataOff][parentOff] and tags the
|
|
1810
1864
|
// TYPED ptr with aux|view, exactly like new TypedArray(buffer, byteOffset, length).
|
|
1811
1865
|
ctx.core.emit['.typed:subarray'] = (arr, begin, end) => {
|
|
1866
|
+
ctx.features.typedView = true // zero-copy view aliases the receiver — covers inline `a.subarray(1)[i]=…` the bound-decl path in analyze.js misses
|
|
1812
1867
|
const r = resolveElem(arr)
|
|
1813
|
-
if (!r)
|
|
1868
|
+
if (!r) {
|
|
1869
|
+
// Elem type / view-ness not statically known (owned→view reassigned binding).
|
|
1870
|
+
// Dispatch off the runtime aux byte instead of crashing on empty IR.
|
|
1871
|
+
inc('__subarray')
|
|
1872
|
+
return typed(['call', '$__subarray',
|
|
1873
|
+
['i64.reinterpret_f64', asF64(emit(arr))],
|
|
1874
|
+
begin == null ? ['i32.const', 0] : asI32(emit(begin)),
|
|
1875
|
+
end == null ? ['i32.const', 0] : asI32(emit(end)),
|
|
1876
|
+
['i32.const', end == null ? 0 : 1]], 'f64')
|
|
1877
|
+
}
|
|
1814
1878
|
const { et, isView, isBigInt } = r
|
|
1815
1879
|
const shift = SHIFT[et]
|
|
1816
1880
|
const viewAux = et | 8 | (isBigInt ? 16 : 0)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jz",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Ahead-of-time compiler for the numeric core of JavaScript (DSP, audio, math, parsers) to lean GC-free WASM — valid jz is valid JS, no type annotations, no runtime.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -55,10 +55,14 @@
|
|
|
55
55
|
"bench:size": "node scripts/bench-size.mjs",
|
|
56
56
|
"bench:compile": "node scripts/bench-compile.mjs",
|
|
57
57
|
"bench:startup": "node scripts/bench-startup.mjs",
|
|
58
|
+
"bench:self": "node scripts/bench-selfhost.mjs",
|
|
58
59
|
"bench:readme": "node scripts/bench-readme.mjs",
|
|
60
|
+
"audit:as": "node scripts/audit-assemblyscript.mjs",
|
|
59
61
|
"build": "node scripts/build-dist.mjs",
|
|
60
62
|
"build:examples": "node examples/build.mjs all",
|
|
61
|
-
"
|
|
63
|
+
"prepublishOnly": "npm test && npm run build && npm run test:self",
|
|
64
|
+
"prepare": "npm run build",
|
|
65
|
+
"audit:fixpoint": "node scripts/audit-fixpoint.mjs"
|
|
62
66
|
},
|
|
63
67
|
"repository": {
|
|
64
68
|
"type": "git",
|
|
@@ -72,7 +76,7 @@
|
|
|
72
76
|
"license": "MIT",
|
|
73
77
|
"dependencies": {
|
|
74
78
|
"subscript": "^10.4.17",
|
|
75
|
-
"watr": "^
|
|
79
|
+
"watr": "^5.0.0"
|
|
76
80
|
},
|
|
77
81
|
"keywords": [
|
|
78
82
|
"javascript",
|
package/src/abi/string.js
CHANGED
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
// take. Kept as a one-liner so each op reads as a single `call`.
|
|
57
57
|
const ssoI64 = (sF64) => ['i64.reinterpret_f64', sF64]
|
|
58
58
|
|
|
59
|
-
import { isReassigned } from '../ast.js'
|
|
59
|
+
import { isReassigned, isLeaf } from '../ast.js'
|
|
60
60
|
import { LAYOUT, oobNanIR, ssoBitI64Hex } from '../../layout.js'
|
|
61
61
|
|
|
62
62
|
/** Pre-shifted SSO discriminator — layout.js is cycle-free; memoized at first use. */
|
|
@@ -79,10 +79,6 @@ const allocLocalI32 = (ctx, tag) => {
|
|
|
79
79
|
return name
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
const _isLeaf = (n) => Array.isArray(n) &&
|
|
83
|
-
(n[0] === 'local.get' || n[0] === 'global.get' ||
|
|
84
|
-
n[0] === 'i32.const' || n[0] === 'i64.const' ||
|
|
85
|
-
n[0] === 'f64.const' || n[0] === 'f32.const')
|
|
86
82
|
|
|
87
83
|
/** Per-use cheap form of `charCodeAt` against a pre-decomposed param receiver
|
|
88
84
|
* (shape 1): a bounds check plus a 2-arm SSO/heap byte select reading the four
|
|
@@ -90,22 +86,29 @@ const _isLeaf = (n) => Array.isArray(n) &&
|
|
|
90
86
|
* side-effecting `iI32` is spilled to a scratch local first — leaves are safe
|
|
91
87
|
* to duplicate. `oobNan` picks the OOB contract / result type exactly as in
|
|
92
88
|
* the generic path. */
|
|
93
|
-
function emitDecompCharRead(dec, iI32, ctx, oobNan) {
|
|
89
|
+
function emitDecompCharRead(dec, iI32, ctx, oobNan, inBounds = false) {
|
|
94
90
|
const rt = oobNan ? 'f64' : 'i32'
|
|
95
91
|
let idx = iI32, spill = null
|
|
96
|
-
if (!
|
|
97
|
-
const ssoByteExpr = ['i32.and',
|
|
98
|
-
['
|
|
99
|
-
['
|
|
92
|
+
if (!isLeaf(iI32)) { spill = allocLocalI32(ctx, 'ci'); idx = ['local.get', `$${spill}`] }
|
|
93
|
+
const ssoByteExpr = ['i32.wrap_i64', ['i64.and',
|
|
94
|
+
['i64.shr_u', ['local.get', `$${dec.ptr64}`], ['i64.mul', ['i64.extend_i32_u', idx], ['i64.const', 7]]],
|
|
95
|
+
['i64.const', '0x7f']]]
|
|
100
96
|
const heapByteExpr = ['i32.load8_u', ['i32.add', ['local.get', `$${dec.loadbase}`], idx]]
|
|
101
97
|
const ccByte = ['if', ['result', 'i32'],
|
|
102
98
|
['local.get', `$${dec.sso}`],
|
|
103
99
|
['then', ssoByteExpr],
|
|
104
100
|
['else', heapByteExpr]]
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
101
|
+
// `inBounds`: the index is proven in [0, len) by an enclosing canonical scan
|
|
102
|
+
// (analyze.js inBoundsCharCodeAt / splitCharScanLoops' in-bounds main loop), so
|
|
103
|
+
// the OOB arm is dead — drop the per-char `i >= len` compare. This is what turns
|
|
104
|
+
// a split char-scan loop into a bare load (the `if(sso)` arm is loop-invariant and
|
|
105
|
+
// V8-folds), matching AS/native on tokenizer-shape scans. Otherwise keep the guard.
|
|
106
|
+
const use = inBounds
|
|
107
|
+
? (oobNan ? ['f64.convert_i32_u', ccByte] : ccByte)
|
|
108
|
+
: ['if', ['result', rt],
|
|
109
|
+
['i32.ge_u', idx, ['local.get', `$${dec.len}`]],
|
|
110
|
+
['then', oobNan ? oobNanIR() : ['i32.const', 0]],
|
|
111
|
+
['else', oobNan ? ['f64.convert_i32_u', ccByte] : ccByte]]
|
|
109
112
|
return spill
|
|
110
113
|
? ['block', ['result', rt], ['local.set', `$${spill}`, iI32], use]
|
|
111
114
|
: use
|
|
@@ -131,9 +134,8 @@ export function emitCharDecompPrologue(dec) {
|
|
|
131
134
|
['i64.const', 0]]
|
|
132
135
|
const offMask = `0x${LAYOUT.OFFSET_MASK.toString(16).toUpperCase()}`
|
|
133
136
|
const off = ['i32.wrap_i64', ['i64.and', ptr, ['i64.const', offMask]]]
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
['i32.const', LAYOUT.SSO_BIT - 1]]
|
|
137
|
+
// SSO length lives at payload bits 42-44 (7-bit-codec layout; see module/string.js).
|
|
138
|
+
const ssoLen = ['i32.wrap_i64', ['i64.and', ['i64.shr_u', ptr, ['i64.const', 42]], ['i64.const', 7]]]
|
|
137
139
|
// Heap length is `i32.load(off - 4)`; guard against off<4 (corrupt/non-string
|
|
138
140
|
// payload) so the load doesn't trap on a wrapped-negative address.
|
|
139
141
|
const heapLen = ['if', ['result', 'i32'],
|
|
@@ -146,6 +148,7 @@ export function emitCharDecompPrologue(dec) {
|
|
|
146
148
|
['then',
|
|
147
149
|
['local.set', `$${dec.sso}`, ['i32.const', 1]],
|
|
148
150
|
['local.set', `$${dec.base}`, off],
|
|
151
|
+
['local.set', `$${dec.ptr64}`, ptr], // full payload: SSO chars are 7-bit, span into aux
|
|
149
152
|
['local.set', `$${dec.len}`, ssoLen],
|
|
150
153
|
// SSO: route every per-iter load to address 0 (always valid, byte
|
|
151
154
|
// discarded by the outer select).
|
|
@@ -212,7 +215,7 @@ export const sso = {
|
|
|
212
215
|
* `src/compile.js#emitFunc` — it drains `ctx.func.charDecomp` after the
|
|
213
216
|
* body emit completes and splices an init block between the boxed-param
|
|
214
217
|
* inits and the user statements. */
|
|
215
|
-
charCodeAt: (sF64, iI32, ctx, oobNan = false) => {
|
|
218
|
+
charCodeAt: (sF64, iI32, ctx, oobNan = false, inBounds = false) => {
|
|
216
219
|
// `oobNan` selects the out-of-bounds semantics and result type:
|
|
217
220
|
// - false (default): OOB → `i32.const 0`, result i32 — the raw-byte
|
|
218
221
|
// primitive used by the `buf += s[i]` append-byte fast path.
|
|
@@ -256,14 +259,16 @@ export const sso = {
|
|
|
256
259
|
// discards it). Pre-computing it in the prologue removes a
|
|
257
260
|
// per-iter `select` and lets V8 fold the add into the load.
|
|
258
261
|
const loadbase = `${name}$ccldb`
|
|
262
|
+
const ptr64 = `${name}$ccp64`
|
|
259
263
|
ctx.func.locals.set(base, 'i32')
|
|
260
264
|
ctx.func.locals.set(len, 'i32')
|
|
261
265
|
ctx.func.locals.set(sso, 'i32')
|
|
262
266
|
ctx.func.locals.set(loadbase, 'i32')
|
|
263
|
-
|
|
267
|
+
ctx.func.locals.set(ptr64, 'i64') // full SSO payload for 7-bit char extraction
|
|
268
|
+
dec = { base, len, sso, loadbase, ptr64, param: name }
|
|
264
269
|
ctx.func.charDecomp.set(name, dec)
|
|
265
270
|
}
|
|
266
|
-
return emitDecompCharRead(dec, iI32, ctx, oobNan)
|
|
271
|
+
return emitDecompCharRead(dec, iI32, ctx, oobNan, inBounds)
|
|
267
272
|
}
|
|
268
273
|
}
|
|
269
274
|
|
|
@@ -277,9 +282,6 @@ export const sso = {
|
|
|
277
282
|
// so we MUST spill anything that isn't side-effect-free to a local
|
|
278
283
|
// before referencing it more than once. Leaves (`local.get`, `*.const`,
|
|
279
284
|
// `global.get`) are safe to duplicate; everything else is spilled.
|
|
280
|
-
const isLeaf = (n) => Array.isArray(n) &&
|
|
281
|
-
(n[0] === 'local.get' || n[0] === 'global.get' ||
|
|
282
|
-
n[0] === 'i32.const' || n[0] === 'i64.const' || n[0] === 'f64.const' || n[0] === 'f32.const')
|
|
283
285
|
const sLeaf = isLeaf(sF64)
|
|
284
286
|
const iLeaf = isLeaf(iI32)
|
|
285
287
|
const ptrI64Expr = ssoI64(sF64)
|
|
@@ -301,12 +303,10 @@ export const sso = {
|
|
|
301
303
|
}
|
|
302
304
|
const offMask = `0x${LAYOUT.OFFSET_MASK.toString(16).toUpperCase()}`
|
|
303
305
|
const offExpr = () => ['i32.wrap_i64', ['i64.and', getPtr(), ['i64.const', offMask]]]
|
|
304
|
-
const ssoLen = ['i32.and',
|
|
305
|
-
|
|
306
|
-
['
|
|
307
|
-
|
|
308
|
-
['i32.shr_u', offExpr(), ['i32.mul', getIdx(), ['i32.const', 8]]],
|
|
309
|
-
['i32.const', 0xFF]]
|
|
306
|
+
const ssoLen = ['i32.wrap_i64', ['i64.and', ['i64.shr_u', getPtr(), ['i64.const', 42]], ['i64.const', 7]]]
|
|
307
|
+
const ssoByte = ['i32.wrap_i64', ['i64.and',
|
|
308
|
+
['i64.shr_u', getPtr(), ['i64.mul', ['i64.extend_i32_u', getIdx()], ['i64.const', 7]]],
|
|
309
|
+
['i64.const', '0x7f']]]
|
|
310
310
|
const ssoBranch = ['if', ['result', rt],
|
|
311
311
|
['i32.ge_u', getIdx(), ssoLen],
|
|
312
312
|
['then', mkOob()],
|
|
@@ -373,15 +373,20 @@ export const sso = {
|
|
|
373
373
|
* dispatcher's string/array runtime guess (emit.js) would hijack it into a
|
|
374
374
|
* bogus array concat. A non-builtin name routes through dynamic property
|
|
375
375
|
* dispatch (load the closure slot, call it) correctly. */
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
376
|
+
// `ext` (default false) opts into the bump-EXTEND fast path — sound only when emit
|
|
377
|
+
// proves `a` is dead-after (a self-accumulation `x = x + …`). Otherwise the _fresh twin
|
|
378
|
+
// alloc+copies, never mutating the live `a` operand. (See __str_concat in module/string.js.)
|
|
379
|
+
cat: (aF64, bF64, ctx, ext = false) => {
|
|
380
|
+
const fn = ext ? '__str_concat' : '__str_concat_fresh'
|
|
381
|
+
ctx.core.includes.add(fn)
|
|
382
|
+
return ['call', '$' + fn, ssoI64(aF64), ssoI64(bF64)]
|
|
379
383
|
},
|
|
380
384
|
|
|
381
385
|
/** Concat assuming both sides are already strings (skip ToString). */
|
|
382
|
-
concatRaw: (aF64, bF64, ctx) => {
|
|
383
|
-
|
|
384
|
-
|
|
386
|
+
concatRaw: (aF64, bF64, ctx, ext = false) => {
|
|
387
|
+
const fn = ext ? '__str_concat_raw' : '__str_concat_raw_fresh'
|
|
388
|
+
ctx.core.includes.add(fn)
|
|
389
|
+
return ['call', '$' + fn, ssoI64(aF64), ssoI64(bF64)]
|
|
385
390
|
},
|
|
386
391
|
},
|
|
387
392
|
}
|