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.
- package/README.md +66 -43
- package/bench/README.md +128 -78
- package/bench/bench.svg +32 -42
- package/cli.js +73 -7
- package/dist/interop.js +1 -0
- package/dist/jz.js +6024 -0
- package/index.d.ts +126 -0
- package/index.js +190 -34
- package/interop.js +71 -27
- package/layout.js +5 -0
- package/module/array.js +71 -39
- package/module/collection.js +41 -5
- package/module/core.js +2 -4
- package/module/math.js +138 -2
- package/module/number.js +21 -0
- package/module/object.js +26 -0
- package/module/simd.js +37 -5
- package/module/string.js +41 -12
- package/module/typedarray.js +415 -26
- package/package.json +21 -6
- package/src/autoload.js +3 -0
- package/src/compile/analyze-scans.js +174 -11
- package/src/compile/analyze.js +38 -3
- package/src/compile/emit-assign.js +9 -6
- package/src/compile/emit.js +347 -36
- package/src/compile/index.js +307 -29
- package/src/compile/infer.js +36 -1
- package/src/compile/loop-divmod.js +155 -0
- package/src/compile/narrow.js +108 -11
- package/src/compile/peel-stencil.js +261 -0
- package/src/compile/plan/advise.js +55 -1
- package/src/compile/plan/index.js +4 -0
- package/src/compile/plan/inline.js +5 -2
- package/src/compile/plan/literals.js +220 -5
- package/src/compile/plan/scope.js +115 -39
- package/src/compile/program-facts.js +21 -2
- package/src/ctx.js +45 -7
- package/src/ir.js +55 -7
- package/src/kind-traits.js +27 -0
- package/src/kind.js +65 -3
- package/src/optimize/index.js +344 -45
- package/src/optimize/vectorize.js +2968 -182
- package/src/prepare/index.js +64 -7
- package/src/prepare/lift-iife.js +149 -0
- package/src/reps.js +3 -2
- package/src/static.js +9 -0
- package/src/type.js +10 -6
- package/src/wat/assemble.js +195 -13
- 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
|
-
//
|
|
629
|
-
|
|
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` (
|
|
680
|
-
|
|
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
|
|
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
|
-
|
|
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}`,
|
|
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
|
|
779
|
-
//
|
|
780
|
-
//
|
|
781
|
-
//
|
|
782
|
-
//
|
|
783
|
-
//
|
|
784
|
-
//
|
|
785
|
-
//
|
|
786
|
-
//
|
|
787
|
-
//
|
|
788
|
-
// the
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
//
|
|
792
|
-
//
|
|
793
|
-
//
|
|
794
|
-
//
|
|
795
|
-
//
|
|
796
|
-
|
|
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
|
-
|
|
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}`,
|
|
840
|
+
['local.tee', `$${baseI32}`, arrBase()],
|
|
808
841
|
vi), 'f64')
|
|
809
842
|
}
|
|
810
|
-
// Known
|
|
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 (
|
|
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}`,
|
|
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')
|
package/module/collection.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3
|
-
* (v128). Lets a jz kernel process 4 lanes per instruction:
|
|
4
|
-
* kernels (mandelbrot, raymarcher) run 4 pixels/rays in masked
|
|
5
|
-
*
|
|
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
|
}
|