jz 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/module/array.js CHANGED
@@ -9,8 +9,9 @@
9
9
  */
10
10
 
11
11
  import { typed, asF64, asI64, asI32, NULL_NAN, UNDEF_NAN, temp, tempI32, allocPtr, multiCount, arrayLoop, elemLoad, elemStore, truthyIR, extractF64Bits, appendStaticSlots, mkPtrIR, slotAddr, isLiteralStr, resolveValType, undefExpr } from '../src/ir.js'
12
- import { emit, materializeMulti } from '../src/emit.js'
13
- import { valTypeOf, lookupValType, VAL, extractParams, updateRep, staticPropertyKey } from '../src/analyze.js'
12
+ import { emit, buildArrayWithSpreads } from '../src/emit.js'
13
+ import { valTypeOf, lookupValType, lookupNotString, VAL, extractParams, updateRep, staticPropertyKey, staticObjectProps, inlineArraySid } from '../src/analyze.js'
14
+ import { structInline } from '../src/abi/index.js'
14
15
  import { ctx, inc, err, PTR, LAYOUT } from '../src/ctx.js'
15
16
  import { strHashLiteral } from './collection.js'
16
17
 
@@ -44,6 +45,25 @@ function hoistArrayValue(arr) {
44
45
 
45
46
  const arrayLenFromPtr = ptr => ['i32.load', ['i32.sub', ['local.get', `$${ptr}`], ['i32.const', 8]]]
46
47
 
48
+ /** K schema-ordered field-value AST nodes of an object literal `{S}` for a
49
+ * structInline `.push({S})`, or null if `lit` is not a plain static-key `{}`
50
+ * literal carrying exactly schema `sid`'s fields. Mapped by name into schema
51
+ * order so push sites with differing key order flatten to the same cell run. */
52
+ function structLiteralFields(lit, sid) {
53
+ if (!Array.isArray(lit) || lit[0] !== '{}') return null
54
+ const parsed = staticObjectProps(lit.slice(1))
55
+ const schema = ctx.schema.list[sid]
56
+ if (!parsed || parsed.names.length !== schema.length) return null
57
+ const byName = new Map()
58
+ for (let i = 0; i < parsed.names.length; i++) byName.set(parsed.names[i], parsed.values[i])
59
+ const out = []
60
+ for (const name of schema) {
61
+ if (!byName.has(name)) return null
62
+ out.push(byName.get(name))
63
+ }
64
+ return out
65
+ }
66
+
47
67
  // Pure-expression check: no statements, binders, control flow, or assignments.
48
68
  // Inlining is only safe for these — anything else needs the full closure machinery.
49
69
  const NOT_PURE_OPS = new Set([
@@ -115,6 +135,8 @@ function makeCallback(fn, argReps) {
115
135
  : typed(['f64.reinterpret_i64', ['i64.const', UNDEF_NAN]], 'f64')
116
136
  stmts.push(['local.set', `$${fresh}`, ae])
117
137
  }
138
+ // Emit-time rep seeding for inliner-fresh param locals (lifecycle:
139
+ // analysis already finished; these `inl_i` names didn't exist then).
118
140
  // Apply argReps hints (caller knows recv elem val type) to inlined-param
119
141
  // reps so emit(subst) sees `inl_i.val=NUMBER` and elides __to_num/__is_str_key.
120
142
  if (argReps) {
@@ -156,7 +178,7 @@ function callbackArgReps(arr) {
156
178
  const vt = lookupValType(arr)
157
179
  if (vt === VAL.TYPED) itemRep = { val: VAL.NUMBER }
158
180
  else if (vt === VAL.ARRAY) {
159
- const elemVt = ctx.func.repByLocal?.get(arr)?.arrayElemValType
181
+ const elemVt = ctx.func.localReps?.get(arr)?.arrayElemValType
160
182
  if (elemVt) itemRep = { val: elemVt }
161
183
  }
162
184
  } else {
@@ -227,6 +249,7 @@ export default (ctx) => {
227
249
  ],
228
250
  __arr_set_idx_ptr: ['__arr_grow', '__ptr_offset'],
229
251
  __arr_push1: ['__arr_grow_known', '__ptr_offset'],
252
+ __arr_unshift: ['__arr_grow', '__len', '__ptr_offset'],
230
253
  __typed_idx: () => ctx.features.typedarray || ctx.features.external
231
254
  ? ['__len']
232
255
  : ['__len', '__ptr_offset'],
@@ -251,7 +274,13 @@ export default (ctx) => {
251
274
  ctx.core.emit['new.Array'] = (len) => {
252
275
  const n = tempI32('alen')
253
276
  const nIR = ['local.get', `$${n}`]
254
- const out = allocPtr({ type: PTR.ARRAY, len: nIR, cap: nIR, tag: 'newarr' })
277
+ // L3/'speed' bumps the cap floor to skip the first growth cycles (default 0
278
+ // → grow on first push). Length stays exactly what the user requested.
279
+ const minCap = ctx.transform.optimize?.arrayMinCap | 0
280
+ const capIR = minCap > 0
281
+ ? ['select', ['i32.const', minCap], nIR, ['i32.gt_s', ['i32.const', minCap], nIR]]
282
+ : nIR
283
+ const out = allocPtr({ type: PTR.ARRAY, len: nIR, cap: capIR, tag: 'newarr' })
255
284
  return typed(['block', ['result', 'f64'],
256
285
  ['local.set', `$${n}`, len == null ? ['i32.const', 0] : asI32(emit(len))],
257
286
  out.init,
@@ -378,7 +407,7 @@ export default (ctx) => {
378
407
  ctx.core.stdlib['__arr_from'] = `(func $__arr_from (param $src i64) (result f64)
379
408
  (local $len i32) (local $dst i32)
380
409
  (local.set $len (call $__len (local.get $src)))
381
- (local.set $dst (call $__alloc_hdr (local.get $len) (local.get $len) (i32.const 8)))
410
+ (local.set $dst (call $__alloc_hdr (local.get $len) (local.get $len)))
382
411
  (memory.copy (local.get $dst) (call $__ptr_offset (local.get $src)) (i32.shl (local.get $len) (i32.const 3)))
383
412
  (call $__mkptr (i32.const ${PTR.ARRAY}) (i32.const 0) (local.get $dst)))`
384
413
 
@@ -393,7 +422,54 @@ export default (ctx) => {
393
422
  return null
394
423
  }
395
424
 
425
+ // Array.from(items, mapfn): spec step 2 — if mapfn is not undefined and
426
+ // IsCallable(mapfn) is false, throw a TypeError before iterating items.
427
+ // An explicit `undefined` arrives as the literal node [null, undefined];
428
+ // treat it as absent. Statically flag literal forms that can't be callable.
429
+ const isUndefinedNode = (n) => n === undefined
430
+ || (Array.isArray(n) && n[0] == null && n.length === 2 && n[1] === undefined)
431
+ const isNonCallableMapFn = (n) => {
432
+ if (!Array.isArray(n)) return false // undefined / identifier — unknown
433
+ const op = n[0]
434
+ if (op == null) return true // [null,x] literal — null/number/bigint
435
+ if (op === '=>') return false // arrow function — callable
436
+ if (op === '{}' || op === 'str' || op === 'strcat' || op === '//') return true
437
+ if (op === '[]' && n.length < 3) return true // array literal
438
+ if (op === '()' && n[1] === 'Symbol') return true // Symbol(...) result
439
+ return false // calls / member access — unknown
440
+ }
441
+
396
442
  ctx.core.emit['Array.from'] = (src, mapFn) => {
443
+ if (isUndefinedNode(mapFn)) mapFn = undefined
444
+ else if (isNonCallableMapFn(mapFn)) {
445
+ ctx.runtime.throws = true
446
+ return typed(['block', ['result', 'f64'], ['throw', '$__jz_err', ['f64.const', 0]]], 'f64')
447
+ }
448
+ // Array.from(string) → array of single-char strings. The generic __arr_from
449
+ // path memory.copies len*8 bytes from the string's byte storage (1 byte/char),
450
+ // reading far past its end → OOB trap. Iterate __str_idx per char instead.
451
+ if (resolveValType(src, valTypeOf, lookupValType) === VAL.STRING) {
452
+ inc('__str_idx', '__str_len')
453
+ const len = tempI32('sfl'), i = tempI32('sfi'), s = temp('sfs')
454
+ const lenIR = ['local.get', `$${len}`]
455
+ const out = allocPtr({ type: PTR.ARRAY, len: lenIR, tag: 'sfr' })
456
+ const cb = mapFn && makeCallback(mapFn, [null, { val: VAL.NUMBER }])
457
+ const ch = typed(['call', '$__str_idx', ['i64.reinterpret_f64', ['local.get', `$${s}`]], ['local.get', `$${i}`]], 'f64')
458
+ const item = cb ? cb.call([ch, idxArg(cb, i)]) : ch
459
+ const id = ctx.func.uniq++
460
+ return typed(['block', ['result', 'f64'],
461
+ ['local.set', `$${s}`, asF64(emit(src))],
462
+ ['local.set', `$${len}`, ['call', '$__str_len', ['i64.reinterpret_f64', ['local.get', `$${s}`]]]],
463
+ out.init,
464
+ ...(cb ? [cb.setup] : []),
465
+ ['local.set', `$${i}`, ['i32.const', 0]],
466
+ ['block', `$brk${id}`, ['loop', `$loop${id}`,
467
+ ['br_if', `$brk${id}`, ['i32.ge_s', ['local.get', `$${i}`], lenIR]],
468
+ elemStore(out.local, i, asF64(item)),
469
+ ['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
470
+ ['br', `$loop${id}`]]],
471
+ out.ptr], 'f64')
472
+ }
397
473
  const lengthExpr = arrayLikeLength(src)
398
474
  if (lengthExpr) {
399
475
  const len = tempI32('fl'), i = tempI32('fi')
@@ -415,8 +491,26 @@ export default (ctx) => {
415
491
  ['br', `$loop${id}`]]],
416
492
  out.ptr], 'f64')
417
493
  }
418
- inc('__arr_from')
419
- return typed(['call', '$__arr_from', asI64(emit(src))], 'f64')
494
+ if (!mapFn) {
495
+ inc('__arr_from')
496
+ return typed(['call', '$__arr_from', asI64(emit(src))], 'f64')
497
+ }
498
+ // mapfn present: iterate the source array element by element, reading each
499
+ // slot fresh inside the loop so a callback that mutates a not-yet-visited
500
+ // source element sees its update (spec reads source[k] per step).
501
+ inc('__len')
502
+ const cb = makeCallback(mapFn, [null, { val: VAL.NUMBER }])
503
+ const s = temp('afs'), len = tempI32('afl')
504
+ const out = allocPtr({ type: PTR.ARRAY, len: ['local.get', `$${len}`], tag: 'aff' })
505
+ const loop = arrayLoop(typed(['local.get', `$${s}`], 'f64'),
506
+ (_p, _l, i, item) => [elemStore(out.local, i, asF64(cb.call([item, idxArg(cb, i)])))], len)
507
+ return typed(['block', ['result', 'f64'],
508
+ ['local.set', `$${s}`, asF64(emit(src))],
509
+ ['local.set', `$${len}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${s}`]]]],
510
+ out.init,
511
+ cb.setup,
512
+ ...loop,
513
+ out.ptr], 'f64')
420
514
  }
421
515
 
422
516
  // Grow array if capacity insufficient. Returns (possibly new) NaN-boxed pointer.
@@ -434,7 +528,7 @@ export default (ctx) => {
434
528
  (i32.lt_u (local.get $off) (i32.const 8)))
435
529
  (then
436
530
  (local.set $newCap (select (local.get $minCap) (i32.const 4) (i32.gt_s (local.get $minCap) (i32.const 4))))
437
- (local.set $newOff (call $__alloc_hdr (i32.const 0) (local.get $newCap) (i32.const 8)))
531
+ (local.set $newOff (call $__alloc_hdr (i32.const 0) (local.get $newCap)))
438
532
  (return (call $__mkptr (i32.const ${PTR.ARRAY}) (i32.const 0) (local.get $newOff)))))
439
533
  (local.set $oldCap (i32.load (i32.sub (local.get $off) (i32.const 4))))
440
534
  (if (i32.ge_s (local.get $oldCap) (local.get $minCap))
@@ -444,7 +538,7 @@ export default (ctx) => {
444
538
  (i32.shl (local.get $oldCap) (i32.const 1))
445
539
  (i32.gt_s (local.get $minCap) (i32.shl (local.get $oldCap) (i32.const 1)))))
446
540
  (local.set $len (i32.load (i32.sub (local.get $off) (i32.const 8))))
447
- (local.set $newOff (call $__alloc_hdr (local.get $len) (local.get $newCap) (i32.const 8)))
541
+ (local.set $newOff (call $__alloc_hdr (local.get $len) (local.get $newCap)))
448
542
  (memory.copy (local.get $newOff) (local.get $off) (i32.shl (local.get $len) (i32.const 3)))
449
543
  ${headerPropsCopyIR()}
450
544
  ${maybeDynMoveIR()}
@@ -464,7 +558,7 @@ export default (ctx) => {
464
558
  (i32.shl (local.get $oldCap) (i32.const 1))
465
559
  (i32.gt_s (local.get $minCap) (i32.shl (local.get $oldCap) (i32.const 1)))))
466
560
  (local.set $len (i32.load (i32.sub (local.get $off) (i32.const 8))))
467
- (local.set $newOff (call $__alloc_hdr (local.get $len) (local.get $newCap) (i32.const 8)))
561
+ (local.set $newOff (call $__alloc_hdr (local.get $len) (local.get $newCap)))
468
562
  (memory.copy (local.get $newOff) (local.get $off) (i32.shl (local.get $len) (i32.const 3)))
469
563
  ${headerPropsCopyIR()}
470
564
  ${maybeDynMoveIR()}
@@ -476,17 +570,24 @@ export default (ctx) => {
476
570
  // once and read len from the inline header (i32.load base-8) — avoids __len's separate
477
571
  // forwarding follow. On the rare grow path the base is recomputed after relocation.
478
572
  ctx.core.stdlib['__arr_set_idx_ptr'] = `(func $__arr_set_idx_ptr (param $ptr i64) (param $i i32) (param $val f64) (result f64)
479
- (local $base i32) (local $p f64)
573
+ (local $base i32) (local $p f64) (local $oldLen i32) (local $k i32)
480
574
  (local.set $p (f64.reinterpret_i64 (local.get $ptr)))
481
575
  (if (i32.lt_s (local.get $i) (i32.const 0))
482
576
  (then (return (local.get $p))))
483
577
  (local.set $base (call $__ptr_offset (local.get $ptr)))
484
- (if (i32.ge_u (local.get $i)
485
- (i32.load (i32.sub (local.get $base) (i32.const 8))))
578
+ (local.set $oldLen (i32.load (i32.sub (local.get $base) (i32.const 8))))
579
+ (if (i32.ge_u (local.get $i) (local.get $oldLen))
486
580
  (then
487
581
  (local.set $p (call $__arr_grow (local.get $ptr) (i32.add (local.get $i) (i32.const 1))))
488
582
  (local.set $base (call $__ptr_offset (i64.reinterpret_f64 (local.get $p))))
489
- (i32.store (i32.sub (local.get $base) (i32.const 8)) (i32.add (local.get $i) (i32.const 1)))))
583
+ (i32.store (i32.sub (local.get $base) (i32.const 8)) (i32.add (local.get $i) (i32.const 1)))
584
+ ;; gap slots [oldLen, i) are holes — fill with undefined, not zero
585
+ (local.set $k (local.get $oldLen))
586
+ (block $fdone (loop $fill
587
+ (br_if $fdone (i32.ge_u (local.get $k) (local.get $i)))
588
+ (i64.store (i32.add (local.get $base) (i32.shl (local.get $k) (i32.const 3))) (i64.const ${UNDEF_NAN}))
589
+ (local.set $k (i32.add (local.get $k) (i32.const 1)))
590
+ (br $fill)))))
490
591
  (f64.store
491
592
  (i32.add (local.get $base) (i32.shl (local.get $i) (i32.const 3)))
492
593
  (local.get $val))
@@ -522,7 +623,9 @@ export default (ctx) => {
522
623
  const slots = elems.map(e => extractF64Bits(asF64(emit(e))))
523
624
  if (slots.every(b => b !== null)) return staticArrayPtr(slots)
524
625
  }
525
- const a = allocArray(len, Math.max(len, 4)) // min cap=4 for small pushes
626
+ // L3/'speed' bumps the cap floor (default 4) to skip more growth cycles.
627
+ const minCap = Math.max(ctx.transform.optimize?.arrayMinCap | 0, 4)
628
+ const a = allocArray(len, Math.max(len, minCap))
526
629
  const body = [...a.setup]
527
630
  for (let i = 0; i < len; i++)
528
631
  body.push(['f64.store', slotAddr(a.local, i), asF64(emit(elems[i]))])
@@ -530,52 +633,11 @@ export default (ctx) => {
530
633
  return typed(['block', ['result', 'f64'], ...body], 'f64')
531
634
  }
532
635
 
533
- const a = allocArray(0, Math.max(elems.length, 4))
534
- const out = temp('sa'), pos = tempI32('sp')
535
- inc('__arr_set_idx_ptr')
536
-
537
- const body = [
538
- ...a.setup,
539
- ['local.set', `$${out}`, a.ptr],
540
- ['local.set', `$${pos}`, ['i32.const', 0]],
541
- ]
542
-
543
- for (const e of elems) {
544
- if (Array.isArray(e) && e[0] === '...') {
545
- const src = temp('ss'), slen = tempI32('sl'), si = tempI32('si')
546
- const id = ctx.func.uniq++
547
- const spreadVal = multiCount(e[1]) ? materializeMulti(e[1]) : asF64(emit(e[1]))
548
- const srcVT = valTypeOf(e[1])
549
- const spreadItem = srcVT === VAL.ARRAY
550
- ? (inc('__arr_idx_known'), ['call', '$__arr_idx_known', ['i64.reinterpret_f64', ['local.get', `$${src}`]], ['local.get', `$${si}`]])
551
- : srcVT === VAL.STRING
552
- ? (inc('__str_idx'), ['call', '$__str_idx', ['i64.reinterpret_f64', ['local.get', `$${src}`]], ['local.get', `$${si}`]])
553
- : ctx.module.modules['string']
554
- ? ['if', ['result', 'f64'],
555
- ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${src}`]]], ['i32.const', PTR.STRING]],
556
- ['then', (inc('__str_idx'), ['call', '$__str_idx', ['i64.reinterpret_f64', ['local.get', `$${src}`]], ['local.get', `$${si}`]])],
557
- ['else', (['call', '$__typed_idx', ['i64.reinterpret_f64', ['local.get', `$${src}`]], ['local.get', `$${si}`]])]]
558
- : (['call', '$__typed_idx', ['i64.reinterpret_f64', ['local.get', `$${src}`]], ['local.get', `$${si}`]])
559
-
560
- body.push(
561
- ['local.set', `$${src}`, spreadVal],
562
- ['local.set', `$${slen}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${src}`]]]],
563
- ['local.set', `$${si}`, ['i32.const', 0]],
564
- ['block', `$brk${id}`, ['loop', `$loop${id}`,
565
- ['br_if', `$brk${id}`, ['i32.ge_s', ['local.get', `$${si}`], ['local.get', `$${slen}`]]],
566
- ['local.set', `$${out}`, ['call', '$__arr_set_idx_ptr', ['i64.reinterpret_f64', ['local.get', `$${out}`]], ['local.get', `$${pos}`], spreadItem]],
567
- ['local.set', `$${pos}`, ['i32.add', ['local.get', `$${pos}`], ['i32.const', 1]]],
568
- ['local.set', `$${si}`, ['i32.add', ['local.get', `$${si}`], ['i32.const', 1]]],
569
- ['br', `$loop${id}`]]])
570
- } else {
571
- body.push(
572
- ['local.set', `$${out}`, ['call', '$__arr_set_idx_ptr', ['i64.reinterpret_f64', ['local.get', `$${out}`]], ['local.get', `$${pos}`], asF64(emit(e))]],
573
- ['local.set', `$${pos}`, ['i32.add', ['local.get', `$${pos}`], ['i32.const', 1]]])
574
- }
575
- }
576
-
577
- body.push(['local.get', `$${out}`])
578
- return typed(['block', ['result', 'f64'], ...body], 'f64')
636
+ // Spread literal: buildArrayWithSpreads pre-sums the total length, allocates
637
+ // exact, and bulk-copies ARRAY sources with a single memory.copy — vs the
638
+ // per-element __arr_set_idx_ptr grow loop. Normalise the parser's `['...', x]`.
639
+ return buildArrayWithSpreads(elems.map(e =>
640
+ Array.isArray(e) && e[0] === '...' ? ['__spread', e[1]] : e))
579
641
  }
