jz 0.4.0 → 0.5.1

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'],
@@ -399,7 +422,54 @@ export default (ctx) => {
399
422
  return null
400
423
  }
401
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
+
402
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
+ }
403
473
  const lengthExpr = arrayLikeLength(src)
404
474
  if (lengthExpr) {
405
475
  const len = tempI32('fl'), i = tempI32('fi')
@@ -421,8 +491,26 @@ export default (ctx) => {
421
491
  ['br', `$loop${id}`]]],
422
492
  out.ptr], 'f64')
423
493
  }
424
- inc('__arr_from')
425
- 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')
426
514
  }
427
515
 
428
516
  // Grow array if capacity insufficient. Returns (possibly new) NaN-boxed pointer.
@@ -482,17 +570,24 @@ export default (ctx) => {
482
570
  // once and read len from the inline header (i32.load base-8) — avoids __len's separate
483
571
  // forwarding follow. On the rare grow path the base is recomputed after relocation.
484
572
  ctx.core.stdlib['__arr_set_idx_ptr'] = `(func $__arr_set_idx_ptr (param $ptr i64) (param $i i32) (param $val f64) (result f64)
485
- (local $base i32) (local $p f64)
573
+ (local $base i32) (local $p f64) (local $oldLen i32) (local $k i32)
486
574
  (local.set $p (f64.reinterpret_i64 (local.get $ptr)))
487
575
  (if (i32.lt_s (local.get $i) (i32.const 0))
488
576
  (then (return (local.get $p))))
489
577
  (local.set $base (call $__ptr_offset (local.get $ptr)))
490
- (if (i32.ge_u (local.get $i)
491
- (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))
492
580
  (then
493
581
  (local.set $p (call $__arr_grow (local.get $ptr) (i32.add (local.get $i) (i32.const 1))))
494
582
  (local.set $base (call $__ptr_offset (i64.reinterpret_f64 (local.get $p))))
495
- (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)))))
496
591
  (f64.store
497
592
  (i32.add (local.get $base) (i32.shl (local.get $i) (i32.const 3)))
498
593
  (local.get $val))
@@ -538,53 +633,11 @@ export default (ctx) => {
538
633
  return typed(['block', ['result', 'f64'], ...body], 'f64')
539
634
  }
540
635
 
541
- const minCap = Math.max(ctx.transform.optimize?.arrayMinCap | 0, 4)
542
- const a = allocArray(0, Math.max(elems.length, minCap))
543
- const out = temp('sa'), pos = tempI32('sp')
544
- inc('__arr_set_idx_ptr')
545
-
546
- const body = [
547
- ...a.setup,
548
- ['local.set', `$${out}`, a.ptr],
549
- ['local.set', `$${pos}`, ['i32.const', 0]],
550
- ]
551
-
552
- for (const e of elems) {
553
- if (Array.isArray(e) && e[0] === '...') {
554
- const src = temp('ss'), slen = tempI32('sl'), si = tempI32('si')
555
- const id = ctx.func.uniq++
556
- const spreadVal = multiCount(e[1]) ? materializeMulti(e[1]) : asF64(emit(e[1]))
557
- const srcVT = valTypeOf(e[1])
558
- const spreadItem = srcVT === VAL.ARRAY
559
- ? (inc('__arr_idx_known'), ['call', '$__arr_idx_known', ['i64.reinterpret_f64', ['local.get', `$${src}`]], ['local.get', `$${si}`]])
560
- : srcVT === VAL.STRING
561
- ? (inc('__str_idx'), ['call', '$__str_idx', ['i64.reinterpret_f64', ['local.get', `$${src}`]], ['local.get', `$${si}`]])
562
- : ctx.module.modules['string']
563
- ? ['if', ['result', 'f64'],
564
- ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${src}`]]], ['i32.const', PTR.STRING]],
565
- ['then', (inc('__str_idx'), ['call', '$__str_idx', ['i64.reinterpret_f64', ['local.get', `$${src}`]], ['local.get', `$${si}`]])],
566
- ['else', (['call', '$__typed_idx', ['i64.reinterpret_f64', ['local.get', `$${src}`]], ['local.get', `$${si}`]])]]
567
- : (['call', '$__typed_idx', ['i64.reinterpret_f64', ['local.get', `$${src}`]], ['local.get', `$${si}`]])
568
-
569
- body.push(
570
- ['local.set', `$${src}`, spreadVal],
571
- ['local.set', `$${slen}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${src}`]]]],
572
- ['local.set', `$${si}`, ['i32.const', 0]],
573
- ['block', `$brk${id}`, ['loop', `$loop${id}`,
574
- ['br_if', `$brk${id}`, ['i32.ge_s', ['local.get', `$${si}`], ['local.get', `$${slen}`]]],
575
- ['local.set', `$${out}`, ['call', '$__arr_set_idx_ptr', ['i64.reinterpret_f64', ['local.get', `$${out}`]], ['local.get', `$${pos}`], spreadItem]],
576
- ['local.set', `$${pos}`, ['i32.add', ['local.get', `$${pos}`], ['i32.const', 1]]],
577
- ['local.set', `$${si}`, ['i32.add', ['local.get', `$${si}`], ['i32.const', 1]]],
578
- ['br', `$loop${id}`]]])
579
- } else {
580
- body.push(
581
- ['local.set', `$${out}`, ['call', '$__arr_set_idx_ptr', ['i64.reinterpret_f64', ['local.get', `$${out}`]], ['local.get', `$${pos}`], asF64(emit(e))]],
582
- ['local.set', `$${pos}`, ['i32.add', ['local.get', `$${pos}`], ['i32.const', 1]]])
583
- }
584
- }
585
-
586
- body.push(['local.get', `$${out}`])
587
- 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))
588
641
  }
