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.
@@ -12,16 +12,16 @@
12
12
 
13
13
  import { ctx, err, inc, warnDeopt, PTR, LAYOUT } from '../ctx.js'
14
14
  import { T } from '../ast.js'
15
- import { staticPropertyKey, staticIndexKey, staticObjectProps } from '../static.js'
15
+ import { staticPropertyKey, staticIndexKey, staticObjectProps, inlineArraySid, structLiteralFields, inplaceKey } from '../static.js'
16
+ import { packedI32, structInline } from '../abi/index.js'
16
17
  import { i64Hex, encodePtrHi } from '../../layout.js'
17
- import { inplaceKey } from './inplace-store.js'
18
18
  import { recordDynFnTableWrite } from './dyn-closure-tables.js'
19
19
  import { valTypeOf, shapeOf } from '../kind.js'
20
20
  import { VAL, lookupValType, repOf } from '../reps.js'
21
21
  import {
22
22
  typed, asF64, asI32, asI64, temp, tempI32, withTemp, block64,
23
23
  ptrOffsetIR, ptrTypeEq, boxedAddr, writeVar, isGlobal, isBoundName, isLiteralStr,
24
- usesDynProps, needsDynShadow, boolBoxIR, carrierF64, mkPtrIR, isNumericIR,
24
+ usesDynProps, needsDynShadow, boolBoxIR, carrierF64, mkPtrIR, isNumericIR, undefExpr,
25
25
  } from '../ir.js'
26
26
  import { emit } from '../bridge.js'
27
27
 
