jz 0.9.0 → 0.9.2
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 +19 -7
- package/bench/bench.svg +18 -18
- package/dist/interop.js +1 -1
- package/dist/jz.js +2026 -1429
- package/interop.js +73 -6
- package/jzify/index.js +7 -1
- package/jzify/transform.js +21 -2
- package/jzify/webrt.js +145 -0
- package/layout.js +13 -3
- package/module/array.js +49 -42
- package/module/collection.js +222 -111
- package/module/console.js +4 -0
- package/module/core.js +86 -7
- package/module/crypto.js +124 -0
- package/module/index.js +3 -1
- package/module/math.js +5 -0
- package/module/navigator.js +26 -0
- package/module/schema.js +11 -3
- package/module/string.js +433 -28
- package/module/timer.js +42 -1
- package/module/typedarray.js +370 -60
- package/package.json +3 -3
- package/src/abi/array.js +24 -16
- package/src/abi/index.js +2 -2
- package/src/abi/object.js +17 -0
- package/src/ast.js +53 -0
- package/src/autoload.js +19 -3
- package/src/compile/analyze.js +190 -21
- package/src/compile/emit-assign.js +111 -7
- package/src/compile/emit.js +84 -20
- package/src/compile/index.js +37 -4
- package/src/compile/inplace-store.js +10 -9
- package/src/compile/narrow.js +71 -1
- package/src/compile/plan/index.js +5 -0
- package/src/compile/plan/literals.js +11 -2
- package/src/ctx.js +14 -0
- package/src/ir.js +26 -1
- package/src/optimize/index.js +1 -0
- package/src/optimize/vectorize.js +80 -6
- package/src/prepare/index.js +6 -0
- package/src/static.js +31 -0
- package/src/type.js +356 -47
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jz",
|
|
3
|
-
"version": "0.9.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.9.2",
|
|
4
|
+
"description": "Compile the numeric JavaScript you already test to lean GC-free WASM — same source, no rewrite, type annotations, or runtime.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
7
7
|
"type": "module",
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"license": "MIT",
|
|
77
77
|
"dependencies": {
|
|
78
78
|
"subscript": "^10.7.0",
|
|
79
|
-
"watr": "^5.4
|
|
79
|
+
"watr": "^5.7.4"
|
|
80
80
|
},
|
|
81
81
|
"keywords": [
|
|
82
82
|
"javascript",
|
package/src/abi/array.js
CHANGED
|
@@ -66,23 +66,31 @@ export const taggedLinear = {
|
|
|
66
66
|
},
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
// structInline(K) — SRoA carrier factory. Logical element `idx`
|
|
70
|
-
// consecutive 8-byte cells
|
|
71
|
-
//
|
|
72
|
-
//
|
|
69
|
+
// structInline(K, packed) — SRoA carrier factory. Logical element `idx`
|
|
70
|
+
// occupies `cpe` consecutive 8-byte cells: K for f64 fields, ⌈K/2⌉ when the
|
|
71
|
+
// schema packs i32 fields (ctx.schema.inlineCellI32 — two i32s per physical
|
|
72
|
+
// cell, odd K pads). The carrier hands back the byte address of the element's
|
|
73
|
+
// first cell; field `f` is then `+f*8` (tagged f64 slots) or `+f*4`
|
|
74
|
+
// (packedI32) composed by the schema slot machinery. `len`/`cap` count
|
|
75
|
+
// physical 8-byte cells either way, so every stride-8 helper (`__alloc_hdr`,
|
|
76
|
+
// `__arr_grow*`, `__len`) is reused untouched. Built on demand per schema
|
|
73
77
|
// (`K = ctx.schema.list[sid].length`) — not a `ctx.abi` default.
|
|
74
|
-
export const structInline = (K) =>
|
|
75
|
-
K
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
78
|
+
export const structInline = (K, packed = false) => {
|
|
79
|
+
const cpe = packed ? (K + 1) >> 1 : K // physical 8-byte cells per element
|
|
80
|
+
return {
|
|
81
|
+
K,
|
|
82
|
+
cpe,
|
|
83
|
+
ops: {
|
|
84
|
+
// Byte address of logical element `idx`'s first cell off i32 `base`.
|
|
85
|
+
// A JS-integer idx folds to a constant; an IR-node idx emits `idx*cpe`
|
|
86
|
+
// then the `<<3` (via `addr`).
|
|
87
|
+
elemAddr: (base, idx) =>
|
|
88
|
+
typeof idx === 'number'
|
|
89
|
+
? addr(base, idx * cpe)
|
|
90
|
+
: addr(base, cpe === 1 ? idx : ['i32.mul', idx, ['i32.const', cpe]]),
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
}
|
|
86
94
|
|
|
87
95
|
// Default carrier — picked when the narrower has no stronger evidence.
|
|
88
96
|
// Reached via `ctx.abi.array`.
|
package/src/abi/index.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import nanboxF64 from './number.js'
|
|
8
8
|
import sso, { jsstring } from './string.js'
|
|
9
|
-
import tagged from './object.js'
|
|
9
|
+
import tagged, { packedI32 } from './object.js'
|
|
10
10
|
import taggedLinear, { structInline } from './array.js'
|
|
11
11
|
|
|
12
12
|
/** All carriers per value type — keyed by stable id for rep.carrier lookup. */
|
|
@@ -48,4 +48,4 @@ export function makeAbi() {
|
|
|
48
48
|
return { ...DEFAULTS, carriers: CARRIERS, resolve: resolveCarrier }
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
export { nanboxF64, sso, jsstring, tagged, taggedLinear, structInline }
|
|
51
|
+
export { nanboxF64, sso, jsstring, tagged, packedI32, taggedLinear, structInline }
|
package/src/abi/object.js
CHANGED
|
@@ -33,6 +33,23 @@
|
|
|
33
33
|
// untouched — matches `slotAddr` so routed sites stay byte-identical.
|
|
34
34
|
const addr = (base, i) => i === 0 ? base : ['i32.add', base, ['i32.const', i * 8]]
|
|
35
35
|
|
|
36
|
+
// 4-byte sibling for packed i32 cells.
|
|
37
|
+
const addr4 = (base, i) => i === 0 ? base : ['i32.add', base, ['i32.const', i * 4]]
|
|
38
|
+
|
|
39
|
+
/** Packed i32 field cells — the carrier for structInline arrays whose schema
|
|
40
|
+
* is all-strict-int32 (ctx.schema.inlineCellI32): field `i` is a raw i32 at
|
|
41
|
+
* `base + i*4`. Values are exact by the slotI32Certain census (every write
|
|
42
|
+
* provably int32, never -0), so loads/stores skip the f64 boxing layer
|
|
43
|
+
* entirely. Reached only through cursor/cell nodes tagged `.cellI32` — a
|
|
44
|
+
* standalone object of the same schema keeps `tagged` f64 slots. */
|
|
45
|
+
export const packedI32 = {
|
|
46
|
+
ops: {
|
|
47
|
+
addr: addr4,
|
|
48
|
+
load: (base, i) => ['i32.load', addr4(base, i)],
|
|
49
|
+
store: (base, i, val) => ['i32.store', addr4(base, i), val],
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
|
|
36
53
|
export const tagged = {
|
|
37
54
|
// Field operations the compiler routes object access through.
|
|
38
55
|
ops: {
|
package/src/ast.js
CHANGED
|
@@ -93,6 +93,59 @@ export function isReassigned(body, name) {
|
|
|
93
93
|
return false
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/** First reference to `name` in EVALUATION order: 'write' (an unconditional
|
|
97
|
+
* plain assignment reached before any read — definitely-assigned), 'read'
|
|
98
|
+
* (a read, a compound/inc write — those read first — or ANY reference inside
|
|
99
|
+
* conditionally-executed code: if/ternary/short-circuit arms, loop bodies and
|
|
100
|
+
* steps, switch cases, try/catch regions, closures), or null (no reference).
|
|
101
|
+
* Drives the uninit-`let` maybeNullish flag: only a first-ref 'write' proves
|
|
102
|
+
* the binding never reads its `undefined` init (`let s; while ((s = …) < K)`). */
|
|
103
|
+
export function firstRefKind(n, name) {
|
|
104
|
+
const hasRef = (m) => m === name || (Array.isArray(m) && m.slice(1).some(hasRef))
|
|
105
|
+
const condRef = (...parts) => parts.some(hasRef) ? 'read' : null
|
|
106
|
+
const walk = (m) => {
|
|
107
|
+
if (m === name) return 'read'
|
|
108
|
+
if (!Array.isArray(m)) return null
|
|
109
|
+
const op = m[0]
|
|
110
|
+
if (op === '=>') return condRef(m) // body runs at call time
|
|
111
|
+
if (op === '=' && m[1] === name) return walk(m[2]) ?? 'write' // rhs evaluates first
|
|
112
|
+
if ((ASSIGN_OPS.has(op) || op === '++' || op === '--') && m[1] === name) return 'read'
|
|
113
|
+
if (op === 'let' || op === 'const') {
|
|
114
|
+
for (let k = 1; k < m.length; k++) {
|
|
115
|
+
const d = m[k]
|
|
116
|
+
if (d === name) return null // the decl itself, not a read
|
|
117
|
+
if (Array.isArray(d) && d[0] === '=' && d[1] === name) return walk(d[2]) ?? 'write'
|
|
118
|
+
const r = walk(d)
|
|
119
|
+
if (r) return r
|
|
120
|
+
}
|
|
121
|
+
return null
|
|
122
|
+
}
|
|
123
|
+
if (op === 'if' || op === '?:') {
|
|
124
|
+
const c = walk(m[1])
|
|
125
|
+
if (c) return c
|
|
126
|
+
// two-armed if/ternary where BOTH arms' first ref is an unconditional
|
|
127
|
+
// write: the join is definitely-assigned (`let s; if (c) s = a; else
|
|
128
|
+
// s = b` — the mandelbrot setView preamble shape)
|
|
129
|
+
if (m.length === 4 && m[3] !== undefined) {
|
|
130
|
+
const t = walk(m[2]), e = walk(m[3])
|
|
131
|
+
if (t === 'write' && e === 'write') return 'write'
|
|
132
|
+
return (t || e) ? 'read' : null
|
|
133
|
+
}
|
|
134
|
+
return condRef(...m.slice(2))
|
|
135
|
+
}
|
|
136
|
+
if (op === '&&' || op === '||' || op === '??') return walk(m[1]) ?? condRef(m[2])
|
|
137
|
+
if (op === 'while') return walk(m[1]) ?? condRef(m[2]) // cond evaluates ≥ once
|
|
138
|
+
if (op === 'for' && m.length === 5) // init + first cond eval run once
|
|
139
|
+
return walk(m[1]) ?? walk(m[2]) ?? condRef(m[3], m[4])
|
|
140
|
+
if (op === 'for-of' || op === 'for-in') return walk(m[2]) ?? condRef(m[1], m[3])
|
|
141
|
+
if (op === 'switch') return walk(m[1]) ?? condRef(...m.slice(2))
|
|
142
|
+
if (op === 'try' || op === 'catch' || op === 'finally' || op === '?.') return condRef(m)
|
|
143
|
+
for (let k = 1; k < m.length; k++) { const r = walk(m[k]); if (r) return r }
|
|
144
|
+
return null
|
|
145
|
+
}
|
|
146
|
+
return walk(n)
|
|
147
|
+
}
|
|
148
|
+
|
|
96
149
|
// A deeply-constant array literal (every element a compile-time literal), safe to
|
|
97
150
|
// allocate once and share. At emit time an array literal is `['[', e0, e1, …]` (flat
|
|
98
151
|
// elements); an INDEX access is `['[]', base, idx]` (op `[]`) — NOT a literal.
|
package/src/autoload.js
CHANGED
|
@@ -35,6 +35,10 @@ export const PROP_MODULES = Object.assign(Object.create(null), {
|
|
|
35
35
|
indexOf: ['core', 'string', 'array'], lastIndexOf: ['core', 'string', 'array'],
|
|
36
36
|
includes: ['core', 'string', 'array'],
|
|
37
37
|
length: ['core', 'string', 'array', 'typedarray'],
|
|
38
|
+
toBase64: ['core', 'typedarray', 'string'], toHex: ['core', 'typedarray', 'string'],
|
|
39
|
+
setFromBase64: ['core', 'typedarray', 'string', 'collection'],
|
|
40
|
+
setFromHex: ['core', 'typedarray', 'string', 'collection'],
|
|
41
|
+
encodeInto: ['core', 'string', 'typedarray', 'collection'],
|
|
38
42
|
})
|
|
39
43
|
|
|
40
44
|
export const OP_MODULES = {
|
|
@@ -56,7 +60,7 @@ export const OP_MODULES = {
|
|
|
56
60
|
'**=': ['math'], // desugars to `name = name ** val` at emit — needs the same module
|
|
57
61
|
}
|
|
58
62
|
|
|
59
|
-
export const TYPED_CTORS = ['Float64Array','Float32Array','Int32Array','Uint32Array','Int16Array','Uint16Array','Int8Array','Uint8Array','BigInt64Array','BigUint64Array','ArrayBuffer','DataView']
|
|
63
|
+
export const TYPED_CTORS = ['Float64Array','Float32Array','Float16Array','Int32Array','Uint32Array','Int16Array','Uint16Array','Int8Array','Uint8Array','Uint8ClampedArray','BigInt64Array','BigUint64Array','ArrayBuffer','DataView']
|
|
60
64
|
|
|
61
65
|
export const CALL_MODULES = dict({
|
|
62
66
|
ArrayBuffer: ['core', 'typedarray'],
|
|
@@ -67,6 +71,8 @@ export const CALL_MODULES = dict({
|
|
|
67
71
|
parseInt: ['number', 'string'],
|
|
68
72
|
encodeURIComponent: ['core', 'string', 'number'],
|
|
69
73
|
decodeURIComponent: ['core', 'string', 'number'],
|
|
74
|
+
encodeURI: ['core', 'string', 'number'],
|
|
75
|
+
decodeURI: ['core', 'string', 'number'],
|
|
70
76
|
String: ['core', 'string', 'number'],
|
|
71
77
|
Number: ['number', 'string'],
|
|
72
78
|
Boolean: ['number'],
|
|
@@ -77,6 +83,8 @@ export const CALL_MODULES = dict({
|
|
|
77
83
|
'console.log': ['core', 'string', 'number', 'console'],
|
|
78
84
|
'console.warn': ['core', 'string', 'number', 'console'],
|
|
79
85
|
'console.error': ['core', 'string', 'number', 'console'],
|
|
86
|
+
'console.info': ['core', 'string', 'number', 'console'],
|
|
87
|
+
'console.debug': ['core', 'string', 'number', 'console'],
|
|
80
88
|
'Object.fromEntries': ['core', 'object', 'collection', 'string'],
|
|
81
89
|
'Object.keys': ['core', 'object', 'string'],
|
|
82
90
|
'Object.getOwnPropertyNames': ['core', 'object', 'string'],
|
|
@@ -114,6 +122,12 @@ export const CALL_MODULES = dict({
|
|
|
114
122
|
'fs.write': ['core', 'string', 'fs'],
|
|
115
123
|
'String.fromCharCode': ['core', 'string'],
|
|
116
124
|
'String.fromCodePoint': ['core', 'string'],
|
|
125
|
+
'Uint8Array.fromBase64': ['core', 'typedarray', 'string'],
|
|
126
|
+
'Uint8Array.fromHex': ['core', 'typedarray', 'string'],
|
|
127
|
+
atob: ['core', 'string'],
|
|
128
|
+
btoa: ['core', 'string'],
|
|
129
|
+
'crypto.getRandomValues': ['core', 'typedarray', 'crypto'],
|
|
130
|
+
'crypto.randomUUID': ['core', 'string', 'crypto'],
|
|
117
131
|
'BigInt.asIntN': ['number'],
|
|
118
132
|
'BigInt.asUintN': ['number'],
|
|
119
133
|
...Object.fromEntries(TYPED_CTORS.filter(n => n.endsWith('Array')).map(n => [`${n}.from`, ['core', 'typedarray', 'array']])),
|
|
@@ -133,17 +147,19 @@ export const GENERIC_METHOD_MODULES = dict({
|
|
|
133
147
|
hasOwnProperty: ['core', 'object', 'string', 'collection'],
|
|
134
148
|
})
|
|
135
149
|
|
|
136
|
-
export const CTORS = ['Float64Array','Float32Array','Int32Array','Uint32Array','Int16Array','Uint16Array','Int8Array','Uint8Array','BigInt64Array','BigUint64Array','Set','Map','WeakSet','WeakMap','Date']
|
|
150
|
+
export const CTORS = ['Float64Array','Float32Array','Float16Array','Int32Array','Uint32Array','Int16Array','Uint16Array','Int8Array','Uint8Array','Uint8ClampedArray','BigInt64Array','BigUint64Array','Set','Map','WeakSet','WeakMap','Date']
|
|
137
151
|
// WeakSet/WeakMap fold to Set/Map (the `new` handler rewrites the ctor name).
|
|
138
152
|
// jz has no GC, so weakness is unobservable; this also accepts primitive keys
|
|
139
153
|
// (real WeakMap throws TypeError) and exposes `.size`/iteration — a deliberate
|
|
140
154
|
// semantic deviation documented in README. Compilers lean on them as identity
|
|
141
155
|
// caches / cycle-detection sets and never observe the missing weak semantics.
|
|
142
156
|
export const COLLECTION_CTORS = ['Set', 'Map', 'WeakSet', 'WeakMap']
|
|
143
|
-
export const TIMER_NAMES = new Set(['setTimeout', 'clearTimeout', 'setInterval', 'clearInterval'])
|
|
157
|
+
export const TIMER_NAMES = new Set(['setTimeout', 'clearTimeout', 'setInterval', 'clearInterval', 'requestAnimationFrame', 'cancelAnimationFrame'])
|
|
144
158
|
|
|
145
159
|
const MOD_DEPS = {
|
|
146
160
|
number: ['core', 'string'],
|
|
161
|
+
crypto: ['core'],
|
|
162
|
+
navigator: ['core'],
|
|
147
163
|
atomics: ['core', 'typedarray'],
|
|
148
164
|
string: ['core', 'number'],
|
|
149
165
|
array: ['core'],
|
package/src/compile/analyze.js
CHANGED
|
@@ -22,7 +22,7 @@ import { commaList, ASSIGN_OPS, isReassigned, STMT_OPS, isBlockBody, isLiteralSt
|
|
|
22
22
|
import { ctx, err } from '../ctx.js'
|
|
23
23
|
import { VAL, repOf, repOfGlobal, updateRep, updateGlobalRep, lookupValType, lookupNotString } from '../reps.js'
|
|
24
24
|
import { valTypeOf, jsonConstString, shapeOf, shapeOfObjectLiteralAst } from '../kind.js'
|
|
25
|
-
import { intLiteralValue, nonNegIntLiteral, constIntExpr, NO_VALUE, staticPropertyKey, staticValue, staticObjectProps, staticArrayElems, objLiteralSchemaId, exprSchemaId, inlineArraySid } from '../static.js'
|
|
25
|
+
import { intLiteralValue, nonNegIntLiteral, constIntExpr, NO_VALUE, staticPropertyKey, staticValue, staticObjectProps, staticArrayElems, objLiteralSchemaId, exprSchemaId, inlineArraySid, inplaceKey } from '../static.js'
|
|
26
26
|
import { typedElemCtor, typedStaticLen, MIXED_CTORS, isCondExpr, ternaryCtorOfRhs, scanBoundedLoops, inBoundsCharCodeAt, exprType, intCertainMap } from '../type.js'
|
|
27
27
|
import { TYPED_ELEM_CODE, TYPED_ELEM_VIEW_FLAG, TYPED_ELEM_BIGINT_FLAG, encodeTypedElemAux, typedElemAux, TYPED_ELEM_NAMES, ctorFromElemAux } from '../../layout.js'
|
|
28
28
|
|
|
@@ -100,12 +100,26 @@ export function resetBodyFactsCache() { _bodyFactsCache = new WeakMap() }
|
|
|
100
100
|
// (updateRep), so the param would be polymorphic (Map | object) — which the self-host
|
|
101
101
|
// kernel cannot statically type, mis-dispatching `store.set` on the object form to the
|
|
102
102
|
// Map builtin. Direct closure calls sidestep method dispatch entirely.
|
|
103
|
+
//
|
|
104
|
+
// An UNRESOLVABLE observation (`vt` null — `let sub = node[i]` where the RHS's type
|
|
105
|
+
// isn't provable) is STILL an observation and must poison exactly like a conflicting
|
|
106
|
+
// definite one, not merely a no-op when nothing was set yet. The walk has no CFG/
|
|
107
|
+
// dominance info, so it cannot tell a later definite write (`sub = 'nan'`) that
|
|
108
|
+
// UNCONDITIONALLY overwrites the unresolved initializer (safe to adopt) from one
|
|
109
|
+
// reachable only on some paths (`if (...) sub = 'nan'` — the initializer's value
|
|
110
|
+
// survives on the other path). Treating "unresolved" as "no information" let the
|
|
111
|
+
// conditional case's definite arm win outright: `valTypeOf(sub)` then reported
|
|
112
|
+
// STRING everywhere, so `content += sub` skipped ToString and read a raw NUMBER
|
|
113
|
+
// box as string bits — empty output instead of the number's digits. Poisoning here
|
|
114
|
+
// costs only the narrow straight-line-overwrite case (`let x = f(); x = 5` no longer
|
|
115
|
+
// infers NUMBER for the dead initializer) — "default is never wrong, only sometimes
|
|
116
|
+
// wider than necessary" (module header, src/infer.js).
|
|
103
117
|
const makeValTracker = (get, set, del) => {
|
|
104
118
|
const poison = new Set()
|
|
105
119
|
return (name, vt) => {
|
|
106
120
|
if (poison.has(name)) return
|
|
121
|
+
if (!vt) { poison.add(name); del(name); return }
|
|
107
122
|
const prev = get(name)
|
|
108
|
-
if (!vt) { if (prev) poison.add(name); del(name); return }
|
|
109
123
|
if (prev && prev !== vt) { poison.add(name); del(name); return }
|
|
110
124
|
set(name, vt)
|
|
111
125
|
}
|
|
@@ -140,7 +154,13 @@ const makeTypedTracker = (get, set, del, getLen, setLen, delLen) => {
|
|
|
140
154
|
// makeValTracker comment above — a captured Map would orphan on the
|
|
141
155
|
// per-function ctx.types reset).
|
|
142
156
|
if (setLen) {
|
|
143
|
-
|
|
157
|
+
// A name alias (`let x = a` — the inliner's param-alias splice) carries
|
|
158
|
+
// the source's static length: typed arrays never resize, and typedLen
|
|
159
|
+
// facts are single-def-stable by construction (validate strips written
|
|
160
|
+
// params; the tracker invalidates redefs), so the copy is exact.
|
|
161
|
+
const len = typedStaticLen(rhs) ?? (typeof rhs === 'string'
|
|
162
|
+
? getLen(rhs) ?? ctx.types.typedLen?.get(rhs) ?? ctx.scope?.globalTypedLen?.get(rhs) ?? null
|
|
163
|
+
: null)
|
|
144
164
|
const prevLen = getLen(name)
|
|
145
165
|
if (len == null || (prevLen !== undefined && prevLen !== len)) delLen(name)
|
|
146
166
|
else setLen(name, len)
|
|
@@ -149,6 +169,16 @@ const makeTypedTracker = (get, set, del, getLen, setLen, delLen) => {
|
|
|
149
169
|
}
|
|
150
170
|
const ctor = typedElemCtor(rhs)
|
|
151
171
|
if (ctor) return setOrInvalidate(ctor)
|
|
172
|
+
// Bare name alias (`let x = a` — the inliner's param-alias splice): the
|
|
173
|
+
// source's settled ctor and static length copy to the alias — typed arrays
|
|
174
|
+
// never resize, and a buffer-sharing VIEW arrives via the subarray arm
|
|
175
|
+
// below, never as a bare name. A name with no typed fact stays untracked
|
|
176
|
+
// (same silence as today).
|
|
177
|
+
if (typeof rhs === 'string') {
|
|
178
|
+
const c = resolveName(rhs)
|
|
179
|
+
if (c) return setOrInvalidate(c)
|
|
180
|
+
return
|
|
181
|
+
}
|
|
152
182
|
// `recv.subarray(...)` is a zero-copy VIEW aliasing the receiver's buffer — its elem
|
|
153
183
|
// ctor is the receiver's type with the `.view` flag, so the binding unboxes to a typed
|
|
154
184
|
// pointer and element writes take the descriptor-indirected path (not desc-as-data).
|
|
@@ -1346,6 +1376,9 @@ export function analyzeStructInline(funcFacts, programFacts) {
|
|
|
1346
1376
|
if (!inlineArray || !ctx.schema?.list) return
|
|
1347
1377
|
const { paramReps } = programFacts
|
|
1348
1378
|
const cand = new Set() // sids observed as an `Array<S>` element schema
|
|
1379
|
+
// env-gated debug — dist/jz.js runs in browsers where `process` doesn't
|
|
1380
|
+
// exist, and WASI hosts strip `process.env`
|
|
1381
|
+
const DBG = typeof process !== 'undefined' && process.env?.JZ_DBG_INLARR
|
|
1349
1382
|
const black = new Set() // sids disqualified by some use
|
|
1350
1383
|
|
|
1351
1384
|
const propsOf = (sid) => ctx.schema.list[sid] || []
|
|
@@ -1397,10 +1430,23 @@ export function analyzeStructInline(funcFacts, programFacts) {
|
|
|
1397
1430
|
for (let i = 1; i < node.length; i++) poisonAll(node[i])
|
|
1398
1431
|
}
|
|
1399
1432
|
|
|
1433
|
+
const cursorsByFunc = new Map() // sig → Map<name, sid> — feeds inlineCellCursors
|
|
1434
|
+
const bracketKeyed = new Set() // sids with `p['x']` cursor reads — those route
|
|
1435
|
+
// through the boxed dyn path (f64 slots), so
|
|
1436
|
+
// they stay on f64 cells (no i32 packing)
|
|
1437
|
+
// A no-array frame needs the call-composition walk ONLY when some function
|
|
1438
|
+
// program-wide returns an `Array<S>` (its result could flow into an
|
|
1439
|
+
// un-sanctioned call position here). With zero such functions, no `mk()`
|
|
1440
|
+
// call can carry an inline array, so the walk is a guaranteed no-op — skip
|
|
1441
|
+
// it (compile-time: the self-host compiler has hundreds of array-free frames
|
|
1442
|
+
// whose full-body walk was pure waste).
|
|
1443
|
+
const anyArrRetFn = ctx.func.list.some(f => f?.arrayElemSchema != null && !f.raw)
|
|
1400
1444
|
for (const [func, facts] of funcFacts) {
|
|
1401
1445
|
const body = func?.body
|
|
1402
|
-
|
|
1403
|
-
|
|
1446
|
+
// A reps-less frame (zero locals/params — a composing `main`) still gets
|
|
1447
|
+
// the walk: its call compositions can forward inline-carried returns.
|
|
1448
|
+
const reps = facts?.localReps ?? new Map()
|
|
1449
|
+
if (func?.raw || body == null || typeof body !== 'object') continue
|
|
1404
1450
|
|
|
1405
1451
|
// `Array<S>` bindings of this function (codegen truth) and their schemas.
|
|
1406
1452
|
const arrName = new Map() // name → sid
|
|
@@ -1411,13 +1457,21 @@ export function analyzeStructInline(funcFacts, programFacts) {
|
|
|
1411
1457
|
cand.add(sid)
|
|
1412
1458
|
arrName.set(name, sid)
|
|
1413
1459
|
}
|
|
1414
|
-
|
|
1460
|
+
// A frame with no tracked arrays of its own still gets the walk when the
|
|
1461
|
+
// program has Array<S>-returning functions: it can FORWARD inline-carried
|
|
1462
|
+
// returns through call compositions (`use(mk())` in a helper-free main,
|
|
1463
|
+
// `mk().length`) — the verify walk's call rules must see those sites (this
|
|
1464
|
+
// closed a live wrong-value: `mk().length` read the PHYSICAL cell count
|
|
1465
|
+
// K·n). When nothing returns Array<S>, the walk is provably a no-op.
|
|
1466
|
+
if (!arrName.size && !anyArrRetFn) continue
|
|
1415
1467
|
|
|
1416
1468
|
// A structInline `Array<S>` value is only ever born from an empty `[]`
|
|
1417
1469
|
// grown by structInline `.push`. `expr` is such a producer of `Array<sid>`
|
|
1418
1470
|
// iff it is: a tracked `Array<sid>` alias, an empty `[]` literal, or a call
|
|
1419
|
-
// to a user function
|
|
1420
|
-
//
|
|
1471
|
+
// to a user function whose settled return fact IS `Array<sid>` (narrow's
|
|
1472
|
+
// fact — the exact agreement the receiving binding's own rep derives from;
|
|
1473
|
+
// a fact-less callee could return an inline-carried array into a binding
|
|
1474
|
+
// read as plain, `mk().length`-class). Every other source — a non-empty
|
|
1421
1475
|
// `[{S},…]` literal, a builtin call (`JSON.parse`, `Object.values`, `.map`,
|
|
1422
1476
|
// `.slice`, a member access onto a parsed object) — yields a taggedLinear
|
|
1423
1477
|
// array and must poison sid.
|
|
@@ -1426,8 +1480,10 @@ export function analyzeStructInline(funcFacts, programFacts) {
|
|
|
1426
1480
|
if (!Array.isArray(expr)) return false
|
|
1427
1481
|
const elems = staticArrayElems(expr)
|
|
1428
1482
|
if (elems) return elems.length === 0
|
|
1429
|
-
return expr[0] === '()' && typeof expr[1] === 'string' &&
|
|
1483
|
+
return expr[0] === '()' && typeof expr[1] === 'string' &&
|
|
1484
|
+
ctx.func.map?.get(expr[1])?.arrayElemSchema === sid
|
|
1430
1485
|
}
|
|
1486
|
+
const isUserCall = (e) => Array.isArray(e) && e[0] === '()' && typeof e[1] === 'string'
|
|
1431
1487
|
|
|
1432
1488
|
// Pass 1 — collect `const p = a[i]` cursors; drop on name clash / re-decl.
|
|
1433
1489
|
const cursor = new Map() // name → sid
|
|
@@ -1452,6 +1508,7 @@ export function analyzeStructInline(funcFacts, programFacts) {
|
|
|
1452
1508
|
for (let i = 1; i < node.length; i++) collectCursors(node[i])
|
|
1453
1509
|
}
|
|
1454
1510
|
collectCursors(body)
|
|
1511
|
+
if (cursor.size) cursorsByFunc.set(func.sig, cursor)
|
|
1455
1512
|
|
|
1456
1513
|
// A `['[]', arrName, idx]` element read of a tracked array → its sid.
|
|
1457
1514
|
const elemArrSid = (n) =>
|
|
@@ -1468,6 +1525,29 @@ export function analyzeStructInline(funcFacts, programFacts) {
|
|
|
1468
1525
|
}
|
|
1469
1526
|
const visitChild = (c) => { if (!flag(c)) verify(c) }
|
|
1470
1527
|
|
|
1528
|
+
// Argument walk of a direct user call — the one sanctioned way to verify
|
|
1529
|
+
// a call node. `Array<S>` values may cross a call boundary only when the
|
|
1530
|
+
// callee's param carries the same settled elem fact (a structInline-
|
|
1531
|
+
// carried array read as plain on the other side misinterprets cells —
|
|
1532
|
+
// both name args and `g(mk())` call-expr args need the agreement).
|
|
1533
|
+
function verifyCall(node) {
|
|
1534
|
+
const callee = node[1]
|
|
1535
|
+
const args = argsOf(node)
|
|
1536
|
+
const known = typeof callee === 'string' && ctx.func.map?.has(callee)
|
|
1537
|
+
const cParams = known ? paramReps?.get(callee) : null
|
|
1538
|
+
for (let k = 0; k < args.length; k++) {
|
|
1539
|
+
const arg = args[k]
|
|
1540
|
+
if (typeof arg === 'string' && arrName.has(arg)) {
|
|
1541
|
+
const sid = arrName.get(arg)
|
|
1542
|
+
if (!(known && cParams?.get(k)?.arrayElemSchema === sid)) black.add(sid)
|
|
1543
|
+
} else if (isUserCall(arg) && ctx.func.map?.get(arg[1])?.arrayElemSchema != null) {
|
|
1544
|
+
const rsid = ctx.func.map.get(arg[1]).arrayElemSchema
|
|
1545
|
+
if (!(known && cParams?.get(k)?.arrayElemSchema === rsid)) black.add(rsid)
|
|
1546
|
+
verifyCall(arg)
|
|
1547
|
+
} else if (!flag(arg)) verify(arg)
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1471
1551
|
function verify(node) {
|
|
1472
1552
|
if (!Array.isArray(node)) return
|
|
1473
1553
|
const op = node[0]
|
|
@@ -1501,25 +1581,80 @@ export function analyzeStructInline(funcFacts, programFacts) {
|
|
|
1501
1581
|
const o = node[1], k = node[2]
|
|
1502
1582
|
if (typeof o === 'string') {
|
|
1503
1583
|
if (arrName.has(o)) black.add(arrName.get(o)) // element value escape
|
|
1504
|
-
else if (cursor.has(o)) {
|
|
1584
|
+
else if (cursor.has(o)) {
|
|
1585
|
+
if (!(isStrLit(k) && inSchema(cursor.get(o), k[1]))) black.add(cursor.get(o))
|
|
1586
|
+
else bracketKeyed.add(cursor.get(o)) // legal, but f64-cells-only
|
|
1587
|
+
}
|
|
1505
1588
|
if (k != null) visitChild(k)
|
|
1506
1589
|
return
|
|
1507
1590
|
}
|
|
1508
1591
|
const esid = elemArrSid(o)
|
|
1509
1592
|
if (esid != null) {
|
|
1510
1593
|
if (!(isStrLit(k) && inSchema(esid, k[1]))) black.add(esid)
|
|
1594
|
+
else bracketKeyed.add(esid)
|
|
1511
1595
|
visitChild(o[2])
|
|
1512
1596
|
} else if (o != null) visitChild(o)
|
|
1513
1597
|
if (k != null) visitChild(k)
|
|
1514
1598
|
return
|
|
1515
1599
|
}
|
|
1516
1600
|
|
|
1601
|
+
// Property WRITES on a tracked array (`a.length = n`, `a.length++`) —
|
|
1602
|
+
// the `.` receiver rule below allows `.length` READS only; a resize in
|
|
1603
|
+
// LOGICAL units through the physical-cell header would corrupt the
|
|
1604
|
+
// carrier's length semantics. Any dot-target write/update poisons.
|
|
1605
|
+
if ((op === '++' || op === '--' || ASSIGN_OPS.has(op)) &&
|
|
1606
|
+
Array.isArray(node[1]) && (node[1][0] === '.' || node[1][0] === '?.') &&
|
|
1607
|
+
typeof node[1][1] === 'string' && arrName.has(node[1][1])) {
|
|
1608
|
+
black.add(arrName.get(node[1][1]))
|
|
1609
|
+
for (let i = 2; i < node.length; i++) visitChild(node[i])
|
|
1610
|
+
return
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
// Wholesale element replace `a[i] = {S-literal}` — the immutable-update
|
|
1614
|
+
// idiom. Handled iff the whole-program alias sweep (scanInplaceStores)
|
|
1615
|
+
// proved every same-content store safe (content-keyed — node identity
|
|
1616
|
+
// does not survive analyzeFuncForEmit's loop rewrites) WITH target-
|
|
1617
|
+
// binding reuse: a same-index tracked cursor precedes the store, so the
|
|
1618
|
+
// replace idiom is separated from append-builders (`out[len] = {…}`),
|
|
1619
|
+
// which stay on the plain layout where extend keeps JS semantics. A
|
|
1620
|
+
// value-position `x = (a[i] = {…})` poisons the sid inside the sweep
|
|
1621
|
+
// itself (its `[]` target walks as a value read), so a surviving verdict
|
|
1622
|
+
// implies statement position. Index must be an int-certain name — a
|
|
1623
|
+
// fractional/negative index is a sidecar PROPERTY write in JS, which the
|
|
1624
|
+
// inline arm cannot express (it drops OOB writes like the checked typed
|
|
1625
|
+
// store). Emit lowers via emit-assign's tryStructInlineReplaceStore.
|
|
1626
|
+
if (op === '=' && Array.isArray(node[1]) && node[1][0] === '[]' && node[1].length === 3 &&
|
|
1627
|
+
typeof node[1][1] === 'string' && arrName.has(node[1][1])) {
|
|
1628
|
+
const sid = arrName.get(node[1][1])
|
|
1629
|
+
const rhs = node[2], idx = node[1][2]
|
|
1630
|
+
const entry = Array.isArray(rhs) && rhs[0] === '{}'
|
|
1631
|
+
? ctx.schema.inplaceStores?.get(inplaceKey(node[1][1], rhs)) : null
|
|
1632
|
+
const ok = typeof idx === 'string' && reps.get(idx)?.intCertain === true &&
|
|
1633
|
+
entry != null && entry.alias != null && entry.idx === idx &&
|
|
1634
|
+
objLiteralSchemaId(rhs) === sid
|
|
1635
|
+
if (!ok) {
|
|
1636
|
+
if (DBG) console.error('[inlarr-store-reject]', func.name, node[1][1], 'sid', sid,
|
|
1637
|
+
'idxIntCertain', typeof idx === 'string' && reps.get(idx)?.intCertain === true,
|
|
1638
|
+
'entry', entry, 'litSid', Array.isArray(rhs) ? objLiteralSchemaId(rhs) : null)
|
|
1639
|
+
black.add(sid)
|
|
1640
|
+
if (idx != null) visitChild(idx)
|
|
1641
|
+
if (rhs != null) visitChild(rhs)
|
|
1642
|
+
return
|
|
1643
|
+
}
|
|
1644
|
+
if (idx != null) visitChild(idx)
|
|
1645
|
+
// literal is a fresh value consumed by the store — verify slot values only
|
|
1646
|
+
const props = rhs.length === 2 && Array.isArray(rhs[1]) && rhs[1][0] === ',' ? rhs[1].slice(1) : rhs.slice(1)
|
|
1647
|
+
for (const pr of props) visitChild(Array.isArray(pr) && pr[0] === ':' ? pr[2] : pr)
|
|
1648
|
+
return
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1517
1651
|
// Reassignment of the array binding — the rhs must be a structInline
|
|
1518
1652
|
// `Array<S>` producer; an alias is left un-walked (flagging it would
|
|
1519
1653
|
// self-poison), other producers are walked to verify their subtree.
|
|
1520
1654
|
if (op === '=' && typeof node[1] === 'string' && arrName.has(node[1])) {
|
|
1521
1655
|
const sid = arrName.get(node[1])
|
|
1522
1656
|
if (!safeArrSource(node[2], sid)) black.add(sid)
|
|
1657
|
+
else if (isUserCall(node[2])) verifyCall(node[2])
|
|
1523
1658
|
else if (typeof node[2] !== 'string') visitChild(node[2])
|
|
1524
1659
|
return
|
|
1525
1660
|
}
|
|
@@ -1550,16 +1685,17 @@ export function analyzeStructInline(funcFacts, programFacts) {
|
|
|
1550
1685
|
return
|
|
1551
1686
|
}
|
|
1552
1687
|
if (typeof callee === 'string') {
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1688
|
+
// A call reached through GENERIC descent is an un-sanctioned
|
|
1689
|
+
// position for an `Array<S>`-returning callee — a receiver
|
|
1690
|
+
// (`mk().length` reads the PHYSICAL cell count), an operand, a
|
|
1691
|
+
// spread, a bare statement. Sanctioned positions (decl init /
|
|
1692
|
+
// return with fact agreement, agreement-checked call args) route
|
|
1693
|
+
// through verifyCall directly and never reach this poison. An
|
|
1694
|
+
// expression-bodied arrow's whole body is its return position —
|
|
1695
|
+
// sanction it under the same fact agreement.
|
|
1696
|
+
const retSid = ctx.func.map?.get(callee)?.arrayElemSchema
|
|
1697
|
+
if (retSid != null && !(node === body && func.arrayElemSchema === retSid)) black.add(retSid)
|
|
1698
|
+
verifyCall(node)
|
|
1563
1699
|
return
|
|
1564
1700
|
}
|
|
1565
1701
|
visitChild(callee)
|
|
@@ -1580,6 +1716,15 @@ export function analyzeStructInline(funcFacts, programFacts) {
|
|
|
1580
1716
|
black.add(func.arrayElemSchema)
|
|
1581
1717
|
const esid = elemArrSid(e)
|
|
1582
1718
|
if (esid != null) { black.add(esid); visitChild(e[2]); return }
|
|
1719
|
+
if (isUserCall(e)) {
|
|
1720
|
+
// `return g()` in a function with NO matching elem fact lets an
|
|
1721
|
+
// inline-carried array escape into fact-less land — poison unless
|
|
1722
|
+
// the facts agree (the agreeing case is the sanctioned position).
|
|
1723
|
+
const rsid = ctx.func.map?.get(e[1])?.arrayElemSchema
|
|
1724
|
+
if (rsid != null && func.arrayElemSchema !== rsid) black.add(rsid)
|
|
1725
|
+
verifyCall(e)
|
|
1726
|
+
return
|
|
1727
|
+
}
|
|
1583
1728
|
if (e != null) visitChild(e)
|
|
1584
1729
|
return
|
|
1585
1730
|
}
|
|
@@ -1597,7 +1742,9 @@ export function analyzeStructInline(funcFacts, programFacts) {
|
|
|
1597
1742
|
if (typeof name === 'string' && arrName.has(name)) {
|
|
1598
1743
|
const sid = arrName.get(name)
|
|
1599
1744
|
if (!safeArrSource(rhs, sid)) black.add(sid) // non-structInline producer
|
|
1600
|
-
|
|
1745
|
+
// [] / fact-agreeing user call — sanctioned; verify args/subtree
|
|
1746
|
+
else if (isUserCall(rhs)) verifyCall(rhs)
|
|
1747
|
+
else if (typeof rhs !== 'string') visitChild(rhs)
|
|
1601
1748
|
continue
|
|
1602
1749
|
}
|
|
1603
1750
|
if (typeof name !== 'string') visitChild(name)
|
|
@@ -1616,6 +1763,28 @@ export function analyzeStructInline(funcFacts, programFacts) {
|
|
|
1616
1763
|
if (ctx.module?.moduleInits) for (const mi of ctx.module.moduleInits) poisonAll(mi)
|
|
1617
1764
|
|
|
1618
1765
|
for (const sid of cand) if (!black.has(sid)) inlineArray.add(sid)
|
|
1766
|
+
|
|
1767
|
+
// Packed i32 cells (inlineCellI32): all slots strict-int32 (slotI32Certain —
|
|
1768
|
+
// every censused write exactly-int32, never -0, hazard-belted), K ≥ 2 (a
|
|
1769
|
+
// 1-field element still occupies one 8-byte cell — packing buys nothing),
|
|
1770
|
+
// and no bracket-keyed cursor reads (those route through the boxed dyn
|
|
1771
|
+
// path, which assumes f64 slots). Elements then pack K raw i32 fields into
|
|
1772
|
+
// ⌈K/2⌉ physical cells — C's record layout; loads/stores drop the
|
|
1773
|
+
// trunc_sat/convert layer. The packed decision is consumed through cursor
|
|
1774
|
+
// nodes (inlineCellCursors → readVar's `.cellI32` tag), never the bare sid:
|
|
1775
|
+
// a standalone `{S}` object of the same sid keeps tagged f64 slots.
|
|
1776
|
+
for (const sid of inlineArray) {
|
|
1777
|
+
const props = propsOf(sid)
|
|
1778
|
+
if (props.length >= 2 && !bracketKeyed.has(sid) &&
|
|
1779
|
+
props.every(p => ctx.schema.slotI32CertainBySid?.(sid, p)))
|
|
1780
|
+
ctx.schema.inlineCellI32.add(sid)
|
|
1781
|
+
}
|
|
1782
|
+
for (const [sig, cur] of cursorsByFunc) {
|
|
1783
|
+
let set = null
|
|
1784
|
+
for (const [name, sid] of cur) if (ctx.schema.inlineCellI32.has(sid)) (set ??= new Set()).add(name)
|
|
1785
|
+
if (set) ctx.schema.inlineCellCursors.set(sig, set)
|
|
1786
|
+
}
|
|
1787
|
+
if (DBG) console.error('[inlarr]', 'eligible:', [...inlineArray], 'packedI32:', [...ctx.schema.inlineCellI32])
|
|
1619
1788
|
}
|
|
1620
1789
|
|
|
1621
1790
|
/** Schema id when `name` is bound (codegen truth) to a structInline `Array<S>`,
|