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/package.json
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jz",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "Ahead-of-time compiler for the numeric core of JavaScript (DSP, audio, math, parsers) to lean GC-free WASM — valid jz is valid JS, no type annotations, no runtime.",
|
|
5
5
|
"main": "index.js",
|
|
6
|
+
"types": "index.d.ts",
|
|
6
7
|
"type": "module",
|
|
7
8
|
"exports": {
|
|
8
|
-
".":
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./index.d.ts",
|
|
11
|
+
"default": "./index.js"
|
|
12
|
+
},
|
|
9
13
|
"./wasi": "./wasi.js",
|
|
10
14
|
"./interop": "./interop.js",
|
|
11
15
|
"./transform": "./transform.js"
|
|
12
16
|
},
|
|
13
17
|
"files": [
|
|
14
18
|
"index.js",
|
|
19
|
+
"index.d.ts",
|
|
15
20
|
"cli.js",
|
|
16
21
|
"wasi.js",
|
|
17
22
|
"interop.js",
|
|
@@ -20,6 +25,8 @@
|
|
|
20
25
|
"jzify",
|
|
21
26
|
"src",
|
|
22
27
|
"module",
|
|
28
|
+
"dist/jz.js",
|
|
29
|
+
"dist/interop.js",
|
|
23
30
|
"README.md",
|
|
24
31
|
"jz.svg",
|
|
25
32
|
"alternatives.svg",
|
|
@@ -48,9 +55,10 @@
|
|
|
48
55
|
"bench:size": "node scripts/bench-size.mjs",
|
|
49
56
|
"bench:compile": "node scripts/bench-compile.mjs",
|
|
50
57
|
"bench:startup": "node scripts/bench-startup.mjs",
|
|
58
|
+
"bench:readme": "node scripts/bench-readme.mjs",
|
|
51
59
|
"build": "node scripts/build-dist.mjs",
|
|
52
|
-
"build:examples": "
|
|
53
|
-
"
|
|
60
|
+
"build:examples": "node examples/build.mjs all",
|
|
61
|
+
"prepare": "npm run build"
|
|
54
62
|
},
|
|
55
63
|
"repository": {
|
|
56
64
|
"type": "git",
|
|
@@ -81,10 +89,17 @@
|
|
|
81
89
|
"audio",
|
|
82
90
|
"creative-coding",
|
|
83
91
|
"assemblyscript",
|
|
84
|
-
"porffor"
|
|
92
|
+
"porffor",
|
|
93
|
+
"numeric",
|
|
94
|
+
"signal-processing",
|
|
95
|
+
"math",
|
|
96
|
+
"audio-worklet",
|
|
97
|
+
"js-to-wasm",
|
|
98
|
+
"performance"
|
|
85
99
|
],
|
|
86
100
|
"devDependencies": {
|
|
87
101
|
"esbuild": "^0.28.0",
|
|
102
|
+
"sprae": "^13.3.8",
|
|
88
103
|
"tst": "^9.4.0"
|
|
89
104
|
}
|
|
90
105
|
}
|
package/src/autoload.js
CHANGED
|
@@ -127,6 +127,9 @@ const MOD_DEPS = {
|
|
|
127
127
|
date: ['core', 'number', 'string'],
|
|
128
128
|
console: ['core', 'string', 'number'],
|
|
129
129
|
regex: ['core', 'string', 'array'],
|
|
130
|
+
// f64x2.sin/cos lower to math's $math.sin2/$math.cos2 WAT helpers; pull math in so
|
|
131
|
+
// they're registered. Unused math helpers are pruned by reachability — no output cost.
|
|
132
|
+
simd: ['math'],
|
|
130
133
|
}
|
|
131
134
|
|
|
132
135
|
export function includeModule(name) {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { ASSIGN_OPS, collectParamNames, extractParams, REFS_IN_EXPR, refsName, T, isLiteralStr } from '../ast.js'
|
|
7
7
|
import { ctx } from '../ctx.js'
|
|
8
|
-
import { staticObjectProps } from '../static.js'
|
|
8
|
+
import { staticObjectProps, staticArrayElems, staticIndexKey, staticValue, NO_VALUE } from '../static.js'
|
|
9
9
|
import { exprType } from '../type.js'
|
|
10
10
|
|
|
11
11
|
export function findFreeVars(node, bound, free, scope) {
|
|
@@ -168,7 +168,7 @@ export function scanBindingUses(body) {
|
|
|
168
168
|
const use = (name, kind, extra) => slot(name).uses.push(extra ? { kind, ...extra } : { kind })
|
|
169
169
|
|
|
170
170
|
// Static string key of a `[]` index node, else null (computed).
|
|
171
|
-
const litKey = (k) => (Array.isArray(k) && k[0] === 'str' && typeof k[1] === 'string') ? k[1] :
|
|
171
|
+
const litKey = (k) => (Array.isArray(k) && k[0] === 'str' && typeof k[1] === 'string') ? k[1] : staticIndexKey(k)
|
|
172
172
|
|
|
173
173
|
// A child sitting in a value position. A bare string there is a real use —
|
|
174
174
|
// `walk` alone silently drops non-array children, so every value-position
|
|
@@ -350,6 +350,10 @@ export function scanBindingUses(body) {
|
|
|
350
350
|
* Field `i` of binding `o` lives in WASM local `o#${i}` (`#` cannot occur in a
|
|
351
351
|
* jz identifier, so the name is collision-free).
|
|
352
352
|
*/
|
|
353
|
+
// Largest array literal that dissolves into scalar slots. Beyond this a single
|
|
354
|
+
// constant data segment is cheaper than N locals (+ the per-slot init prologue).
|
|
355
|
+
const FLAT_ARRAY_MAX = 8
|
|
356
|
+
|
|
353
357
|
export function scanFlatObjects(body) {
|
|
354
358
|
const cand = new Map() // name → {names, values}
|
|
355
359
|
|
|
@@ -357,19 +361,48 @@ export function scanFlatObjects(body) {
|
|
|
357
361
|
// slots). Used only to reject a self-referential initializer — a literal
|
|
358
362
|
// whose own field values mention the binding is not a self-contained object.
|
|
359
363
|
for (const [name, s] of scanBindingUses(body)) {
|
|
360
|
-
if (s.decls !== 1 || !Array.isArray(s.initRhs)
|
|
361
|
-
|
|
364
|
+
if (s.decls !== 1 || !Array.isArray(s.initRhs)) continue
|
|
365
|
+
// Candidate aggregate: an object literal `{…}` (string keys) or a small array
|
|
366
|
+
// literal `[…]` (index keys "0","1",…). An array dissolves into `name#i` scalar
|
|
367
|
+
// locals exactly like an object — same `.`/`[]` flat hooks, no heap alloc — when
|
|
368
|
+
// every use is a static-index read/write. Capped at FLAT_ARRAY_MAX: a larger
|
|
369
|
+
// literal belongs in one constant data-segment region, not N spilled locals.
|
|
370
|
+
let props
|
|
371
|
+
if (s.initRhs[0] === '{}') {
|
|
372
|
+
props = staticObjectProps(s.initRhs.slice(1))
|
|
373
|
+
} else if (s.initRhs[0] === '[' || s.initRhs[0] === '[]') {
|
|
374
|
+
const elems = staticArrayElems(s.initRhs)
|
|
375
|
+
if (!elems || !elems.length || elems.length > FLAT_ARRAY_MAX) continue
|
|
376
|
+
// Holes (`[1,,3]`) and spreads (`[...x]`) aren't a fixed positional schema.
|
|
377
|
+
if (elems.some(e => e == null || (Array.isArray(e) && e[0] === '...'))) continue
|
|
378
|
+
// Only compile-time-constant *value* elements dissolve — number/string/bool/null
|
|
379
|
+
// ("arrays hold JSON values"). A non-literal element (identifier, call, closure,
|
|
380
|
+
// arithmetic on a runtime var) can carry a function/closure whose call-indirect
|
|
381
|
+
// table index binds to the array, not a scalar local — dissolving the slot
|
|
382
|
+
// desyncs the `elem` section. Conservative: any non-constant element keeps the
|
|
383
|
+
// array heap-backed.
|
|
384
|
+
if (!elems.every(e => staticValue(e) !== NO_VALUE)) continue
|
|
385
|
+
props = { names: elems.map((_, i) => String(i)), values: elems }
|
|
386
|
+
} else continue
|
|
387
|
+
const isArr = s.initRhs[0] !== '{}'
|
|
362
388
|
if (!props || new Set(props.names).size !== props.names.length) continue
|
|
363
389
|
if (props.values.some(v => refsName(v, name, REFS_IN_EXPR))) continue
|
|
364
390
|
|
|
365
|
-
// Schema = literal keys ∪ plain literal-key member writes.
|
|
366
|
-
// monotonically extends the static field universe (the new field reads
|
|
367
|
-
// `undefined` until the write runs, exactly as JS does)
|
|
368
|
-
//
|
|
391
|
+
// Schema = literal keys ∪ plain literal-key member writes. For an OBJECT such a
|
|
392
|
+
// write monotonically extends the static field universe (the new field reads
|
|
393
|
+
// `undefined` until the write runs, exactly as JS does). An ARRAY has a *fixed*
|
|
394
|
+
// positional schema: `a.length = …` / `a[n] = …` (off the literal indices) resize
|
|
395
|
+
// or grow it — not a field add — so arrays never extend, and any off-schema write
|
|
396
|
+
// (including `.length`, which isn't a slot) disqualifies below.
|
|
397
|
+
// `written` = the keys a MEMBER_W reassigns — a slot is write-once (its
|
|
398
|
+
// value-type is exactly its literal initializer's) iff its key is absent here.
|
|
369
399
|
const schema = new Set(props.names)
|
|
400
|
+
const written = new Set()
|
|
370
401
|
for (const u of s.uses)
|
|
371
|
-
if (u.kind === USE.MEMBER_W && !u.compound && !u.computed && u.key != null)
|
|
372
|
-
schema.add(u.key)
|
|
402
|
+
if (u.kind === USE.MEMBER_W && !u.compound && !u.computed && u.key != null) {
|
|
403
|
+
if (!isArr) schema.add(u.key)
|
|
404
|
+
written.add(u.key)
|
|
405
|
+
}
|
|
373
406
|
|
|
374
407
|
// Flat iff every mention is an in-schema literal-key `.`/`[]` READ, or an
|
|
375
408
|
// in-schema literal-key plain `.`/`[]` WRITE. Any other use kind — `?.`,
|
|
@@ -385,7 +418,7 @@ export function scanFlatObjects(body) {
|
|
|
385
418
|
const names = props.names.slice(), values = props.values.slice()
|
|
386
419
|
for (const k of schema)
|
|
387
420
|
if (!names.includes(k)) { names.push(k); values.push(undefined) }
|
|
388
|
-
cand.set(name, { names, values })
|
|
421
|
+
cand.set(name, { names, values, written })
|
|
389
422
|
}
|
|
390
423
|
return cand
|
|
391
424
|
}
|
|
@@ -432,6 +465,136 @@ export function scanSliceViews(body) {
|
|
|
432
465
|
return views
|
|
433
466
|
}
|
|
434
467
|
|
|
468
|
+
/**
|
|
469
|
+
* Never-relocated array bindings — reads through them may skip the realloc-forwarding
|
|
470
|
+
* follow (`__ptr_offset`). A fresh array-literal binding is never relocated iff EVERY
|
|
471
|
+
* occurrence of it is a pure READ — `a[i]` (any index) or `a.length`. Anything else
|
|
472
|
+
* grows or escapes it: a grow method (push/unshift/shift/splice), a `.length`/element
|
|
473
|
+
* write (incl. compound `a.length += 1`), a bare value use (alias `let b=a`, store
|
|
474
|
+
* `w.x=a`, return, call argument, spread), a reassignment, or a dynamic call
|
|
475
|
+
* `a[i]()`/`a.m()`.
|
|
476
|
+
*
|
|
477
|
+
* MEMORY-SAFETY CRITICAL and so DEFAULT-DENY + self-contained: it does NOT trust the
|
|
478
|
+
* `escapes` map, which misses member-write RHS (`w.data = a`) and compound assigns. If
|
|
479
|
+
* the analysis is wrong and the array IS relocated, a read through the stale base
|
|
480
|
+
* corrupts memory — so any unrecognized use disqualifies. (Growing an INNER array,
|
|
481
|
+
* `a[0].push(x)`, never relocates `a` itself, so `a` stays eligible — see safeReads.)
|
|
482
|
+
*/
|
|
483
|
+
const grownOrEscapes = (op) => ASSIGN_OPS.has(op) || op === '++' || op === '--' || op === 'delete'
|
|
484
|
+
function safeReads(node, name) {
|
|
485
|
+
if (typeof node === 'string') return node !== name // bare value use → escape
|
|
486
|
+
if (!Array.isArray(node)) return true
|
|
487
|
+
const op = node[0]
|
|
488
|
+
// `a(…)` / `a.m(…)` / `a[i](…)` — calling `a` or a method/element of it may grow/escape it.
|
|
489
|
+
if (op === '()') {
|
|
490
|
+
const c = node[1]
|
|
491
|
+
if (c === name) return false
|
|
492
|
+
if (Array.isArray(c) && (c[0] === '.' || c[0] === '?.' || c[0] === '[]' || c[0] === '?.[]') && c[1] === name) return false
|
|
493
|
+
}
|
|
494
|
+
// write / update / delete on `a`, `a[..]`, or `a.x` (incl. `a.length = …` and compounds)
|
|
495
|
+
if (grownOrEscapes(op)) {
|
|
496
|
+
const t = node[1]
|
|
497
|
+
if (t === name) return false
|
|
498
|
+
if (Array.isArray(t) && (t[0] === '[]' || t[0] === '.' || t[0] === '?.') && t[1] === name) return false
|
|
499
|
+
}
|
|
500
|
+
// declaration: check each initializer RHS (so `let b = a` aliasing disqualifies);
|
|
501
|
+
// the bound names themselves are definitions, not uses (skips `a`'s own decl).
|
|
502
|
+
if (op === 'let' || op === 'const' || op === 'var') {
|
|
503
|
+
for (let i = 1; i < node.length; i++) {
|
|
504
|
+
const d = node[i]
|
|
505
|
+
if (Array.isArray(d) && d[0] === '=' && !safeReads(d[2], name)) return false
|
|
506
|
+
}
|
|
507
|
+
return true
|
|
508
|
+
}
|
|
509
|
+
// the only safe forms: `a.length` read, and `a[i]` index read (recurse the index expr).
|
|
510
|
+
if ((op === '.' || op === '?.') && node[1] === name) return node[2] === 'length'
|
|
511
|
+
if (op === '[]' && node[1] === name) return safeReads(node[2], name)
|
|
512
|
+
if (op === '...' && node[1] === name) return false // spread → escape
|
|
513
|
+
for (let i = 1; i < node.length; i++) if (!safeReads(node[i], name)) return false
|
|
514
|
+
return true
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
export function scanNeverGrown(body) {
|
|
518
|
+
const out = new Set()
|
|
519
|
+
for (const [name, s] of scanBindingUses(body)) {
|
|
520
|
+
// Candidate: a single-declaration binding initialized from a fresh array literal.
|
|
521
|
+
if (s.decls !== 1 || !Array.isArray(s.initRhs)) continue
|
|
522
|
+
if (s.initRhs[0] !== '[' && !(s.initRhs[0] === '[]' && s.initRhs.length <= 2)) continue
|
|
523
|
+
if (safeReads(body, name)) out.add(name)
|
|
524
|
+
}
|
|
525
|
+
return out
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Numeric-fill arrays — the construct-then-fill counterpart of an all-number array
|
|
530
|
+
* literal. A fresh `Array(n)` / `new Array(n)` / `[]` binding whose EVERY element write
|
|
531
|
+
* stores a provably-NUMBER value, and which never escapes, aliases, is reassigned, grows
|
|
532
|
+
* by method, or takes a non-numeric / compound element write, holds only Numbers (unwritten
|
|
533
|
+
* holes read as 0 in jz, also a Number). So its `a[i]` reads can skip the polymorphic
|
|
534
|
+
* `__to_num` coercion — exactly the win `[1,2,3]` already gets, extended to the dominant
|
|
535
|
+
* numeric-kernel shape `let a = Array(n); for (..) a[i] = expr`.
|
|
536
|
+
*
|
|
537
|
+
* Default-deny and self-contained, like scanNeverGrown (the same memory-safety discipline):
|
|
538
|
+
* any occurrence that isn't a pure index/length READ or a NUMBER-valued `a[i] = …` write
|
|
539
|
+
* disqualifies — so `w.x = a`, `f(a)`, `let b = a`, `a.push(x)`, `a[i] += x` all bail.
|
|
540
|
+
* `isNumericRhs` injects the value-type judgement (valTypeOf === VAL.NUMBER) the syntactic
|
|
541
|
+
* scan can't make itself.
|
|
542
|
+
*/
|
|
543
|
+
// Both `Array(n)` and `new Array(n)` normalize to a `new.Array` call by prepare; an
|
|
544
|
+
// empty literal stays `['[]', null]`. (Typed ctors become `new.Float64Array` etc. — the
|
|
545
|
+
// exact-match on `new.Array` keeps them out.)
|
|
546
|
+
const isFreshArrayCtor = (rhs) =>
|
|
547
|
+
Array.isArray(rhs) && (
|
|
548
|
+
(rhs[0] === '[]' && rhs.length <= 2) || // empty `[]`
|
|
549
|
+
(rhs[0] === '()' && rhs[1] === 'new.Array') // `Array(n)` / `new Array(n)` / `Array()`
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
function numFillSafe(node, name, isNumericRhs) {
|
|
553
|
+
if (typeof node === 'string') return node !== name // bare value use → escape
|
|
554
|
+
if (!Array.isArray(node)) return true
|
|
555
|
+
const op = node[0]
|
|
556
|
+
// `a[i] = rhs` — the fill write. Allowed iff rhs is provably NUMBER; recurse the index
|
|
557
|
+
// and rhs so a stray `a` inside either still disqualifies. (Compound `a[i] += …` is NOT
|
|
558
|
+
// matched here, so it falls through to the deny below — conservative for v1.)
|
|
559
|
+
if (op === '=' && Array.isArray(node[1]) && node[1][0] === '[]' && node[1][1] === name)
|
|
560
|
+
return isNumericRhs(node[2], name) &&
|
|
561
|
+
numFillSafe(node[1][2], name, isNumericRhs) && numFillSafe(node[2], name, isNumericRhs)
|
|
562
|
+
// calling `a`, `a.m(…)`, `a[i](…)` may grow/escape it
|
|
563
|
+
if (op === '()') {
|
|
564
|
+
const c = node[1]
|
|
565
|
+
if (c === name) return false
|
|
566
|
+
if (Array.isArray(c) && (c[0] === '.' || c[0] === '?.' || c[0] === '[]' || c[0] === '?.[]') && c[1] === name) return false
|
|
567
|
+
}
|
|
568
|
+
// any other write/update/delete on `a`, `a[..]`, `a.x` (incl. `a.length = …`, compounds)
|
|
569
|
+
if (grownOrEscapes(op)) {
|
|
570
|
+
const t = node[1]
|
|
571
|
+
if (t === name) return false
|
|
572
|
+
if (Array.isArray(t) && (t[0] === '[]' || t[0] === '.' || t[0] === '?.') && t[1] === name) return false
|
|
573
|
+
}
|
|
574
|
+
if (op === 'let' || op === 'const' || op === 'var') {
|
|
575
|
+
for (let i = 1; i < node.length; i++) {
|
|
576
|
+
const d = node[i]
|
|
577
|
+
if (Array.isArray(d) && d[0] === '=' && !numFillSafe(d[2], name, isNumericRhs)) return false
|
|
578
|
+
}
|
|
579
|
+
return true
|
|
580
|
+
}
|
|
581
|
+
// the only safe forms: `a.length` read, and `a[i]` index read (recurse the index expr).
|
|
582
|
+
if ((op === '.' || op === '?.') && node[1] === name) return node[2] === 'length'
|
|
583
|
+
if (op === '[]' && node[1] === name) return numFillSafe(node[2], name, isNumericRhs)
|
|
584
|
+
if (op === '...' && node[1] === name) return false // spread → escape
|
|
585
|
+
for (let i = 1; i < node.length; i++) if (!numFillSafe(node[i], name, isNumericRhs)) return false
|
|
586
|
+
return true
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
export function scanNumericFill(body, isNumericRhs) {
|
|
590
|
+
const out = new Set()
|
|
591
|
+
for (const [name, s] of scanBindingUses(body)) {
|
|
592
|
+
if (s.decls !== 1 || !isFreshArrayCtor(s.initRhs)) continue
|
|
593
|
+
if (numFillSafe(body, name, isNumericRhs)) out.add(name)
|
|
594
|
+
}
|
|
595
|
+
return out
|
|
596
|
+
}
|
|
597
|
+
|
|
435
598
|
/**
|
|
436
599
|
* Narrow uint32 accumulator locals to unsigned i32. A local qualifies when its
|
|
437
600
|
* initializer is a non-negative integer literal in [0, 2^32), every
|
package/src/compile/analyze.js
CHANGED
|
@@ -76,7 +76,7 @@ import { TYPED_ELEM_CODE, TYPED_ELEM_VIEW_FLAG, TYPED_ELEM_BIGINT_FLAG, encodeTy
|
|
|
76
76
|
import {
|
|
77
77
|
findFreeVars, findMutations, boxedCaptures,
|
|
78
78
|
collectI32SafeIndexVars, collectF64StridedIndexVars, narrowUint32, scanBindingUses,
|
|
79
|
-
scanFlatObjects, scanSliceViews, USE,
|
|
79
|
+
scanFlatObjects, scanSliceViews, scanNeverGrown, scanNumericFill, USE,
|
|
80
80
|
} from './analyze-scans.js'
|
|
81
81
|
|
|
82
82
|
export { findFreeVars, findMutations, boxedCaptures } from './analyze-scans.js'
|
|
@@ -124,6 +124,15 @@ const makeTypedTracker = (get, set, del) => {
|
|
|
124
124
|
}
|
|
125
125
|
const ctor = typedElemCtor(rhs)
|
|
126
126
|
if (ctor) return setOrInvalidate(ctor)
|
|
127
|
+
// `recv.subarray(...)` is a zero-copy VIEW aliasing the receiver's buffer — its elem
|
|
128
|
+
// ctor is the receiver's type with the `.view` flag, so the binding unboxes to a typed
|
|
129
|
+
// pointer and element writes take the descriptor-indirected path (not desc-as-data).
|
|
130
|
+
if (Array.isArray(rhs) && rhs[0] === '()' && Array.isArray(rhs[1]) && rhs[1][0] === '.'
|
|
131
|
+
&& rhs[1][2] === 'subarray' && typeof rhs[1][1] === 'string') {
|
|
132
|
+
const recvCtor = resolveName(rhs[1][1])
|
|
133
|
+
if (recvCtor) return setOrInvalidate((recvCtor.endsWith('.view') ? recvCtor.slice(0, -5) : recvCtor) + '.view')
|
|
134
|
+
return
|
|
135
|
+
}
|
|
127
136
|
// TYPED-narrowed call result carries its elem aux on f.sig.ptrAux — reverse-map
|
|
128
137
|
// to a canonical ctor so the unboxed local's rep restores the same aux.
|
|
129
138
|
if (Array.isArray(rhs) && rhs[0] === '()' && typeof rhs[1] === 'string') {
|
|
@@ -319,8 +328,12 @@ export function analyzeBody(body) {
|
|
|
319
328
|
}
|
|
320
329
|
// `x = arr[i]` where `arr` is a known array-of-arrays → `x`'s elements take
|
|
321
330
|
// `arr`'s nested element kind (the missing index-step in observeArrValType).
|
|
331
|
+
// `arr` may be a function-local (arrElemElemValTypes) or a module-level const
|
|
332
|
+
// table (global rep, recorded by recordGlobalRep) — the latter dynWrite-guarded.
|
|
322
333
|
if (Array.isArray(rhs) && rhs[0] === '[]' && rhs.length === 3 && typeof rhs[1] === 'string') {
|
|
323
334
|
const nested = arrElemElemValTypes.get(rhs[1])
|
|
335
|
+
?? (!ctx.func.localReps?.has(rhs[1]) && !ctx.types?.dynWriteVars?.has(rhs[1])
|
|
336
|
+
? ctx.scope.globalReps?.get(rhs[1])?.arrayElemElemValType : null)
|
|
324
337
|
if (nested) observeArrValType(name, nested)
|
|
325
338
|
}
|
|
326
339
|
}
|
|
@@ -517,7 +530,7 @@ export function analyzeBody(body) {
|
|
|
517
530
|
const prevTypedOverlay = ctx.func.localTypedElemsOverlay
|
|
518
531
|
ctx.func.localValTypesOverlay = valTypes
|
|
519
532
|
ctx.func.localTypedElemsOverlay = typedElems
|
|
520
|
-
let unsignedLocals
|
|
533
|
+
let unsignedLocals, numericFill
|
|
521
534
|
try {
|
|
522
535
|
walk(body)
|
|
523
536
|
widenLocalTypes(body, locals)
|
|
@@ -526,6 +539,18 @@ export function analyzeBody(body) {
|
|
|
526
539
|
// reconsidered with final types — and stays f64, since a relational compare
|
|
527
540
|
// is a non-transparent read that disqualifies narrowing anyway.
|
|
528
541
|
unsignedLocals = narrowUint32(body, locals)
|
|
542
|
+
// Numeric-fill arrays — fresh `Array(n)`/`[]` whose every element write stores a
|
|
543
|
+
// Number, so `a[i]` reads can skip __to_num (the win `[1,2,3]` already gets, for the
|
|
544
|
+
// construct-then-fill kernel shape). Runs HERE, inside the val-type overlay, so a
|
|
545
|
+
// write of a bare numeric local (`a[i] = out`) resolves via the just-built `valTypes`.
|
|
546
|
+
// A bare read of the array's OWN elements (`a[i] = a[j]`, heapsort) is Numeric by
|
|
547
|
+
// induction; any genuinely non-numeric write still fails the test and disqualifies.
|
|
548
|
+
const numericFillRhs = (rhs, selfName) => {
|
|
549
|
+
if (Array.isArray(rhs) && rhs[0] === '[]' && rhs[1] === selfName) return true
|
|
550
|
+
if (typeof rhs === 'string') return valTypes.get(rhs) === VAL.NUMBER || exprElemSourceVal(rhs) === VAL.NUMBER
|
|
551
|
+
return valTypeOf(rhs) === VAL.NUMBER
|
|
552
|
+
}
|
|
553
|
+
numericFill = scanNumericFill(body, numericFillRhs)
|
|
529
554
|
} finally {
|
|
530
555
|
ctx.func.localValTypesOverlay = prevOverlay
|
|
531
556
|
ctx.func.localTypedElemsOverlay = prevTypedOverlay
|
|
@@ -545,7 +570,10 @@ export function analyzeBody(body) {
|
|
|
545
570
|
// Consumed by emitDecl, which lowers the initializer to a SLICE_BIT view.
|
|
546
571
|
const sliceViews = doSchemas ? scanSliceViews(body) : new Set()
|
|
547
572
|
|
|
548
|
-
|
|
573
|
+
// Never-relocated array bindings — reads may skip the realloc-forwarding follow.
|
|
574
|
+
const neverGrown = doSchemas ? scanNeverGrown(body) : new Set()
|
|
575
|
+
|
|
576
|
+
const result = { locals, valTypes, arrElemSchemas, arrElemValTypes, typedElems, escapes, flatObjects, sliceViews, unsignedLocals, neverGrown, numericFill }
|
|
549
577
|
_bodyFactsCache.set(body, result)
|
|
550
578
|
return result
|
|
551
579
|
}
|
|
@@ -677,6 +705,13 @@ export function analyzeValTypes(body) {
|
|
|
677
705
|
for (const [name, vt] of facts.arrElemValTypes) {
|
|
678
706
|
if (vt != null) updateRep(name, { arrayElemValType: vt })
|
|
679
707
|
}
|
|
708
|
+
// Construct-then-fill numeric arrays (`let a = Array(n); a[i] = expr`) carry no
|
|
709
|
+
// element evidence at their decl, so the walk above leaves them untyped. scanNumericFill
|
|
710
|
+
// proved every write Numeric and every other use a pure read — record NUMBER so `arr[i]`
|
|
711
|
+
// reads skip __to_num, unless an observation already poisoned the slot to a conflict.
|
|
712
|
+
for (const name of facts.numericFill || []) {
|
|
713
|
+
if (facts.arrElemValTypes.get(name) !== null) updateRep(name, { arrayElemValType: VAL.NUMBER })
|
|
714
|
+
}
|
|
680
715
|
// Propagate body-observed array-elem schemas to localReps so unboxablePtrs's
|
|
681
716
|
// `let p = arr[i]` rule (which only consults rep) sees the schema and can unbox `p`
|
|
682
717
|
// to an i32 offset. Without this, `arr.push({x,y,z})` followed by `arr[i].x` reads
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
* @module compile/emit-assign
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { ctx, err, inc, PTR } from '../ctx.js'
|
|
13
|
+
import { ctx, err, inc, warnDeopt, PTR } from '../ctx.js'
|
|
14
14
|
import { T } from '../ast.js'
|
|
15
|
-
import { staticPropertyKey } from '../static.js'
|
|
15
|
+
import { staticPropertyKey, staticIndexKey } from '../static.js'
|
|
16
16
|
import { valTypeOf, shapeOf } from '../kind.js'
|
|
17
17
|
import { VAL, lookupValType, repOf } from '../reps.js'
|
|
18
18
|
import {
|
|
@@ -74,8 +74,9 @@ function storeArrayPayload(arrExpr, idxNode, valueExpr, persist) {
|
|
|
74
74
|
/** Strict-mode guard for dynamic property writes — emitted in branches that
|
|
75
75
|
* fall through to `__dyn_set` or its key-kind dispatch. */
|
|
76
76
|
function ensureDynSetAllowed(arr) {
|
|
77
|
-
if (!ctx.transform.strict) return
|
|
78
77
|
const arrLabel = typeof arr === 'string' ? arr : '<expr>'
|
|
78
|
+
warnDeopt('deopt-dyn-write', `dynamic property write \`${arrLabel}[…] = …\` couldn't resolve a static type — it falls back to a runtime hash store (~2× slower than a typed/slot write, far worse in a hot loop). Use a literal key, a numeric typed-array index, or a Map for genuinely dynamic keys.`)
|
|
79
|
+
if (!ctx.transform.strict) return
|
|
79
80
|
err(`strict mode: dynamic property assignment \`${arrLabel}[<expr>] = ...\` falls back to __dyn_set. Use a literal key or known array/typed-array numeric index, or pass { strict: false }.`)
|
|
80
81
|
}
|
|
81
82
|
|
|
@@ -159,10 +160,12 @@ export function emitElementAssign(arr, idx, val) {
|
|
|
159
160
|
: typeof arr === 'string' && lookupValType(arr) === VAL.OBJECT ? staticPropertyKey(idx)
|
|
160
161
|
: null
|
|
161
162
|
|
|
162
|
-
// 1. SRoA flat object: `o['k'] = x` → `local.set $o#i` (no heap
|
|
163
|
-
|
|
163
|
+
// 1. SRoA flat object/array: `o['k'] = x` / `a[2] = x` → `local.set $o#i` (no heap
|
|
164
|
+
// store). A bare integer index resolves its slot key here (not via `litKey`).
|
|
165
|
+
if (typeof arr === 'string' && ctx.func.flatObjects?.has(arr)) {
|
|
164
166
|
const fo = ctx.func.flatObjects.get(arr)
|
|
165
|
-
const
|
|
167
|
+
const flatKey = litKey != null ? litKey : staticIndexKey(idx)
|
|
168
|
+
const fi = flatKey != null ? fo.names.indexOf(flatKey) : -1
|
|
166
169
|
if (fi >= 0) return withTemp(valueExpr, t => [
|
|
167
170
|
['local.set', `$${arr}#${fi}`, ['local.get', `$${t}`]],
|
|
168
171
|
['local.get', `$${t}`]])
|