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/src/ctx.js CHANGED
@@ -385,6 +385,20 @@ export function reset(proto, globals, bridge) {
385
385
  // Populated whole-program by `analyzeStructInline`
386
386
  // (default-disqualify); read by the array
387
387
  // push/index/length codegen.
388
+ inlineCellI32: new Set(), // inlineArray subset — every slot strict-int32
389
+ // (slotI32Certain) and K ≥ 2, so the element
390
+ // packs K raw i32 cells (⌈K/2⌉ physical 8-byte
391
+ // cells, C's exact record layout): raw i32
392
+ // loads/stores, no trunc_sat/convert per field.
393
+ // Standalone `{S}` objects of the same sid keep
394
+ // f64 slots — the packed decision rides the
395
+ // CURSOR (inlineCellCursors → node.cellI32),
396
+ // never the bare sid.
397
+ inlineCellCursors: new Map(), // sig → Set<name>: `const p = a[i]` cursor
398
+ // locals of packed (inlineCellI32) arrays in
399
+ // that function — readVar tags their reads
400
+ // with `.cellI32` so slot access picks the
401
+ // packed i32 ops instead of f64 cells.
388
402
  }
389
403
 
390
404
  ctx.closure = {
package/src/ir.js CHANGED
@@ -835,7 +835,7 @@ export const cloneIR = (n) => Array.isArray(n) ? n.map(cloneIR) : n
835
835
  * anything else → itself. `valIR` must be side-effect-free (a local read) — it is duplicated,
836
836
  * so each occurrence gets a fresh clone. Used for bindings flagged in ctx.func.maybeNullish;
837
837
  * a real number isn't either sentinel, so it falls through the `else` unchanged. */
838
- const coerceNullishToNum = (valIR) => typed(
838
+ export const coerceNullishToNum = (valIR) => typed(
839
839
  ['if', ['result', 'f64'],
840
840
  ['i64.eq', ['i64.reinterpret_f64', cloneIR(valIR)], ['i64.const', NULL_NAN]],
841
841
  ['then', ['f64.const', 0]],
@@ -857,6 +857,26 @@ export function toNumF64(node, v) {
857
857
  // not a number — skipping coercion would reinterpret pointer bits as an f64.
858
858
  // Only a plain i32 (loop counter, `x|0`) is genuinely already-numeric.
859
859
  if ((v.type === 'i32' && v.ptrKind == null) || isLit(v)) return asF64(v)
860
+ // Checked typed-array read (`.typed:[]` tags checkedNumRead): number|undefined
861
+ // with the undefined confined to a CONSTANT miss arm. ToNumber of that arm
862
+ // folds statically (undefined → canonical NaN) — the hit arm is already a
863
+ // plain-number load. Without the fold the UNDEF sentinel enters f64 arithmetic
864
+ // as a "number" (valTypeOf claims NUMBER from the ELEMENT type, blind to the
865
+ // OOB path — checked BEFORE the vt fast-outs below for exactly that reason),
866
+ // and hardware NaN propagation carries its PAYLOAD to the escape, where the
867
+ // boundary decodes it back as `undefined` (JS: NaN).
868
+ if (v.checkedNumRead && Array.isArray(v)) {
869
+ const foldArm = (n) => Array.isArray(n) && n[0] === 'f64.const' && n[1] === `nan:${UNDEF_NAN}`
870
+ ? ['f64.const', 'nan'] : n
871
+ if (v[0] === 'if') // (if (result f64) cond (then load) (else UNDEF))
872
+ return typed(v.map(c => Array.isArray(c) && c[0] === 'else' && c.length === 2
873
+ ? ['else', foldArm(c[1])] : c), 'f64')
874
+ if (v[0] === 'block') { // (block (result f64) …sets (select load UNDEF in))
875
+ const tail = v[v.length - 1]
876
+ if (Array.isArray(tail) && tail[0] === 'select')
877
+ return typed([...v.slice(0, -1), ['select', tail[1], foldArm(tail[2]), tail[3]]], 'f64')
878
+ }
879
+ }
860
880
  // A binding assigned a nullish literal may hold null/undefined here — coerce per ToNumber
861
881
  // (null→+0, undefined→NaN); a real number falls through unchanged. Only flagged bindings pay
862
882
  // this, so the numeric kernels jz optimizes for (which never assign null) stay untouched.
@@ -1240,6 +1260,11 @@ export function readVar(name) {
1240
1260
  node.ptrKind = rep.ptrKind
1241
1261
  const aux = rep.ptrAux ?? ctx.schema.idOf?.(name)
1242
1262
  if (aux != null) node.ptrAux = aux
1263
+ // structInline cursor into a PACKED (i32-cell) array: slot access must
1264
+ // pick the packedI32 ops, not the f64 slot layout — the flag rides the
1265
+ // node because a standalone object of the same sid keeps f64 slots.
1266
+ if (rep.ptrKind === VAL.OBJECT && ctx.schema.inlineCellCursors?.get(ctx.func.current)?.has(name))
1267
+ node.cellI32 = true
1243
1268
  }
1244
1269
  return node
1245
1270
  }
@@ -166,6 +166,7 @@ const LEVEL_PRESETS = Object.freeze({
166
166
  unrollRecurrence: false, // ×2 body duplication is a size regression — speed-only
167
167
  clampPeel: false, // edge-clamp peel triples a stencil loop (clamp-free interior + 2 edges) to vectorize — speed-only
168
168
  versionTypedBounds: false,// typed-bounds loop versioning duplicates every proven nest (guarded fast arm + checked twin, ×1.5-3 on small kernels) — the branchless checked reads alone are the size-tier lowering; speed-only trade
169
+ leanCheckedIdx: true, // unproven typed reads emit the if-form (guard → direct load, else undefined) — ~6 ops/site smaller than the select-clamp form, which exists only so SPEED-tier kernel bodies stay branch-free for the SIMD lift (off here)
169
170
 
170
171
  boolConvertToSelect: false, // adds a const + op per site — speed-only latency trade
171
172
  devirtIndirect: false, // guards + duplicated args grow bytes — speed-only trade
@@ -978,6 +978,32 @@ function matchLaneOffset(off, ind, offsetTees, allowAos, aosPix, idxTees) {
978
978
  return null
979
979
  }
980
980
 
981
+ /**
982
+ * Mirror lane address: `base + ((INV − iv) << K)` — the DESCENDING twin of the
983
+ * canonical lane address (symmetric fills: `inp[N−k] = lm`). INV is an i32
984
+ * const or a bare local; the CALLER must verify a named INV is body-invariant
985
+ * (not in the body writes set). f64 store sites only (2 lanes): the vector for
986
+ * (iv, iv+1) mirrors to (INV−iv, INV−iv−1) — contiguous descending, stored as
987
+ * one v128 at INV−iv−1 with the lanes swapped.
988
+ */
989
+ function matchMirrorAddr(addr, ind) {
990
+ if (!isArr(addr) || addr[0] !== 'i32.add' || addr.length !== 3) return null
991
+ for (const k of [1, 2]) {
992
+ const off = addr[k]
993
+ if (!isArr(off) || off[0] !== 'i32.shl' || off.length !== 3) continue
994
+ const K = constNum(off[2])
995
+ if (K == null || K < 0 || K > 3) continue
996
+ const sub = off[1]
997
+ if (!isArr(sub) || sub[0] !== 'i32.sub' || sub.length !== 3) continue
998
+ if (!isLocalGet(sub[2], ind)) continue
999
+ const inv = sub[1]
1000
+ const invName = isArr(inv) && inv[0] === 'local.get' && typeof inv[1] === 'string' ? inv[1] : null
1001
+ if (!invName && constNum(inv) == null) continue
1002
+ return { base: addr[k === 1 ? 2 : 1], invExpr: inv, invName, strideLog2: K }
1003
+ }
1004
+ return null
1005
+ }
1006
+
981
1007
  /**
982
1008
  * Match an address expression `(i32.add base OFFSET)`, with optional outer
983
1009
  * `(local.tee $A ...)`. OFFSET is matched by matchLaneOffset (which also
@@ -1267,6 +1293,7 @@ function tryVectorize(bl, fnLocals, freshIdRef, pureFuncMap, constLocals) {
1267
1293
  const allowAos = true
1268
1294
  const aosPix = new Map()
1269
1295
  let aosPixelStride = 1
1296
+ const mirrorSites = [] // mirror stores a[INV−iv] — invariance of INV checked post-scan
1270
1297
  // Pixel-INDEX locals: `$J = P*i` (the `const j = 3*i` of an AoS loop, kept as its own local
1271
1298
  // pre-watr). A channel address is then `base + ((local.get $J) << K)`; idxTees lets
1272
1299
  // matchLaneOffset resolve $J → pixel-stride P. Value -1 marks an inconsistent local (bail).
@@ -1357,8 +1384,20 @@ function tryVectorize(bl, fnLocals, freshIdRef, pureFuncMap, constLocals) {
1357
1384
  if (laneType != null && sty !== laneType && !narrowing) return false
1358
1385
  if (laneType == null) { laneType = sty; stride = LANE_INFO[laneType].stride }
1359
1386
  const memarg = typeof node[1] === 'string' && node[1].startsWith('offset=')
1360
- const m = matchLaneAddr(memAddr(node), incVar, addrLocals, offsetTees, allowAos, aosPix, idxTees)
1361
- if (!m) return false
1387
+ let m = matchLaneAddr(memAddr(node), incVar, addrLocals, offsetTees, allowAos, aosPix, idxTees)
1388
+ if (!m) {
1389
+ // mirror store `a[INV − iv] = lane` (f64, full-width, no memarg): the
1390
+ // descending twin — accepted as its own site class; INV invariance is
1391
+ // verified post-scan against the body writes set.
1392
+ const mm = !memarg && sty === 'f64' && laneType === 'f64' && matchMirrorAddr(memAddr(node), incVar)
1393
+ if (mm && (1 << mm.strideLog2) === stride) {
1394
+ mirrorSites.push(mm)
1395
+ siteStrides.push(1)
1396
+ loadStoreSites.push({ parent, idx: pi, kind: 'store' })
1397
+ return scanForLoadsStores(node[2], node, 2)
1398
+ }
1399
+ return false
1400
+ }
1362
1401
  if ((1 << m.strideLog2) !== (narrowing ? LANE_INFO[sty].stride : stride)) return false
1363
1402
  if (!recordAos(m)) return false
1364
1403
  if (m.teeName) addrLocals.set(m.teeName, { strideLog2: m.strideLog2, pixelStride: m.pixelStride, base: m.base })
@@ -1410,6 +1449,10 @@ function tryVectorize(bl, fnLocals, freshIdRef, pureFuncMap, constLocals) {
1410
1449
  const writes = new Set()
1411
1450
  for (const s of body) collectWrites(s, writes)
1412
1451
  if (boundLocal && writes.has(boundLocal)) return null // bound varies in body → bail
1452
+ // a mirror INV written in the body is not invariant — the descending window would drift
1453
+ for (const mm of mirrorSites) if (mm.invName && writes.has(mm.invName)) return null
1454
+ // AoS gather/scatter and mirror windows don't compose (distinct per-step deltas)
1455
+ if (mirrorSites.length && aosPixelStride > 1) return null
1413
1456
 
1414
1457
  const localKind = new Map() // name → 'lane' | 'invariant' | 'addr'
1415
1458
  // Walk to collect ALL referenced names
@@ -1542,9 +1585,14 @@ function tryVectorize(bl, fnLocals, freshIdRef, pureFuncMap, constLocals) {
1542
1585
  ]
1543
1586
  ]
1544
1587
 
1545
- // Bound setup: simdBoundName = bound & ~(lanes-1)
1588
+ // Bound setup: align the SPAN, not the bound — simdBound = iv + ((bound iv)
1589
+ // & ~(lanes−1)). `bound & mask` assumed a 0 entry: a loop entering at k=1
1590
+ // (symmetric fills start past the DC bin) would run its last vector step at
1591
+ // k = bound−1 and overrun one lane past the bound. iv holds the entry value
1592
+ // here (the setup precedes the SIMD block).
1546
1593
  const boundSetup = ['local.set', simdBoundName,
1547
- ['i32.and', boundExpr, ['i32.const', mask]]]
1594
+ ['i32.add', ['local.get', incVar],
1595
+ ['i32.and', ['i32.sub', boundExpr, ['local.get', incVar]], ['i32.const', mask]]]]
1548
1596
 
1549
1597
  // Synthetic outer wrapper — has no result, no label, just sequences.
1550
1598
  // A clone of any LICM-hoisted preamble runs first (so the SIMD block sees the
@@ -2359,7 +2407,10 @@ function tryMapReduceVectorize(bl, fnLocals, freshIdRef) {
2359
2407
  ...lifted,
2360
2408
  ['local.set', incVar, ['i32.add', ['local.get', incVar], ['i32.const', 2]]],
2361
2409
  ['br', simdLoop]]]
2362
- const boundSetup = ['local.set', simdBoundName, ['i32.and', boundExpr, ['i32.const', -2]]]
2410
+ // span-aligned (same entry≠0 hazard as tryVectorize's bound see there)
2411
+ const boundSetup = ['local.set', simdBoundName,
2412
+ ['i32.add', ['local.get', incVar],
2413
+ ['i32.and', ['i32.sub', boundExpr, ['local.get', incVar]], ['i32.const', -2]]]]
2363
2414
  const wrapper = ['block', boundSetup, simdBlock, bl.blockNode]
2364
2415
  return { wrapper, newLocalDecls: [['local', simdBoundName, 'i32'], ...newLocalDecls] }
2365
2416
  }
@@ -2807,6 +2858,26 @@ function liftStmt(stmt, ctx) {
2807
2858
  // Handle memarg if present (last positional after addr/val): unlikely in
2808
2859
  // pre-watr IR for this shape; bail if more than 3 children.
2809
2860
  if (stmt.length !== 3) return liftFail(ctx, `${op} with memarg`)
2861
+ // Mirror store `a[INV − iv] = lane` (f64, 2 lanes): the vector's lanes
2862
+ // (iv, iv+1) mirror to (INV−iv, INV−iv−1) — one v128 store at INV−iv−1
2863
+ // with the f64 lanes SWAPPED. The scalar remainder keeps the plain form.
2864
+ if (ctx.laneType === 'f64' && sty === 'f64') {
2865
+ const mm = matchMirrorAddr(addr, ctx.incVar)
2866
+ if (mm) {
2867
+ const v = liftExprV(stmt[2], ctx)
2868
+ if (ctx.fail) return null
2869
+ const vt = `$__mirv${ctx.freshIdRef.next++}`
2870
+ ctx.extraLocals.push(['local', vt, 'v128'])
2871
+ const mAddr = ['i32.add', mm.base,
2872
+ ['i32.shl', ['i32.sub', ['i32.sub', mm.invExpr, ['local.get', ctx.incVar]], ['i32.const', 1]],
2873
+ ['i32.const', mm.strideLog2]]]
2874
+ return ['__seq__',
2875
+ ['local.set', vt, v],
2876
+ ['v128.store', mAddr,
2877
+ ['i8x16.shuffle', '8', '9', '10', '11', '12', '13', '14', '15', '0', '1', '2', '3', '4', '5', '6', '7',
2878
+ ['local.get', vt], ['local.get', vt]]]]
2879
+ }
2880
+ }
2810
2881
  // Narrowing store: a narrower element written from a wider float lane (`o[i] =
2811
2882
  // narrow(f(x))` — codec encode / downsample). The scalar store value carries a
2812
2883
  // conversion (f32.demote_f64, or the float→int ToInt32 idiom); peel it, lift the
@@ -3992,7 +4063,10 @@ function tryRampMap(blockNode, fnLocals, freshIdRef) {
3992
4063
  ...lifted,
3993
4064
  ...scaledIncs,
3994
4065
  ['br', simdLoopLabel]]]
3995
- const boundSetup = ['local.set', simdBoundName, ['i32.and', boundExpr, ['i32.const', -LANES]]]
4066
+ // span-aligned (same entry≠0 hazard as tryVectorize's bound see there)
4067
+ const boundSetup = ['local.set', simdBoundName,
4068
+ ['i32.add', ['local.get', ivName],
4069
+ ['i32.and', ['i32.sub', boundExpr, ['local.get', ivName]], ['i32.const', -LANES]]]]
3996
4070
  const wrapper = ['block', boundSetup, simdBlock, blockNode]
3997
4071
  const newLocalDecls = [
3998
4072
  ['local', simdBoundName, 'i32'],
@@ -1051,6 +1051,12 @@ export const GLOBALS = Object.assign(Object.create(null), {
1051
1051
  parseFloat: 'number',
1052
1052
  encodeURIComponent: 'encodeURIComponent',
1053
1053
  decodeURIComponent: 'decodeURIComponent',
1054
+ encodeURI: 'encodeURI',
1055
+ decodeURI: 'decodeURI',
1056
+ atob: 'atob',
1057
+ btoa: 'btoa',
1058
+ crypto: 'crypto',
1059
+ navigator: 'navigator',
1054
1060
  Error: 'Error',
1055
1061
  // Error subclasses: distinct names in JS, but jz doesn't carry typed error
1056
1062
  // info — `throw` accepts any value and stringification goes through the
package/src/static.js CHANGED
@@ -163,6 +163,37 @@ export function objLiteralSchemaId(expr) {
163
163
  return parsed ? ctx.schema.register(parsed.names) : null
164
164
  }
165
165
 
166
+ /** Canonical content key for an inplace/structInline replace-store site —
167
+ * the ','-wrapper around literal props is normalized away between plan and
168
+ * emit, so flatten before serializing. Shared by scanInplaceStores (plan),
169
+ * analyzeStructInline (eligibility), and the emit arms; lives here so the
170
+ * three importers stay acyclic (the self-host resolver rejects cycles). */
171
+ export const inplaceKey = (arrName, lit) => {
172
+ const props = lit.slice(1)
173
+ const flat = props.length === 1 && Array.isArray(props[0]) && props[0][0] === ',' ? props[0].slice(1) : props
174
+ return `${arrName}|${JSON.stringify(flat)}`
175
+ }
176
+
177
+ /** K schema-ordered field-value AST nodes of an object literal `{S}` — the
178
+ * cell-store order for a structInline `.push({S})` / `a[i] = {S}` — or null if
179
+ * `lit` is not a plain static-key `{}` literal carrying exactly schema `sid`'s
180
+ * fields. Mapped by name into schema order so sites with differing key order
181
+ * flatten to the same cell run. */
182
+ export function structLiteralFields(lit, sid) {
183
+ if (!Array.isArray(lit) || lit[0] !== '{}') return null
184
+ const parsed = staticObjectProps(lit.slice(1))
185
+ const schema = ctx.schema.list[sid]
186
+ if (!parsed || parsed.names.length !== schema.length) return null
187
+ const byName = new Map()
188
+ for (let i = 0; i < parsed.names.length; i++) byName.set(parsed.names[i], parsed.values[i])
189
+ const out = []
190
+ for (const name of schema) {
191
+ if (!byName.has(name)) return null
192
+ out.push(byName.get(name))
193
+ }
194
+ return out
195
+ }
196
+
166
197
  /** Resolve schemaId of an expression, given a per-function schemaId map for locals.
167
198
  * Used for both intra-function arr elem-schema observation and func.arrayElemSchema
168
199
  * return inference. Recognizes: object literals, var names with bound schemaId,