jz 0.3.1 → 0.5.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 +277 -258
- package/cli.js +10 -52
- package/index.js +181 -29
- package/{src/host.js → interop.js} +291 -88
- package/module/array.js +195 -76
- package/module/collection.js +249 -20
- package/module/console.js +1 -1
- package/module/core.js +267 -119
- package/module/date.js +9 -5
- package/module/function.js +11 -1
- package/module/json.js +367 -61
- package/module/math.js +568 -128
- package/module/number.js +390 -227
- package/module/object.js +352 -39
- package/module/regex.js +123 -16
- package/module/schema.js +15 -1
- package/module/string.js +555 -96
- package/module/timer.js +1 -1
- package/module/typedarray.js +674 -57
- package/package.json +12 -6
- package/src/abi/array.js +89 -0
- package/src/abi/index.js +35 -0
- package/src/abi/number.js +137 -0
- package/src/abi/object.js +57 -0
- package/src/abi/string.js +529 -0
- package/src/analyze.js +1872 -487
- package/src/assemble.js +58 -20
- package/src/autoload.js +13 -1
- package/src/codegen.js +177 -0
- package/src/compile.js +462 -186
- package/src/ctx.js +85 -18
- package/src/emit.js +668 -197
- package/src/infer.js +548 -0
- package/src/ir.js +302 -27
- package/src/jzify.js +898 -259
- package/src/narrow.js +455 -246
- package/src/optimize.js +263 -99
- package/src/plan.js +713 -35
- package/src/prepare.js +818 -293
- package/src/resolve.js +85 -0
- package/src/vectorize.js +99 -18
- package/src/ast.js +0 -160
- package/src/auto-config.js +0 -120
package/module/typedarray.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* @module typed
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { typed, asF64, asI32, asI64, UNDEF_NAN, allocPtr, mkPtrIR, ptrOffsetIR, temp, tempI32, undefExpr } from '../src/ir.js'
|
|
10
|
+
import { typed, asF64, asI32, asI64, toNumF64, UNDEF_NAN, allocPtr, mkPtrIR, ptrOffsetIR, temp, tempI32, tempI64, undefExpr, truthyIR } from '../src/ir.js'
|
|
11
11
|
import { emit } from '../src/emit.js'
|
|
12
12
|
import { valTypeOf, lookupValType, VAL } from '../src/analyze.js'
|
|
13
13
|
import { inc, PTR } from '../src/ctx.js'
|
|
@@ -209,8 +209,11 @@ export default (ctx) => {
|
|
|
209
209
|
__typed_set_idx: ['__ptr_aux', '__ptr_offset'],
|
|
210
210
|
})
|
|
211
211
|
|
|
212
|
-
// .map
|
|
213
|
-
|
|
212
|
+
// .map invokes with arity 1; .forEach/.find/.some/.every/.filter/.findIndex
|
|
213
|
+
// invoke with (item, idx) → arity 2. Reduce with (acc, item) → arity 2.
|
|
214
|
+
// (jz omits the `arr` arg array-spec callbacks normally receive — matches
|
|
215
|
+
// array.js convention.)
|
|
216
|
+
ctx.closure.floor = Math.max(ctx.closure.floor ?? 0, 2)
|
|
214
217
|
|
|
215
218
|
inc('__mkptr', '__alloc', '__len')
|
|
216
219
|
|
|
@@ -335,16 +338,61 @@ export default (ctx) => {
|
|
|
335
338
|
// ArrayBuffer: first-class byte storage with [-8:byteLen][-4:byteCap][bytes].
|
|
336
339
|
// DataView: passthrough ptr to the same BUFFER — DataView methods operate on raw bytes via offset.
|
|
337
340
|
|
|
338
|
-
//
|
|
341
|
+
// ToIndex + allocation-ceiling for ArrayBuffer byte lengths: ToInteger (NaN→0,
|
|
342
|
+
// trunc toward zero), then a RangeError for negatives and for any size jz
|
|
343
|
+
// cannot represent as an i32 byte count. The ceiling sits just below 2^31 so
|
|
344
|
+
// the trunc never traps and leaves room for the 8-byte buffer header — this
|
|
345
|
+
// also rejects the spec's ≥2^53 case and genuinely un-allocatable sizes.
|
|
346
|
+
ctx.core.stdlib['__ab_len'] = `(func $__ab_len (param $n f64) (result i32)
|
|
347
|
+
(if (f64.ne (local.get $n) (local.get $n)) (then (local.set $n (f64.const 0))))
|
|
348
|
+
(local.set $n (f64.trunc (local.get $n)))
|
|
349
|
+
(if (i32.or
|
|
350
|
+
(f64.lt (local.get $n) (f64.const 0))
|
|
351
|
+
(f64.ge (local.get $n) (f64.const 2147483640)))
|
|
352
|
+
(then (throw $__jz_err (f64.const 0))))
|
|
353
|
+
(i32.trunc_f64_s (local.get $n)))`
|
|
354
|
+
|
|
355
|
+
// new ArrayBuffer(n) → allocate n bytes, return as BUFFER pointer.
|
|
356
|
+
// Length is ToNumber-coerced (ToIndex) so a Symbol length raises a TypeError;
|
|
357
|
+
// __ab_len then throws a RangeError on a negative or oversized request.
|
|
339
358
|
const arrayBufferCtor = (sizeExpr) => {
|
|
340
|
-
|
|
359
|
+
ctx.runtime.throws = true
|
|
360
|
+
inc('__ab_len')
|
|
361
|
+
const n = typed(['call', '$__ab_len', toNumF64(sizeExpr, emit(sizeExpr))], 'i32')
|
|
341
362
|
const out = allocPtr({ type: PTR.BUFFER, len: n, stride: 1, tag: 'ab' })
|
|
342
363
|
return typed(['block', ['result', 'f64'], out.init, out.ptr], 'f64')
|
|
343
364
|
}
|
|
344
365
|
ctx.core.emit['new.ArrayBuffer'] = arrayBufferCtor
|
|
345
366
|
|
|
346
|
-
// new DataView(buffer
|
|
347
|
-
|
|
367
|
+
// new DataView(buffer, byteOffset?, byteLength?) — a first-class view object,
|
|
368
|
+
// represented exactly like a typed-array subview: a 16-byte descriptor
|
|
369
|
+
// [byteLen:i32][dataOff:i32][parentOff:i32][pad] behind a TYPED|view pointer.
|
|
370
|
+
// byteOffset/byteLength are ToIndex-coerced; the no-length form snapshots the
|
|
371
|
+
// remaining buffer (buffer.byteLength - byteOffset). Because the view extent
|
|
372
|
+
// lives in the descriptor, .byteLength/.byteOffset/.buffer and the get/set
|
|
373
|
+
// bounds checks all read it at runtime — correct regardless of how the
|
|
374
|
+
// receiver variable was assigned, and ArrayBuffer.isView(dv) is now true.
|
|
375
|
+
ctx.core.emit['new.DataView'] = (bufExpr, offExpr, lenExpr) => {
|
|
376
|
+
ctx.features.typedarray = true
|
|
377
|
+
const src = temp('dvs')
|
|
378
|
+
const parentOff = tempI32('dvp')
|
|
379
|
+
const off = tempI32('dvo')
|
|
380
|
+
const dst = tempI32('dvd')
|
|
381
|
+
return typed(['block', ['result', 'f64'],
|
|
382
|
+
['local.set', `$${src}`, asF64(emit(bufExpr))],
|
|
383
|
+
['local.set', `$${parentOff}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${src}`]]]],
|
|
384
|
+
['local.set', `$${off}`, offExpr == null ? ['i32.const', 0] : dvIndex(offExpr)],
|
|
385
|
+
['local.set', `$${dst}`, ['call', '$__alloc', ['i32.const', 16]]],
|
|
386
|
+
['i32.store', ['local.get', `$${dst}`],
|
|
387
|
+
lenExpr == null
|
|
388
|
+
? ['i32.sub', ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${src}`]]], ['local.get', `$${off}`]]
|
|
389
|
+
: dvIndex(lenExpr)],
|
|
390
|
+
['i32.store', ['i32.add', ['local.get', `$${dst}`], ['i32.const', 4]],
|
|
391
|
+
['i32.add', ['local.get', `$${parentOff}`], ['local.get', `$${off}`]]],
|
|
392
|
+
['i32.store', ['i32.add', ['local.get', `$${dst}`], ['i32.const', 8]],
|
|
393
|
+
['local.get', `$${parentOff}`]],
|
|
394
|
+
mkPtrIR(PTR.TYPED, 8, ['local.get', `$${dst}`])], 'f64')
|
|
395
|
+
}
|
|
348
396
|
|
|
349
397
|
// BigInt64Array(buffer) (bare form, legacy): coerce to same data, Float64Array-compatible storage.
|
|
350
398
|
ctx.core.emit['BigInt64Array'] = (bufExpr) => {
|
|
@@ -353,13 +401,13 @@ export default (ctx) => {
|
|
|
353
401
|
return mkPtrIR(PTR.TYPED, typedAux('BigInt64Array'), ['call', '$__ptr_offset', ['i64.reinterpret_f64', va]])
|
|
354
402
|
}
|
|
355
403
|
|
|
356
|
-
// .buffer — always aliased (zero-copy). BUFFER
|
|
404
|
+
// .buffer — always aliased (zero-copy). BUFFER: passthrough.
|
|
357
405
|
// Owned TYPED: retag as BUFFER at same offset — the byteLen header is shared.
|
|
358
|
-
// TYPED view: BUFFER at descriptor[8] (root parent data offset).
|
|
406
|
+
// TYPED view (incl. DataView): BUFFER at descriptor[8] (root parent data offset).
|
|
359
407
|
ctx.core.emit['.buffer'] = (obj) => {
|
|
360
408
|
if (typeof obj === 'string') {
|
|
361
409
|
const ctor = ctx.types.typedElem?.get(obj)
|
|
362
|
-
if (ctor === 'new.ArrayBuffer'
|
|
410
|
+
if (ctor === 'new.ArrayBuffer') return asF64(emit(obj))
|
|
363
411
|
if (ctor?.startsWith('new.')) {
|
|
364
412
|
const isView = ctor.endsWith('.view')
|
|
365
413
|
const name = isView ? ctor.slice(4, -5) : ctor.slice(4)
|
|
@@ -375,11 +423,12 @@ export default (ctx) => {
|
|
|
375
423
|
return typed(['call', '$__to_buffer', asI64(emit(obj))], 'f64')
|
|
376
424
|
}
|
|
377
425
|
|
|
378
|
-
// .byteLength — BUFFER: raw __len. Owned TYPED: elemCount * stride.
|
|
426
|
+
// .byteLength — BUFFER: raw __len. Owned TYPED: elemCount * stride.
|
|
427
|
+
// View TYPED (incl. DataView): descriptor[0], via the __byte_length fallback.
|
|
379
428
|
ctx.core.emit['.byteLength'] = (obj) => {
|
|
380
429
|
if (typeof obj === 'string') {
|
|
381
430
|
const ctor = ctx.types.typedElem?.get(obj)
|
|
382
|
-
if (ctor === 'new.ArrayBuffer'
|
|
431
|
+
if (ctor === 'new.ArrayBuffer') {
|
|
383
432
|
return typed(['f64.convert_i32_s', ['call', '$__len', ['i64.reinterpret_f64', asF64(emit(obj))]]], 'f64')
|
|
384
433
|
}
|
|
385
434
|
if (ctor && ctor.startsWith('new.')) {
|
|
@@ -433,8 +482,9 @@ export default (ctx) => {
|
|
|
433
482
|
(i32.load (i32.add (local.get $off) (i32.const 8)))))
|
|
434
483
|
(else (i32.const 0))))`
|
|
435
484
|
|
|
436
|
-
// ArrayBuffer.isView(x) — true iff x is a TYPED pointer.
|
|
437
|
-
//
|
|
485
|
+
// ArrayBuffer.isView(x) — true iff x is a TYPED pointer. Typed arrays and
|
|
486
|
+
// DataViews are both TYPED-tagged (a DataView is a TYPED|view descriptor), so
|
|
487
|
+
// both report true; a bare ArrayBuffer is BUFFER-tagged and reports false.
|
|
438
488
|
ctx.core.emit['ArrayBuffer.isView'] = (v) => {
|
|
439
489
|
if (v === undefined) return typed(['f64.const', 0], 'f64')
|
|
440
490
|
const va = asF64(emit(v))
|
|
@@ -442,6 +492,19 @@ export default (ctx) => {
|
|
|
442
492
|
['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', va]], ['i32.const', PTR.TYPED]]], 'f64')
|
|
443
493
|
}
|
|
444
494
|
|
|
495
|
+
// x instanceof Float64Array | Int32Array | … — typed-pointer predicate emitted
|
|
496
|
+
// by jzify. NaN-check first, then __ptr_type === PTR.TYPED. Aux-byte ctor
|
|
497
|
+
// discrimination (Float64 vs Int32) lives downstream in typedElem analysis —
|
|
498
|
+
// this predicate only asserts "is some TypedArray". Result i32 (boolean).
|
|
499
|
+
ctx.core.emit['__is_typed'] = (x) => {
|
|
500
|
+
if (x === undefined) return typed(['i32.const', 0], 'i32')
|
|
501
|
+
const v = asF64(emit(x))
|
|
502
|
+
const t = temp('ityp')
|
|
503
|
+
return typed(['i32.and',
|
|
504
|
+
['f64.ne', ['local.tee', `$${t}`, v], ['local.get', `$${t}`]],
|
|
505
|
+
['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${t}`]]], ['i32.const', PTR.TYPED]]], 'i32')
|
|
506
|
+
}
|
|
507
|
+
|
|
445
508
|
// buf.slice(begin?, end?) on a BUFFER → fresh BUFFER with the byte range copied.
|
|
446
509
|
// Only dispatches statically when obj is a tracked ArrayBuffer/DataView variable.
|
|
447
510
|
ctx.core.emit['.buf:slice'] = (obj, beginExpr, endExpr) => {
|
|
@@ -470,44 +533,268 @@ export default (ctx) => {
|
|
|
470
533
|
out.ptr], 'f64')
|
|
471
534
|
}
|
|
472
535
|
|
|
473
|
-
// DataView
|
|
536
|
+
// DataView endianness — wasm memory is natively little-endian. The DV `littleEndian`
|
|
537
|
+
// arg defaults to `false` per ECMA-262 (big-endian); when LE is false we have to
|
|
538
|
+
// byte-swap. `staticLE` peels a literal `[null, x]` AST node and returns:
|
|
539
|
+
// true/false — known endianness, no runtime branch needed
|
|
540
|
+
// null — dynamic, emit `if le (native) else (bswap)`
|
|
541
|
+
// We treat `undefined`/`null`/`0`/`''` as falsy (BE) per ToBoolean.
|
|
542
|
+
const staticLE = (node) => {
|
|
543
|
+
if (node === undefined) return false // arg omitted → BE
|
|
544
|
+
if (Array.isArray(node) && node[0] == null) {
|
|
545
|
+
// [] → undefined, [null, x] → literal x
|
|
546
|
+
if (node.length === 1) return false
|
|
547
|
+
const v = node[1]
|
|
548
|
+
if (v === undefined || v === null) return false
|
|
549
|
+
if (typeof v === 'boolean' || typeof v === 'number' || typeof v === 'string') return !!v
|
|
550
|
+
}
|
|
551
|
+
return null // dynamic
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// bswap16: i32 bytes [b0 b1] → [b1 b0]. Result is unsigned 16-bit; signed-16
|
|
555
|
+
// sign-extension is applied by the load op (i32.load16_s) at the call site.
|
|
556
|
+
const bswap16I32 = (v) => {
|
|
557
|
+
const t = tempI32('bs16')
|
|
558
|
+
return ['block', ['result', 'i32'],
|
|
559
|
+
['local.set', `$${t}`, v],
|
|
560
|
+
['i32.or',
|
|
561
|
+
['i32.shl', ['i32.and', ['local.get', `$${t}`], ['i32.const', 0xff]], ['i32.const', 8]],
|
|
562
|
+
['i32.and', ['i32.shr_u', ['local.get', `$${t}`], ['i32.const', 8]], ['i32.const', 0xff]]]]
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// bswap32: i32 bytes [b0 b1 b2 b3] → [b3 b2 b1 b0]. Symmetric — used for both
|
|
566
|
+
// load (after native LE load) and store (before native LE store).
|
|
567
|
+
const bswap32I32 = (v) => {
|
|
568
|
+
const t = tempI32('bs32')
|
|
569
|
+
return ['block', ['result', 'i32'],
|
|
570
|
+
['local.set', `$${t}`, v],
|
|
571
|
+
['i32.or',
|
|
572
|
+
['i32.or',
|
|
573
|
+
['i32.shl', ['local.get', `$${t}`], ['i32.const', 24]],
|
|
574
|
+
['i32.shl', ['i32.and', ['local.get', `$${t}`], ['i32.const', 0xff00]], ['i32.const', 8]]],
|
|
575
|
+
['i32.or',
|
|
576
|
+
['i32.and', ['i32.shr_u', ['local.get', `$${t}`], ['i32.const', 8]], ['i32.const', 0xff00]],
|
|
577
|
+
['i32.and', ['i32.shr_u', ['local.get', `$${t}`], ['i32.const', 24]], ['i32.const', 0xff]]]]]
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// bswap64: rotate bytes via two 32-bit halves. We need a runtime helper because
|
|
581
|
+
// i64 doesn't have inline byte-swap and we'd otherwise emit a 64-line tree.
|
|
582
|
+
ctx.core.stdlib['__bswap64'] = `(func $__bswap64 (param $v i64) (result i64)
|
|
583
|
+
(local $r i64)
|
|
584
|
+
(local.set $r (i64.shl (i64.and (local.get $v) (i64.const 0xff)) (i64.const 56)))
|
|
585
|
+
(local.set $r (i64.or (local.get $r) (i64.shl (i64.and (local.get $v) (i64.const 0xff00)) (i64.const 40))))
|
|
586
|
+
(local.set $r (i64.or (local.get $r) (i64.shl (i64.and (local.get $v) (i64.const 0xff0000)) (i64.const 24))))
|
|
587
|
+
(local.set $r (i64.or (local.get $r) (i64.shl (i64.and (local.get $v) (i64.const 0xff000000)) (i64.const 8))))
|
|
588
|
+
(local.set $r (i64.or (local.get $r) (i64.shr_u (i64.and (local.get $v) (i64.const 0xff00000000)) (i64.const 8))))
|
|
589
|
+
(local.set $r (i64.or (local.get $r) (i64.shr_u (i64.and (local.get $v) (i64.const 0xff0000000000)) (i64.const 24))))
|
|
590
|
+
(local.set $r (i64.or (local.get $r) (i64.shr_u (i64.and (local.get $v) (i64.const 0xff000000000000)) (i64.const 40))))
|
|
591
|
+
(local.set $r (i64.or (local.get $r) (i64.shr_u (local.get $v) (i64.const 56))))
|
|
592
|
+
(local.get $r))`
|
|
593
|
+
const bswap64I64 = (v) => { inc('__bswap64'); return ['call', '$__bswap64', v] }
|
|
594
|
+
|
|
595
|
+
// DV float reads return raw f64 bits, which may be non-canonical NaN (sign-flipped
|
|
596
|
+
// or with payload). jz's `__eq` fast path only treats canonical NaN (0x7FF8…0000)
|
|
597
|
+
// as NaN, so non-canonical NaN would break `v !== v` semantics. Canonicalize here
|
|
598
|
+
// (cheap: 4 wasm ops) so `getFloat32`/`getFloat64` return a real-spec NaN value.
|
|
599
|
+
ctx.core.stdlib['__canon_nan'] = `(func $__canon_nan (param $v f64) (result f64)
|
|
600
|
+
(select
|
|
601
|
+
(f64.reinterpret_i64 (i64.const 0x7FF8000000000000))
|
|
602
|
+
(local.get $v)
|
|
603
|
+
(f64.ne (local.get $v) (local.get $v))))`
|
|
604
|
+
const canonNaN = (vIR) => { inc('__canon_nan'); return typed(['call', '$__canon_nan', vIR], 'f64') }
|
|
605
|
+
|
|
606
|
+
// ToIndex for DataView byte offsets (spec 25.1.2.x getViewValue/setViewValue):
|
|
607
|
+
// ToIntegerOrInfinity then reject negatives and >2^53-1 with a RangeError. The
|
|
608
|
+
// throw rides jz's $__jz_err tag so a user `try/catch` (or assert.throws) sees it.
|
|
609
|
+
// trunc_sat keeps a huge-but-passing index from trapping the conversion — the
|
|
610
|
+
// subsequent memory access fails the same way an oversized index always has.
|
|
611
|
+
ctx.core.stdlib['__dv_index'] = `(func $__dv_index (param $idx f64) (result i32)
|
|
612
|
+
(if (f64.ne (local.get $idx) (local.get $idx)) (then (local.set $idx (f64.const 0))))
|
|
613
|
+
(local.set $idx (f64.trunc (local.get $idx)))
|
|
614
|
+
(if (i32.or
|
|
615
|
+
(f64.lt (local.get $idx) (f64.const 0))
|
|
616
|
+
(f64.gt (local.get $idx) (f64.const 9007199254740991)))
|
|
617
|
+
(then (throw $__jz_err (f64.const 0))))
|
|
618
|
+
(i32.trunc_sat_f64_s (local.get $idx)))`
|
|
619
|
+
|
|
620
|
+
// ToIndex the DV byte offset, throwing RangeError on a negative/oversized value.
|
|
621
|
+
// The offset is ToNumber-coerced first (per ToIndex) so a Symbol byte offset
|
|
622
|
+
// raises a TypeError instead of silently truncating to 0.
|
|
623
|
+
const dvIndex = (offNode) => {
|
|
624
|
+
ctx.runtime.throws = true
|
|
625
|
+
inc('__dv_index')
|
|
626
|
+
return typed(['call', '$__dv_index', toNumF64(offNode, emit(offNode))], 'i32')
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// A DataView receiver resolves to its 16-byte descriptor pointer
|
|
630
|
+
// [byteLen:i32][dataOff:i32][parentOff:i32]. get/set hoist this once, then
|
|
631
|
+
// read dataOff (desc[4]) for the access address and byteLen (desc[0]) as the
|
|
632
|
+
// view size for the bounds check.
|
|
633
|
+
const dvDescriptor = (dv) => ptrOffsetIR(emit(dv), VAL.TYPED)
|
|
634
|
+
const dvDataOff = (descLocal) => ['i32.load', ['i32.add', ['local.get', `$${descLocal}`], ['i32.const', 4]]]
|
|
635
|
+
const dvViewSize = (descLocal) => ['i32.load', ['local.get', `$${descLocal}`]]
|
|
636
|
+
|
|
637
|
+
// ToIndex the DV byte offset, then bounds-check `getIndex + elementSize > viewSize`
|
|
638
|
+
// (spec 25.3.1.1 GetViewValue / SetViewValue step 13) — a RangeError when the
|
|
639
|
+
// access would run past the view. elementSize moves to the static side of the
|
|
640
|
+
// compare so a saturated/huge index can't overflow the i32 addition. viewSize is
|
|
641
|
+
// read live from the descriptor, so the check holds for every DataView receiver.
|
|
642
|
+
const dvIndexChecked = (offNode, size, viewSize) => {
|
|
643
|
+
ctx.runtime.throws = true
|
|
644
|
+
const idxT = tempI32('dvb')
|
|
645
|
+
return typed(['block', ['result', 'i32'],
|
|
646
|
+
['local.set', `$${idxT}`, dvIndex(offNode)],
|
|
647
|
+
['if', ['i32.gt_s', ['local.get', `$${idxT}`], ['i32.sub', viewSize, ['i32.const', size]]],
|
|
648
|
+
['then', ['throw', '$__jz_err', ['f64.const', 0]]]],
|
|
649
|
+
['local.get', `$${idxT}`]], 'i32')
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// DataView set methods: extract i32 offset from f64 ptr, optionally byte-swap, store.
|
|
653
|
+
// 8-bit ops ignore the LE arg entirely (single byte — no swap possible).
|
|
474
654
|
const DV_SET = {
|
|
475
|
-
setInt8: 'i32.store8', setUint8: 'i32.store8',
|
|
476
|
-
setInt16: 'i32.store16', setUint16: 'i32.store16',
|
|
477
|
-
setInt32: 'i32.store', setUint32: 'i32.store',
|
|
478
|
-
setFloat32: 'f32.store', setFloat64: 'f64.store',
|
|
479
|
-
setBigInt64: 'i64.store', setBigUint64: 'i64.store',
|
|
480
|
-
}
|
|
481
|
-
for (const [method, storeOp] of Object.entries(DV_SET)) {
|
|
482
|
-
ctx.core.emit[`.${method}`] = (dv, off, val,
|
|
483
|
-
|
|
484
|
-
const
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
655
|
+
setInt8: ['i32.store8', 'i32', 1], setUint8: ['i32.store8', 'i32', 1],
|
|
656
|
+
setInt16: ['i32.store16', 'i32', 2], setUint16: ['i32.store16', 'i32', 2],
|
|
657
|
+
setInt32: ['i32.store', 'i32', 4], setUint32: ['i32.store', 'i32', 4],
|
|
658
|
+
setFloat32: ['f32.store', 'f32', 4], setFloat64: ['f64.store', 'f64', 8],
|
|
659
|
+
setBigInt64: ['i64.store', 'i64', 8], setBigUint64: ['i64.store', 'i64', 8],
|
|
660
|
+
}
|
|
661
|
+
for (const [method, [storeOp, valType, size]] of Object.entries(DV_SET)) {
|
|
662
|
+
ctx.core.emit[`.${method}`] = (dv, off, val, leNode) => {
|
|
663
|
+
// Resolve the receiver's descriptor once; the store reads dataOff/byteLen from it.
|
|
664
|
+
const desc = tempI32('dvD')
|
|
665
|
+
const fin = (body) => ['block', ['local.set', `$${desc}`, dvDescriptor(dv)], body]
|
|
666
|
+
const addr = ['i32.add', dvDataOff(desc), dvIndexChecked(off, size, dvViewSize(desc))]
|
|
667
|
+
// Coerce value into the wasm value type the store op consumes. Non-BigInt
|
|
668
|
+
// stores ToNumber the value first (per SetViewValue) so a Symbol value
|
|
669
|
+
// raises a TypeError and a string value parses instead of truncating to 0.
|
|
670
|
+
let v
|
|
671
|
+
if (valType === 'i64') v = typed(['i64.reinterpret_f64', asF64(emit(val))], 'i64')
|
|
672
|
+
else if (valType === 'f64') v = asF64(toNumF64(val, emit(val)))
|
|
673
|
+
else if (valType === 'f32') v = typed(['f32.demote_f64', asF64(toNumF64(val, emit(val)))], 'f32')
|
|
674
|
+
else v = asI32(toNumF64(val, emit(val)))
|
|
675
|
+
|
|
676
|
+
if (size === 1) return fin([storeOp, addr, v])
|
|
677
|
+
|
|
678
|
+
// For BE we byte-swap the integer payload; floats route through bitcast i↔f.
|
|
679
|
+
const swap = (iVal) => size === 2 ? bswap16I32(iVal) : size === 4 ? bswap32I32(iVal) : bswap64I64(iVal)
|
|
680
|
+
const beStore = () => {
|
|
681
|
+
if (valType === 'f32') {
|
|
682
|
+
const swapped = typed(['f32.reinterpret_i32', swap(typed(['i32.reinterpret_f32', v], 'i32'))], 'f32')
|
|
683
|
+
return [storeOp, addr, swapped]
|
|
684
|
+
}
|
|
685
|
+
if (valType === 'f64') {
|
|
686
|
+
const swapped = typed(['f64.reinterpret_i64', bswap64I64(typed(['i64.reinterpret_f64', v], 'i64'))], 'f64')
|
|
687
|
+
return [storeOp, addr, swapped]
|
|
688
|
+
}
|
|
689
|
+
return [storeOp, addr, swap(v)]
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const le = staticLE(leNode)
|
|
693
|
+
if (le === true) return fin([storeOp, addr, v])
|
|
694
|
+
if (le === false) return fin(beStore())
|
|
695
|
+
// Dynamic: hoist value + addr into temps so each branch can re-use.
|
|
696
|
+
const aT = tempI32('dvsa')
|
|
697
|
+
const vLoc = valType === 'i64' ? tempI64('dvsv') : valType === 'i32' ? tempI32('dvsv') : temp('dvsv')
|
|
698
|
+
// Re-declare with correct type for f32 (temp() defaults to f64).
|
|
699
|
+
if (valType === 'f32') ctx.func.locals.set(vLoc, 'f32')
|
|
700
|
+
const leT = tempI32('dvsle')
|
|
701
|
+
return fin(['block',
|
|
702
|
+
['local.set', `$${aT}`, addr],
|
|
703
|
+
['local.set', `$${vLoc}`, v],
|
|
704
|
+
['local.set', `$${leT}`, truthyIR(emit(leNode))],
|
|
705
|
+
['if',
|
|
706
|
+
['local.get', `$${leT}`],
|
|
707
|
+
['then', [storeOp, ['local.get', `$${aT}`], typed(['local.get', `$${vLoc}`], valType)]],
|
|
708
|
+
['else', (() => {
|
|
709
|
+
const refV = typed(['local.get', `$${vLoc}`], valType)
|
|
710
|
+
if (valType === 'f32') return [storeOp, ['local.get', `$${aT}`], typed(['f32.reinterpret_i32', swap(typed(['i32.reinterpret_f32', refV], 'i32'))], 'f32')]
|
|
711
|
+
if (valType === 'f64') return [storeOp, ['local.get', `$${aT}`], typed(['f64.reinterpret_i64', bswap64I64(typed(['i64.reinterpret_f64', refV], 'i64'))], 'f64')]
|
|
712
|
+
return [storeOp, ['local.get', `$${aT}`], swap(refV)]
|
|
713
|
+
})()]]])
|
|
492
714
|
}
|
|
493
715
|
}
|
|
494
716
|
|
|
495
|
-
// DataView get methods: extract i32 offset, load value, return as f64
|
|
717
|
+
// DataView get methods: extract i32 offset, load value, optionally byte-swap, return as f64.
|
|
718
|
+
// 8-bit ops ignore the LE arg entirely (single byte).
|
|
496
719
|
const DV_GET = {
|
|
497
|
-
getInt8: ['i32.load8_s', 'i32'],
|
|
498
|
-
getInt16: ['i32.load16_s', 'i32'], getUint16: ['i32.load16_u', 'i32'],
|
|
499
|
-
getInt32: ['i32.load', 'i32'],
|
|
500
|
-
getFloat32: ['f32.load', 'f32'], getFloat64: ['f64.load', 'f64'],
|
|
501
|
-
getBigInt64: ['i64.load', 'i64'], getBigUint64: ['i64.load', 'i64'],
|
|
502
|
-
}
|
|
503
|
-
for (const [method, [loadOp, resultType]] of Object.entries(DV_GET)) {
|
|
504
|
-
ctx.core.emit[`.${method}`] = (dv, off,
|
|
505
|
-
|
|
506
|
-
const
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
720
|
+
getInt8: ['i32.load8_s', 'i32', 1, true], getUint8: ['i32.load8_u', 'i32', 1, false],
|
|
721
|
+
getInt16: ['i32.load16_s', 'i32', 2, true], getUint16: ['i32.load16_u', 'i32', 2, false],
|
|
722
|
+
getInt32: ['i32.load', 'i32', 4, true], getUint32: ['i32.load', 'i32', 4, false],
|
|
723
|
+
getFloat32: ['f32.load', 'f32', 4, false], getFloat64: ['f64.load', 'f64', 8, false],
|
|
724
|
+
getBigInt64: ['i64.load', 'i64', 8, true], getBigUint64: ['i64.load', 'i64', 8, false],
|
|
725
|
+
}
|
|
726
|
+
for (const [method, [loadOp, resultType, size, signed]] of Object.entries(DV_GET)) {
|
|
727
|
+
ctx.core.emit[`.${method}`] = (dv, off, leNode) => {
|
|
728
|
+
// Resolve the receiver's descriptor once; the load reads dataOff/byteLen from it.
|
|
729
|
+
const desc = tempI32('dvD')
|
|
730
|
+
const fin = (body) => typed(['block', ['result', 'f64'],
|
|
731
|
+
['local.set', `$${desc}`, dvDescriptor(dv)], asF64(body)], 'f64')
|
|
732
|
+
const addr = ['i32.add', dvDataOff(desc), dvIndexChecked(off, size, dvViewSize(desc))]
|
|
733
|
+
|
|
734
|
+
// Convert a wasm-typed raw value back into the f64 ABI return. Float reads
|
|
735
|
+
// canonicalize NaN (see __canon_nan) so downstream `v !== v` works.
|
|
736
|
+
const toF64 = (raw) => {
|
|
737
|
+
if (resultType === 'f64') return canonNaN(raw)
|
|
738
|
+
if (resultType === 'f32') return canonNaN(typed(['f64.promote_f32', raw], 'f64'))
|
|
739
|
+
if (resultType === 'i64') return typed(['f64.reinterpret_i64', raw], 'f64')
|
|
740
|
+
return typed(signed ? ['f64.convert_i32_s', raw] : ['f64.convert_i32_u', raw], 'f64')
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
if (size === 1) return fin(toF64(typed([loadOp, addr], resultType)))
|
|
744
|
+
|
|
745
|
+
// LE path: native wasm load is already little-endian.
|
|
746
|
+
// BE path: load as raw int (always little-endian on wasm), byte-swap, then
|
|
747
|
+
// reinterpret to the requested type so sign-extension matches.
|
|
748
|
+
// For unsigned 16-bit, bswap16 only spans the low half — no extra masking needed.
|
|
749
|
+
// For signed 16-bit BE, swap then sign-extend via `i32.extend16_s`.
|
|
750
|
+
const beLoad = () => {
|
|
751
|
+
if (size === 2) {
|
|
752
|
+
const rawU = bswap16I32(typed(['i32.load16_u', addr], 'i32'))
|
|
753
|
+
return toF64(typed(signed ? ['i32.extend16_s', rawU] : rawU, 'i32'))
|
|
754
|
+
}
|
|
755
|
+
if (size === 4) {
|
|
756
|
+
if (resultType === 'f32') {
|
|
757
|
+
return toF64(typed(['f32.reinterpret_i32', bswap32I32(typed(['i32.load', addr], 'i32'))], 'f32'))
|
|
758
|
+
}
|
|
759
|
+
return toF64(typed(bswap32I32(typed(['i32.load', addr], 'i32')), 'i32'))
|
|
760
|
+
}
|
|
761
|
+
// size === 8 (f64 or i64).
|
|
762
|
+
if (resultType === 'f64') {
|
|
763
|
+
return toF64(typed(['f64.reinterpret_i64', bswap64I64(typed(['i64.load', addr], 'i64'))], 'f64'))
|
|
764
|
+
}
|
|
765
|
+
return typed(['f64.reinterpret_i64', bswap64I64(typed(['i64.load', addr], 'i64'))], 'f64')
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const le = staticLE(leNode)
|
|
769
|
+
if (le === true) return fin(toF64(typed([loadOp, addr], resultType)))
|
|
770
|
+
if (le === false) return fin(beLoad())
|
|
771
|
+
// Dynamic LE: hoist addr, branch on leNode at runtime.
|
|
772
|
+
const aT = tempI32('dvga')
|
|
773
|
+
const leT = tempI32('dvgle')
|
|
774
|
+
return fin(typed(['block', ['result', 'f64'],
|
|
775
|
+
['local.set', `$${aT}`, addr],
|
|
776
|
+
['local.set', `$${leT}`, truthyIR(emit(leNode))],
|
|
777
|
+
['if', ['result', 'f64'],
|
|
778
|
+
['local.get', `$${leT}`],
|
|
779
|
+
['then', asF64(toF64(typed([loadOp, ['local.get', `$${aT}`]], resultType)))],
|
|
780
|
+
['else', asF64((() => {
|
|
781
|
+
// Replicate beLoad but using hoisted addr.
|
|
782
|
+
const a = ['local.get', `$${aT}`]
|
|
783
|
+
if (size === 2) {
|
|
784
|
+
const rawU = bswap16I32(typed(['i32.load16_u', a], 'i32'))
|
|
785
|
+
return toF64(typed(signed ? ['i32.extend16_s', rawU] : rawU, 'i32'))
|
|
786
|
+
}
|
|
787
|
+
if (size === 4) {
|
|
788
|
+
if (resultType === 'f32') {
|
|
789
|
+
return toF64(typed(['f32.reinterpret_i32', bswap32I32(typed(['i32.load', a], 'i32'))], 'f32'))
|
|
790
|
+
}
|
|
791
|
+
return toF64(typed(bswap32I32(typed(['i32.load', a], 'i32')), 'i32'))
|
|
792
|
+
}
|
|
793
|
+
if (resultType === 'f64') {
|
|
794
|
+
return toF64(typed(['f64.reinterpret_i64', bswap64I64(typed(['i64.load', a], 'i64'))], 'f64'))
|
|
795
|
+
}
|
|
796
|
+
return typed(['f64.reinterpret_i64', bswap64I64(typed(['i64.load', a], 'i64'))], 'f64')
|
|
797
|
+
})())]]], 'f64'))
|
|
511
798
|
}
|
|
512
799
|
}
|
|
513
800
|
|
|
@@ -550,13 +837,27 @@ export default (ctx) => {
|
|
|
550
837
|
|
|
551
838
|
// .length handled by ptr.js's __len (reads from memory header [-8:len])
|
|
552
839
|
|
|
553
|
-
/** Resolve element type + view-ness for a known TypedArray
|
|
554
|
-
* Returns { et, isView, isBigInt } or null.
|
|
840
|
+
/** Resolve element type + view-ness for a known TypedArray expression.
|
|
841
|
+
* Returns { et, isView, isBigInt } or null. Handles:
|
|
842
|
+
* - bare binding: `xs` (in typedElem)
|
|
843
|
+
* - element-preserving method chain: `xs.filter(...)` / `xs.map(...)` /
|
|
844
|
+
* `xs.slice(...)` — walks back to the root binding. View-ness clears
|
|
845
|
+
* at the chain output (the result is always an owned typed array). */
|
|
846
|
+
const TYPED_CHAIN_METHODS = new Set(['map', 'filter', 'slice'])
|
|
555
847
|
const resolveElem = (arr) => {
|
|
556
|
-
|
|
848
|
+
let receiver = arr, chainOutput = false
|
|
849
|
+
// Walk method-call chain inward. `arr.method(...)` parses as
|
|
850
|
+
// ['()', ['.', recv, 'method'], ...args] — peel until we hit a name.
|
|
851
|
+
while (Array.isArray(receiver) && receiver[0] === '()' &&
|
|
852
|
+
Array.isArray(receiver[1]) && receiver[1][0] === '.' &&
|
|
853
|
+
TYPED_CHAIN_METHODS.has(receiver[1][2])) {
|
|
854
|
+
receiver = receiver[1][1]
|
|
855
|
+
chainOutput = true
|
|
856
|
+
}
|
|
857
|
+
const ctor = typeof receiver === 'string' && ctx.types.typedElem?.get(receiver)
|
|
557
858
|
if (!ctor) return null
|
|
558
|
-
const isView = ctor.endsWith('.view')
|
|
559
|
-
const name =
|
|
859
|
+
const isView = !chainOutput && ctor.endsWith('.view')
|
|
860
|
+
const name = ctor.endsWith('.view') ? ctor.slice(4, -5) : ctor.slice(4)
|
|
560
861
|
const et = ELEM[name]
|
|
561
862
|
return et == null ? null : { et, isView, isBigInt: name === 'BigInt64Array' || name === 'BigUint64Array' }
|
|
562
863
|
}
|
|
@@ -782,11 +1083,22 @@ export default (ctx) => {
|
|
|
782
1083
|
|
|
783
1084
|
// .map() on TypedArrays — SIMD auto-vectorization when pattern detected
|
|
784
1085
|
ctx.core.emit['.typed:map'] = (arr, fn) => {
|
|
785
|
-
// Resolve element type + view-ness
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
const
|
|
789
|
-
const elemType =
|
|
1086
|
+
// Resolve element type + view-ness. `resolveElem` handles bare bindings AND
|
|
1087
|
+
// chained method receivers (`xs.filter(…).map(…)`) by walking back to the
|
|
1088
|
+
// root typedElem-tracked binding.
|
|
1089
|
+
const r = resolveElem(arr)
|
|
1090
|
+
const elemType = r?.et
|
|
1091
|
+
const isView = r?.isView
|
|
1092
|
+
const elemName = r && (r.isBigInt
|
|
1093
|
+
? (elemType === 7 ? 'BigInt64Array' : 'BigUint64Array') // unreachable: SIMD path gated below
|
|
1094
|
+
: ['Int8Array','Uint8Array','Int16Array','Uint16Array','Int32Array','Uint32Array','Float32Array','Float64Array'][elemType])
|
|
1095
|
+
|
|
1096
|
+
// BigInt typed arrays: SIMD path doesn't support them; defer to scalar map.
|
|
1097
|
+
if (r?.isBigInt) {
|
|
1098
|
+
// Fall through to generic .map below — keeps i64-via-NaN-box semantics intact.
|
|
1099
|
+
if (ctx.core.emit['.map']) return ctx.core.emit['.map'](arr, fn)
|
|
1100
|
+
return null
|
|
1101
|
+
}
|
|
790
1102
|
|
|
791
1103
|
// Try SIMD: inline arrow with recognizable pattern
|
|
792
1104
|
if (elemType != null && Array.isArray(fn) && fn[0] === '=>') {
|
|
@@ -846,4 +1158,309 @@ export default (ctx) => {
|
|
|
846
1158
|
if (ctx.core.emit['.map']) return ctx.core.emit['.map'](arr, fn)
|
|
847
1159
|
return null
|
|
848
1160
|
}
|
|
1161
|
+
|
|
1162
|
+
// === Shared typed iteration core ===
|
|
1163
|
+
//
|
|
1164
|
+
// typedLoop(arr, bodyFn, opts?) emits the common shape every typed iteration
|
|
1165
|
+
// method needs: resolve elemType from the receiver's tracked ctor, set up
|
|
1166
|
+
// (ptr, len, i) locals, walk i in [0, len), per iteration load arr[i] as f64
|
|
1167
|
+
// and pass it to bodyFn. Returns the IR setup statements. Returns null if the
|
|
1168
|
+
// element type can't be resolved — callers fall back to the generic array
|
|
1169
|
+
// emitter.
|
|
1170
|
+
//
|
|
1171
|
+
// bodyFn(loadElem, i, len, ptr, exitLabel) returns IR statements. loadElem is
|
|
1172
|
+
// a function (called lazily so it isn't materialized for unused-item paths)
|
|
1173
|
+
// returning f64 IR for the current element. `exitLabel` lets the body break
|
|
1174
|
+
// out of the loop (e.g. `.find` after a hit, `.some`/`.every` on early
|
|
1175
|
+
// resolution). `i`/`len`/`ptr` are i32 local-name strings.
|
|
1176
|
+
const typedLoop = (arr, bodyFn) => {
|
|
1177
|
+
const r = resolveElem(arr)
|
|
1178
|
+
if (!r) return null
|
|
1179
|
+
const { et, isView, isBigInt } = r
|
|
1180
|
+
if (isBigInt) return null // BigInt: defer to generic .map (returns BigInts via f64-bits NaN-box)
|
|
1181
|
+
const va = emit(arr)
|
|
1182
|
+
const len = tempI32('tll'), ptr = tempI32('tlp'), i = tempI32('tli')
|
|
1183
|
+
const id = ctx.func.uniq++
|
|
1184
|
+
const exit = `$brk${id}`
|
|
1185
|
+
inc('__len')
|
|
1186
|
+
const loadElem = () => {
|
|
1187
|
+
const off = ['i32.add', ['local.get', `$${ptr}`], ['i32.shl', ['local.get', `$${i}`], ['i32.const', SHIFT[et]]]]
|
|
1188
|
+
if (et === 7) return typed(['f64.load', off], 'f64')
|
|
1189
|
+
if (et === 6) return typed(['f64.promote_f32', ['f32.load', off]], 'f64')
|
|
1190
|
+
return typed([(et & 1) ? 'f64.convert_i32_u' : 'f64.convert_i32_s', [LOAD[et], off]], 'f64')
|
|
1191
|
+
}
|
|
1192
|
+
const setup = [
|
|
1193
|
+
['local.set', `$${ptr}`, typedDataAddr(asF64(va), isView)],
|
|
1194
|
+
['local.set', `$${len}`, ['call', '$__len', ['i64.reinterpret_f64', asF64(va)]]],
|
|
1195
|
+
['local.set', `$${i}`, ['i32.const', 0]],
|
|
1196
|
+
['block', exit, ['loop', `$loop${id}`,
|
|
1197
|
+
['br_if', exit, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${len}`]]],
|
|
1198
|
+
...bodyFn(loadElem, i, len, ptr, exit),
|
|
1199
|
+
['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
|
|
1200
|
+
['br', `$loop${id}`]]]]
|
|
1201
|
+
return { setup, ptr, len, i, exit, et, isView, loadElem }
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// === Typed iteration emitters ===
|
|
1205
|
+
//
|
|
1206
|
+
// Each emitter dispatches via the `.typed:method` key on `ctx.core.emit`.
|
|
1207
|
+
// emit.js:2211 picks `.typed:<m>` over `.${m}` when the receiver's val type
|
|
1208
|
+
// is VAL.TYPED ('typed'), so a user-visible `arr.forEach(...)` on a tracked
|
|
1209
|
+
// typed-array binding routes here automatically.
|
|
1210
|
+
//
|
|
1211
|
+
// Calling convention: callbacks receive (item, idx) — matches array.js
|
|
1212
|
+
// (omits the `arr` arg that array-method spec passes; jz keeps the closure
|
|
1213
|
+
// ABI width at 2 to spare a slot across the whole program). Reduce passes
|
|
1214
|
+
// (acc, item). Closure invocation goes through `ctx.closure.call` directly.
|
|
1215
|
+
// The element-type-name list is needed by allocPtr for typedAux:
|
|
1216
|
+
const ET_NAME = ['Int8Array','Uint8Array','Int16Array','Uint16Array','Int32Array','Uint32Array','Float32Array','Float64Array']
|
|
1217
|
+
|
|
1218
|
+
// .forEach: callback (item, idx). Result is 0 to match array.js's
|
|
1219
|
+
// convention (spec says undefined; both modules pick 0 since f() exposes the
|
|
1220
|
+
// result as f64 and NaN-boxed undef reads as NaN).
|
|
1221
|
+
// Pre-allocate locals BEFORE typedLoop — bodyFn captures `cbLoc` by closure;
|
|
1222
|
+
// TDZ would fire if we declared it after.
|
|
1223
|
+
ctx.core.emit['.typed:forEach'] = (arr, fn) => {
|
|
1224
|
+
const cbLoc = temp('tfc')
|
|
1225
|
+
const loop = typedLoop(arr, (load, i) => [
|
|
1226
|
+
['drop', asF64(ctx.closure.call(
|
|
1227
|
+
typed(['local.get', `$${cbLoc}`], 'f64'),
|
|
1228
|
+
[load(), typed(['f64.convert_i32_s', ['local.get', `$${i}`]], 'f64')]))]
|
|
1229
|
+
])
|
|
1230
|
+
if (!loop) return null
|
|
1231
|
+
return typed(['block', ['result', 'f64'],
|
|
1232
|
+
['local.set', `$${cbLoc}`, asF64(emit(fn))],
|
|
1233
|
+
...loop.setup,
|
|
1234
|
+
['f64.const', 0]], 'f64')
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
// .reduce: callback (acc, item) → acc. Without init, slot 0 seeds acc and
|
|
1238
|
+
// the callback skips on iteration 0. Matches JS semantics; the (idx, arr)
|
|
1239
|
+
// callback args are dropped (consistent with array.js).
|
|
1240
|
+
ctx.core.emit['.typed:reduce'] = (arr, fn, init) => {
|
|
1241
|
+
const cbLoc = temp('trc'), acc = temp('trv'), seeded = init !== undefined
|
|
1242
|
+
const loop = typedLoop(arr, (load, i) => {
|
|
1243
|
+
const step = ['local.set', `$${acc}`, asF64(ctx.closure.call(
|
|
1244
|
+
typed(['local.get', `$${cbLoc}`], 'f64'),
|
|
1245
|
+
[typed(['local.get', `$${acc}`], 'f64'), load()]))]
|
|
1246
|
+
if (seeded) return [step]
|
|
1247
|
+
// Unseeded: iteration 0 just stashes the value into acc.
|
|
1248
|
+
return [
|
|
1249
|
+
['if', ['i32.eqz', ['local.get', `$${i}`]],
|
|
1250
|
+
['then', ['local.set', `$${acc}`, load()]],
|
|
1251
|
+
['else', step]]]
|
|
1252
|
+
})
|
|
1253
|
+
if (!loop) return null
|
|
1254
|
+
return typed(['block', ['result', 'f64'],
|
|
1255
|
+
['local.set', `$${cbLoc}`, asF64(emit(fn))],
|
|
1256
|
+
['local.set', `$${acc}`, seeded ? asF64(emit(init)) : undefExpr()],
|
|
1257
|
+
...loop.setup,
|
|
1258
|
+
['local.get', `$${acc}`]], 'f64')
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// .indexOf: scalar value-equality search. Returns -1 on miss. Compare on f64
|
|
1262
|
+
// — Array.prototype.indexOf uses strict equality (NaN ≠ NaN).
|
|
1263
|
+
ctx.core.emit['.typed:indexOf'] = (arr, val) => {
|
|
1264
|
+
const found = tempI32('tif'), needle = temp('tin')
|
|
1265
|
+
const loop = typedLoop(arr, (load, i, _len, _ptr, exit) => [
|
|
1266
|
+
['if', ['f64.eq', load(), ['local.get', `$${needle}`]],
|
|
1267
|
+
['then',
|
|
1268
|
+
['local.set', `$${found}`, ['local.get', `$${i}`]],
|
|
1269
|
+
['br', exit]]]
|
|
1270
|
+
])
|
|
1271
|
+
if (!loop) return null
|
|
1272
|
+
return typed(['block', ['result', 'f64'],
|
|
1273
|
+
['local.set', `$${needle}`, asF64(emit(val))],
|
|
1274
|
+
['local.set', `$${found}`, ['i32.const', -1]],
|
|
1275
|
+
...loop.setup,
|
|
1276
|
+
['f64.convert_i32_s', ['local.get', `$${found}`]]], 'f64')
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// .includes: like indexOf but NaN-equal-NaN (JS spec). Stash needle bits as
|
|
1280
|
+
// i64 and compare via i64.eq so two NaNs with matching bit patterns match
|
|
1281
|
+
// (f64.eq would say false).
|
|
1282
|
+
ctx.core.emit['.typed:includes'] = (arr, val) => {
|
|
1283
|
+
const found = tempI32('thf'), needle = temp('thn')
|
|
1284
|
+
const loop = typedLoop(arr, (load, _i, _len, _ptr, exit) => [
|
|
1285
|
+
['if',
|
|
1286
|
+
['i32.or',
|
|
1287
|
+
['f64.eq', load(), ['local.get', `$${needle}`]],
|
|
1288
|
+
['i64.eq',
|
|
1289
|
+
['i64.reinterpret_f64', load()],
|
|
1290
|
+
['i64.reinterpret_f64', ['local.get', `$${needle}`]]]],
|
|
1291
|
+
['then', ['local.set', `$${found}`, ['i32.const', 1]], ['br', exit]]]
|
|
1292
|
+
])
|
|
1293
|
+
if (!loop) return null
|
|
1294
|
+
return typed(['block', ['result', 'f64'],
|
|
1295
|
+
['local.set', `$${needle}`, asF64(emit(val))],
|
|
1296
|
+
['local.set', `$${found}`, ['i32.const', 0]],
|
|
1297
|
+
...loop.setup,
|
|
1298
|
+
['f64.convert_i32_s', ['local.get', `$${found}`]]], 'f64')
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
// .find / .findIndex: linear scan, first truthy callback wins. Miss returns
|
|
1302
|
+
// undefined / -1 respectively.
|
|
1303
|
+
const findCommon = (arr, fn, returnIndex) => {
|
|
1304
|
+
const cbLoc = temp('tfc'), result = temp('tfr'), foundIdx = tempI32('tfi')
|
|
1305
|
+
const loop = typedLoop(arr, (load, i, _len, _ptr, exit) => {
|
|
1306
|
+
const itemLoc = temp('tfit')
|
|
1307
|
+
return [
|
|
1308
|
+
['local.set', `$${itemLoc}`, load()],
|
|
1309
|
+
['if', truthyIR(ctx.closure.call(
|
|
1310
|
+
typed(['local.get', `$${cbLoc}`], 'f64'),
|
|
1311
|
+
[typed(['local.get', `$${itemLoc}`], 'f64'),
|
|
1312
|
+
typed(['f64.convert_i32_s', ['local.get', `$${i}`]], 'f64')])),
|
|
1313
|
+
['then',
|
|
1314
|
+
returnIndex
|
|
1315
|
+
? ['local.set', `$${foundIdx}`, ['local.get', `$${i}`]]
|
|
1316
|
+
: ['local.set', `$${result}`, typed(['local.get', `$${itemLoc}`], 'f64')],
|
|
1317
|
+
['br', exit]]]
|
|
1318
|
+
]
|
|
1319
|
+
})
|
|
1320
|
+
if (!loop) return null
|
|
1321
|
+
return typed(['block', ['result', 'f64'],
|
|
1322
|
+
['local.set', `$${cbLoc}`, asF64(emit(fn))],
|
|
1323
|
+
returnIndex
|
|
1324
|
+
? ['local.set', `$${foundIdx}`, ['i32.const', -1]]
|
|
1325
|
+
: ['local.set', `$${result}`, undefExpr()],
|
|
1326
|
+
...loop.setup,
|
|
1327
|
+
returnIndex
|
|
1328
|
+
? typed(['f64.convert_i32_s', ['local.get', `$${foundIdx}`]], 'f64')
|
|
1329
|
+
: typed(['local.get', `$${result}`], 'f64')], 'f64')
|
|
1330
|
+
}
|
|
1331
|
+
ctx.core.emit['.typed:find'] = (arr, fn) => findCommon(arr, fn, false)
|
|
1332
|
+
ctx.core.emit['.typed:findIndex'] = (arr, fn) => findCommon(arr, fn, true)
|
|
1333
|
+
|
|
1334
|
+
// .some / .every: short-circuit boolean reduction. some=∃, every=∀.
|
|
1335
|
+
const anyAllCommon = (arr, fn, isEvery) => {
|
|
1336
|
+
const cbLoc = temp('tac'), result = tempI32('tar')
|
|
1337
|
+
const loop = typedLoop(arr, (load, i, _len, _ptr, exit) => {
|
|
1338
|
+
const test = truthyIR(ctx.closure.call(
|
|
1339
|
+
typed(['local.get', `$${cbLoc}`], 'f64'),
|
|
1340
|
+
[load(), typed(['f64.convert_i32_s', ['local.get', `$${i}`]], 'f64')]))
|
|
1341
|
+
// every: exit on falsy with result=0. some: exit on truthy with result=1.
|
|
1342
|
+
return [
|
|
1343
|
+
['if', isEvery ? ['i32.eqz', test] : test,
|
|
1344
|
+
['then', ['local.set', `$${result}`, ['i32.const', isEvery ? 0 : 1]], ['br', exit]]]
|
|
1345
|
+
]
|
|
1346
|
+
})
|
|
1347
|
+
if (!loop) return null
|
|
1348
|
+
return typed(['block', ['result', 'f64'],
|
|
1349
|
+
['local.set', `$${cbLoc}`, asF64(emit(fn))],
|
|
1350
|
+
['local.set', `$${result}`, ['i32.const', isEvery ? 1 : 0]],
|
|
1351
|
+
...loop.setup,
|
|
1352
|
+
['f64.convert_i32_s', ['local.get', `$${result}`]]], 'f64')
|
|
1353
|
+
}
|
|
1354
|
+
ctx.core.emit['.typed:some'] = (arr, fn) => anyAllCommon(arr, fn, false)
|
|
1355
|
+
ctx.core.emit['.typed:every'] = (arr, fn) => anyAllCommon(arr, fn, true)
|
|
1356
|
+
|
|
1357
|
+
// .filter: produces a TYPED array of the same element type. Allocates worst-
|
|
1358
|
+
// case (len slots) then patches the byte-count header at the end with the
|
|
1359
|
+
// actual passed count. Mirrors .filter in array.js but with typed-aware
|
|
1360
|
+
// load/store.
|
|
1361
|
+
ctx.core.emit['.typed:filter'] = (arr, fn) => {
|
|
1362
|
+
const r = resolveElem(arr)
|
|
1363
|
+
if (!r || r.isBigInt) return null
|
|
1364
|
+
const { et, isView } = r
|
|
1365
|
+
const cbLoc = temp('tfc'), arrLoc = temp('tfa')
|
|
1366
|
+
const count = tempI32('tfn'), maxLen = tempI32('tfm')
|
|
1367
|
+
const srcPtr = tempI32('tfsp'), srcLen = tempI32('tfsl'), srci = tempI32('tfi')
|
|
1368
|
+
inc('__len')
|
|
1369
|
+
const loadAt = (ptrLoc, iLoc) => {
|
|
1370
|
+
const off = ['i32.add', ['local.get', `$${ptrLoc}`], ['i32.shl', ['local.get', `$${iLoc}`], ['i32.const', SHIFT[et]]]]
|
|
1371
|
+
if (et === 7) return typed(['f64.load', off], 'f64')
|
|
1372
|
+
if (et === 6) return typed(['f64.promote_f32', ['f32.load', off]], 'f64')
|
|
1373
|
+
return typed([(et & 1) ? 'f64.convert_i32_u' : 'f64.convert_i32_s', [LOAD[et], off]], 'f64')
|
|
1374
|
+
}
|
|
1375
|
+
const storeAt = (ptrLoc, iLoc, valF64) => {
|
|
1376
|
+
const off = ['i32.add', ['local.get', `$${ptrLoc}`], ['i32.shl', ['local.get', `$${iLoc}`], ['i32.const', SHIFT[et]]]]
|
|
1377
|
+
if (et === 7) return ['f64.store', off, valF64]
|
|
1378
|
+
if (et === 6) return ['f32.store', off, ['f32.demote_f64', valF64]]
|
|
1379
|
+
return [STORE[et], off, [(et & 1) ? 'i32.trunc_f64_u' : 'i32.trunc_f64_s', valF64]]
|
|
1380
|
+
}
|
|
1381
|
+
const dst = allocPtr({ type: PTR.TYPED, aux: typedAux(ET_NAME[et]),
|
|
1382
|
+
len: ['i32.shl', ['local.get', `$${maxLen}`], ['i32.const', SHIFT[et]]],
|
|
1383
|
+
stride: 1, tag: 'tfd' })
|
|
1384
|
+
const id = ctx.func.uniq++
|
|
1385
|
+
const passes = truthyIR(ctx.closure.call(
|
|
1386
|
+
typed(['local.get', `$${cbLoc}`], 'f64'),
|
|
1387
|
+
[loadAt(srcPtr, srci), typed(['f64.convert_i32_s', ['local.get', `$${srci}`]], 'f64')]))
|
|
1388
|
+
return typed(['block', ['result', 'f64'],
|
|
1389
|
+
['local.set', `$${cbLoc}`, asF64(emit(fn))],
|
|
1390
|
+
['local.set', `$${arrLoc}`, asF64(emit(arr))],
|
|
1391
|
+
['local.set', `$${srcPtr}`, typedDataAddr(typed(['local.get', `$${arrLoc}`], 'f64'), isView)],
|
|
1392
|
+
['local.set', `$${srcLen}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${arrLoc}`]]]],
|
|
1393
|
+
['local.set', `$${maxLen}`, ['local.get', `$${srcLen}`]],
|
|
1394
|
+
dst.init,
|
|
1395
|
+
['local.set', `$${count}`, ['i32.const', 0]],
|
|
1396
|
+
['local.set', `$${srci}`, ['i32.const', 0]],
|
|
1397
|
+
['block', `$brk${id}`, ['loop', `$loop${id}`,
|
|
1398
|
+
['br_if', `$brk${id}`, ['i32.ge_s', ['local.get', `$${srci}`], ['local.get', `$${srcLen}`]]],
|
|
1399
|
+
['if', passes,
|
|
1400
|
+
['then',
|
|
1401
|
+
storeAt(dst.local, count, loadAt(srcPtr, srci)),
|
|
1402
|
+
['local.set', `$${count}`, ['i32.add', ['local.get', `$${count}`], ['i32.const', 1]]]]],
|
|
1403
|
+
['local.set', `$${srci}`, ['i32.add', ['local.get', `$${srci}`], ['i32.const', 1]]],
|
|
1404
|
+
['br', `$loop${id}`]]],
|
|
1405
|
+
// Patch byte-count header — __len reads from -8 then __typed_shift's by
|
|
1406
|
+
// the typed-elem stride, so we store `count * stride` bytes here.
|
|
1407
|
+
['i32.store', ['i32.sub', ['local.get', `$${dst.local}`], ['i32.const', 8]],
|
|
1408
|
+
['i32.shl', ['local.get', `$${count}`], ['i32.const', SHIFT[et]]]],
|
|
1409
|
+
dst.ptr], 'f64')
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
// .slice(start, end): produces TYPED array of same element type. Mirrors JS
|
|
1413
|
+
// semantics — negative indices wrap from end, out-of-range clamps to len.
|
|
1414
|
+
// Bulk copy via `memory.copy`.
|
|
1415
|
+
ctx.core.emit['.typed:slice'] = (arr, start, end) => {
|
|
1416
|
+
const r = resolveElem(arr)
|
|
1417
|
+
if (!r || r.isBigInt) return null
|
|
1418
|
+
const { et, isView } = r
|
|
1419
|
+
const arrLoc = temp('tsa'), srcPtr = tempI32('tssp'), srcLen = tempI32('tssl')
|
|
1420
|
+
const lo = tempI32('tslo'), hi = tempI32('tshi'), n = tempI32('tsn')
|
|
1421
|
+
inc('__len')
|
|
1422
|
+
// ECMAScript ToInteger + clamp: idx := bound; idx := idx<0 ? max(0,idx+len) : min(len,idx).
|
|
1423
|
+
// Select-based to dodge nested if-arity rules.
|
|
1424
|
+
// defaultExpr: when boundExpr is omitted (e.g. arr.slice() / arr.slice(2)),
|
|
1425
|
+
// start defaults to 0 and end defaults to len.
|
|
1426
|
+
const clamp = (boundExpr, fallback, defaultExpr) => {
|
|
1427
|
+
if (boundExpr == null) return defaultExpr
|
|
1428
|
+
const idx = tempI32(fallback)
|
|
1429
|
+
return ['block', ['result', 'i32'],
|
|
1430
|
+
['local.set', `$${idx}`, asI32(emit(boundExpr))],
|
|
1431
|
+
['select',
|
|
1432
|
+
// negative branch: max(0, idx + len)
|
|
1433
|
+
['select',
|
|
1434
|
+
['i32.add', ['local.get', `$${idx}`], ['local.get', `$${srcLen}`]],
|
|
1435
|
+
['i32.const', 0],
|
|
1436
|
+
['i32.gt_s', ['i32.add', ['local.get', `$${idx}`], ['local.get', `$${srcLen}`]], ['i32.const', 0]]],
|
|
1437
|
+
// non-negative branch: min(len, idx)
|
|
1438
|
+
['select',
|
|
1439
|
+
['local.get', `$${srcLen}`],
|
|
1440
|
+
['local.get', `$${idx}`],
|
|
1441
|
+
['i32.gt_s', ['local.get', `$${idx}`], ['local.get', `$${srcLen}`]]],
|
|
1442
|
+
['i32.lt_s', ['local.get', `$${idx}`], ['i32.const', 0]]]]
|
|
1443
|
+
}
|
|
1444
|
+
const dst = allocPtr({ type: PTR.TYPED, aux: typedAux(ET_NAME[et]),
|
|
1445
|
+
len: ['i32.shl', ['local.get', `$${n}`], ['i32.const', SHIFT[et]]],
|
|
1446
|
+
stride: 1, tag: 'tsd' })
|
|
1447
|
+
return typed(['block', ['result', 'f64'],
|
|
1448
|
+
['local.set', `$${arrLoc}`, asF64(emit(arr))],
|
|
1449
|
+
['local.set', `$${srcPtr}`, typedDataAddr(typed(['local.get', `$${arrLoc}`], 'f64'), isView)],
|
|
1450
|
+
['local.set', `$${srcLen}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${arrLoc}`]]]],
|
|
1451
|
+
['local.set', `$${lo}`, clamp(start, 'tslo2', ['i32.const', 0])],
|
|
1452
|
+
['local.set', `$${hi}`, clamp(end, 'tshi2', ['local.get', `$${srcLen}`])],
|
|
1453
|
+
['local.set', `$${n}`,
|
|
1454
|
+
['select',
|
|
1455
|
+
['i32.sub', ['local.get', `$${hi}`], ['local.get', `$${lo}`]],
|
|
1456
|
+
['i32.const', 0],
|
|
1457
|
+
['i32.gt_s', ['local.get', `$${hi}`], ['local.get', `$${lo}`]]]],
|
|
1458
|
+
dst.init,
|
|
1459
|
+
// memory.copy dst, src+lo*stride, n*stride
|
|
1460
|
+
['memory.copy',
|
|
1461
|
+
['local.get', `$${dst.local}`],
|
|
1462
|
+
['i32.add', ['local.get', `$${srcPtr}`], ['i32.shl', ['local.get', `$${lo}`], ['i32.const', SHIFT[et]]]],
|
|
1463
|
+
['i32.shl', ['local.get', `$${n}`], ['i32.const', SHIFT[et]]]],
|
|
1464
|
+
dst.ptr], 'f64')
|
|
1465
|
+
}
|
|
849
1466
|
}
|