@@ -324,7 +324,7 @@ function tryInplaceReplaceStore(arr, idx, val) {
324
324
  const reuse = aliasOk && aliasType === 'f64' ? ['local.get', `$${entry.alias}`] : null
325
325
  inc('__alloc_hdr')
326
326
  if (!reuse) inc('__arr_idx')
327
- const kT = tempI32('ipk'), eT = temp('ipe'), oT = tempI32('ipo'), hT = tempI32('iph')
327
+ const kT = tempI32('ipk'), eT = temp('ipe'), oT = tempI32('ipo'), hT = tempI32('iph'), aTb = temp('ipa')
328
328
  const bitsE = () => ['i64.reinterpret_f64', ['local.get', `$${eT}`]]
329
329
  const fast = ['block', ['result', 'f64'],
330
330
  ['local.set', `$${oT}`, ['i32.wrap_i64', bitsE()]],
@@ -333,12 +333,17 @@ function tryInplaceReplaceStore(arr, idx, val) {
333
333
  const slow = ['block', ['result', 'f64'],
334
334
  ['local.set', `$${hT}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', ops.allocSlots(schema.length)]]],
335
335
  ...slots.map((slot, i) => ops.store(['local.get', `$${hT}`], slot, ['local.get', `$${vTs[i]}`])),
336
- storeArrayPayload(asF64(emit(arr)), ['f64.convert_i32_s', ['local.get', `$${kT}`]],
336
+ storeArrayPayload(typed(['local.get', `$${aTb}`], 'f64'), ['f64.convert_i32_s', ['local.get', `$${kT}`]],
337
337
  mkPtrIR(PTR.OBJECT, sid, ['local.get', `$${hT}`]), persistBinding(arr))]
338
+ // JS member-store order: GetValue(base), then the property key, then the RHS
339
+ // values — `a[i++] = {x: i}`'s x must see the incremented i while the store
340
+ // still lands at the old index (caught by a differential probe; the values-
341
+ // first order shipped that divergence at every optimize level).
338
342
  return typed(['block', ['result', 'f64'],
339
- ...parsed.values.map((v, i) => ['local.set', `$${vTs[i]}`, storedValue(v)]),
343
+ ['local.set', `$${aTb}`, asF64(emit(arr))],
340
344
  ['local.set', `$${kT}`, asI32(emit(idx))],
341
- ['local.set', `$${eT}`, reuse ?? ['call', '$__arr_idx', asI64(emit(arr)), ['local.get', `$${kT}`]]],
345
+ ...parsed.values.map((v, i) => ['local.set', `$${vTs[i]}`, storedValue(v)]),
346
+ ['local.set', `$${eT}`, reuse ?? ['call', '$__arr_idx', ['i64.reinterpret_f64', ['local.get', `$${aTb}`]], ['local.get', `$${kT}`]]],
342
347
  ['if', ['result', 'f64'],
343
348
  ['i64.eq',
344
349
  ['i64.and', bitsE(), ['i64.const', '0xFFFFFFFF00000000']],
@@ -347,6 +352,88 @@ function tryInplaceReplaceStore(arr, idx, val) {
347
352
  ['else', slow]]], 'f64')
348
353
  }
349
354
 
355
+ /** structInline Array<S> wholesale element replace `a[i] = {S-literal}` — K
356
+ * f64 cell stores into the element's inline cells: no allocation, no box
357
+ * read, no identity guard (cells cannot hold aliens — the layout is fixed by
358
+ * analyzeStructInline's whole-program proof, which accepted this store via
359
+ * the inplace sweep's alias-liveness + reuse verdicts).
360
+ *
361
+ * Evaluation order (JS: member target before RHS): the index and — on the
362
+ * non-cursor path — the receiver box spill FIRST, then the slot values (they
363
+ * may read the old element's fields: `a[i] = {x: p.y, y: p.x}` swaps).
364
+ *
365
+ * Bounds: one `cellIdx < physLen` u-compare. In-bounds → K stores; anything
366
+ * else — i ≥ length (JS: array-extend) or a negative int-certain index
367
+ * (JS: sidecar property) — DROPS the write, the same contract as the
368
+ * checked-by-default typed store (OOB writes ignored). By then JS itself
369
+ * would have thrown at the cursor's `p.x` projection (undefined.x), which
370
+ * the carrier's unchecked cursor read already deviates on; the analyzer only
371
+ * accepts stores preceded by a same-index cursor read (reuse verdict), so
372
+ * append-idiom builders (`out[out.length] = {…}`) stay on the plain layout.
373
+ * No grow call exists in the arm, so loop-invariant base hoists stay sound.
374
+ *
375
+ * Address: with the sweep's target-binding reuse, the cursor IS the cell
376
+ * address — base derives as `cursor − cellIdx*8` (pure arith); otherwise via
377
+ * __ptr_offset on the spilled box. */
378
+ function tryStructInlineReplaceStore(arr, idx, val) {
379
+ if (typeof arr !== 'string') return null
380
+ const sid = inlineArraySid(arr)
381
+ if (sid == null) return null
382
+ const schema = ctx.schema.list[sid]
383
+ const fields = structLiteralFields(val, sid)
384
+ // analyzeStructInline accepted every store site for this sid — a shape this
385
+ // arm cannot lower here means the phases disagree; never fall through to a
386
+ // boxed store on cell memory.
387
+ if (!fields) err(`structInline replace-store expects { ${schema.join(', ')} } literal`)
388
+ const void_ = ctx.func._expect === 'void'
389
+ const K = schema.length
390
+ const packed = ctx.schema.inlineCellI32?.has(sid)
391
+ const cpe = structInline(K, packed).cpe // physical 8-byte cells per element
392
+ const ops = packed ? packedI32.ops : ctx.abi.object.ops
393
+ const entry = ctx.schema.inplaceStores?.get(inplaceKey(arr, val))
394
+ // Cursor reuse under the same conditions as tryInplaceReplaceStore's
395
+ // strongest form: the tracked alias is an unboxed i32 OBJECT pointer of this
396
+ // schema reading the same index — for the inline carrier that IS the cell
397
+ // address (array.js '[]' structInline arm).
398
+ const alias = entry?.alias && typeof idx === 'string' && idx === entry.idx &&
399
+ !ctx.func.boxed?.has(entry.alias) && ctx.func.locals.get(entry.alias) === 'i32' &&
400
+ repOf(entry.alias)?.ptrKind === VAL.OBJECT &&
401
+ (ctx.schema.vars.get(entry.alias) ?? repOf(entry.alias)?.schemaId) === sid
402
+ ? entry.alias : null
403
+ const kT = tempI32('sik'), cT = tempI32('sic'), bT = tempI32('sib'), aT = tempI32('sia')
404
+ const vTs = fields.map(() => packed ? tempI32('siv') : temp('siv'))
405
+ const boxT = alias ? null : temp('sit')
406
+ if (!alias) inc('__ptr_offset')
407
+ const cellIdx = cpe === 1 ? ['local.get', `$${kT}`] : ['i32.mul', ['local.get', `$${kT}`], ['i32.const', cpe]]
408
+ const body = [
409
+ ['local.set', `$${kT}`, asI32(emit(idx))],
410
+ ...(boxT ? [['local.set', `$${boxT}`, asF64(emit(arr))]] : []),
411
+ // packed values are int32-exact by the slotI32Certain census
412
+ ...fields.map((v, i) => ['local.set', `$${vTs[i]}`, packed ? asI32(emit(v)) : storedValue(v)]),
413
+ ['local.set', `$${cT}`, cellIdx],
414
+ ['local.set', `$${bT}`, alias
415
+ ? ['i32.sub', ['local.get', `$${alias}`], ['i32.shl', ['local.get', `$${cT}`], ['i32.const', 3]]]
416
+ : ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${boxT}`]]]],
417
+ ]
418
+ const inBounds = ['i32.lt_u', ['local.get', `$${cT}`],
419
+ ['i32.load', ['i32.sub', ['local.get', `$${bT}`], ['i32.const', 8]]]]
420
+ const stores = [
421
+ ['local.set', `$${aT}`, ['i32.add', ['local.get', `$${bT}`], ['i32.shl', ['local.get', `$${cT}`], ['i32.const', 3]]]],
422
+ ...fields.map((v, i) => ops.store(['local.get', `$${aT}`], i, ['local.get', `$${vTs[i]}`])),
423
+ ]
424
+ if (void_) return typed(['block', ...body, ['if', inBounds, ['then', ...stores]]], 'void')
425
+ // Value position is analyzer-poisoned (sweep candidates are statement-only);
426
+ // belt for exotic statement shapes: yield the element as a boxed pointer —
427
+ // under the inline carrier the cell address IS the object identity. A
428
+ // PACKED cell address cannot be boxed (slot reads through a box assume f64
429
+ // cells), so the phases disagreeing there is a compile error, not bytes.
430
+ if (packed) err('structInline packed replace-store in value position — analyzeStructInline must poison this shape')
431
+ return typed(['block', ['result', 'f64'], ...body,
432
+ ['if', ['result', 'f64'], inBounds,
433
+ ['then', ...stores, mkPtrIR(PTR.OBJECT, sid, ['local.get', `$${aT}`])],
434
+ ['else', undefExpr()]]], 'f64')
435
+ }
436
+
350
437
  export function emitElementAssign(arr, idx, val) {
351
438
  // 0. `obj.prop[idx] = val` where `obj`'s type is fully unknown (so `obj`
352
439
  // could be a host EXTERNAL object at runtime) — `__ext_prop` (interop.js)
@@ -389,6 +476,11 @@ export function emitElementAssign(arr, idx, val) {
389
476
  ['i64.reinterpret_f64', ['local.get', `$${arrTmp}`]]]],
390
477
  ['local.get', `$${resultTmp}`])
391
478
  }
479
+ // structInline receivers first: once analyzeStructInline committed the sid
480
+ // to the inline-cell layout, a boxed store (generic/inplace paths below)
481
+ // would corrupt cell memory — this arm is the only sound lowering.
482
+ const sIn = tryStructInlineReplaceStore(arr, idx, val)
483
+ if (sIn) return sIn
392
484
  const rmw = ctx.transform.optimize ? tryHashRmwFusion(arr, idx, val) : null
