jz 0.6.0 → 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.
Files changed (49) hide show
  1. package/README.md +66 -43
  2. package/bench/README.md +128 -78
  3. package/bench/bench.svg +32 -42
  4. package/cli.js +73 -7
  5. package/dist/interop.js +1 -0
  6. package/dist/jz.js +6024 -0
  7. package/index.d.ts +126 -0
  8. package/index.js +190 -34
  9. package/interop.js +71 -27
  10. package/layout.js +5 -0
  11. package/module/array.js +71 -39
  12. package/module/collection.js +41 -5
  13. package/module/core.js +2 -4
  14. package/module/math.js +138 -2
  15. package/module/number.js +21 -0
  16. package/module/object.js +26 -0
  17. package/module/simd.js +37 -5
  18. package/module/string.js +41 -12
  19. package/module/typedarray.js +415 -26
  20. package/package.json +21 -6
  21. package/src/autoload.js +3 -0
  22. package/src/compile/analyze-scans.js +174 -11
  23. package/src/compile/analyze.js +38 -3
  24. package/src/compile/emit-assign.js +9 -6
  25. package/src/compile/emit.js +347 -36
  26. package/src/compile/index.js +307 -29
  27. package/src/compile/infer.js +36 -1
  28. package/src/compile/loop-divmod.js +155 -0
  29. package/src/compile/narrow.js +108 -11
  30. package/src/compile/peel-stencil.js +261 -0
  31. package/src/compile/plan/advise.js +55 -1
  32. package/src/compile/plan/index.js +4 -0
  33. package/src/compile/plan/inline.js +5 -2
  34. package/src/compile/plan/literals.js +220 -5
  35. package/src/compile/plan/scope.js +115 -39
  36. package/src/compile/program-facts.js +21 -2
  37. package/src/ctx.js +45 -7
  38. package/src/ir.js +55 -7
  39. package/src/kind-traits.js +27 -0
  40. package/src/kind.js +65 -3
  41. package/src/optimize/index.js +344 -45
  42. package/src/optimize/vectorize.js +2968 -182
  43. package/src/prepare/index.js +64 -7
  44. package/src/prepare/lift-iife.js +149 -0
  45. package/src/reps.js +3 -2
  46. package/src/static.js +9 -0
  47. package/src/type.js +10 -6
  48. package/src/wat/assemble.js +195 -13
  49. package/src/wat/optimize.js +363 -185