580
642
 
581
643
  // === Index read ===
@@ -587,6 +649,8 @@ export default (ctx) => {
587
649
  if (typeof arr !== 'string' && !(Array.isArray(arr) && arr[0] === 'local.get')) {
588
650
  const vtArr = valTypeOf(arr)
589
651
  const h = temp('ai')
652
+ // Emit-time rep seed on fresh hoist-temp `h` so the recursive emit
653
+ // below (`ctx.core.emit['[]'](h, idx)`) takes the typed-arr fast path.
590
654
  if (vtArr) updateRep(h, { val: vtArr })
591
655
  const setup = ['local.set', `$${h}`, asF64(emit(arr))]
592
656
  const result = ctx.core.emit['[]'](h, idx)
@@ -604,12 +668,17 @@ export default (ctx) => {
604
668
  const litKey = isLiteralStr(idx) ? idx[1]
605
669
  : typeof arr === 'string' && lookupValType(arr) === VAL.OBJECT ? staticPropertyKey(idx)
606
670
  : null
671
+ // SRoA flat object: `o['k']` → `local.get $o#i` (analyze.js scanFlatObjects).
672
+ if (litKey != null && typeof arr === 'string' && ctx.func.flatObjects?.has(arr)) {
673
+ const fo = ctx.func.flatObjects.get(arr)
674
+ const fi = fo.names.indexOf(litKey)
675
+ if (fi >= 0) return typed(['local.get', `$${arr}#${fi}`], 'f64')
676
+ }
607
677
  if (litKey != null && typeof arr === 'string' && ctx.schema.find) {
608
678
  const slot = ctx.schema.find(arr, litKey)
609
679
  if (slot >= 0) {
610
680
  inc('__ptr_offset')
611
- return typed(['f64.load',
612
- ['i32.add', ['call', '$__ptr_offset', ['i64.reinterpret_f64', asF64(emit(arr))]], ['i32.const', slot * 8]]], 'f64')
681
+ return typed(ctx.abi.object.ops.load(['call', '$__ptr_offset', ['i64.reinterpret_f64', asF64(emit(arr))]], slot), 'f64')
613
682
  }
614
683
  }
615
684
  if (litKey != null && typeof arr === 'string' && lookupValType(arr) === VAL.HASH) {
@@ -647,9 +716,9 @@ export default (ctx) => {
647
716
  if (keyType === VAL.STRING) return typed(dynLoad(asF64(emit(arr)), asF64(emit(idx))), 'f64')
648
717
  if (useRuntimeKeyDispatch)
649
718
  return emitDynamicKeyDispatch(asF64(emit(arr)), keyExpr =>
650
- ['f64.load', ['i32.add', ['call', '$__ptr_offset', ['i64.reinterpret_f64', inner]], ['i32.shl', asI32(typed(keyExpr, 'f64')), ['i32.const', 3]]]])
719
+ ctx.abi.array.ops.load(['call', '$__ptr_offset', ['i64.reinterpret_f64', inner]], asI32(typed(keyExpr, 'f64'))))
651
720
  return typed(
652
- ['f64.load', ['i32.add', ['call', '$__ptr_offset', ['i64.reinterpret_f64', inner]], ['i32.shl', vi, ['i32.const', 3]]]],
721
+ ctx.abi.array.ops.load(['call', '$__ptr_offset', ['i64.reinterpret_f64', inner]], vi),
653
722
  'f64')
654
723
  }
655
724
  // HASH receiver with runtime string key: probe the HASH directly via
@@ -676,6 +745,24 @@ export default (ctx) => {
676
745
  if (keyType === VAL.STRING)
677
746
  return typed(dynLoad(ptrExpr, asF64(emit(idx))), 'f64')
678
747
  if (vt === 'array') {
748
+ // structInline Array<S>: element i is K consecutive inline f64 schema
749
+ // cells — no per-row heap object, no stored element pointer. `arr[i]` is
750
+ // the byte address of the element's first cell, returned as a first-class
751
+ // unboxed OBJECT pointer (schema S); `arr[i].field` then composes a plain
752
+ // `+field*8` off it. The narrower proved every use of this binding is one
753
+ // structInline handles (src/analyze.js analyzeStructInline).
754
+ const inlSid = inlineArraySid(arr)
755
+ if (inlSid != null) {
756
+ inc('__ptr_offset')
757
+ const baseI32 = tempI32('ab')
758
+ const K = ctx.schema.list[inlSid].length
759
+ const cell = typed(structInline(K).ops.elemAddr(
760
+ ['local.tee', `$${baseI32}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ptrExpr]]],
761
+ vi), 'i32')
762
+ cell.ptrKind = VAL.OBJECT
763
+ cell.ptrAux = inlSid
764
+ return cell
765
+ }
679
766
  // Known-ARRAY → __arr_idx (single forwarding follow + inline bounds check),
680
767
  // not __typed_idx (which does __len + __ptr_offset = two forwarding follows
681
768
  // plus type-dispatch overhead irrelevant for plain arrays).
@@ -690,7 +777,7 @@ export default (ctx) => {
690
777
  // NaN-boxed pointer for OBJECT/STRING; downstream typed() handles both).
691
778
  // Fast path fires on schemaId (Array<{x,y,z}> shapes) OR plain elem-val
692
779
  // (Array<NUMBER> from `.map(x => x*k)` etc.).
693
- const rep = typeof arr === 'string' ? ctx.func.repByLocal?.get(arr) : null
780
+ const rep = typeof arr === 'string' ? ctx.func.localReps?.get(arr) : null
694
781
  const hasElemFact = rep?.arrayElemSchema != null
695
782
  if (hasElemFact && keyIsNum) {
696
783
  inc('__ptr_offset')
@@ -699,11 +786,9 @@ export default (ctx) => {
699
786
  // `i32.wrap_i64 (i64.reinterpret_f64 (f64.load …))` → `i32.load …`
700
787
  // when this load feeds a ptrUnboxed OBJECT field.
701
788
  const baseI32 = tempI32('ab')
702
- return typed(['f64.load',
703
- ['i32.add',
704
- ['local.tee', `$${baseI32}`,
705
- ['call', '$__ptr_offset', ['i64.reinterpret_f64', ptrExpr]]],
706
- ['i32.shl', vi, ['i32.const', 3]]]], 'f64')
789
+ return typed(ctx.abi.array.ops.load(
790
+ ['local.tee', `$${baseI32}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ptrExpr]]],
791
+ vi), 'f64')
707
792
  }
708
793
  const baseTmp = temp()
709
794
  // Numeric key (literal or known-NUMBER name) → skip __is_str_key dispatch;
@@ -739,10 +824,23 @@ export default (ctx) => {
739
824
  })
740
825
  return typed((['call', '$__typed_idx', ['i64.reinterpret_f64', ptrExpr], vi]), 'f64')
741
826
  }
742
- if (useRuntimeKeyDispatch)
827
+ // Pure-write narrowing: an `xs[i] = v` / `xs.length = n` site in this
828
+ // body, with no offsetting string-shape evidence (typeof string check,
829
+ // STRING_ONLY method call, string-literal assignment), proves `arr` isn't
830
+ // a primitive string. Skip the runtime `__ptr_type==STRING` gate —
831
+ // `__typed_idx` already handles both ARRAY and TYPED tags internally.
832
+ // Discharge analysis lives in src/infer.js (notStringEvidence source); flow-sensitive
833
+ // notString refinements (from `if (typeof x === 'string') return ...`) overlay via lookup.
834
+ const notString = typeof arr === 'string' && lookupNotString(arr)
835
+ // A provably-NUMBER key can never be a string key, so the `__is_str_key`
836
+ // dispatch is statically dead — its numeric arm is what runs. Fall through
837
+ // to the direct ptr_type==STRING ? __str_idx : __typed_idx form below, which
838
+ // indexes with the i32 `vi` and skips the per-element f64 round-trip + call.
839
+ // Mirrors the `&& !keyIsNum` guard in the known-array/typed branches above.
840
+ if (useRuntimeKeyDispatch && keyType !== VAL.NUMBER)
743
841
  return emitDynamicKeyDispatch(ptrExpr, keyExpr => {
744
842
  const keyI32 = asI32(typed(keyExpr, 'f64'))
745
- if (ctx.module.modules['string']) {
843
+ if (ctx.module.modules['string'] && !notString) {
746
844
  return ['if', ['result', 'f64'],
747
845
  ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ptrExpr]], ['i32.const', PTR.STRING]],
748
846
  ['then', (inc('__str_idx'), ['call', '$__str_idx', ['i64.reinterpret_f64', ptrExpr], keyI32])],
@@ -751,7 +849,7 @@ export default (ctx) => {
751
849
  return (['call', '$__typed_idx', ['i64.reinterpret_f64', ptrExpr], keyI32])
752
850
  })
753
851
  // Unknown → runtime dispatch (string module loaded → check ptr_type)
754
- if (ctx.module.modules['string'])
852
+ if (ctx.module.modules['string'] && !notString)
755
853
  return typed(
756
854
  ['if', ['result', 'f64'],
757
855
  ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ptrExpr]], ['i32.const', PTR.STRING]],
@@ -765,6 +863,23 @@ export default (ctx) => {
765
863
 
766
864
  // .push(val) → append, increment len, return array (possibly reallocated pointer)
767
865
  ctx.core.emit['.push'] = (arr, ...vals) => {
866
+ // structInline Array<S>: `.push({S})` writes the K schema fields as K
867
+ // consecutive f64 cells. Flatten the struct literal into K schema-ordered
868
+ // field-value nodes and fall through to the general multi-value store path
869
+ // — `len`/`cap` count physical cells, so `__arr_grow_known` and the cell
870
+ // loop are reused untouched; `.push` then returns the logical element count
871
+ // (`len / K`). K=1 stays a single value → the `__arr_push1` fast path.
872
+ const inlSid = inlineArraySid(arr)
873
+ const inlK = inlSid != null ? ctx.schema.list[inlSid].length : 0
874
+ if (inlSid != null) {
875
+ const flat = []
876
+ for (const v of vals) {
877
+ const fields = structLiteralFields(v, inlSid)
878
+ if (!fields) err(`structInline Array.push expects { ${ctx.schema.list[inlSid].join(', ')} } literal arguments`)
879
+ flat.push(...fields)
880
+ }
881
+ vals = flat
882
+ }
768
883
  // Out-of-line fast path: single value, named known-ARRAY receiver. One call +
769
884
  // var update instead of ~30 inlined instructions — the dominant size cost of
770
885
  // push-heavy code (e.g. watr's WASM emitter).
@@ -848,7 +963,11 @@ export default (ctx) => {
848
963
  else
849
964
  body.push(['local.set', `$${arr}`, ['local.get', `$${t}`]])
850
965
  }
851
- body.push(['f64.convert_i32_s', ['local.get', `$${len}`]])
966
+ // structInline: `len` counts physical cells — `.push` returns the JS array
967
+ // length, i.e. the logical element count `len / K`.
968
+ body.push(['f64.convert_i32_s', inlK > 1
969
+ ? ['i32.div_s', ['local.get', `$${len}`], ['i32.const', inlK]]
970
+ : ['local.get', `$${len}`]])
852
971
 
853
972
  return typed(['block', ['result', 'f64'], ...body], 'f64')
854
973
  }
@@ -1638,5 +1757,5 @@ export default (ctx) => {
1638
1757
 
1639
1758
  // .join(sep) → concatenate array elements with separator string
1640
1759
  ctx.core.emit['.join'] = (arr, sep) => (inc('__str_join'),
1641
- typed(['call', '$__str_join', asI64(emit(arr)), asI64(emit(sep))], 'f64'))
1760
+ typed(['call', '$__str_join', asI64(emit(arr)), asI64(emit(sep == null ? ['str', ','] : sep))], 'f64'))
1642
1761
  }