589
642
 
590
643
  // === Index read ===
@@ -596,6 +649,8 @@ export default (ctx) => {
596
649
  if (typeof arr !== 'string' && !(Array.isArray(arr) && arr[0] === 'local.get')) {
597
650
  const vtArr = valTypeOf(arr)
598
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.
599
654
  if (vtArr) updateRep(h, { val: vtArr })
600
655
  const setup = ['local.set', `$${h}`, asF64(emit(arr))]
601
656
  const result = ctx.core.emit['[]'](h, idx)
@@ -613,12 +668,17 @@ export default (ctx) => {
613
668
  const litKey = isLiteralStr(idx) ? idx[1]
614
669
  : typeof arr === 'string' && lookupValType(arr) === VAL.OBJECT ? staticPropertyKey(idx)
615
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
+ }
616
677
  if (litKey != null && typeof arr === 'string' && ctx.schema.find) {
617
678
  const slot = ctx.schema.find(arr, litKey)
618
679
  if (slot >= 0) {
619
680
  inc('__ptr_offset')
620
- return typed(['f64.load',
621
- ['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')
622
682
  }
623
683
  }
624
684
  if (litKey != null && typeof arr === 'string' && lookupValType(arr) === VAL.HASH) {
@@ -656,9 +716,9 @@ export default (ctx) => {
656
716
  if (keyType === VAL.STRING) return typed(dynLoad(asF64(emit(arr)), asF64(emit(idx))), 'f64')
657
717
  if (useRuntimeKeyDispatch)
658
718
  return emitDynamicKeyDispatch(asF64(emit(arr)), keyExpr =>
659
- ['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'))))
660
720
  return typed(
661
- ['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),
662
722
  'f64')
663
723
  }
664
724
  // HASH receiver with runtime string key: probe the HASH directly via
@@ -685,6 +745,24 @@ export default (ctx) => {
685
745
  if (keyType === VAL.STRING)
686
746
  return typed(dynLoad(ptrExpr, asF64(emit(idx))), 'f64')
687
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
+ }
688
766
  // Known-ARRAY → __arr_idx (single forwarding follow + inline bounds check),
689
767
  // not __typed_idx (which does __len + __ptr_offset = two forwarding follows
690
768
  // plus type-dispatch overhead irrelevant for plain arrays).
@@ -699,7 +777,7 @@ export default (ctx) => {
699
777
  // NaN-boxed pointer for OBJECT/STRING; downstream typed() handles both).
700
778
  // Fast path fires on schemaId (Array<{x,y,z}> shapes) OR plain elem-val
701
779
  // (Array<NUMBER> from `.map(x => x*k)` etc.).
702
- const rep = typeof arr === 'string' ? ctx.func.repByLocal?.get(arr) : null
780
+ const rep = typeof arr === 'string' ? ctx.func.localReps?.get(arr) : null
703
781
  const hasElemFact = rep?.arrayElemSchema != null
704
782
  if (hasElemFact && keyIsNum) {
705
783
  inc('__ptr_offset')
@@ -708,11 +786,9 @@ export default (ctx) => {
708
786
  // `i32.wrap_i64 (i64.reinterpret_f64 (f64.load …))` → `i32.load …`
709
787
  // when this load feeds a ptrUnboxed OBJECT field.
710
788
  const baseI32 = tempI32('ab')
711
- return typed(['f64.load',
712
- ['i32.add',
713
- ['local.tee', `$${baseI32}`,
714
- ['call', '$__ptr_offset', ['i64.reinterpret_f64', ptrExpr]]],
715
- ['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')
716
792
  }
717
793
  const baseTmp = temp()
718
794
  // Numeric key (literal or known-NUMBER name) → skip __is_str_key dispatch;
@@ -748,10 +824,23 @@ export default (ctx) => {
748
824
  })
749
825
  return typed((['call', '$__typed_idx', ['i64.reinterpret_f64', ptrExpr], vi]), 'f64')
750
826
  }
751
- 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)
752
841
  return emitDynamicKeyDispatch(ptrExpr, keyExpr => {
753
842
  const keyI32 = asI32(typed(keyExpr, 'f64'))
754
- if (ctx.module.modules['string']) {
843
+ if (ctx.module.modules['string'] && !notString) {
755
844
  return ['if', ['result', 'f64'],
756
845
  ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ptrExpr]], ['i32.const', PTR.STRING]],
757
846
  ['then', (inc('__str_idx'), ['call', '$__str_idx', ['i64.reinterpret_f64', ptrExpr], keyI32])],
@@ -760,7 +849,7 @@ export default (ctx) => {
760
849
  return (['call', '$__typed_idx', ['i64.reinterpret_f64', ptrExpr], keyI32])
761
850
  })
762
851
  // Unknown → runtime dispatch (string module loaded → check ptr_type)
763
- if (ctx.module.modules['string'])
852
+ if (ctx.module.modules['string'] && !notString)
764
853
  return typed(
765
854
  ['if', ['result', 'f64'],
766
855
  ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ptrExpr]], ['i32.const', PTR.STRING]],
@@ -774,6 +863,23 @@ export default (ctx) => {
774
863
 
775
864
  // .push(val) → append, increment len, return array (possibly reallocated pointer)
776
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
+ }
777
883
  // Out-of-line fast path: single value, named known-ARRAY receiver. One call +
778
884
  // var update instead of ~30 inlined instructions — the dominant size cost of
779
885
  // push-heavy code (e.g. watr's WASM emitter).
@@ -857,7 +963,11 @@ export default (ctx) => {
857
963
  else
858
964
  body.push(['local.set', `$${arr}`, ['local.get', `$${t}`]])
859
965
  }
860
- 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}`]])
861
971
 
862
972
  return typed(['block', ['result', 'f64'], ...body], 'f64')
863
973
  }
@@ -1647,5 +1757,5 @@ export default (ctx) => {
1647
1757
 
1648
1758
  // .join(sep) → concatenate array elements with separator string
1649
1759
  ctx.core.emit['.join'] = (arr, sep) => (inc('__str_join'),
1650
- 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'))
1651
1761
  }