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/src/ctx.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { makeAbi } from './abi/index.js'
|
|
11
|
-
export { HEAP, LAYOUT, PTR, ATOM, nanPrefixHex, atomNanHex, ssoBitI64Hex, sliceBitI64Hex, ptrNanHex, ptrBoxPrefixBigInt, encodePtrHi, decodePtrType, decodePtrAux, ATOM_HI, oobNanLiteral, oobNanIR, followForwardingWat } from '../layout.js'
|
|
11
|
+
export { HEAP, LAYOUT, PTR, ATOM, FORWARDING_MASK, nanPrefixHex, atomNanHex, ssoBitI64Hex, sliceBitI64Hex, ptrNanHex, ptrBoxPrefixBigInt, encodePtrHi, decodePtrType, decodePtrAux, ATOM_HI, oobNanLiteral, oobNanIR, followForwardingWat } from '../layout.js'
|
|
12
12
|
|
|
13
13
|
// === Carrier layout ===
|
|
14
14
|
// Canonical bit layout lives in layout.js (compiler-free). Re-exported above for
|
|
@@ -144,18 +144,45 @@ export const getter = (fn) => (fn.getter = true, fn)
|
|
|
144
144
|
* Each module co-locates its own deps with its stdlib registrations at init time. */
|
|
145
145
|
export function resolveIncludes() {
|
|
146
146
|
const graph = ctx.core.stdlibDeps
|
|
147
|
+
const stdlib = ctx.core.stdlib
|
|
148
|
+
// Auto-derived deps: a stdlib template that calls `$__foo` (a registered stdlib
|
|
149
|
+
// func) depends on it, whether or not the hand-maintained `deps()` list says so.
|
|
150
|
+
// Scanning the *realized* template keeps the graph honest, so a missing manual
|
|
151
|
+
// entry can't silently drop a transitively-needed helper (the bug class the old
|
|
152
|
+
// blanket `inc('__mkptr','__alloc')` masked). Factory templates are realized
|
|
153
|
+
// (called) so feature-gated branches — `${hasExt ? '(call $__ext_prop …)' : ''}`
|
|
154
|
+
// — resolve before scanning; reading raw source would over-pull the dead branch.
|
|
155
|
+
// jz's templates are pure string builders, so realizing here (and again at
|
|
156
|
+
// emission) is side-effect-free. A `$__foo` naming a global (not a stdlib func)
|
|
157
|
+
// is skipped. Realization can fail if called before its inputs are ready — then
|
|
158
|
+
// we return nothing *without caching*, so a later pass retries. Memoized per compile.
|
|
159
|
+
const autoCache = ctx.core._autoDeps ??= new Map()
|
|
160
|
+
const autoDepsOf = (name) => {
|
|
161
|
+
let found = autoCache.get(name)
|
|
162
|
+
if (found !== undefined) return found
|
|
163
|
+
const v = stdlib[name]
|
|
164
|
+
let text
|
|
165
|
+
if (typeof v === 'string') text = v
|
|
166
|
+
else if (typeof v === 'function') { try { text = v() } catch { return [] } }
|
|
167
|
+
if (typeof text !== 'string') return (autoCache.set(name, []), [])
|
|
168
|
+
found = []
|
|
169
|
+
const seen = new Set()
|
|
170
|
+
for (const m of text.matchAll(/\$(__[A-Za-z0-9_]+)/g)) {
|
|
171
|
+
const d = m[1]
|
|
172
|
+
if (d !== name && stdlib[d] && !seen.has(d)) { seen.add(d); found.push(d) }
|
|
173
|
+
}
|
|
174
|
+
autoCache.set(name, found)
|
|
175
|
+
return found
|
|
176
|
+
}
|
|
147
177
|
let changed = true
|
|
148
178
|
while (changed) {
|
|
149
179
|
changed = false
|
|
150
180
|
for (const name of [...ctx.core.includes]) {
|
|
151
181
|
const entry = graph[name]
|
|
152
182
|
const deps = typeof entry === 'function' ? entry() : entry
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
changed = true
|
|
157
|
-
}
|
|
158
|
-
}
|
|
183
|
+
const add = (dep) => { if (!ctx.core.includes.has(dep)) { ctx.core.includes.add(dep); changed = true } }
|
|
184
|
+
if (deps) for (const dep of deps) add(dep)
|
|
185
|
+
for (const dep of autoDepsOf(name)) add(dep)
|
|
159
186
|
}
|
|
160
187
|
}
|
|
161
188
|
}
|
|
@@ -241,6 +268,7 @@ export function reset(proto, globals, bridge) {
|
|
|
241
268
|
ctx.types = {
|
|
242
269
|
typedElem: null,
|
|
243
270
|
dynKeyVars: null,
|
|
271
|
+
dynWriteVars: null,
|
|
244
272
|
anyDynKey: false,
|
|
245
273
|
}
|
|
246
274
|
|
|
@@ -316,6 +344,7 @@ export function reset(proto, globals, bridge) {
|
|
|
316
344
|
ctx.memory = {
|
|
317
345
|
shared: false,
|
|
318
346
|
pages: 0,
|
|
347
|
+
max: 0, // 0 = unbounded; >0 emits a maximum on the memory type (cap growth)
|
|
319
348
|
}
|
|
320
349
|
|
|
321
350
|
ctx.error = {
|
|
@@ -440,6 +469,15 @@ export function warn(code, message, meta = {}, loc = null) {
|
|
|
440
469
|
ctx.warnings.sink.entries.push(entry)
|
|
441
470
|
}
|
|
442
471
|
|
|
472
|
+
/** Advise that an emit site fell back to generic runtime dispatch (the slow,
|
|
473
|
+
* un-inferred path). Called from the actual emission point so it fires only when
|
|
474
|
+
* inference/optimization truly couldn't fold it — never a false positive on a
|
|
475
|
+
* case that vectorized/unrolled/slot-folded. `ctx.error.loc` is the current AST
|
|
476
|
+
* node's byte offset (kept up to date by the emit walk), giving line/column. */
|
|
477
|
+
export function warnDeopt(code, message) {
|
|
478
|
+
warn(code, message, { fn: ctx.func.current?.name }, ctx.error.loc)
|
|
479
|
+
}
|
|
480
|
+
|
|
443
481
|
/** Throw with source location context. */
|
|
444
482
|
export function err(msg, cause) {
|
|
445
483
|
let detail = msg
|
package/src/ir.js
CHANGED
|
@@ -55,6 +55,15 @@ function boxPtrIR(i32node, ptrType, aux = 0) {
|
|
|
55
55
|
* `(x >>> 0)` uint32 idiom converts to a positive f64 in [0, 2^32) instead of sign-flipping. */
|
|
56
56
|
export const asF64 = n => {
|
|
57
57
|
if (n == null) err(`compiler internal: expected emitted IR value in ${ctx.func.current?.name || '<module>'}, got empty value`)
|
|
58
|
+
// A v128 (SIMD) value can't be NaN-boxed into the uniform f64 closure ABI — there is no
|
|
59
|
+
// lossless f64 carrier for 128 bits. This is reached only at a closure boundary: a SIMD
|
|
60
|
+
// value captured by, passed to, returned from, or flowing through a `(…)=>…` used as a
|
|
61
|
+
// VALUE — most commonly an IIFE `(() => f32x4.…)()`, which jz lowers via the closure path.
|
|
62
|
+
// Without this guard the coercion emits `f64.convert_i32_s(<v128>)` and dies in the wasm
|
|
63
|
+
// validator with an opaque type error. Keep SIMD inside a NAMED top-level function called
|
|
64
|
+
// directly (`let sdf = (x) => f32x4.…; sdf(v)` lowers to a typed v128 `call`, no boxing),
|
|
65
|
+
// and extract scalars with `f64x2.lane` / `f32x4.lane` before crossing a closure boundary.
|
|
66
|
+
if (n.type === 'v128') err(`SIMD (v128) values can't cross a closure/IIFE boundary — closures use the uniform f64 ABI. Move the SIMD into a named top-level function called directly, or extract a lane (f64x2.lane / f32x4.lane) to an f64 first.`)
|
|
58
67
|
if (n.ptrKind != null) return boxPtrIR(n, valKindToPtr(n.ptrKind), n.ptrAux || 0)
|
|
59
68
|
if (n.type === 'f64') return n
|
|
60
69
|
if (n.type === 'i64') {
|
|
@@ -97,6 +106,22 @@ export const asPtrOffset = (n, ptrKind) => {
|
|
|
97
106
|
/** Coerce emitted IR to a target WASM param type ('i32' | 'i64' | 'f64'). */
|
|
98
107
|
export const asParamType = (n, t) => t === 'i32' ? asI32(n) : t === 'i64' ? asI64(n) : t === 'v128' ? n : asF64(n)
|
|
99
108
|
|
|
109
|
+
// Sound upper bound on the value of a masking expr (`&` / `>>>`), so a product
|
|
110
|
+
// against it can be proven < 2^53 and narrow to i32.mul instead of the guarded f64
|
|
111
|
+
// path. `& m` with a non-negative mask m clamps the result to [0, m] (regardless of
|
|
112
|
+
// the other operand's sign); `>>> k` is logical, so it's ≤ 2^(32−k). Anything else
|
|
113
|
+
// (signed shift, plain locals, negative mask) stays the full i32 range.
|
|
114
|
+
export const maskBound = (x) => {
|
|
115
|
+
if (!Array.isArray(x)) return 2 ** 31
|
|
116
|
+
if (x[0] === 'i32.const') return x[1] >= 0 ? x[1] : 2 ** 31
|
|
117
|
+
if (x[0] === 'i32.and') return Math.min(maskBound(x[1]), maskBound(x[2]))
|
|
118
|
+
if (x[0] === 'i32.shr_u') {
|
|
119
|
+
const k = Array.isArray(x[2]) && x[2][0] === 'i32.const' ? (x[2][1] & 31) : 0
|
|
120
|
+
return k > 0 ? 2 ** (32 - k) : 2 ** 31
|
|
121
|
+
}
|
|
122
|
+
return 2 ** 31
|
|
123
|
+
}
|
|
124
|
+
|
|
100
125
|
/**
|
|
101
126
|
* Narrow an f64 arithmetic tree under ToInt32 — the general int-accumulator path.
|
|
102
127
|
*
|
|
@@ -459,7 +484,7 @@ const PURE_F64_OPS = new Set([
|
|
|
459
484
|
* is correctly NOT numeric, while `cond ? n*2 : n*3` is. Conservative: any shape
|
|
460
485
|
* not provably numeric (property gets, user calls, local.get, f64.const nan:…)
|
|
461
486
|
* returns false, so the caller keeps the __to_num coercion. */
|
|
462
|
-
const isNumericIR = (r) => {
|
|
487
|
+
export const isNumericIR = (r) => {
|
|
463
488
|
if (!Array.isArray(r)) return false
|
|
464
489
|
const op = r[0]
|
|
465
490
|
if (PURE_F64_OPS.has(op)) return true
|
|
@@ -550,19 +575,21 @@ export function buildRefcount(fn) {
|
|
|
550
575
|
* existing ids in a single walk. Replaces the per-pass
|
|
551
576
|
* `while (fn.some(... === $__prefixK)) k++` (O(K·N)) with one O(N) scan. */
|
|
552
577
|
export function nextLocalId(fn, prefix) {
|
|
553
|
-
|
|
578
|
+
// HIGH-WATER mark (max existing + 1), NOT the first free id. Callers allocate sequentially
|
|
579
|
+
// (id++), so a first-gap start would walk straight into an existing higher local once watr's
|
|
580
|
+
// coalesce has left non-contiguous numbering (e.g. $__pe0,$__pe1,$__pe5 → start at 2, then
|
|
581
|
+
// mint 3,4,5 and collide on $__pe5 = "Duplicate local"). High-water is always collision-free.
|
|
554
582
|
const needle = `$__${prefix}`
|
|
583
|
+
let id = 0
|
|
555
584
|
const walk = (n) => {
|
|
556
585
|
if (!Array.isArray(n)) return
|
|
557
586
|
if (n[0] === 'local' && typeof n[1] === 'string' && n[1].startsWith(needle)) {
|
|
558
587
|
const tail = n[1].slice(needle.length)
|
|
559
|
-
if (/^\d+$/.test(tail))
|
|
588
|
+
if (/^\d+$/.test(tail)) { const k = +tail; if (k >= id) id = k + 1 }
|
|
560
589
|
}
|
|
561
590
|
for (let i = 0; i < n.length; i++) walk(n[i])
|
|
562
591
|
}
|
|
563
592
|
walk(fn)
|
|
564
|
-
let id = 0
|
|
565
|
-
while (seen.has(id)) id++
|
|
566
593
|
return id
|
|
567
594
|
}
|
|
568
595
|
|
|
@@ -731,10 +758,15 @@ export function toNumF64(node, v) {
|
|
|
731
758
|
if (ctx.schema.slotIntCertainAt?.(node[1], node[2]) === true) return asF64(v)
|
|
732
759
|
}
|
|
733
760
|
// IR-level shapes that produce real f64 numbers (never NaN-boxed pointers):
|
|
734
|
-
// i32→f64 conversions, stdlib clock helper
|
|
761
|
+
// i32→f64 conversions, stdlib clock helper, length/ptr helpers.
|
|
762
|
+
// Skip the __to_num call wrapper for these — they always return plain f64.
|
|
735
763
|
if (Array.isArray(v)) {
|
|
736
764
|
if (v[0] === 'f64.convert_i32_s' || v[0] === 'f64.convert_i32_u') return v
|
|
737
765
|
if (v[0] === 'call' && v[1] === '$__time_ms') return v
|
|
766
|
+
// __length, __str_len return f64.convert_i32_s of an i32 — never a boxed pointer.
|
|
767
|
+
if (v[0] === 'call' && (v[1] === '$__length' || v[1] === '$__len' || v[1] === '$__str_len')) return v
|
|
768
|
+
// __ptr_type returns i32 tag, __ptr_offset returns i32 offset — both numeric.
|
|
769
|
+
if (v[0] === 'call' && (v[1] === '$__ptr_type' || v[1] === '$__ptr_offset')) return v
|
|
738
770
|
}
|
|
739
771
|
// f64 arithmetic ops and math intrinsics never produce NaN-boxed pointers — the
|
|
740
772
|
// result is always a plain f64 number. Skip __to_num for these, eliminating the
|
|
@@ -785,6 +817,14 @@ export function toStrI64(node, v) {
|
|
|
785
817
|
return typed(['call', '$__to_str', prim], 'i64')
|
|
786
818
|
}
|
|
787
819
|
}
|
|
820
|
+
// Provably-integer operand → render with the i32-only formatter, bypassing __to_str's
|
|
821
|
+
// float machinery (__ftoa/__toExp/__pow10, ~2 KB). A raw i32 value (`n|0`, a bitwise
|
|
822
|
+
// result, a loop counter) carries no NaN-box, so its ToString is just digits + sign.
|
|
823
|
+
// ptrKind != null means it's an unboxed pointer (i32 offset), NOT a number — exclude.
|
|
824
|
+
if (v.type === 'i32' && v.ptrKind == null) {
|
|
825
|
+
inc('__i32_to_str')
|
|
826
|
+
return typed(['i64.reinterpret_f64', ['call', '$__i32_to_str', v]], 'i64')
|
|
827
|
+
}
|
|
788
828
|
inc('__to_str')
|
|
789
829
|
return typed(['call', '$__to_str', asI64(v)], 'i64')
|
|
790
830
|
}
|
|
@@ -809,8 +849,16 @@ const numericTruthy = e => {
|
|
|
809
849
|
|
|
810
850
|
// i32 ops whose result is already a 0/1 boolean (comparisons + eqz) — safe to use
|
|
811
851
|
// directly as a truthiness without a redundant `!= 0`.
|
|
852
|
+
// Ops whose result is already a canonical i32 boolean (0 or 1) — a condition built
|
|
853
|
+
// from one needs no `i32.ne(_, 0)` normalization. Every wasm comparison returns 0/1,
|
|
854
|
+
// so the f64/f32/i64 relations belong here too (they were missing — a `a > b ? …`
|
|
855
|
+
// f64 compare was wrapped in a dead `i32.ne(f64.gt …, 0)` in every branch/select).
|
|
812
856
|
const I32_BOOL_OPS = new Set(['i32.eq', 'i32.ne', 'i32.lt_s', 'i32.lt_u', 'i32.gt_s', 'i32.gt_u',
|
|
813
|
-
'i32.le_s', 'i32.le_u', 'i32.ge_s', 'i32.ge_u', 'i32.eqz'
|
|
857
|
+
'i32.le_s', 'i32.le_u', 'i32.ge_s', 'i32.ge_u', 'i32.eqz',
|
|
858
|
+
'f64.eq', 'f64.ne', 'f64.lt', 'f64.gt', 'f64.le', 'f64.ge',
|
|
859
|
+
'f32.eq', 'f32.ne', 'f32.lt', 'f32.gt', 'f32.le', 'f32.ge',
|
|
860
|
+
'i64.eq', 'i64.ne', 'i64.lt_s', 'i64.lt_u', 'i64.gt_s', 'i64.gt_u',
|
|
861
|
+
'i64.le_s', 'i64.le_u', 'i64.ge_s', 'i64.ge_u', 'i64.eqz'])
|
|
814
862
|
|
|
815
863
|
export function truthyIR(e) {
|
|
816
864
|
// An i32 *constant* is a concrete number, not a known 0/1 boolean — fold it to its
|
package/src/kind-traits.js
CHANGED
|
@@ -94,9 +94,36 @@ export function methodValType(method, obj, objType, ctx) {
|
|
|
94
94
|
if (objType === VAL.STRING || objType === VAL.ARRAY || objType === VAL.TYPED) return objType
|
|
95
95
|
return null
|
|
96
96
|
}
|
|
97
|
+
// .subarray / .toReversed / .toSorted / .with all return a typed array of the same kind.
|
|
98
|
+
if (method === 'subarray' || method === 'toReversed' || method === 'toSorted' || method === 'with')
|
|
99
|
+
return objType === VAL.TYPED ? VAL.TYPED : null
|
|
97
100
|
return null
|
|
98
101
|
}
|
|
99
102
|
|
|
103
|
+
// Built-in PROPERTY val-types — the property-read mirror of methodValType.
|
|
104
|
+
// These are language invariants: `.length` is always a number on the sized
|
|
105
|
+
// value kinds, `.size` on Set/Map, `.byteLength`/`.byteOffset` on typed arrays.
|
|
106
|
+
// Without this, `arr.length + x` sees `.length` as untyped and routes `+`
|
|
107
|
+
// through the __is_str_key string-concat dispatch — even though `.length` can
|
|
108
|
+
// never be a string on a known sized kind. (Object schema slots override this
|
|
109
|
+
// earlier in VT['.'], so `{length:'x'}.length` keeps its true slot type.)
|
|
110
|
+
//
|
|
111
|
+
// Gate on a known objType: an untyped receiver could be an object with a
|
|
112
|
+
// string-valued shadow of the same name, so leave it null there (conservative).
|
|
113
|
+
// null-proto: user code reads `.valueOf`, `.toString` etc. — a plain `{}` would
|
|
114
|
+
// expose inherited Object.prototype members as bogus "numeric props".
|
|
115
|
+
const NUMERIC_PROPS = Object.assign(Object.create(null), {
|
|
116
|
+
length: new Set([VAL.STRING, VAL.ARRAY, VAL.TYPED]),
|
|
117
|
+
byteLength: new Set([VAL.TYPED, VAL.BUFFER]),
|
|
118
|
+
byteOffset: new Set([VAL.TYPED]),
|
|
119
|
+
size: new Set([VAL.SET, VAL.MAP]),
|
|
120
|
+
})
|
|
121
|
+
export function propValType(prop, objType) {
|
|
122
|
+
if (objType == null) return null
|
|
123
|
+
const kinds = NUMERIC_PROPS[prop]
|
|
124
|
+
return kinds && kinds.has(objType) ? VAL.NUMBER : null
|
|
125
|
+
}
|
|
126
|
+
|
|
100
127
|
export function typedCtorElemValType(ctor) {
|
|
101
128
|
if (!ctor) return null
|
|
102
129
|
const isView = ctor.endsWith('.view')
|
package/src/kind.js
CHANGED
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
|
|
9
9
|
import { ctx } from './ctx.js'
|
|
10
10
|
import { VAL, lookupValType } from './reps.js'
|
|
11
|
-
import { intLiteralValue } from './static.js'
|
|
11
|
+
import { intLiteralValue, staticIndexKey } from './static.js'
|
|
12
12
|
import {
|
|
13
13
|
BOOL_OPS, NUMERIC_BINARY_OPS, NUMERIC_UNARY_OPS, COMPOUND_NUMERIC_OPS,
|
|
14
|
-
calleeValType, methodValType, typedCtorElemValType,
|
|
14
|
+
calleeValType, methodValType, propValType, typedCtorElemValType,
|
|
15
15
|
} from './kind-traits.js'
|
|
16
16
|
|
|
17
17
|
export { typedCtorElemValType } from './kind-traits.js'
|
|
@@ -145,7 +145,11 @@ VT['?:'] = (args) => {
|
|
|
145
145
|
// (a condition/guard) and the other has a known non-boolean type,
|
|
146
146
|
// return the non-boolean type — common in `condition && numericValue`
|
|
147
147
|
// guard patterns where the falsey boolean is coerced to 0 in numeric context.
|
|
148
|
-
|
|
148
|
+
// `a && b` / `a || b` / `a ?? b` all yield one of the two operands, so the result
|
|
149
|
+
// type is their common type (else unknown). Giving `??` a type — not just ||/&& —
|
|
150
|
+
// lets `numA ?? numB` read NaN-safe (value-typed NUMBER → f64.eq) instead of routing
|
|
151
|
+
// through the bit-comparing __is_truthy, which mis-reads a non-canonical NaN.
|
|
152
|
+
VT['&&'] = VT['||'] = VT['??'] = (args) => {
|
|
149
153
|
const ta = valTypeOf(args[0]), tb = valTypeOf(args[1])
|
|
150
154
|
if (ta && ta === tb) return ta
|
|
151
155
|
if (ta === VAL.BOOL && tb && tb !== VAL.BOOL) return tb
|
|
@@ -158,6 +162,20 @@ VT['&&'] = VT['||'] = (args) => {
|
|
|
158
162
|
// Index access: `arr[i]` → ['[]', arr, i].
|
|
159
163
|
VT['[]'] = (args) => {
|
|
160
164
|
if (args.length < 2) return VAL.ARRAY
|
|
165
|
+
// SRoA flat-array slot read: `a[k]` (static index) where `a` dissolved into
|
|
166
|
+
// scalar `a#i` locals (scanFlatObjects). A write-once slot's value-type is its
|
|
167
|
+
// element literal's — same numeric-binding as the `VT['.']` object case, so
|
|
168
|
+
// `a[0] * 2` stays a plain f64 op instead of the polymorphic ToNumber battery.
|
|
169
|
+
if (typeof args[0] === 'string') {
|
|
170
|
+
const flat = ctx.func.flatObjects?.get(args[0])
|
|
171
|
+
if (flat) {
|
|
172
|
+
const k = staticIndexKey(args[1])
|
|
173
|
+
if (k != null && !flat.written?.has(k)) {
|
|
174
|
+
const i = flat.names.indexOf(k)
|
|
175
|
+
if (i >= 0 && flat.values[i] !== undefined) return valTypeOf(flat.values[i])
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
161
179
|
// Indexed read on a known typed-array receiver yields Number except for
|
|
162
180
|
// BigInt64Array/BigUint64Array, whose i64 carriers must stay BigInt-typed.
|
|
163
181
|
if (typeof args[0] === 'string' && lookupValType(args[0]) === VAL.TYPED)
|
|
@@ -170,6 +188,25 @@ VT['[]'] = (args) => {
|
|
|
170
188
|
if (typeof args[0] === 'string') {
|
|
171
189
|
const elemVt = ctx.func.localReps?.get(args[0])?.arrayElemValType
|
|
172
190
|
if (elemVt) return elemVt
|
|
191
|
+
// Module-level const array (a numeric/uniform table): its element val-type was
|
|
192
|
+
// recorded on the global rep at decl time. Trust it only when no function element-
|
|
193
|
+
// writes the array — dynWriteVars holds every var written via a non-named-property
|
|
194
|
+
// index, so a `X[i]=str` anywhere disables this and falls back to the untyped read.
|
|
195
|
+
if (!ctx.func.localReps?.has(args[0])) {
|
|
196
|
+
const gElem = ctx.scope.globalReps?.get(args[0])?.arrayElemValType
|
|
197
|
+
if (gElem && !ctx.types?.dynWriteVars?.has(args[0])) return gElem
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// Direct double-index on a module-level nested numeric table — `C[i][j]` where
|
|
201
|
+
// `C = [[…number…], …]`. The receiver is itself a single-index read of a global
|
|
202
|
+
// array whose nested element kind was recorded at decl time. Same dynWriteVars
|
|
203
|
+
// guard (now root-aware, so a `C[i][j]=…` write anywhere disables it).
|
|
204
|
+
if (Array.isArray(args[0]) && args[0][0] === '[]' && args[0].length === 3 && typeof args[0][1] === 'string') {
|
|
205
|
+
const base = args[0][1]
|
|
206
|
+
if (!ctx.func.localReps?.has(base)) {
|
|
207
|
+
const gNested = ctx.scope.globalReps?.get(base)?.arrayElemElemValType
|
|
208
|
+
if (gNested && !ctx.types?.dynWriteVars?.has(base)) return gNested
|
|
209
|
+
}
|
|
173
210
|
}
|
|
174
211
|
// Indexed read on an inline all-numeric array literal — `[2,4,2,9][i]` (floatbeat
|
|
175
212
|
// chord/pattern tables; literal op is `[`, elements inline). Every element is a
|
|
@@ -183,6 +220,21 @@ VT['[]'] = (args) => {
|
|
|
183
220
|
|
|
184
221
|
VT['.'] = (args) => {
|
|
185
222
|
if (typeof args[1] !== 'string') return null
|
|
223
|
+
// SRoA flat-object slot read: `p.x` where `p` dissolved into scalar `p#i`
|
|
224
|
+
// locals (scanFlatObjects). A write-once slot's value-type IS its literal
|
|
225
|
+
// initializer's, so bind by it — exactly as a plain `let slot = value` local
|
|
226
|
+
// would. Without this `p.x * 2` looks like "could be anything" and pulls the
|
|
227
|
+
// ToNumber + string-format battery, though it can only be numeric. Computed
|
|
228
|
+
// on-demand (not cached at analyze time) because param val-types — `{x:n}`'s
|
|
229
|
+
// `n` is numeric-by-divergence — are only seeded at emit. A reassigned slot
|
|
230
|
+
// (`p.x = …`) stays untyped: its runtime value may differ from the literal.
|
|
231
|
+
if (typeof args[0] === 'string') {
|
|
232
|
+
const flat = ctx.func.flatObjects?.get(args[0])
|
|
233
|
+
if (flat && !flat.written?.has(args[1])) {
|
|
234
|
+
const i = flat.names.indexOf(args[1])
|
|
235
|
+
if (i >= 0 && flat.values[i] !== undefined) return valTypeOf(flat.values[i])
|
|
236
|
+
}
|
|
237
|
+
}
|
|
186
238
|
// Schema slot read: when `varName` has a bound schemaId and `.prop` resolves
|
|
187
239
|
// to a slot whose VAL kind is monomorphic across program-wide observations,
|
|
188
240
|
// return that kind. Lets `+`, `===`, method dispatch skip runtime str-key
|
|
@@ -200,6 +252,14 @@ VT['.'] = (args) => {
|
|
|
200
252
|
const child = sh.props[args[1]]
|
|
201
253
|
if (child) return child.val
|
|
202
254
|
}
|
|
255
|
+
// Built-in property on a known sized kind — `.length` on STRING/ARRAY/TYPED,
|
|
256
|
+
// `.size` on SET/MAP, `.byteLength`/`.byteOffset` on TYPED/BUFFER. These are
|
|
257
|
+
// language invariants (the property is always a number on that kind), so typing
|
|
258
|
+
// them NUMBER lets `+` skip the string-concat dispatch. Object schema slots
|
|
259
|
+
// resolved above override this, keeping user-defined same-name slots sound.
|
|
260
|
+
const objType = typeof args[0] === 'string' ? lookupValType(args[0]) : valTypeOf(args[0])
|
|
261
|
+
const pvt = propValType(args[1], objType)
|
|
262
|
+
if (pvt) return pvt
|
|
203
263
|
return null
|
|
204
264
|
}
|
|
205
265
|
|
|
@@ -248,6 +308,8 @@ VT['()'] = (args) => {
|
|
|
248
308
|
const t = valTypeOf(args[1])
|
|
249
309
|
return t === VAL.SET || t === VAL.MAP ? VAL.ARRAY : t
|
|
250
310
|
}
|
|
311
|
+
// for-in's read-only key list (src/prepare) — always an Array of key strings.
|
|
312
|
+
if (callee === '__keys_ro') return VAL.ARRAY
|
|
251
313
|
// Ternary is parsed as call to '?' operator: ['()', ['?', cond, a, b]]
|
|
252
314
|
if (Array.isArray(callee) && callee[0] === '?') {
|
|
253
315
|
const truthy = literalTruthiness(callee[1])
|