jz 0.9.1 → 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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "jz",
3
- "version": "0.9.1",
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.",
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.2"
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` occupies K
70
- // consecutive 8-byte cells; the carrier hands back the byte address of the
71
- // element's first cell, and field `f` is then a plain `+f*8` composed by the
72
- // schema slot machinery (`ctx.abi.object.ops`). Built on demand per schema
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
- ops: {
77
- // Byte address of logical element `idx`'s first cell off i32 `base`.
78
- // A JS-integer idx folds to a constant; an IR-node idx emits `idx*K`
79
- // then the `<<3` (via `addr`).
80
- elemAddr: (base, idx) =>
81
- typeof idx === 'number'
82
- ? addr(base, idx * K)
83
- : addr(base, K === 1 ? idx : ['i32.mul', idx, ['i32.const', K]]),
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'],
@@ -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
- const len = typedStaticLen(rhs)
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
- const reps = facts?.localReps
1403
- if (func?.raw || !reps || body == null || typeof body !== 'object') continue
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
- if (!arrName.size) continue
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 (whose returned array is structInline whenever sid
1420
- // survives this whole-program pass). Every other source a non-empty
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' && !!ctx.func.map?.has(expr[1])
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)) { if (!(isStrLit(k) && inSchema(cursor.get(o), k[1]))) black.add(cursor.get(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
- const args = argsOf(node)
1554
- const known = ctx.func.map?.has(callee)
1555
- const cParams = paramReps?.get(callee)
1556
- for (let k = 0; k < args.length; k++) {
1557
- const arg = args[k]
1558
- if (typeof arg === 'string' && arrName.has(arg)) {
1559
- const sid = arrName.get(arg)
1560
- if (!(known && cParams?.get(k)?.arrayElemSchema === sid)) black.add(sid)
1561
- } else if (!flag(arg)) verify(arg)
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
- else if (typeof rhs !== 'string') visitChild(rhs) // [] / user-call — verify subtree
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>`,