package/layout.js CHANGED
@@ -41,6 +41,11 @@ export const PTR = {
41
41
  /** Reserved atom aux ids (PTR.ATOM). */
42
42
  export const ATOM = { NULL: 1, UNDEF: 2, FALSE: 4, TRUE: 5 }
43
43
 
44
+ /** Tags whose heap block can relocate on growth (ARRAY/HASH/SET/MAP) — leaving a
45
+ * forwarding header that `__ptr_offset` must follow. `(1 << tag) & FORWARDING_MASK`
46
+ * tests membership in one shl+and, replacing a 4-way tag-equality OR. */
47
+ export const FORWARDING_MASK = (1 << PTR.ARRAY) | (1 << PTR.HASH) | (1 << PTR.SET) | (1 << PTR.MAP)
48
+
44
49
  // =============================================================================
45
50
  // PTR.TYPED element-type aux codec — which typed-array flavor lives in the aux
46
51
  // field of a PTR.TYPED box. Pure (no compiler state) → lives with the NaN-box
package/module/array.js CHANGED
@@ -10,13 +10,13 @@
10
10
 
11
11
  import { typed, asF64, asI64, asI32, NULL_NAN, UNDEF_NAN, temp, tempI32, allocPtr, multiCount, arrayLoop, elemLoad, elemStore, truthyIR, extractF64Bits, appendStaticSlots, mkPtrIR, slotAddr, isLiteralStr, resolveValType, undefExpr, ptrTypeEq } from '../src/ir.js'
12
12
  import { inBoundsArrIdx } from '../src/type.js'
13
- import { emit, spread, deps } from '../src/bridge.js'
13
+ import { emit, spread, deps, idx as emitIndex } from '../src/bridge.js'
14
14
  import { valTypeOf } from '../src/kind.js'
15
15
  import { extractParams, classifyParam, ASSIGN_OPS, refsName, REFS_IN_EXPR } from '../src/ast.js'
16
- import { staticPropertyKey, staticObjectProps, inlineArraySid } from '../src/static.js'
16
+ import { staticPropertyKey, staticObjectProps, inlineArraySid, staticIndexKey } from '../src/static.js'
17
17
  import { VAL, lookupValType, lookupNotString, updateRep } from '../src/reps.js'
18
18
  import { structInline } from '../src/abi/index.js'
19
- import { ctx, inc, err, PTR, LAYOUT, followForwardingWat } from '../src/ctx.js'
19
+ import { ctx, inc, err, warnDeopt, PTR, LAYOUT, followForwardingWat } from '../src/ctx.js'
20
20
  import { strHashLiteral } from './collection.js'
21
21
 
22
22
 
@@ -33,7 +33,7 @@ function allocArray(len, cap) {
33
33
  * only an 8-byte header left off-16 pointing at adjacent data-segment bytes, so
34
34
  * for-in / named-prop lookup (which read off-16 as the props-sidecar pointer)
35
35
  * walked garbage → OOB (test262 built-ins/Object/keys sparse-array). */
36
- function staticArrayPtr(slots) {
36
+ export function staticArrayPtr(slots) {
37
37
  if (!ctx.runtime.data) ctx.runtime.data = ''
38
38
  while (ctx.runtime.data.length % 8 !== 0) ctx.runtime.data += '\0'
39
39
  const headerOff = ctx.runtime.data.length
@@ -625,8 +625,14 @@ export default (ctx) => {
625
625
  if (!hasSpread) {
626
626
  const len = elems.length
627
627
  // R: Static data segment for arrays of pure-literal elements (own-memory only).
628
- // Saves N×(alloc+store) instructions in __start; raw f64 bits embedded directly.
629
- if (len >= 4 && !ctx.memory.shared) {
628
+ // Raw f64 bits embedded directly a constant array becomes a const pointer with no
629
+ // alloc and no per-element store. A static array aliases ONE shared data-segment
630
+ // region, so this is sound only when no caller expects a fresh instance per
631
+ // evaluation: at module scope the literal runs exactly once. A function-local
632
+ // literal (which would leak in-place mutations across calls — a latent bug the old
633
+ // len≥4 gate also had) allocs fresh instead. Module scope lifts the size floor too,
634
+ // so `const x = [1, 2, 3]` is a data segment, not an alloc.
635
+ if (ctx.func.atModuleScope && len >= 1 && !ctx.memory.shared) {
630
636
  // asF64 folds i32.const → f64.const literally, so int-literal arrays also qualify.
631
637
  const slots = elems.map(e => extractF64Bits(asF64(emit(e))))
632
638
  if (slots.every(b => b !== null)) return staticArrayPtr(slots)
@@ -676,10 +682,13 @@ export default (ctx) => {
676
682
  const litKey = isLiteralStr(idx) ? idx[1]
677
683
  : typeof arr === 'string' && lookupValType(arr) === VAL.OBJECT ? staticPropertyKey(idx)
678
684
  : null
679
- // SRoA flat object: `o['k']` → `local.get $o#i` (analyze.js scanFlatObjects).
680
- if (litKey != null && typeof arr === 'string' && ctx.func.flatObjects?.has(arr)) {
685
+ // SRoA flat object/array: `o['k']` / `a[2]` → `local.get $o#i` (scanFlatObjects).
686
+ // A bare integer index resolves its slot key here (not via `litKey`, which stays
687
+ // null for arrays so the heap-array / schema paths below are untouched).
688
+ if (typeof arr === 'string' && ctx.func.flatObjects?.has(arr)) {
681
689
  const fo = ctx.func.flatObjects.get(arr)
682
- const fi = fo.names.indexOf(litKey)
690
+ const flatKey = litKey != null ? litKey : staticIndexKey(idx)
691
+ const fi = flatKey != null ? fo.names.indexOf(flatKey) : -1
683
692
  if (fi >= 0) return typed(['local.get', `$${arr}#${fi}`], 'f64')
684
693
  }
685
694
  if (litKey != null && typeof arr === 'string' && ctx.schema.slotOf) {
@@ -700,10 +709,24 @@ export default (ctx) => {
700
709
  // Multi-value calls are materialized at call site (see '()' handler), so
701
710
  // func()[i] works naturally — func() returns a heap array pointer, [i] indexes it.
702
711
  const vt = typeof arr === 'string' ? lookupValType(arr) : valTypeOf(arr)
703
- const va = emit(arr), vi = asI32(emit(idx))
712
+ // Literal string key on a receiver that isn't a known array/typed/string:
713
+ // `x['a']` IS `x.a` — delegate to the dot emitter so an untyped/OBJECT/external
714
+ // receiver gets the same polymorphic dispatch (__dyn_get_any_t_h, host-external
715
+ // aware). The local `dynLoad` fallback below calls __dyn_get, which only probes
716
+ // the internal HASH layout — so `x['a']` on a host object silently returned
717
+ // undefined while `x.a` worked. (Known ARRAY/TYPED/STRING fall through unchanged:
718
+ // their string-key semantics differ from a HASH/OBJECT property read.)
719
+ if (litKey != null && vt !== VAL.ARRAY && vt !== VAL.TYPED && vt !== VAL.STRING)
720
+ return emit(['.', arr, litKey])
721
+ // emitIndex (not bare asI32(emit)) narrows integer index arithmetic — incl. a
722
+ // literal term like the `+1` of `a[i*W + x + 1]` — to i32 ops instead of the
723
+ // f64 convert/trunc round-trip. Non-i32 keys (string dispatch) fall back to
724
+ // asI32(emit) inside emitIndex, so this is a strict improvement for every branch.
725
+ const va = emit(arr), vi = emitIndex(idx)
704
726
  const ptrExpr = asF64(va)
705
727
  const dynLoad = (objExpr, keyExpr) => {
706
728
  if (ctx.transform.strict) err(`strict mode: dynamic property access \`${typeof arr === 'string' ? arr : '<expr>'}[<expr>]\` falls back to __dyn_get. Use a literal key or known typed-array receiver, or pass { strict: false }.`)
729
+ warnDeopt('deopt-dyn-read', `dynamic property read \`${typeof arr === 'string' ? arr : '<expr>'}[…]\` couldn't resolve a static type — it falls back to a runtime hash lookup (~1.5–2× slower than a typed/slot read, far worse in a hot loop). Use a literal key, a typed-array receiver, or a Map for genuinely dynamic keys.`)
707
730
  inc('__dyn_get')
708
731
  return ['f64.reinterpret_i64', ['call', '$__dyn_get', ['i64.reinterpret_f64', objExpr], ['i64.reinterpret_f64', keyExpr]]]
709
732
  }
@@ -753,6 +776,16 @@ export default (ctx) => {
753
776
  if (keyType === VAL.STRING)
754
777
  return typed(dynLoad(ptrExpr, asF64(emit(idx))), 'f64')
755
778
  if (vt === 'array') {
779
+ // Base offset of the array's data region. A binding proven never relocated
780
+ // (scanNeverGrown — a fresh array literal whose every use is a pure read, so no
781
+ // grow op can ever run) skips the realloc-forwarding follow: its base is the raw
782
+ // post-header offset `wrap(reinterpret(ptr) & OFFSET_MASK)`, no __ptr_offset call.
783
+ // Memory-safe ONLY under that proof — a relocated array read through this stale
784
+ // base would corrupt memory (see scanNeverGrown's default-deny rationale).
785
+ const neverGrown = typeof arr === 'string' && ctx.func.localReps?.get(arr)?.neverGrown === true
786
+ const arrBase = () => neverGrown
787
+ ? ['i32.wrap_i64', ['i64.and', ['i64.reinterpret_f64', ptrExpr], ['i64.const', LAYOUT.OFFSET_MASK]]]
788
+ : (inc('__ptr_offset'), ['call', '$__ptr_offset', ['i64.reinterpret_f64', ptrExpr]])
756
789
  // structInline Array<S>: element i is K consecutive inline f64 schema
757
790
  // cells — no per-row heap object, no stored element pointer. `arr[i]` is
758
791
  // the byte address of the element's first cell, returned as a first-class
@@ -761,11 +794,10 @@ export default (ctx) => {
761
794
  // structInline handles (src/analyze.js analyzeStructInline).
762
795
  const inlSid = inlineArraySid(arr)
763
796
  if (inlSid != null) {
764
- inc('__ptr_offset')
765
797
  const baseI32 = tempI32('ab')
766
798
  const K = ctx.schema.list[inlSid].length
767
799
  const cell = typed(structInline(K).ops.elemAddr(
768
- ['local.tee', `$${baseI32}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ptrExpr]]],
800
+ ['local.tee', `$${baseI32}`, arrBase()],
769
801
  vi), 'i32')
770
802
  cell.ptrKind = VAL.OBJECT
771
803
  cell.ptrAux = inlSid
@@ -775,51 +807,51 @@ export default (ctx) => {
775
807
  // not __typed_idx (which does __len + __ptr_offset = two forwarding follows
776
808
  // plus type-dispatch overhead irrelevant for plain arrays).
777
809
  const keyIsNum = keyType === VAL.NUMBER
778
- // Inline fast path: when arr has a known element schema and key is a
779
- // known number, the type-tag check and bounds check inside __arr_idx are
780
- // dead weight in hot kernels. Emit (f64.load (i32.add base (shl i 3)))
781
- // directly. base goes through __ptr_offset (still does forwarding follow),
782
- // and hoistAddrBase will CSE the (base, i) pair across the iteration body.
783
- // Array<NUMBER>/<STRING>/<OBJECT> all use 8-byte f64 slot layout direct
784
- // f64.load is correct regardless of elem kind (returns raw f64 for NUMBER,
785
- // NaN-boxed pointer for OBJECT/STRING; downstream typed() handles both).
786
- // Fast path fires on schemaId (Array<{x,y,z}> shapes) OR plain elem-val
787
- // (Array<NUMBER> from `.map(x => x*k)` etc.) both are proven facts from
788
- // the narrowing fixpoint (carried onto params via paramReps).
789
- const rep = typeof arr === 'string' ? ctx.func.localReps?.get(arr) : null
790
- const hasElemFact = rep?.arrayElemSchema != null || rep?.arrayElemValType != null
791
- // Take the unchecked inline load ONLY when the index is proven in-bounds by an
792
- // enclosing canonical loop `for (let i=C; i<arr.length; i++)`. Skipping the bounds
793
- // check on an arbitrary numeric index is unsound: `a[1]` on a length-1 array would
794
- // read the raw (uninitialized) cell instead of undefined. Constant / unproven
795
- // indices fall through to __arr_idx(_known) below, which bounds-checks → UNDEF_NAN.
796
- const idxProvenInBounds = hasElemFact && keyIsNum
810
+ // Inline fast path for any known plain ARRAY + numeric key: the type-tag
811
+ // dispatch and bounds check inside __arr_idx(_known) are dead weight in hot
812
+ // kernels most visibly AST walkers doing `node[i]` over heterogeneous
813
+ // arrays, where no element schema/valType is ever inferred. Emit the
814
+ // f64.load directly. base goes through __ptr_offset (still the forwarding
815
+ // follow), and hoistAddrBase CSEs the (base, i) pair across the iteration
816
+ // body. taggedLinear stores every element as one 8-byte f64 cell
817
+ // Array<NUMBER>/<STRING>/<OBJECT> alike so a direct f64.load is correct
818
+ // regardless of elem kind (raw f64 for NUMBER, NaN-boxed pointer for
819
+ // OBJECT/STRING; downstream typed() handles both). The load shape is fixed
820
+ // by the carrier, not by any rep fact, so we do NOT gate on a known element
821
+ // schema/valType: a bare `let a = [...]` walked by index gets the same
822
+ // inline load as an Array<{x,y,z}>. (structInline arrays returned above via
823
+ // inlineArraySid; typed arrays are VAL.TYPED, handled below.)
824
+ //
825
+ // Take the UNCHECKED inline load only when the index is proven in-bounds by
826
+ // an enclosing canonical loop `for (let i=C; i<arr.length; i++)` a pure
827
+ // index<length structural proof (scanBoundedArrIdx, src/type.js), itself
828
+ // element-kind-independent. Skipping the bounds check on an arbitrary numeric
829
+ // index is unsound: `a[1]` on a length-1 array would read the raw cell instead
830
+ // of undefined; those fall through to the inline bounds-checked load below.
831
+ const idxProvenInBounds = keyIsNum
797
832
  && typeof arr === 'string' && typeof idx === 'string'
798
833
  && inBoundsArrIdx(ctx).has(arr + '\x00' + idx)
799
834
  if (idxProvenInBounds) {
800
- inc('__ptr_offset')
801
- // __ptr_offset returns i32 — base local must be i32 (not the default
802
- // f64 NaN-box temp). Flat tee form so downstream peepholes can fold
835
+ // base local must be i32. Flat tee form so downstream peepholes can fold
803
836
  // `i32.wrap_i64 (i64.reinterpret_f64 (f64.load …))` → `i32.load …`
804
837
  // when this load feeds a ptrUnboxed OBJECT field.
805
838
  const baseI32 = tempI32('ab')
806
839
  return typed(ctx.abi.array.ops.load(
807
- ['local.tee', `$${baseI32}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ptrExpr]]],
840
+ ['local.tee', `$${baseI32}`, arrBase()],
808
841
  vi), 'f64')
809
842
  }
810
- // Known-elem array, numeric key, NOT proven in-bounds → inline bounds-checked
843
+ // Known plain array, numeric key, NOT proven in-bounds → inline bounds-checked
811
844
  // load: `idx < len ? load : undefined`. Same semantics as __arr_idx_known but
812
845
  // inline, so watr hoists the loop-invariant len load and CSEs the base — the
813
846
  // residual cost is a single (predictable) compare per access, not a call. Skipping
814
847
  // the check would read raw memory for OOB indices (e.g. `a[1]` on a length-1 array).
815
- if (hasElemFact && keyIsNum) {
816
- inc('__ptr_offset')
848
+ if (keyIsNum) {
817
849
  const baseI32 = tempI32('ab'), idxI32 = tempI32('ai')
818
850
  return typed(['if', ['result', 'f64'],
819
851
  ['i32.lt_u',
820
852
  ['local.tee', `$${idxI32}`, vi],
821
853
  ['i32.load', ['i32.sub',
822
- ['local.tee', `$${baseI32}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ptrExpr]]],
854
+ ['local.tee', `$${baseI32}`, arrBase()],
823
855
  ['i32.const', 8]]]],
824
856
  ['then', ctx.abi.array.ops.load(['local.get', `$${baseI32}`], ['local.get', `$${idxI32}`])],
825
857
  ['else', undefExpr()]], 'f64')
@@ -752,22 +752,30 @@ export default (ctx) => {
752
752
  // statically proven (e.g. a Map read off an object field): resolve MAP vs SET
753
753
  // vs ARRAY once at runtime. ARRAY.values() is the array itself; .keys() yields
754
754
  // indices; .entries() yields [i, el]. Any other receiver passes through.
755
- const collViewDyn = (mapWalk, setWalk, arrWalk) => (expr) => {
755
+ // A typed array (PTR.TYPED) is NOT PTR.ARRAY, so without an explicit branch it fell
756
+ // through to `local.get $t` (return the receiver). For .values that is correct (the
757
+ // receiver iterates its values), but .keys then yielded values instead of indices and
758
+ // .entries yielded scalars instead of [i, value] pairs. The typedWalk arg restores the
759
+ // right behavior for typed receivers (keys → index array, entries → typed-aware pairs).
760
+ const collViewDyn = (mapWalk, setWalk, arrWalk, typedWalk) => (expr) => {
756
761
  inc('__ptr_type')
757
762
  const t = temp('cd')
758
763
  const pt = () => ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]
759
764
  const branch = (tag, walk, rest) =>
760
765
  ['if', ['result', 'f64'], ['i32.eq', pt(), ['i32.const', tag]], ['then', walk(t)], ['else', rest]]
761
766
  const tree = branch(PTR.MAP, mapWalk, branch(PTR.SET, setWalk,
762
- branch(PTR.ARRAY, arrWalk, ['local.get', `$${t}`])))
767
+ branch(PTR.ARRAY, arrWalk, branch(PTR.TYPED, typedWalk, ['local.get', `$${t}`]))))
763
768
  return typed(['block', ['result', 'f64'], ['local.set', `$${t}`, asF64(emit(expr))], tree], 'f64')
764
769
  }
770
+ // keys: index array [0..len-1] for both plain and typed (length-based, no element reads).
765
771
  ctx.core.emit['.keys'] = collViewDyn(
766
- t => collKeysFromTemp(t, MAP_ENTRY, 8), t => collKeysFromTemp(t, SET_ENTRY, 8), arrIdxFromTemp)
772
+ t => collKeysFromTemp(t, MAP_ENTRY, 8), t => collKeysFromTemp(t, SET_ENTRY, 8), arrIdxFromTemp, arrIdxFromTemp)
773
+ // values: the receiver iterates its own elements (correct for plain and typed alike).
767
774
  ctx.core.emit['.values'] = collViewDyn(
768
- t => collKeysFromTemp(t, MAP_ENTRY, 16), t => collKeysFromTemp(t, SET_ENTRY, 8), t => ['local.get', `$${t}`])
775
+ t => collKeysFromTemp(t, MAP_ENTRY, 16), t => collKeysFromTemp(t, SET_ENTRY, 8), t => ['local.get', `$${t}`], t => ['local.get', `$${t}`])
776
+ // entries: [i, element] pairs; the typed variant reads elements width/kind-aware.
769
777
  ctx.core.emit['.entries'] = collViewDyn(
770
- t => collEntriesFromTemp(t, MAP_ENTRY, 8, 16), t => collEntriesFromTemp(t, SET_ENTRY, 8, 8), arrEntriesFromTemp)
778
+ t => collEntriesFromTemp(t, MAP_ENTRY, 8, 16), t => collEntriesFromTemp(t, SET_ENTRY, 8, 8), arrEntriesFromTemp, arrEntriesFromTempTyped)
771
779
 
772
780
  // Map/Set forEach(cb): invoke cb(value, key) per live entry in insertion order.
773
781
  // Map yields (value=val@16, key=key@8); Set yields (value=key@8, key@8) — the
@@ -1754,3 +1762,31 @@ function arrEntriesFromTemp(t) {
1754
1762
  ['br', `$aeloop${id}`]]],
1755
1763
  out.ptr]
1756
1764
  }
1765
+
1766
+ // TypedArray.prototype.entries() → dense Array of [index, element] pairs, reading each
1767
+ // element width/kind-aware via __typed_get_idx (the plain arrEntriesFromTemp uses a raw
1768
+ // f64.load, wrong for non-f64 typed arrays). Guarded on __typed_get_idx being registered:
1769
+ // it only is when the typedarray module is loaded, which is exactly when a PTR.TYPED
1770
+ // value can exist — so when absent, this branch is dead and falls back to the receiver.
1771
+ function arrEntriesFromTempTyped(t) {
1772
+ if (!ctx.core.stdlib['__typed_get_idx']) return ['local.get', `$${t}`]
1773
+ inc('__len', '__typed_get_idx', '__alloc_hdr')
1774
+ const n = tempI32('aetn'), i = tempI32('aeti'), pair = tempI32('aetp')
1775
+ const out = allocPtr({ type: PTR.ARRAY, len: ['local.get', `$${n}`], tag: 'aet' })
1776
+ const id = ctx.func.uniq++
1777
+ const P = () => ['i64.reinterpret_f64', ['local.get', `$${t}`]]
1778
+ return ['block', ['result', 'f64'],
1779
+ ['local.set', `$${n}`, ['call', '$__len', P()]],
1780
+ out.init,
1781
+ ['local.set', `$${i}`, ['i32.const', 0]],
1782
+ ['block', `$aetbrk${id}`, ['loop', `$aetloop${id}`,
1783
+ ['br_if', `$aetbrk${id}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${n}`]]],
1784
+ ['local.set', `$${pair}`, ['call', '$__alloc_hdr', ['i32.const', 2], ['i32.const', 2]]],
1785
+ ['f64.store', ['local.get', `$${pair}`], ['f64.convert_i32_s', ['local.get', `$${i}`]]],
1786
+ ['f64.store', ['i32.add', ['local.get', `$${pair}`], ['i32.const', 8]],
1787
+ ['call', '$__typed_get_idx', P(), ['local.get', `$${i}`]]],
1788
+ elemStore(out.local, i, mkPtrIR(PTR.ARRAY, 0, ['local.get', `$${pair}`])),
1789
+ ['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
1790
+ ['br', `$aetloop${id}`]]],
1791
+ out.ptr]
1792
+ }
package/module/core.js CHANGED
@@ -16,7 +16,7 @@ import { valTypeOf, shapeOf } from '../src/kind.js'
16
16
  import { T } from '../src/ast.js'
17
17
  import { inlineArraySid } from '../src/static.js'
18
18
  import { VAL, lookupValType, lookupNotString, repOf, updateRep } from '../src/reps.js'
19
- import { ctx, err, inc, PTR, LAYOUT, HEAP, emitArity, followForwardingWat, declGlobal } from '../src/ctx.js'
19
+ import { ctx, err, inc, PTR, LAYOUT, HEAP, FORWARDING_MASK, emitArity, followForwardingWat, declGlobal } from '../src/ctx.js'
20
20
  import { ptrOffsetFwdWat, STR_INTERN_BIT } from '../layout.js'
21
21
  import { nanPrefixHex } from '../layout.js'
22
22
  import { initSchema } from './schema.js'
@@ -153,9 +153,7 @@ export default (ctx) => {
153
153
  ;; (cap=-1 sentinel at -4, new offset at -8). Other types never forward, so they skip
154
154
  ;; the loop; a well-formed ptr without forwarding pays one bounds + cap check per hop.
155
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.or (i32.eq (local.get $t) (i32.const ${PTR.ARRAY}))
157
- (i32.or (i32.eq (local.get $t) (i32.const ${PTR.HASH}))
158
- (i32.or (i32.eq (local.get $t) (i32.const ${PTR.SET})) (i32.eq (local.get $t) (i32.const ${PTR.MAP})))))
156
+ (if (i32.and (i32.shl (i32.const 1) (local.get $t)) (i32.const ${FORWARDING_MASK}))
159
157
  (then
160
158
  ${followForwardingWat('$off', { lowGuard: true })}))
161
159
  (local.get $off))`
package/module/math.js CHANGED
@@ -129,7 +129,11 @@ export default (ctx) => {
129
129
  // undefined→NaN, and strings get parsed. Without this, raw NaN-boxed pointers
130
130
  // (null/undefined/strings) would propagate through math.log etc. and surface
131
131
  // as the original null/undefined sentinel after decode.
132
- const fn = (name, ...args) => typed(['call', `$${name}`, ...args.map(a => toNumF64(a, emit(a)))], 'f64')
132
+ // A canon'd operand feeding a math call is redundant: the callee (log/sin/exp/…)
133
+ // propagates a non-canonical NaN identically and re-canon-izes its own result.
134
+ // `Math.log(Math.log(x))` thus sheds the inner per-call select + f64.ne.
135
+ const stripCanon = (v) => (v && v.canonOf != null) ? typed(v.canonOf, 'f64') : v
136
+ const fn = (name, ...args) => typed(['call', `$${name}`, ...args.map(a => stripCanon(toNumF64(a, emit(a))))], 'f64')
133
137
 
134
138
  // Canonicalize a possibly-NaN f64 result. A wasm arithmetic op that mints a
135
139
  // fresh NaN (f64.sqrt of a negative, f64.min/max with a NaN operand) leaves
@@ -139,12 +143,18 @@ export default (ctx) => {
139
143
  // untyped === / typeof. So fold any NaN back to canonical where one is born.
140
144
  const canon = (node) => {
141
145
  const t = temp('cn')
142
- return typed(['block', ['result', 'f64'],
146
+ const ir = typed(['block', ['result', 'f64'],
143
147
  ['local.set', `$${t}`, node],
144
148
  ['select',
145
149
  ['f64.const', 'nan'],
146
150
  ['local.get', `$${t}`],
147
151
  ['f64.ne', ['local.get', `$${t}`], ['local.get', `$${t}`]]]], 'f64')
152
+ // Tag the wrapper so a NaN-propagating f64 consumer (`f64.add`/`mul`/… that
153
+ // itself canon-izes on escape) can strip the redundant inner canon: the raw
154
+ // op result `node` propagates a freshly-minted NaN identically through the
155
+ // consumer, and only the OUTERMOST escaping value needs the canonical form.
156
+ ir.canonOf = node
157
+ return ir
148
158
  }
149
159
 
150
160
  // sqrt(x) needs no NaN-canon when its argument is provably ≥ 0 with no spurious NaN:
@@ -499,6 +509,132 @@ export default (ctx) => {
499
509
  (if (f64.eq (f64.abs (local.get $x)) (f64.const inf)) (then (return (f64.const nan))))
500
510
  (f64.div (call $math.sin (local.get $x)) (call $math.cos (local.get $x))))`)
501
511
 
512
+ // ── f64x2 SIMD sin/cos — both lanes through one polynomial ───────────────────
513
+ // The scalar sin_core/cos_core algorithm lifted to two f64 lanes: same
514
+ // round-to-nearest π reduction, same minimax poly (SIN_C/COS_C), same quadrant
515
+ // parity — but every branch becomes branchless so two independent angles cost one
516
+ // evaluation. A kernel computing sin and cos of distinct args (rotations, de Jong /
517
+ // Clifford maps, oscillator banks) packs them two-per-vector and ≈halves trig cost.
518
+ // • Both reduction passes run unconditionally: for an in-range r the second pass'
519
+ // q2 = nearest(r/π) = 0, so it's an exact no-op — no per-lane branch needed, and
520
+ // it still rescues |x| up to ~1e15 just like the scalar's gated pass.
521
+ // • NaN and ±∞ fall out as NaN through the arithmetic (∞ − ∞·π = NaN); a v128 lane
522
+ // is raw f64, not a NaN-box, so the canonical-NaN guard the scalar needs is moot.
523
+ // • Sign flip for odd quadrants is `r XOR (mask & −0.0)` (mask = |q|>0.5); final
524
+ // min/max clamps the ~1e-8 poly overshoot to [−1,1], same as scalar.
525
+ const splat = (c) => `(f64x2.splat (f64.const ${c}))`
526
+ const horner2 = (cs, v = '$r2') => cs.reduceRight((acc, c, i) =>
527
+ i === cs.length - 1 ? splat(c)
528
+ : `(f64x2.add ${splat(c)} (f64x2.mul (local.get ${v}) ${acc}))`, '')
529
+ // fdlibm log's even/odd split, as f64x2 coefficient arrays (the scalar $math.log inlines them):
530
+ // t1 = w·(L3 + w·(L5 + w·L7)), t2 = z·(L2 + w·(L4 + w·(L6 + w·L8))). Vectorized log_v reuses these.
531
+ const LOG_T1 = [0.3999999999940941908, 0.2222219843214978396, 0.1531383769920937332]
532
+ const LOG_T2 = [0.6666666666666735130, 0.2857142874366239149, 0.1818357216161805012, 0.1479819860511658591]
533
+ // Shared reduce → r ∈ [−π/2,π/2] in $r, quadrant parity in $q (branchless, 2 passes).
534
+ const reduce2 = `
535
+ (local.set $q (f64x2.nearest (f64x2.mul (local.get $x) ${splat(INV_PI)})))
536
+ (local.set $r (f64x2.sub (local.get $x) (f64x2.mul (local.get $q) ${splat(PI)})))
537
+ (local.set $q2 (f64x2.nearest (f64x2.mul (local.get $r) ${splat(INV_PI)})))
538
+ (local.set $r (f64x2.sub (local.get $r) (f64x2.mul (local.get $q2) ${splat(PI)})))
539
+ (local.set $q (f64x2.add (local.get $q) (local.get $q2)))
540
+ (local.set $q (f64x2.sub (local.get $q) (f64x2.mul ${splat(2)} (f64x2.nearest (f64x2.mul (local.get $q) ${splat(0.5)})))))
541
+ (local.set $r2 (f64x2.mul (local.get $r) (local.get $r)))`
542
+ // r XOR (|q|>0.5 ? −0.0 : 0), then clamp to [−1,1].
543
+ const signClamp = `
544
+ (local.set $r (v128.xor (local.get $r)
545
+ (v128.and (f64x2.gt (f64x2.abs (local.get $q)) ${splat(0.5)}) ${splat('-0.0')})))
546
+ (f64x2.min (f64x2.max (local.get $r) ${splat(-1)}) ${splat(1)})`
547
+ wat('math.sin2', `(func $math.sin2 (param $x v128) (result v128)
548
+ (local $q v128) (local $q2 v128) (local $r v128) (local $r2 v128)${reduce2}
549
+ (local.set $r (f64x2.mul (local.get $r) ${horner2(SIN_C)}))${signClamp})`)
550
+ wat('math.cos2', `(func $math.cos2 (param $x v128) (result v128)
551
+ (local $q v128) (local $q2 v128) (local $r v128) (local $r2 v128)${reduce2}
552
+ (local.set $r ${horner2(COS_C)})${signClamp})`)
553
+ // pow has no cheap 2-lane polynomial (it is exp(y·ln x) with cancellation-sensitive reductions),
554
+ // so the f64x2 mirror computes each lane with the scalar $math.pow and repacks — BIT-EXACT by
555
+ // construction. No transcendental speedup, but it keeps a pow-bearing pixel kernel's surrounding
556
+ // f64x2 arithmetic vectorized (the per-pixel-color pass only emits this when a truly-2-wide op —
557
+ // sin2/cos2/sqrt — already justifies the pair, so the extract/repack never makes a kernel slower).
558
+ wat('math.pow2', `(func $math.pow2 (param $x v128) (param $y v128) (result v128)
559
+ (f64x2.replace_lane 1
560
+ (f64x2.splat (call $math.pow (f64x2.extract_lane 0 (local.get $x)) (f64x2.extract_lane 0 (local.get $y))))
561
+ (call $math.pow (f64x2.extract_lane 1 (local.get $x)) (f64x2.extract_lane 1 (local.get $y)))))`, ['math.pow'])
562
+
563
+ // atan2/hypot/log have no cheap 2-lane polynomial (multi-`return` fdlibm bodies), so — like pow2 —
564
+ // each f64x2 mirror computes both lanes with the SCALAR helper and repacks: BIT-EXACT by
565
+ // construction. The per-pixel-color pass only emits these when a truly-2-wide op (sin2/cos2/sqrt)
566
+ // already justifies the f64x2 pair, so the extract/repack never makes a kernel slower.
567
+ // NOTE: names avoid the $math.log2/$math.exp2 collision (those are log-/exp-BASE-2).
568
+ wat('math.atan2_2', `(func $math.atan2_2 (param $y v128) (param $x v128) (result v128)
569
+ (f64x2.replace_lane 1
570
+ (f64x2.splat (call $math.atan2 (f64x2.extract_lane 0 (local.get $y)) (f64x2.extract_lane 0 (local.get $x))))
571
+ (call $math.atan2 (f64x2.extract_lane 1 (local.get $y)) (f64x2.extract_lane 1 (local.get $x)))))`, ['math.atan2'])
572
+ wat('math.hypot_2', `(func $math.hypot_2 (param $x v128) (param $y v128) (result v128)
573
+ (f64x2.replace_lane 1
574
+ (f64x2.splat (call $math.hypot (f64x2.extract_lane 0 (local.get $x)) (f64x2.extract_lane 0 (local.get $y))))
575
+ (call $math.hypot (f64x2.extract_lane 1 (local.get $x)) (f64x2.extract_lane 1 (local.get $y)))))`, ['math.hypot'])
576
+ // True f64x2 log — both lanes through one fdlibm poly (≈2× over two scalar calls). The HOT path
577
+ // (both lanes a normal finite x>0) mirrors $math.log's normal branch op-for-op: bit-exact (the
578
+ // sqrt2-center conditional becomes a per-lane bitselect; the i32 exponent k becomes an f64 via the
579
+ // 2^52 magic-add, identical to convert_i32_s for |k|≤1075). Any other lane (≤0/∞/NaN/denormal)
580
+ // routes BOTH lanes to the scalar fallback → bit-exact by construction, edges never lose precision.
581
+ wat('math.log_v', `(func $math.log_v (param $x v128) (result v128)
582
+ (local $k v128) (local $m v128) (local $mask v128) (local $f v128) (local $s v128) (local $z v128) (local $w v128) (local $hfsq v128)
583
+ (if (result v128)
584
+ (i64x2.all_true (v128.and
585
+ (f64x2.ge (local.get $x) (f64x2.splat (f64.const 0x1p-1022)))
586
+ (f64x2.lt (local.get $x) (f64x2.splat (f64.const inf)))))
587
+ (then
588
+ (local.set $k (f64x2.sub
589
+ (v128.or (v128.and (i64x2.shr_u (local.get $x) (i32.const 52)) (i64x2.splat (i64.const 0x7ff)))
590
+ (i64x2.splat (i64.const 0x4330000000000000)))
591
+ (f64x2.splat (f64.const 4503599627371519))))
592
+ (local.set $m (v128.or (v128.and (local.get $x) (i64x2.splat (i64.const 0x000fffffffffffff))) (i64x2.splat (i64.const 0x3ff0000000000000))))
593
+ (local.set $mask (f64x2.ge (local.get $m) (f64x2.splat (f64.const 1.4142135623730951))))
594
+ (local.set $m (v128.bitselect (f64x2.mul (local.get $m) (f64x2.splat (f64.const 0.5))) (local.get $m) (local.get $mask)))
595
+ (local.set $k (f64x2.add (local.get $k) (v128.and (local.get $mask) (f64x2.splat (f64.const 1.0)))))
596
+ (local.set $f (f64x2.sub (local.get $m) (f64x2.splat (f64.const 1.0))))
597
+ (local.set $s (f64x2.div (local.get $f) (f64x2.add (local.get $f) (f64x2.splat (f64.const 2.0)))))
598
+ (local.set $z (f64x2.mul (local.get $s) (local.get $s)))
599
+ (local.set $w (f64x2.mul (local.get $z) (local.get $z)))
600
+ (local.set $hfsq (f64x2.mul (f64x2.splat (f64.const 0.5)) (f64x2.mul (local.get $f) (local.get $f))))
601
+ (f64x2.add
602
+ (f64x2.mul (local.get $k) (f64x2.splat (f64.const ${Math.LN2})))
603
+ (f64x2.add (f64x2.sub (local.get $f) (local.get $hfsq))
604
+ (f64x2.mul (local.get $s) (f64x2.add (local.get $hfsq)
605
+ (f64x2.add
606
+ (f64x2.mul (local.get $w) ${horner2(LOG_T1, '$w')})
607
+ (f64x2.mul (local.get $z) ${horner2(LOG_T2, '$w')})))))))
608
+ (else
609
+ (f64x2.replace_lane 1
610
+ (f64x2.splat (call $math.log (f64x2.extract_lane 0 (local.get $x))))
611
+ (call $math.log (f64x2.extract_lane 1 (local.get $x)))))))`, ['math.log'])
612
+
613
+ // True f64x2 exp2 — hot path (round(y) ∈ (−1023,1024), the normal-result range) mirrors $math.exp2's
614
+ // single-IEEE-build branch op-for-op (Horner over f=y−round(y), 2^k via (k+1023)<<52); edges
615
+ // (overflow/underflow/denormal/NaN) route both lanes to the scalar fallback → bit-exact.
616
+ wat('math.exp2_v', `(func $math.exp2_v (param $y v128) (result v128)
617
+ (local $k v128) (local $f v128)
618
+ (local.set $k (f64x2.nearest (local.get $y)))
619
+ (if (result v128)
620
+ (i64x2.all_true (v128.and
621
+ (f64x2.gt (local.get $k) (f64x2.splat (f64.const -1023)))
622
+ (f64x2.lt (local.get $k) (f64x2.splat (f64.const 1024)))))
623
+ (then
624
+ (local.set $f (f64x2.sub (local.get $y) (local.get $k)))
625
+ (f64x2.mul ${horner2(EXP2_C, '$f')}
626
+ (i64x2.shl (i64x2.add
627
+ (i64x2.extend_low_i32x4_s (i32x4.trunc_sat_f64x2_s_zero (local.get $k)))
628
+ (i64x2.splat (i64.const 1023))) (i32.const 52))))
629
+ (else
630
+ (f64x2.replace_lane 1
631
+ (f64x2.splat (call $math.exp2 (f64x2.extract_lane 0 (local.get $y))))
632
+ (call $math.exp2 (f64x2.extract_lane 1 (local.get $y)))))))`, ['math.exp2'])
633
+
634
+ // e^x = 2^(x·log2e) — defers to exp2_v exactly as scalar $math.exp defers to $math.exp2. Bit-exact.
635
+ wat('math.exp_v', `(func $math.exp_v (param $x v128) (result v128)
636
+ (call $math.exp2_v (f64x2.mul (local.get $x) (f64x2.splat (f64.const ${Math.LOG2E})))))`, ['math.exp2_v'])
637
+
502
638
  // e^x = 2^(x·log2 e) — defer to the faster $math.exp2 (one multiply, no division, and
503
639
  // exp2's NaN/overflow/underflow guards cover exp's). Accurate to exp2's ~6e-9, better
504
640
  // than the old 7-term Taylor, and it shares one code path with `2**`.
package/module/number.js CHANGED
@@ -155,6 +155,7 @@ export default (ctx) => {
155
155
  deps({
156
156
  __mkstr: ['__alloc'],
157
157
  __ftoa: ['__itoa', '__pow10', '__mkstr', '__static_str', '__toExp'],
158
+ __i32_to_str: ['__itoa', '__mkstr'],
158
159
  __toExp: ['__itoa', '__pow10', '__mkstr', '__static_str'],
159
160
  __radix_str: ['__mkstr'],
160
161
  __num_radix: ['__ftoa', '__mkstr'],
@@ -215,6 +216,26 @@ export default (ctx) => {
215
216
  ${reverseBytesWat()}
216
217
  (local.get $len))`
217
218
 
219
+ // __i32_to_str(val: i32) → f64 (NaN-boxed string) — ToString for a value the
220
+ // compiler proved is a signed i32. The whole point is to bypass __ftoa's float
221
+ // machinery (shortest-repr search, __toExp, __pow10): a known integer renders with
222
+ // just __itoa over its magnitude plus a sign byte. Lets `"id " + (n|0)` and integer
223
+ // templates skip the ~2 KB float formatter the generic ToString hard-pulls.
224
+ ctx.core.stdlib['__i32_to_str'] = `(func $__i32_to_str (param $val i32) (result f64)
225
+ (local $buf i32) (local $len i32) (local $u i32)
226
+ (local.set $buf (call $__alloc (i32.const 12)))
227
+ (if (i32.lt_s (local.get $val) (i32.const 0))
228
+ (then
229
+ (i32.store8 (local.get $buf) (i32.const 45)) ;; '-'
230
+ ;; magnitude as unsigned: negate via 0 - val (INT_MIN maps to itself, read u below)
231
+ (local.set $u (i32.sub (i32.const 0) (local.get $val)))
232
+ (local.set $len (call $__itoa (local.get $u) (i32.add (local.get $buf) (i32.const 1))))
233
+ (return (call $__mkstr (local.get $buf) (i32.add (local.get $len) (i32.const 1)))))
234
+ (else
235
+ (local.set $len (call $__itoa (local.get $val) (local.get $buf)))
236
+ (return (call $__mkstr (local.get $buf) (local.get $len)))))
237
+ (f64.const 0))`
238
+
218
239
  // __radix_str(val: i64, radix: i32) → f64 (NaN-boxed string)
219
240
  // Signed integer → radix string for BigInt.prototype.toString(radix). Digits go
220
241
  // 0-9 then a-z (lowercase, per spec); magnitude is taken unsigned so i64.MIN
package/module/object.js CHANGED
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { typed, asF64, asI64, NULL_NAN, UNDEF_NAN, temp, tempI32, tempI64, block64, ptrTypeEq, dispatchByPtrType, allocPtr, needsDynShadow, mkPtrIR, extractF64Bits, appendStaticSlots, slotAddr, elemLoad, elemStore, boolBoxIR } from '../src/ir.js'
11
11
  import { emit } from '../src/bridge.js'
12
+ import { staticArrayPtr } from './array.js'
12
13
  import { valTypeOf, shapeOf } from '../src/kind.js'
13
14
  import { VAL, lookupValType, repOf, updateRep } from '../src/reps.js'
14
15
  import { ctx, err, inc, PTR, LAYOUT, declGlobal } from '../src/ctx.js'
@@ -246,6 +247,31 @@ export default (ctx) => {
246
247
  }
247
248
  ctx.core.emit['Object.getOwnPropertyNames'] = ctx.core.emit['Object.keys']
248
249
 
250
+ // for-in's read-only key enumeration (src/prepare for…in lowering). Identical to
251
+ // Object.keys EXCEPT: when the receiver is a bare variable with a complete static
252
+ // schema, the key list is a compile-time constant, so it pools ONE static-data
253
+ // array (no per-evaluation alloc) — eliminating for-in's hot-loop heap-growth
254
+ // cliff and its unbounded-allocation OOM. The pooled array is shared/read-only,
255
+ // which is sound because for-in only reads ks[i]/ks.length (Object.keys can't pool:
256
+ // user code may `.sort()`/`.reverse()` the result in place). Anything not a static
257
+ // schema bare-var — arrays/strings/HASH/dyn-props/expressions — delegates to
258
+ // Object.keys (evaluates the receiver, full runtime enumeration).
259
+ ctx.core.emit['__keys_ro'] = (obj) => {
260
+ // Pool only when the receiver's enumerable key set is provably the static
261
+ // schema: a bare var with NO computed-key writes (`o[k]=v`) — those are the
262
+ // only writes that add enumerable keys (computed reads / dot-adds don't).
263
+ // `mayHaveDynProps` is too coarse here — it also flags computed-READ receivers,
264
+ // and for-in's own `o[k]` read would otherwise veto its own pooling.
265
+ if (typeof obj === 'string' && !ctx.types.dynWriteVars?.has(obj) && !isHashTyped(obj) && !arrayValType(obj) && !stringValType(obj)) {
266
+ const schema = resolveSchema(obj)
267
+ if (schema) {
268
+ const slots = schema.map(name => extractF64Bits(asF64(emit(['str', name]))))
269
+ if (slots.every(b => b !== null)) return staticArrayPtr(slots)
270
+ }
271
+ }
272
+ return ctx.core.emit['Object.keys'](obj)
273
+ }
274
+
249
275
  // Object.prototype.hasOwnProperty(key) — own-property presence check.
250
276
  // Compile-time fold for literal keys against object literals or variables
251
277
  // with known schemas; runtime path delegates to the `in` operator (same
package/module/simd.js CHANGED
@@ -1,8 +1,9 @@
1
1
  /**
2
- * SIMD module — source-level f32x4 / i32x4 intrinsics that lower 1:1 to wasm SIMD
3
- * (v128). Lets a jz kernel process 4 lanes per instruction: the per-pixel-parallel
4
- * kernels (mandelbrot, raymarcher) run 4 pixels/rays in masked lockstep and beat
5
- * scalar V8 several-fold (validated: SIMD-4 mandelbrot 4.6× a warm-V8 scalar loop).
2
+ * SIMD module — source-level f32x4 / i32x4 / f64x2 intrinsics that lower 1:1 to wasm
3
+ * SIMD (v128). Lets a jz kernel process 4 (or, for f64x2, 2) lanes per instruction:
4
+ * the per-pixel-parallel kernels (mandelbrot, raymarcher) run 4 pixels/rays in masked
5
+ * lockstep, and the attractors kernel packs its two sines + two cosines two-per-f64x2.
6
+ * Several-fold over scalar V8 (validated: SIMD-4 mandelbrot ≈ 4.6× a warm-V8 scalar loop).
6
7
  *
7
8
  * A v128 value is just a local whose wasm type is `v128` (exprType returns 'v128'
8
9
  * for these calls; emit passes it through without an f64/i32 coercion). Building
@@ -18,11 +19,14 @@
18
19
  * i32x4.splat(n) / add/sub(a,b) / lane(v,k)
19
20
  * v128.and/or/xor(a,b) / not(a) / bitselect(t,f,mask)
20
21
  * v128.anyTrue(v) / v128.allTrue(v) → i32 (loop-exit tests)
22
+ * f64x2.splat(x) / lanes(a,b) / add/sub/mul/div/min/max / sqrt/abs/neg/… / cmp
23
+ * f64x2.sin(v) / f64x2.cos(v) → both lanes through one poly ($math.sin2/$math.cos2)
24
+ * f64x2.lane(v, k) extract lane k (0..1) as a number
21
25
  *
22
26
  * @module simd
23
27
  */
24
28
  import { typed, asF64, asI32 } from '../src/ir.js'
25
- import { emit } from '../src/bridge.js'
29
+ import { emit, emitter } from '../src/bridge.js'
26
30
  import { err } from '../src/ctx.js'
27
31
 
28
32
  export default (ctx) => {
@@ -82,4 +86,32 @@ export default (ctx) => {
82
86
  // ── lane extract (read a single lane back to a scalar) ───────────────────────
83
87
  e['f32x4.lane'] = (v, k) => F(['f64.promote_f32', ['f32x4.extract_lane', laneIdx(k), op(v)]])
84
88
  e['i32x4.lane'] = (v, k) => I(['i32x4.extract_lane', laneIdx(k), op(v)])
89
+
90
+ // ── f64x2 — two full-precision f64 lanes (no f32 demotion) ───────────────────
91
+ // For kernels whose hot value is f64: two independent angles/coords per instruction.
92
+ // `f64x2.sin`/`f64x2.cos` lower to the shared $math.sin2/$math.cos2 poly (module/math.js)
93
+ // — sin and cos of distinct args pack two-per-vector and ≈halve transcendental cost.
94
+ const lane2 = (k) => {
95
+ const v = typeof k === 'number' ? k : (Array.isArray(k) && k.length === 2 && k[0] == null && typeof k[1] === 'number') ? k[1] : null
96
+ if (v == null || (v | 0) !== v || v < 0 || v > 1)
97
+ err(`f64x2 lane index must be a 0..1 literal (got ${JSON.stringify(k)}).`)
98
+ return v
99
+ }
100
+ e['f64x2.splat'] = (a) => V(['f64x2.splat', asF64(emit(a))])
101
+ e['f64x2.lanes'] = (a, b) => V(['f64x2.replace_lane', 1, ['f64x2.splat', asF64(emit(a))], asF64(emit(b))])
102
+ for (const o of ['add', 'sub', 'mul', 'div', 'min', 'max'])
103
+ e[`f64x2.${o}`] = (a, b) => V([`f64x2.${o}`, op(a), op(b)])
104
+ for (const o of ['sqrt', 'abs', 'neg', 'floor', 'ceil', 'trunc', 'nearest'])
105
+ e[`f64x2.${o}`] = (a) => V([`f64x2.${o}`, op(a)])
106
+ for (const o of ['eq', 'ne', 'lt', 'le', 'gt', 'ge'])
107
+ e[`f64x2.${o}`] = (a, b) => V([`f64x2.${o}`, op(a), op(b)])
108
+ e['f64x2.sin'] = emitter(['math.sin2'], (a) => V(['call', '$math.sin2', op(a)]))
109
+ e['f64x2.cos'] = emitter(['math.cos2'], (a) => V(['call', '$math.cos2', op(a)]))
110
+ // f64x2.log/exp/exp2 lower to the true-vectorized $math.log_v/$math.exp_v/$math.exp2_v polys —
111
+ // both lanes through one fdlibm/2^f evaluation (≈2× over two scalar calls), bit-exact via a
112
+ // hot-path-vectorized + scalar-edge-fallback split (module/math.js).
113
+ e['f64x2.log'] = emitter(['math.log_v'], (a) => V(['call', '$math.log_v', op(a)]))
114
+ e['f64x2.exp'] = emitter(['math.exp_v'], (a) => V(['call', '$math.exp_v', op(a)]))
115
+ e['f64x2.exp2'] = emitter(['math.exp2_v'], (a) => V(['call', '$math.exp2_v', op(a)]))
116
+ e['f64x2.lane'] = (v, k) => F(['f64x2.extract_lane', lane2(k), op(v)])
85
117
  }