393
485
  if (rmw) return rmw
394
486
  const inplace = ctx.transform.optimize ? tryInplaceReplaceStore(arr, idx, val) : null
@@ -646,6 +738,18 @@ export function emitPropertyAssign(obj, prop, val) {
646
738
  if (si >= 0) {
647
739
  const shadow = needsDynShadow(typeof obj === 'string' ? obj : null)
648
740
  if (shadow) inc('__dyn_set')
741
+ // Packed i32 cells (structInline cursor, `.cellI32` node tag): the
742
+ // field is a raw i32 at +si*4 — i32.store, no f64 boxing. The store
743
+ // value is int32-exact by the slotI32Certain census (packing
744
+ // precondition); the expression result converts back at one op.
745
+ if (vaProbe.cellI32) {
746
+ const t = tempI32('pcw')
747
+ return block64(
748
+ ['local.set', `$${t}`, asI32(emit(val))],
749
+ packedI32.ops.store(ptrOffsetIR(vaProbe, VAL.OBJECT), si, ['local.get', `$${t}`]),
750
+ ...(shadow ? [['drop', ['call', '$__dyn_set', ['i64.reinterpret_f64', asF64(emit(obj))], asI64(emit(['str', prop])), ['i64.reinterpret_f64', ['f64.convert_i32_s', ['local.get', `$${t}`]]]]]] : []),
751
+ ['f64.convert_i32_s', ['local.get', `$${t}`]])
752
+ }
649
753
  return withTemp(storedValue(val), t => [
650
754
  ctx.abi.object.ops.store(ptrOffsetIR(asF64(emit(obj)), VAL.OBJECT), si, ['local.get', `$${t}`]),
651
755
  ...(shadow ? [['drop', ['call', '$__dyn_set', ['i64.reinterpret_f64', asF64(emit(obj))], asI64(emit(['str', prop])), ['i64.reinterpret_f64', ['local.get', `$${t}`]]]]] : []),
@@ -24,7 +24,7 @@
24
24
  import {
25
25
  commaList, T, isBlockBody, isReassigned, mutatesArrayLength, isConstLiteral, constLiteralHoistable,
26
26
  hasOwnContinue, hasLabeledContinueTo, hasOwnBreakOrContinue, extractParams, classifyParam, JZ_UNDEF, TYPEOF,
27
- ASSIGN_OPS,
27
+ ASSIGN_OPS, firstRefKind,
28
28
  } from '../ast.js'
29
29
  import { ctx, err, inc, warnDeopt, PTR, ssoBitI64Hex, LAYOUT } from '../ctx.js'
30
30
  import { i64Hex, encodePtrHi, STR_HCACHE_BIT, typedElemAux } from '../../layout.js'
@@ -111,6 +111,13 @@ const tryI32Arith = (wasmOp, astOp, a, b, va, vb) => {
111
111
  // own emit (math.js / unary `-`), so they reach canonNum already canonical.
112
112
  const NAN_MINTING = new Set(['f64.div', 'f64.add', 'f64.sub', 'f64.mul'])
113
113
 
114
+ // Sign+exponent mask isolating "negative NaN or -Infinity" — used only after an
115
+ // f64.eq(v,v) self-check has already failed (so -Infinity is excluded, leaving
116
+ // only negative NaN). Pointers/atoms are always emitted sign-clear (nanPrefixMaskHex,
117
+ // layout.js), so a sign-bit-set NaN can only be a genuine float NaN. Mirrors
118
+ // $__typeof's dynamic dispatch (module/core.js) bit-for-bit.
119
+ const NEG_NAN_MASK = 0xFFF0000000000000n
120
+
114
121
  const canonNum = (node) => {
115
122
  // Fold a possibly-non-canonical NaN to the canonical number-NaN before it reaches a
116
123
  // bit-comparing consumer (__is_truthy / untyped === / typeof), which match the canonical
@@ -329,7 +336,19 @@ export function emitTypeofCmp(a, b, cmpOp) {
329
336
  if (code === TYPEOF.number) {
330
337
  // typeof "number": v===v rejects NaN-box pointers; BOOL carrier is 0/1 → still typeof "boolean".
331
338
  if (resolveValType(typeofExpr, valTypeOf, lookupValType) === VAL.BOOL) return typed(['i32.const', eq ? 0 : 1], 'i32')
332
- return typed([eq ? 'f64.eq' : 'f64.ne', ['local.tee', `$${t}`, va], ['local.get', `$${t}`]], 'i32')
339
+ // v===v alone is WRONG for the one payload that legitimately means "the number
340
+ // NaN": the canonical box prefix (tag=ATOM aux=0) that $__typeof (module/core.js)
341
+ // also carves out, plus any sign-bit-set NaN (pointers are always emitted
342
+ // sign-clear, so a negative NaN — e.g. x86's uncanonicalized 0/0 — can only be a
343
+ // real float NaN). Must mirror $__typeof's dynamic dispatch exactly, or
344
+ // `typeof NaN === 'number'` folds to false here while the general path says true.
345
+ const again = ['local.get', `$${t}`]
346
+ const notNan = ['f64.eq', ['local.tee', `$${t}`, va], again]
347
+ const bits = ['i64.reinterpret_f64', again]
348
+ const numberNan = ['i32.or',
349
+ ['i64.eq', bits, ['i64.const', i64Hex(LAYOUT.NAN_PREFIX_BITS)]],
350
+ ['i64.eq', ['i64.and', bits, ['i64.const', i64Hex(NEG_NAN_MASK)]], ['i64.const', i64Hex(NEG_NAN_MASK)]]]
351
+ return wrap(['i32.or', notNan, numberNan])
333
352
  }
334
353
  if (code === TYPEOF.string) return isPtrKind(PTR.STRING)
335
354
  if (code === TYPEOF.undefined) return wrap(isNullish(va))
@@ -1185,6 +1204,16 @@ export function emitDecl(...inits) {
1185
1204
  const i = inits[ii]
1186
1205
  if (typeof i === 'string') {
1187
1206
  const undef = undefExpr()
1207
+ // An uninitialized `let x` holds `undefined` until its first assignment —
1208
+ // a read may see the sentinel, so arithmetic on it must coerce (same flag
1209
+ // as explicit nullish inits below) UNLESS the first reference in
1210
+ // evaluation order is an UNCONDITIONAL write (`let ixSq; while ((ixSq =
1211
+ // …) …)` — the fractal-kernel shape): definitely-assigned-before-read
1212
+ // needs no coercion, and the per-read canon would break the SIMD
1213
+ // recognizers' body shapes. i32-narrowed locals are exempt either way:
1214
+ // the narrowing proof is assigned-before-read, and they zero-init.
1215
+ if (ctx.func.locals.get(i) !== 'i32' && firstRefKind(ctx.func.body, i) !== 'write')
1216
+ ctx.func.maybeNullish?.add(i)
1188
1217
  if (ctx.func.boxed.has(i)) {
1189
1218
  const cell = ctx.func.boxed.get(i)
1190
1219
  ctx.func.locals.set(cell, 'i32')
@@ -3076,7 +3105,13 @@ function compoundAssign(name, val, f64op, i32op) {
3076
3105
  }
3077
3106
  if (i32op && va.type === 'i32' && vbi.type === 'i32')
3078
3107
  return writeVar(name, i32op(va, vbi), void_)
3079
- return writeVar(name, f64op(asF64(va), asF64(vb)), void_)
3108
+ // A checked typed read coerces like a '+' operand: toNumF64's checkedNumRead
3109
+ // seam folds the UNDEF miss arm to canonical NaN. A bare asF64 carries the
3110
+ // sentinel payload through f64 arithmetic to the boundary (decoded back as
3111
+ // `undefined`; JS: NaN) — `s += a[i]` is the accumulator shape the binary
3112
+ // '+' emitter never sees.
3113
+ const vbn = vb.checkedNumRead ? toNumF64(val, vb) : vb
3114
+ return writeVar(name, f64op(asF64(va), asF64(vbn)), void_)
3080
3115
  }
3081
3116
 
3082
3117
  // Ring 0.3 (re-landed after the dispatch rework dropped the uncommitted original):
@@ -3255,7 +3290,13 @@ export const emitter = {
3255
3290
  // to a local first" — pinned in test/parser-bugs.js rather than chased further into
3256
3291
  // the kernel's own call/branch codegen. See .work/todo.md (groundtruth archive).
3257
3292
  const emitted = emit(expr)
3258
- const ir = pk != null ? asPtrOffset(emitted, pk) : asParamType(emitted, rt)
3293
+ // Closure-convention bodies return into a boxed-value position (the ftN f64
3294
+ // slot): a BOOL value must cross as its true/false atom — the result-side
3295
+ // mirror of closure.call's carrierF64 args. Raw funcs keep the plain 0/1
3296
+ // (their direct callers read valResult; boundary/trampoline rebox).
3297
+ const ir = pk != null ? asPtrOffset(emitted, pk)
3298
+ : (ctx.func.boxedResult && rt === 'f64') ? carrierF64(expr, emitted)
3299
+ : asParamType(emitted, rt)
3259
3300
  const ty = pk != null ? 'i32' : rt
3260
3301
  const tcoed = tcoTailRewrite(ir, ty)
3261
3302
  if (Array.isArray(tcoed) && tcoed[0] === 'return_call' && finalizers.length === 0) {
@@ -4179,6 +4220,12 @@ export const emitter = {
4179
4220
  }
4180
4221
  continue
4181
4222
  }
4223
+ // maxIv = the TRUE max iv value at PRE-increment access sites:
4224
+ // bound−1 (strict) / bound (inclusive). A body-advanced iv (bump>0)
4225
+ // exceeds this only AFTER its write — those accesses carry cand.post
4226
+ // and their group widens by a·bump in the extent constants below.
4227
+ // (The old unconditional widening made `maxIv < len` fail exactly
4228
+ // when len == bound — every symmetric half-spectrum loop.)
4182
4229
  const maxIv = tempI64('tvq')
4183
4230
  if (vs.bKind === 'f64') {
4184
4231
  const bF = temp('tvf')
@@ -4186,10 +4233,10 @@ export const emitter = {
4186
4233
  conjs.push(['f64.le', ['f64.abs', ['local.get', `$${bF}`]], ['f64.const', 2147483648]])
4187
4234
  result.push(['local.set', `$${maxIv}`,
4188
4235
  ['i64.trunc_sat_f64_s', [vs.incl ? 'f64.floor' : 'f64.ceil', ['local.get', `$${bF}`]]]])
4189
- if (vs.bump - (vs.incl ? 0 : 1)) result.push(['local.set', `$${maxIv}`,
4190
- ['i64.add', ['local.get', `$${maxIv}`], i64c(vs.bump - (vs.incl ? 0 : 1))]])
4236
+ if (!vs.incl) result.push(['local.set', `$${maxIv}`,
4237
+ ['i64.add', ['local.get', `$${maxIv}`], i64c(-1)]])
4191
4238
  } else {
4192
- const adj = vs.bump - (vs.incl ? 0 : 1)
4239
+ const adj = vs.incl ? 0 : -1
4193
4240
  result.push(['local.set', `$${maxIv}`,
4194
4241
  adj ? ['i64.add', ext(asI32(emit(vs.bound))), i64c(adj)] : ext(asI32(emit(vs.bound)))])
4195
4242
  }
@@ -4221,17 +4268,27 @@ export const emitter = {
4221
4268
  }
4222
4269
  const gk = c.recv + '\x00' + c.a + '\x00' + c.slots.map(t => t.k + '*' + slotKey(t.e)).join('+')
4223
4270
  const g = groups.get(gk)
4224
- if (!g) groups.set(gk, { recv: c.recv, a: c.a, slots: c.slots, maxC: c.bConst, minC: c.bConst })
4225
- else { g.maxC = Math.max(g.maxC, c.bConst); g.minC = Math.min(g.minC, c.bConst) }
4271
+ if (!g) groups.set(gk, { recv: c.recv, a: c.a, slots: c.slots, maxC: c.bConst, minC: c.bConst, anyPost: !!c.post })
4272
+ else { g.maxC = Math.max(g.maxC, c.bConst); g.minC = Math.min(g.minC, c.bConst); if (c.post) g.anyPost = true }
4226
4273
  }
4227
4274
  for (const g of groups.values()) {
4228
- let hi = slotSum(['i64.mul', i64c(g.a), ['local.get', `$${maxIv}`]], g.slots)
4229
- if (g.maxC) hi = ['i64.add', hi, i64c(g.maxC)]
4275
+ // extremes follow the SIGN of a: a·iv is maximal at maxIv for a ≥ 0
4276
+ // but at ENTRY for a < 0 (mirror index `N−k` of symmetric fills),
4277
+ // and minimal at the other end. post-increment groups see iv up to
4278
+ // maxIv+bump — widen through the extent CONSTANT (a·bump).
4279
+ const postW = g.anyPost ? g.a * vs.bump : 0
4280
+ const hiC = g.maxC + (g.a >= 0 ? postW : 0)
4281
+ const loC = g.minC + (g.a < 0 ? postW : 0)
4282
+ const entryIR = () => vs.startC != null ? i64c(vs.startC) : slotI64(vs.iv, vs.ivKind)
4283
+ let hi = slotSum(['i64.mul', i64c(g.a), g.a >= 0 ? ['local.get', `$${maxIv}`] : entryIR()], g.slots)
4284
+ if (hiC) hi = ['i64.add', hi, i64c(hiC)]
4230
4285
  conjs.push(['i64.lt_s', hi, len64Of(g.recv)])
4231
- if (vs.startC != null && !g.slots.length) continue
4232
- let lo = slotSum(vs.startC != null ? i64c(g.a * vs.startC)
4233
- : ['i64.mul', i64c(g.a), slotI64(vs.iv, vs.ivKind)], g.slots, true)
4234
- if (g.minC) lo = ['i64.add', lo, i64c(g.minC)]
4286
+ // a 0 with a STATIC start: lo = a·startC+minC was validated
4287
+ // non-negative at candidate time (slotless), nothing to emit.
4288
+ if (g.a >= 0 && vs.startC != null && !g.slots.length) continue
4289
+ let lo = slotSum(g.a >= 0 && vs.startC != null ? i64c(g.a * vs.startC)
4290
+ : ['i64.mul', i64c(g.a), g.a >= 0 ? slotI64(vs.iv, vs.ivKind) : ['local.get', `$${maxIv}`]], g.slots, true)
4291
+ if (loC) lo = ['i64.add', lo, i64c(loC)]
4235
4292
  conjs.push(['i64.ge_s', lo, i64c(0)])
4236
4293
  }
4237
4294
  // induction cursors (`k += step` in a comma step): value at iteration t is
@@ -4630,13 +4687,20 @@ export function emit(node, expect) {
4630
4687
  // number, silently losing the pointer (a Map came back as e.g. 480360.0,
4631
4688
  // so a caller's `for…of`/`.size` saw a number and read nothing).
4632
4689
  const ptrResult = func?.sig.ptrKind != null
4690
+ // A BOOL-result func carries 0/1 in its raw ABI; the closure ABI is a
4691
+ // boxed-value position, so rebox to the true/false ATOM — the exact
4692
+ // mirror of the boundary wrapper (index.js resultBool). Without it a
4693
+ // field-held function's `=== true` / typeof observed a plain number.
4694
+ const boolResult = !ptrResult && func?.valResult === VAL.BOOL
4633
4695
  const wrapped = ptrResult
4634
4696
  ? `(call $__mkptr (i32.const ${valKindToPtr(func.sig.ptrKind)}) (i32.const ${func.sig.ptrAux ?? 0}) ${callExpr})`
4635
- : resType === 'i32'
4636
- ? (func.sig.unsignedResult ? `(f64.convert_i32_u ${callExpr})` : `(f64.convert_i32_s ${callExpr})`)
4637
- : resType === 'i64'
4638
- ? `(f64.reinterpret_i64 ${callExpr})`
4639
- : callExpr
4697
+ : boolResult
4698
+ ? `(select (f64.const nan:${TRUE_NAN}) (f64.const nan:${FALSE_NAN}) ${resType === 'i32' ? `(i32.ne ${callExpr} (i32.const 0))` : `(f64.ne ${callExpr} (f64.const 0))`})`
4699
+ : resType === 'i32'
4700
+ ? (func.sig.unsignedResult ? `(f64.convert_i32_u ${callExpr})` : `(f64.convert_i32_s ${callExpr})`)
4701
+ : resType === 'i64'
4702
+ ? `(f64.reinterpret_i64 ${callExpr})`
4703
+ : callExpr
4640
4704
  ctx.core.stdlib[trampolineName] = `(func $${trampolineName} ${paramDecls.join(' ')} (result f64) ${restLocals}${restPrelude}${wrapped})`
4641
4705
  inc(trampolineName, ...(ptrResult ? ['__mkptr'] : []), ...(restIdx >= 0 ? ['__alloc_hdr', '__mkptr'] : []))
4642
4706
  }
@@ -67,6 +67,7 @@ import {
67
67
  valKindToPtr, findBodyStart, tcoTailRewrite,
68
68
  boolBoxIR,
69
69
  I32_MIN, I32_MAX, dollar,
70
+ carrierF64,
70
71
  } from '../ir.js'
71
72
  import plan from './plan/index.js'
72
73
  import { foldStaticConstAggregates } from './plan/literals.js'
@@ -395,11 +396,13 @@ function enterFunc(sig, body, { uniq = 0, directClosures = null } = {}) {
395
396
  ctx.func.uniq = uniq
396
397
  ctx.func.current = sig
397
398
  ctx.func.body = body
399
+ ctx.func.boxedResult = false // closure-convention bodies set true after enterFunc (ftN boxed result)
398
400
  ctx.func.directClosures = directClosures
399
401
  ctx.func.localProps = null
400
402
  ctx.func.charDecomp = null
401
403
  ctx.func.charDecompGlobals = false // only emitFunc's named path drains — it re-arms
402
404
  ctx.func.probeHoist = null
405
+ ctx.func.lenHoist = null
403
406
  if (ctx.transform.optimize) {
404
407
  scanAndTagNonEscapingClosures(body)
405
408
  }
@@ -484,6 +487,13 @@ function analyzeFuncForEmit(func, programFacts) {
484
487
  if (!ctx.types.typedElem) ctx.types.typedElem = new Map()
485
488
  if (!ctx.types.typedElem.has(pname)) ctx.types.typedElem.set(pname, r.typedCtor)
486
489
  updateRep(pname, { val: VAL.TYPED })
490
+ // Unanimous static length from the call sites (validateTypedLenParams:
491
+ // module-local callee, never-written param, settled ctor) — the body's
492
+ // reads gain the static-length proof family, `.length` folds literal.
493
+ if (r.typedLen != null) {
494
+ if (!ctx.types.typedLen) ctx.types.typedLen = new Map()
495
+ if (!ctx.types.typedLen.has(pname)) ctx.types.typedLen.set(pname, r.typedLen)
496
+ }
487
497
  }
488
498
  if (r.val && !reassigned && !ctx.func.localReps?.get(pname)?.val) updateRep(pname, { val: r.val })
489
499
  if (r.arrayElemSchema != null) updateRep(pname, { arrayElemSchema: r.arrayElemSchema })
@@ -1142,6 +1152,10 @@ function emitFunc(func, funcFacts, programFacts) {
1142
1152
  if (!ctx.types.typedElem) ctx.types.typedElem = new Map()
1143
1153
  if (!ctx.types.typedElem.has(pname)) ctx.types.typedElem.set(pname, r.typedCtor)
1144
1154
  if (!ctx.func.localReps?.get(pname)?.val) updateRep(pname, { val: VAL.TYPED })
1155
+ if (r.typedLen != null) {
1156
+ if (!ctx.types.typedLen) ctx.types.typedLen = new Map()
1157
+ if (!ctx.types.typedLen.has(pname)) ctx.types.typedLen.set(pname, r.typedLen)
1158
+ }
1145
1159
  }
1146
1160
  if (r.schemaId != null && !reassigned && !exported && !ctx.schema.vars.has(pname)) {
1147
1161
  ctx.schema.vars.set(pname, r.schemaId)
@@ -1242,6 +1256,10 @@ function emitFunc(func, funcFacts, programFacts) {
1242
1256
  // answer is loop-invariant for a stable-global receiver — resolve it once.
1243
1257
  // Mirrors sidecarOverride's arm: primitives (real numbers, strings) can
1244
1258
  // never carry an own override, so only NaN-boxed non-STRING receivers probe.
1259
+ // Function-invariant typed lens (module/typedarray.js leanLen): a stable
1260
+ // PARAM receiver's element count, shared by every checked read/write guard.
1261
+ if (ctx.func.lenHoist) for (const h of ctx.func.lenHoist.values())
1262
+ inits.push(['local.set', `$${h.t}`, h.init])
1245
1263
  if (ctx.func.probeHoist) for (const ph of ctx.func.probeHoist.values()) {
1246
1264
  const g = () => ['i64.reinterpret_f64', ph.recvIR()]
1247
1265
  inits.push(
@@ -1514,6 +1532,9 @@ function emitClosureBody(cb) {
1514
1532
  uniq: Math.max(ctx.func.uniq, 100),
1515
1533
  directClosures: cb.directClosures ? new Map(cb.directClosures) : null,
1516
1534
  })
1535
+ // The ftN f64 result is a boxed-value position: `return <bool>` must cross
1536
+ // as the true/false atom (emit.js 'return' consults this; enterFunc clears it).
1537
+ ctx.func.boxedResult = true
1517
1538
 
1518
1539
  const fn = ['func', `$${cb.name}`]
1519
1540
  fn.push(['param', '$__env', 'f64'])
@@ -1597,7 +1618,7 @@ function emitClosureBody(cb) {
1597
1618
  }
1598
1619
  bodyIR = emitBlockBody(cb.body)
1599
1620
  } else {
1600
- bodyIR = [asF64(emit(cb.body))]
1621
+ bodyIR = [carrierF64(cb.body, emit(cb.body))]
1601
1622
  }
1602
1623
 
1603
1624
  // Pre-allocate cache locals for env unpacking
@@ -2225,9 +2246,21 @@ export default function compile(ast, profiler) {
2225
2246
  ['global.set', '$__init_done', ['i32.const', 1]])
2226
2247
  sFn.splice(2, 0, ['export', '"_initialize"'])
2227
2248
  if (sDirIdx !== -1) sec.start.splice(sDirIdx, 1)
2228
- for (const name of wasiCommandExports) {
2229
- const w = sec.funcs.find(f => Array.isArray(f) && f[1] === `$${name}$wasi`)
2230
- if (w) w.splice(w.findIndex(n => Array.isArray(n) && n[0] === 'drop'), 0, ['call', '$__start'])
2249
+ // REACTOR self-arming: EVERY exported function (commands and plain
2250
+ // reactor exports alike) ensures init ran. A raw `new WebAssembly.
2251
+ // Instance` embedder that never calls `_initialize` (the reactor
2252
+ // contract wasmtime honors automatically) otherwise runs against
2253
+ // unseeded state — `_clear()` resets the heap from a zero
2254
+ // `__heap_reset` and the next alloc writes out of bounds (the
2255
+ // color-space batch harness). One predicted global check per
2256
+ // boundary call; `__init_done` keeps init exactly-once either way.
2257
+ for (const f of sec.funcs) {
2258
+ if (!Array.isArray(f) || f[0] !== 'func') continue
2259
+ if (!f.some(x => Array.isArray(x) && x[0] === 'export')) continue
2260
+ let at = 2
2261
+ while (at < f.length && Array.isArray(f[at]) &&
2262
+ (f[at][0] === 'export' || f[at][0] === 'param' || f[at][0] === 'result' || f[at][0] === 'local')) at++
2263
+ f.splice(at, 0, ['if', ['i32.eqz', ['global.get', '$__init_done']], ['then', ['call', '$__start']]])
2231
2264
  }
2232
2265
  }
2233
2266
  }
@@ -36,17 +36,12 @@
36
36
  */
37
37
  import { ctx } from '../ctx.js'
38
38
  import { analyzeBody } from './analyze.js'
39
- import { staticObjectProps } from '../static.js'
39
+ import { staticObjectProps, inplaceKey } from '../static.js'
40
40
  import { VAL } from '../reps.js'
41
41
 
42
- /** Canonical content key for a store site the ','-wrapper around literal
43
- * props is normalized away between plan and emit, so flatten before
44
- * serializing. Shared by the sweep and emitElementAssign. */
45
- export const inplaceKey = (arrName, lit) => {
46
- const props = lit.slice(1)
47
- const flat = props.length === 1 && Array.isArray(props[0]) && props[0][0] === ',' ? props[0].slice(1) : props
48
- return `${arrName}|${JSON.stringify(flat)}`
49
- }
42
+ // Content key for a store site: `inplaceKey` (src/static.js) shared with
43
+ // analyzeStructInline and the emit arms; hosted there to keep the import
44
+ // graph acyclic (the self-host resolver rejects cycles).
50
45
 
51
46
  // env-gated debug — dist/jz.js runs in browsers where `process` doesn't exist
52
47
  const DBG = typeof process !== 'undefined' && process.env.JZ_DBG_INPLACE
@@ -291,6 +286,12 @@ export function scanInplaceStores(programFacts) {
291
286
  return false
292
287
  }
293
288
 
289
+ // Verdicts are CONTENT-keyed (see inplaceKey): body transforms between this
290
+ // sweep and its consumers (analyzeFuncForEmit's loop rewrites, emit-time
291
+ // inlining) rebuild trees, so node identity does not survive. Sound for the
292
+ // structInline consumer too: a value-position `x = (a[i] = {…})` is never a
293
+ // candidate, but its `[]` target IS walked as a value read (walkVal) and
294
+ // poisons the sid — so every same-content verdict of that sid is false.
294
295
  const verdict = new Map() // key → false | { alias, idx } | { alias: null }
295
296
  for (const c of candidates) {
296
297
  const key = inplaceKey(c.arrName, c.lit)
@@ -14,7 +14,7 @@ import {
14
14
  analyzeBody, findMutations, invalidateLocalsCache, mayBeNullish,
15
15
  } from './analyze.js'
16
16
  import { staticObjectProps } from '../static.js'
17
- import { scanBoundedLoops, exprType, typedElemCtor } from '../type.js'
17
+ import { scanBoundedLoops, exprType, typedElemCtor, typedStaticLen } from '../type.js'
18
18
  import { typedElemAux, ctorFromElemAux } from '../../layout.js'
19
19
  import { observeProgramSlots } from './program-facts.js'
20
20
  import { valTypeOf } from '../kind.js'
@@ -117,6 +117,33 @@ function applyI32ParamSpecialization(paramReps, valueUsed, { skipTyped = false }
117
117
  }
118
118
  }
119
119
 
120
+ // typedLen rides the same safety rails as intConst: only module-local direct
121
+ // callees (not exported / value-used / raw), no rest/default positions, and a
122
+ // body that never writes the param. Additionally requires the SETTLED typedCtor
123
+ // — length evidence for a receiver that never proved typed is dead weight the
124
+ // `.length` fold must not trust.
125
+ function validateTypedLenParams(paramReps, valueUsed) {
126
+ for (const func of ctx.func.list) {
127
+ const hostReachable = func.exported || func.raw || valueUsed.has(func.name)
128
+ const reps = paramReps.get(func.name)
129
+ if (!reps) continue
130
+ const restIdx = func.rest ? func.sig.params.length - 1 : -1
131
+ let candidates = null
132
+ for (const [k, r] of reps) {
133
+ if (r.typedLen == null) continue
134
+ if (hostReachable || !func.body || k === restIdx || k >= func.sig.params.length ||
135
+ r.typedCtor == null) { r.typedLen = null; continue }
136
+ const pname = func.sig.params[k].name
137
+ if (func.defaults?.[pname] != null) { r.typedLen = null; continue }
138
+ ;(candidates ||= new Map()).set(pname, r)
139
+ }
140
+ if (!candidates) continue
141
+ const mutated = new Set()
142
+ findMutations(func.body, new Set(candidates.keys()), mutated)
143
+ for (const name of mutated) candidates.get(name).typedLen = null
144
+ }
145
+ }
146
+
120
147
  function validateIntConstParams(paramReps, valueUsed) {
121
148
  for (const func of ctx.func.list) {
122
149
  if (func.exported || func.raw || valueUsed.has(func.name)) continue
@@ -239,6 +266,28 @@ function buildCallerTypedCtx() {
239
266
  return callerTypedCtx
240
267
  }
241
268
 
269
+ // Static LENGTHS visible per caller — analyzeBody's typedLens (stable
270
+ // single-def `new T(<n>)` bindings; the tracker poisons on redef) shadowing
271
+ // module globals, same shadowing rule as callerTypedElemsFor.
272
+ function buildCallerTypedLenCtx() {
273
+ const out = new Map()
274
+ const globalTL = ctx.scope.globalTypedLen || new Map()
275
+ out.set(null, globalTL)
276
+ for (const func of ctx.func.list) {
277
+ if (!func.body || func.raw) continue
278
+ const facts = analyzeBody(func.body)
279
+ const local = facts.typedLens || new Map()
280
+ if (!globalTL.size) { out.set(func, local); continue }
281
+ const shadowed = new Set(facts.locals.keys())
282
+ for (const p of func.sig?.params || []) shadowed.add(p.name)
283
+ const merged = new Map()
284
+ for (const [k, v] of globalTL) if (!shadowed.has(k)) merged.set(k, v)
285
+ for (const [k, v] of local) merged.set(k, v)
286
+ out.set(func, merged)
287
+ }
288
+ return out
289
+ }
290
+
242
291
  function applyTypedPointerParamAbi(paramReps, valueUsed) {
243
292
  for (const func of ctx.func.list) {
244
293
  if (func.exported || func.raw || valueUsed.has(func.name)) continue
@@ -1197,6 +1246,27 @@ export default function narrowSignatures(programFacts, ast) {
1197
1246
  runTypedFixpoint()
1198
1247
  runTypedFixpoint()
1199
1248
 
1249
+ // STATIC LENGTH down call chains: when every call site passes a typed array
1250
+ // of ONE known static length (`new Float64Array(8192)` — directly, via a
1251
+ // stable caller binding, or via the caller's own already-settled param), the
1252
+ // param carries it. Unlocks the whole static-length proof family inside
1253
+ // callees — typedIdxProven's literal/masked/interval classes and the
1254
+ // `.length` literal fold — where the length was born one frame up (heapsort
1255
+ // reading `a` sized in main; a codec writing `out` sized at the call site).
1256
+ // Exact-agreement only (mergeParamFact poisons on mismatch): the fact also
1257
+ // feeds `.length` folds, so an under-approximating min would miscompile.
1258
+ // Transitive via the same soft-fixpoint + hard-validate driver as typedCtor.
1259
+ const callerTypedLenCtx = buildCallerTypedLenCtx()
1260
+ const inferTypedLen = (arg, callerLens, paramFacts) => {
1261
+ if (typeof arg === 'string') return callerLens?.get(arg) ?? paramFacts?.get(arg) ?? null
1262
+ return typedStaticLen(arg)
1263
+ }
1264
+ runArrElemFixpoint('typedLen', inferTypedLen, callerTypedLenCtx)
1265
+ // A length without a settled ctor is unusable evidence (the receiver never
1266
+ // takes the typed read path) and a length on a host-reachable or rebound
1267
+ // param is unsound — same exclusion discipline as intConst.
1268
+ validateTypedLenParams(paramReps, valueUsed)
1269
+
1200
1270
  // G: TYPED pointer-ABI narrowing — once .typedCtor agrees on a single
1201
1271
  // ctor across all call sites, narrow the param from NaN-boxed f64 to raw
1202
1272
  // i32 offset (with ptrAux carrying the elem-type bits). Eliminates the
@@ -123,6 +123,11 @@ export default function plan(ast, profiler) {
123
123
  }
124
124
 
125
125
  t('narrowSignatures', () => narrowSignatures(programFacts, ast))
126
+ // Boolean/bigint result kinds for funcs the call-site census can't reach —
127
+ // value-used-only functions have no direct sites, but their results still
128
+ // cross boxed positions (closure trampolines, boundary wrappers). Guarded:
129
+ // only ever SETS an unset valResult (see narrowBoolResults doc).
130
+ t('narrowBoolResults', () => narrowBoolResults())
126
131
  // Pass 2: narrowSignatures has now settled `programFacts.paramReps`, so a
127
132
  // global written from a bare parameter alias (`cur = s`, subscript's parse-
128
133
  // state shape) resolves — pass 1 saw only an untyped param and poisoned it.
@@ -246,7 +246,11 @@ const rewriteScalarTypedArrayUses = (node, arrays) => {
246
246
  const slot = slotFor(node[1][2], entry)
247
247
  if (!slot) return node
248
248
  const rhs = node.slice(2).map(part => rewriteScalarTypedArrayUses(part, arrays))
249
- return op === '=' && entry.coerce ? ['=', slot, coerceAST(entry.coerce, rhs[0])] : [op, slot, ...rhs]
249
+ // f64 slots (coerce '') still apply the store's ToNumber via unary plus
250
+ // free for a provably numeric RHS, and it folds a checked read's undefined
251
+ // arm to NaN (raw sentinel bits in the slot would read back as undefined)
252
+ return op === '=' ? ['=', slot, entry.coerce ? coerceAST(entry.coerce, rhs[0]) : ['u+', rhs[0]]]
253
+ : [op, slot, ...rhs]
250
254
  }
251
255
  return node.map((part, i) => i === 0 ? part : rewriteScalarTypedArrayUses(part, arrays))
252
256
  }
@@ -359,7 +363,12 @@ const scalarizeTypedArrayLiteralSeq = (seq) => {
359
363
  }
360
364
  if (unsafe.length) {
361
365
  for (const [name, arr] of unsafe) out.push(...scalarTypedArrayStores(name, arr))
362
- out.push(stmts[i])
366
+ // Only the synced (unsafe-here) arrays go through memory in this
367
+ // statement — every OTHER scalarized array's uses still rewrite to
368
+ // slots. Pushing the statement raw left e.g. `b[0] = a[i]` (unsafe for
369
+ // mirrored `a`, safe for candidate `b`) referencing the dissolved `b`.
370
+ const rest = new Map([...arrays].filter(([nm]) => !unsafe.some(([un]) => un === nm)))
371
+ out.push(rest.size ? rewriteScalarTypedArrayUses(stmts[i], rest) : stmts[i])
363
372
  for (const [name, arr] of unsafe) out.push(...scalarTypedArrayLoads(name, arr))
364
373
  changed = true
365
374
  } else {