jz 0.6.0 → 0.8.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 +99 -72
- package/bench/README.md +253 -100
- package/bench/bench.svg +58 -81
- package/cli.js +85 -12
- package/dist/interop.js +1 -0
- package/dist/jz.js +6196 -0
- package/index.d.ts +126 -0
- package/index.js +222 -35
- package/interop.js +240 -141
- package/layout.js +34 -18
- package/module/array.js +99 -111
- package/module/collection.js +124 -30
- package/module/console.js +1 -1
- package/module/core.js +163 -19
- package/module/json.js +3 -3
- package/module/math.js +162 -3
- package/module/number.js +268 -13
- package/module/object.js +37 -5
- package/module/regex.js +8 -7
- package/module/simd.js +37 -5
- package/module/string.js +203 -168
- package/module/typedarray.js +565 -112
- package/package.json +26 -7
- package/src/abi/string.js +29 -29
- package/src/ast.js +19 -2
- package/src/autoload.js +3 -0
- package/src/compile/analyze-scans.js +174 -11
- package/src/compile/analyze.js +101 -4
- package/src/compile/cse-load.js +200 -0
- package/src/compile/emit-assign.js +82 -20
- package/src/compile/emit.js +592 -51
- package/src/compile/index.js +504 -89
- package/src/compile/infer.js +36 -1
- package/src/compile/loop-divmod.js +109 -0
- package/src/compile/loop-model.js +91 -0
- package/src/compile/loop-square.js +102 -0
- package/src/compile/narrow.js +275 -41
- package/src/compile/peel-stencil.js +215 -0
- package/src/compile/plan/advise.js +55 -1
- package/src/compile/plan/common.js +29 -0
- package/src/compile/plan/index.js +8 -1
- package/src/compile/plan/inline.js +180 -22
- package/src/compile/plan/literals.js +313 -24
- package/src/compile/plan/scope.js +115 -39
- package/src/compile/program-facts.js +21 -2
- package/src/ctx.js +96 -19
- package/src/helper-counters.js +137 -0
- package/src/ir.js +157 -20
- package/src/kind-traits.js +34 -3
- package/src/kind.js +79 -4
- package/src/op-policy.js +5 -2
- package/src/ops.js +119 -0
- package/src/optimize/index.js +1274 -151
- package/src/optimize/recurse.js +182 -0
- package/src/optimize/vectorize.js +4187 -253
- package/src/prepare/index.js +75 -14
- package/src/prepare/lift-iife.js +149 -0
- package/src/reps.js +6 -2
- package/src/static.js +9 -0
- package/src/type.js +63 -51
- package/src/wat/assemble.js +286 -21
- package/src/widen.js +21 -0
- package/src/wat/optimize.js +0 -3760
package/package.json
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jz",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.8.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,14 @@
|
|
|
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:self": "node scripts/bench-selfhost.mjs",
|
|
59
|
+
"bench:readme": "node scripts/bench-readme.mjs",
|
|
60
|
+
"audit:as": "node scripts/audit-assemblyscript.mjs",
|
|
51
61
|
"build": "node scripts/build-dist.mjs",
|
|
52
|
-
"build:examples": "
|
|
53
|
-
"prepublishOnly": "npm test && npm run test:self"
|
|
62
|
+
"build:examples": "node examples/build.mjs all",
|
|
63
|
+
"prepublishOnly": "npm test && npm run build && npm run test:self",
|
|
64
|
+
"prepare": "npm run build",
|
|
65
|
+
"audit:fixpoint": "node scripts/audit-fixpoint.mjs"
|
|
54
66
|
},
|
|
55
67
|
"repository": {
|
|
56
68
|
"type": "git",
|
|
@@ -64,7 +76,7 @@
|
|
|
64
76
|
"license": "MIT",
|
|
65
77
|
"dependencies": {
|
|
66
78
|
"subscript": "^10.4.17",
|
|
67
|
-
"watr": "^4.
|
|
79
|
+
"watr": "^4.7.1"
|
|
68
80
|
},
|
|
69
81
|
"keywords": [
|
|
70
82
|
"javascript",
|
|
@@ -81,10 +93,17 @@
|
|
|
81
93
|
"audio",
|
|
82
94
|
"creative-coding",
|
|
83
95
|
"assemblyscript",
|
|
84
|
-
"porffor"
|
|
96
|
+
"porffor",
|
|
97
|
+
"numeric",
|
|
98
|
+
"signal-processing",
|
|
99
|
+
"math",
|
|
100
|
+
"audio-worklet",
|
|
101
|
+
"js-to-wasm",
|
|
102
|
+
"performance"
|
|
85
103
|
],
|
|
86
104
|
"devDependencies": {
|
|
87
105
|
"esbuild": "^0.28.0",
|
|
106
|
+
"sprae": "^13.3.8",
|
|
88
107
|
"tst": "^9.4.0"
|
|
89
108
|
}
|
|
90
109
|
}
|
package/src/abi/string.js
CHANGED
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
// take. Kept as a one-liner so each op reads as a single `call`.
|
|
57
57
|
const ssoI64 = (sF64) => ['i64.reinterpret_f64', sF64]
|
|
58
58
|
|
|
59
|
-
import { isReassigned } from '../ast.js'
|
|
59
|
+
import { isReassigned, isLeaf } from '../ast.js'
|
|
60
60
|
import { LAYOUT, oobNanIR, ssoBitI64Hex } from '../../layout.js'
|
|
61
61
|
|
|
62
62
|
/** Pre-shifted SSO discriminator — layout.js is cycle-free; memoized at first use. */
|
|
@@ -79,10 +79,6 @@ const allocLocalI32 = (ctx, tag) => {
|
|
|
79
79
|
return name
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
const _isLeaf = (n) => Array.isArray(n) &&
|
|
83
|
-
(n[0] === 'local.get' || n[0] === 'global.get' ||
|
|
84
|
-
n[0] === 'i32.const' || n[0] === 'i64.const' ||
|
|
85
|
-
n[0] === 'f64.const' || n[0] === 'f32.const')
|
|
86
82
|
|
|
87
83
|
/** Per-use cheap form of `charCodeAt` against a pre-decomposed param receiver
|
|
88
84
|
* (shape 1): a bounds check plus a 2-arm SSO/heap byte select reading the four
|
|
@@ -90,22 +86,29 @@ const _isLeaf = (n) => Array.isArray(n) &&
|
|
|
90
86
|
* side-effecting `iI32` is spilled to a scratch local first — leaves are safe
|
|
91
87
|
* to duplicate. `oobNan` picks the OOB contract / result type exactly as in
|
|
92
88
|
* the generic path. */
|
|
93
|
-
function emitDecompCharRead(dec, iI32, ctx, oobNan) {
|
|
89
|
+
function emitDecompCharRead(dec, iI32, ctx, oobNan, inBounds = false) {
|
|
94
90
|
const rt = oobNan ? 'f64' : 'i32'
|
|
95
91
|
let idx = iI32, spill = null
|
|
96
|
-
if (!
|
|
97
|
-
const ssoByteExpr = ['i32.and',
|
|
98
|
-
['
|
|
99
|
-
['
|
|
92
|
+
if (!isLeaf(iI32)) { spill = allocLocalI32(ctx, 'ci'); idx = ['local.get', `$${spill}`] }
|
|
93
|
+
const ssoByteExpr = ['i32.wrap_i64', ['i64.and',
|
|
94
|
+
['i64.shr_u', ['local.get', `$${dec.ptr64}`], ['i64.mul', ['i64.extend_i32_u', idx], ['i64.const', 7]]],
|
|
95
|
+
['i64.const', '0x7f']]]
|
|
100
96
|
const heapByteExpr = ['i32.load8_u', ['i32.add', ['local.get', `$${dec.loadbase}`], idx]]
|
|
101
97
|
const ccByte = ['if', ['result', 'i32'],
|
|
102
98
|
['local.get', `$${dec.sso}`],
|
|
103
99
|
['then', ssoByteExpr],
|
|
104
100
|
['else', heapByteExpr]]
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
101
|
+
// `inBounds`: the index is proven in [0, len) by an enclosing canonical scan
|
|
102
|
+
// (analyze.js inBoundsCharCodeAt / splitCharScanLoops' in-bounds main loop), so
|
|
103
|
+
// the OOB arm is dead — drop the per-char `i >= len` compare. This is what turns
|
|
104
|
+
// a split char-scan loop into a bare load (the `if(sso)` arm is loop-invariant and
|
|
105
|
+
// V8-folds), matching AS/native on tokenizer-shape scans. Otherwise keep the guard.
|
|
106
|
+
const use = inBounds
|
|
107
|
+
? (oobNan ? ['f64.convert_i32_u', ccByte] : ccByte)
|
|
108
|
+
: ['if', ['result', rt],
|
|
109
|
+
['i32.ge_u', idx, ['local.get', `$${dec.len}`]],
|
|
110
|
+
['then', oobNan ? oobNanIR() : ['i32.const', 0]],
|
|
111
|
+
['else', oobNan ? ['f64.convert_i32_u', ccByte] : ccByte]]
|
|
109
112
|
return spill
|
|
110
113
|
? ['block', ['result', rt], ['local.set', `$${spill}`, iI32], use]
|
|
111
114
|
: use
|
|
@@ -131,9 +134,8 @@ export function emitCharDecompPrologue(dec) {
|
|
|
131
134
|
['i64.const', 0]]
|
|
132
135
|
const offMask = `0x${LAYOUT.OFFSET_MASK.toString(16).toUpperCase()}`
|
|
133
136
|
const off = ['i32.wrap_i64', ['i64.and', ptr, ['i64.const', offMask]]]
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
['i32.const', LAYOUT.SSO_BIT - 1]]
|
|
137
|
+
// SSO length lives at payload bits 42-44 (7-bit-codec layout; see module/string.js).
|
|
138
|
+
const ssoLen = ['i32.wrap_i64', ['i64.and', ['i64.shr_u', ptr, ['i64.const', 42]], ['i64.const', 7]]]
|
|
137
139
|
// Heap length is `i32.load(off - 4)`; guard against off<4 (corrupt/non-string
|
|
138
140
|
// payload) so the load doesn't trap on a wrapped-negative address.
|
|
139
141
|
const heapLen = ['if', ['result', 'i32'],
|
|
@@ -146,6 +148,7 @@ export function emitCharDecompPrologue(dec) {
|
|
|
146
148
|
['then',
|
|
147
149
|
['local.set', `$${dec.sso}`, ['i32.const', 1]],
|
|
148
150
|
['local.set', `$${dec.base}`, off],
|
|
151
|
+
['local.set', `$${dec.ptr64}`, ptr], // full payload: SSO chars are 7-bit, span into aux
|
|
149
152
|
['local.set', `$${dec.len}`, ssoLen],
|
|
150
153
|
// SSO: route every per-iter load to address 0 (always valid, byte
|
|
151
154
|
// discarded by the outer select).
|
|
@@ -212,7 +215,7 @@ export const sso = {
|
|
|
212
215
|
* `src/compile.js#emitFunc` — it drains `ctx.func.charDecomp` after the
|
|
213
216
|
* body emit completes and splices an init block between the boxed-param
|
|
214
217
|
* inits and the user statements. */
|
|
215
|
-
charCodeAt: (sF64, iI32, ctx, oobNan = false) => {
|
|
218
|
+
charCodeAt: (sF64, iI32, ctx, oobNan = false, inBounds = false) => {
|
|
216
219
|
// `oobNan` selects the out-of-bounds semantics and result type:
|
|
217
220
|
// - false (default): OOB → `i32.const 0`, result i32 — the raw-byte
|
|
218
221
|
// primitive used by the `buf += s[i]` append-byte fast path.
|
|
@@ -256,14 +259,16 @@ export const sso = {
|
|
|
256
259
|
// discards it). Pre-computing it in the prologue removes a
|
|
257
260
|
// per-iter `select` and lets V8 fold the add into the load.
|
|
258
261
|
const loadbase = `${name}$ccldb`
|
|
262
|
+
const ptr64 = `${name}$ccp64`
|
|
259
263
|
ctx.func.locals.set(base, 'i32')
|
|
260
264
|
ctx.func.locals.set(len, 'i32')
|
|
261
265
|
ctx.func.locals.set(sso, 'i32')
|
|
262
266
|
ctx.func.locals.set(loadbase, 'i32')
|
|
263
|
-
|
|
267
|
+
ctx.func.locals.set(ptr64, 'i64') // full SSO payload for 7-bit char extraction
|
|
268
|
+
dec = { base, len, sso, loadbase, ptr64, param: name }
|
|
264
269
|
ctx.func.charDecomp.set(name, dec)
|
|
265
270
|
}
|
|
266
|
-
return emitDecompCharRead(dec, iI32, ctx, oobNan)
|
|
271
|
+
return emitDecompCharRead(dec, iI32, ctx, oobNan, inBounds)
|
|
267
272
|
}
|
|
268
273
|
}
|
|
269
274
|
|
|
@@ -277,9 +282,6 @@ export const sso = {
|
|
|
277
282
|
// so we MUST spill anything that isn't side-effect-free to a local
|
|
278
283
|
// before referencing it more than once. Leaves (`local.get`, `*.const`,
|
|
279
284
|
// `global.get`) are safe to duplicate; everything else is spilled.
|
|
280
|
-
const isLeaf = (n) => Array.isArray(n) &&
|
|
281
|
-
(n[0] === 'local.get' || n[0] === 'global.get' ||
|
|
282
|
-
n[0] === 'i32.const' || n[0] === 'i64.const' || n[0] === 'f64.const' || n[0] === 'f32.const')
|
|
283
285
|
const sLeaf = isLeaf(sF64)
|
|
284
286
|
const iLeaf = isLeaf(iI32)
|
|
285
287
|
const ptrI64Expr = ssoI64(sF64)
|
|
@@ -301,12 +303,10 @@ export const sso = {
|
|
|
301
303
|
}
|
|
302
304
|
const offMask = `0x${LAYOUT.OFFSET_MASK.toString(16).toUpperCase()}`
|
|
303
305
|
const offExpr = () => ['i32.wrap_i64', ['i64.and', getPtr(), ['i64.const', offMask]]]
|
|
304
|
-
const ssoLen = ['i32.and',
|
|
305
|
-
|
|
306
|
-
['
|
|
307
|
-
|
|
308
|
-
['i32.shr_u', offExpr(), ['i32.mul', getIdx(), ['i32.const', 8]]],
|
|
309
|
-
['i32.const', 0xFF]]
|
|
306
|
+
const ssoLen = ['i32.wrap_i64', ['i64.and', ['i64.shr_u', getPtr(), ['i64.const', 42]], ['i64.const', 7]]]
|
|
307
|
+
const ssoByte = ['i32.wrap_i64', ['i64.and',
|
|
308
|
+
['i64.shr_u', getPtr(), ['i64.mul', ['i64.extend_i32_u', getIdx()], ['i64.const', 7]]],
|
|
309
|
+
['i64.const', '0x7f']]]
|
|
310
310
|
const ssoBranch = ['if', ['result', rt],
|
|
311
311
|
['i32.ge_u', getIdx(), ssoLen],
|
|
312
312
|
['then', mkOob()],
|
package/src/ast.js
CHANGED
|
@@ -66,6 +66,11 @@ export const isBlockBody = (body) =>
|
|
|
66
66
|
export const isLiteralStr = idx => Array.isArray(idx) && idx[0] === 'str' && typeof idx[1] === 'string'
|
|
67
67
|
export const isFuncRef = (node, funcNames) => typeof node === 'string' && funcNames.has(node)
|
|
68
68
|
|
|
69
|
+
/** A value-leaf IR instruction — `local.get`/`global.get`/any `*.const`. Cheap and
|
|
70
|
+
* side-effect-free, so safe to duplicate without spilling to a temp. The one
|
|
71
|
+
* source of truth for "is this trivially duplicatable" across ir/abi/optimize. */
|
|
72
|
+
export const isLeaf = n => Array.isArray(n) && (n[0] === 'local.get' || n[0] === 'global.get' || n[0].endsWith('.const'))
|
|
73
|
+
|
|
69
74
|
// === Assignment / reassignment ===
|
|
70
75
|
|
|
71
76
|
/** Assignment operators — shared across analyze, plan, emit, abi. */
|
|
@@ -385,9 +390,21 @@ export function cloneNode(node) {
|
|
|
385
390
|
/** Structural equality via JSON. AST nodes are JSON-serializable except i64.const
|
|
386
391
|
* BigInt payloads (NaN-box prefixes — dcbb433 routes pointer offsets through boxed
|
|
387
392
|
* forms); the replacer stringifies those as `<n>n` (cf. formatErrorNode in ctx.js). */
|
|
388
|
-
|
|
393
|
+
// Replacer for structural node-equality / dedup keys. JSON.stringify is the fast
|
|
394
|
+
// path, but it silently collapses values it can't round-trip: bigint throws, and —
|
|
395
|
+
// the subtle one — Infinity / -Infinity / NaN ALL stringify to `null` while -0
|
|
396
|
+
// stringifies to `0`. Two nodes differing ONLY in such a constant then serialize
|
|
397
|
+
// identically and compare equal — an unsound merge (SLP packs `[Inf,-Inf]` as one
|
|
398
|
+
// splat lane; CSE/LICM dedups distinct invariants). Tag each so it round-trips with
|
|
399
|
+
// Object.is semantics.
|
|
400
|
+
export const stableKey = (_k, v) => {
|
|
401
|
+
if (typeof v === 'bigint') return `${v}n`
|
|
402
|
+
if (typeof v === 'number' && (v !== v || v === Infinity || v === -Infinity || Object.is(v, -0)))
|
|
403
|
+
return v !== v ? '#NaN' : v === Infinity ? '#Inf' : v === -Infinity ? '#-Inf' : '#-0'
|
|
404
|
+
return v
|
|
405
|
+
}
|
|
389
406
|
export function nodeEqual(a, b) {
|
|
390
|
-
return JSON.stringify(a,
|
|
407
|
+
return JSON.stringify(a, stableKey) === JSON.stringify(b, stableKey)
|
|
391
408
|
}
|
|
392
409
|
|
|
393
410
|
/** Property entries of an object-literal AST node (`['{}', …]`). */
|
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
|