jz 0.5.1 → 0.6.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.
Files changed (80) hide show
  1. package/README.md +288 -314
  2. package/bench/README.md +319 -0
  3. package/bench/bench.svg +112 -0
  4. package/cli.js +32 -24
  5. package/index.js +177 -55
  6. package/interop.js +88 -159
  7. package/jz.svg +5 -0
  8. package/jzify/arguments.js +97 -0
  9. package/jzify/bundler.js +382 -0
  10. package/jzify/classes.js +328 -0
  11. package/jzify/hoist-vars.js +177 -0
  12. package/jzify/index.js +51 -0
  13. package/jzify/names.js +37 -0
  14. package/jzify/switch.js +106 -0
  15. package/jzify/transform.js +349 -0
  16. package/layout.js +179 -0
  17. package/module/array.js +322 -153
  18. package/module/collection.js +603 -145
  19. package/module/console.js +55 -43
  20. package/module/core.js +266 -153
  21. package/module/date.js +15 -3
  22. package/module/function.js +73 -6
  23. package/module/index.js +2 -1
  24. package/module/json.js +226 -61
  25. package/module/math.js +414 -186
  26. package/module/number.js +306 -60
  27. package/module/object.js +448 -184
  28. package/module/regex.js +255 -25
  29. package/module/schema.js +24 -6
  30. package/module/simd.js +85 -0
  31. package/module/string.js +586 -220
  32. package/module/symbol.js +1 -1
  33. package/module/timer.js +9 -14
  34. package/module/typedarray.js +45 -48
  35. package/package.json +41 -12
  36. package/src/abi/index.js +39 -23
  37. package/src/abi/string.js +38 -41
  38. package/src/ast.js +460 -0
  39. package/src/autoload.js +26 -24
  40. package/src/bridge.js +111 -0
  41. package/src/compile/analyze-scans.js +661 -0
  42. package/src/compile/analyze.js +1565 -0
  43. package/src/compile/emit-assign.js +408 -0
  44. package/src/compile/emit.js +3201 -0
  45. package/src/compile/flow-types.js +103 -0
  46. package/src/{compile.js → compile/index.js} +497 -125
  47. package/src/{infer.js → compile/infer.js} +27 -98
  48. package/src/{narrow.js → compile/narrow.js} +302 -96
  49. package/src/compile/plan/advise.js +316 -0
  50. package/src/compile/plan/common.js +150 -0
  51. package/src/compile/plan/index.js +118 -0
  52. package/src/compile/plan/inline.js +679 -0
  53. package/src/compile/plan/literals.js +984 -0
  54. package/src/compile/plan/loops.js +472 -0
  55. package/src/compile/plan/scope.js +573 -0
  56. package/src/compile/program-facts.js +404 -0
  57. package/src/ctx.js +176 -58
  58. package/src/ir.js +540 -171
  59. package/src/kind-traits.js +105 -0
  60. package/src/kind.js +462 -0
  61. package/src/op-policy.js +57 -0
  62. package/src/{optimize.js → optimize/index.js} +1106 -446
  63. package/src/optimize/vectorize.js +1874 -0
  64. package/src/param-reps.js +65 -0
  65. package/src/parse.js +44 -0
  66. package/src/{prepare.js → prepare/index.js} +589 -202
  67. package/src/reps.js +115 -0
  68. package/src/resolve.js +12 -3
  69. package/src/static.js +199 -0
  70. package/src/type.js +647 -0
  71. package/src/{assemble.js → wat/assemble.js} +86 -48
  72. package/src/wat/optimize.js +3760 -0
  73. package/transform.js +21 -0
  74. package/wasi.js +47 -5
  75. package/src/analyze.js +0 -3818
  76. package/src/emit.js +0 -3040
  77. package/src/jzify.js +0 -1580
  78. package/src/plan.js +0 -2132
  79. package/src/vectorize.js +0 -1088
  80. /package/src/{codegen.js → wat/codegen.js} +0 -0
package/module/object.js CHANGED
@@ -7,12 +7,38 @@
7
7
  * @module object
8
8
  */
9
9
 
10
- import { typed, asF64, asI64, temp, tempI32, allocPtr, needsDynShadow, mkPtrIR, extractF64Bits, appendStaticSlots, slotAddr, elemLoad, elemStore } from '../src/ir.js'
11
- import { emit } from '../src/emit.js'
12
- import { valTypeOf, lookupValType, VAL, repOf, updateRep, shapeOf } from '../src/analyze.js'
13
- import { ctx, err, inc, PTR, LAYOUT } from '../src/ctx.js'
14
- import { includeModule } from '../src/autoload.js'
10
+ import { typed, asF64, asI64, NULL_NAN, UNDEF_NAN, temp, tempI32, tempI64, block64, ptrTypeEq, dispatchByPtrType, allocPtr, needsDynShadow, mkPtrIR, extractF64Bits, appendStaticSlots, slotAddr, elemLoad, elemStore, boolBoxIR } from '../src/ir.js'
11
+ import { emit } from '../src/bridge.js'
12
+ import { valTypeOf, shapeOf } from '../src/kind.js'
13
+ import { VAL, lookupValType, repOf, updateRep } from '../src/reps.js'
14
+ import { ctx, err, inc, PTR, LAYOUT, declGlobal } from '../src/ctx.js'
15
15
 
16
+ // Object.prototype.toString tag per value category. Matches what JS engines
17
+ // return for primitive/built-in types; canonicalized from
18
+ // `Object.prototype.toString.call(x)` by jzify (see jzify/bundler.js).
19
+ const OBJECT_TO_STRING_TAGS = {
20
+ [VAL.NUMBER]: '[object Number]',
21
+ [VAL.BIGINT]: '[object BigInt]',
22
+ [VAL.BOOL]: '[object Boolean]',
23
+ [VAL.STRING]: '[object String]',
24
+ [VAL.ARRAY]: '[object Array]',
25
+ [VAL.OBJECT]: '[object Object]',
26
+ [VAL.HASH]: '[object Object]',
27
+ [VAL.SET]: '[object Set]',
28
+ [VAL.MAP]: '[object Map]',
29
+ [VAL.CLOSURE]: '[object Function]',
30
+ [VAL.REGEX]: '[object RegExp]',
31
+ [VAL.DATE]: '[object Date]',
32
+ [VAL.BUFFER]: '[object ArrayBuffer]',
33
+ [VAL.TYPED]: '[object Object]',
34
+ }
35
+
36
+ const objectToStringTagForVal = (obj) => {
37
+ const val = typeof obj === 'string' ? lookupValType(obj) : valTypeOf(obj)
38
+ return val ? OBJECT_TO_STRING_TAGS[val] : null
39
+ }
40
+
41
+ const storedValue = (node) => valTypeOf(node) === VAL.BOOL ? boolBoxIR(emit(node)) : asF64(emit(node))
16
42
 
17
43
  export default (ctx) => {
18
44
  inc('__mkptr', '__alloc', '__alloc_hdr', '__ptr_offset', '__len', '__ptr_type')
@@ -54,11 +80,21 @@ export default (ctx) => {
54
80
  if (Array.isArray(p) && p[0] === ':') { names.push(p[1]); values.push(p[2]) }
55
81
  }
56
82
 
57
- // Use variable's merged schema if available (from Object.assign inference), else register literal schema.
58
- let schemaId = ctx.schema.register(names)
83
+ // Use variable's merged schema if available (from Object.assign inference),
84
+ // else register the literal's own schema. The merged schema is adopted only
85
+ // when it is a *superset* of the literal's own fields — a legitimate
86
+ // accumulation (`let o = {}; o.x = …`) always contains every literal key.
87
+ // A merged schema missing any literal field is a stale cross-function name
88
+ // collision (ctx.schema.vars is module-global, keyed by bare name): adopting
89
+ // it would size the alloc to the wrong schema and overflow the object's
90
+ // slots, corrupting adjacent heap. The literal is authoritative for its own
91
+ // shape, so re-bind the variable to it for precise same-function reads.
92
+ const litId = ctx.schema.register(names)
93
+ let schemaId = litId
59
94
  if (target) {
60
95
  const merged = ctx.schema.resolve(target)
61
- if (merged) schemaId = ctx.schema.idOf(target)
96
+ if (merged && names.every(n => merged.includes(n))) schemaId = ctx.schema.idOf(target)
97
+ else if (names.length) ctx.schema.vars.set(target, litId)
62
98
  }
63
99
  const schema = ctx.schema.list[schemaId]
64
100
  const t = tempI32('obj')
@@ -67,19 +103,35 @@ export default (ctx) => {
67
103
  // R: Static data segment for objects of pure-literal property values (own-memory only).
68
104
  // Even with shadow needed, we can skip alloc + N stores; just feed literal values to __dyn_set.
69
105
  const shadow = needsDynShadow(target)
70
- if (values.length >= 2 && values.length === schema.length && !ctx.memory.shared) {
71
- const emitted = values.map(emit)
106
+ // When the literal adopts a superset/merged schema (schemaId !== litId), the
107
+ // field order in `schema` can differ from the literal's `names`, so each value
108
+ // must land at its named slot `schema.indexOf(name)` — a positional `slot = i`
109
+ // store would scatter values into the wrong (or another field's) slots.
110
+ const slotOf = schemaId === litId ? (i => i) : (i => schema.indexOf(names[i]))
111
+ // SOUNDNESS GATE: a static literal is ONE shared instance — every evaluation
112
+ // returns the same pointer. That is only faithful when the object is never
113
+ // mutated: `let mk = () => ({n:0,m:0}); mk().n++` must not bleed into the
114
+ // next mk(). writtenProps (program-facts) holds every property name ever
115
+ // written through ANY receiver — including expression receivers like
116
+ // `map.get(k).n++` that no alias analysis could attribute — so a literal
117
+ // whose schema intersects it allocates per-evaluation instead.
118
+ const neverWritten = names.every(n => !ctx.module.writtenProps?.has(n))
119
+ if (neverWritten && values.length >= 2 && values.length === schema.length && !ctx.memory.shared) {
120
+ const emitted = values.map(storedValue)
72
121
  // asF64 folds i32.const → f64.const so int-literal values also qualify.
73
- const slots = emitted.map(v => extractF64Bits(asF64(v)))
122
+ const slots = emitted.map(v => extractF64Bits(v))
74
123
  if (slots.every(b => b !== null)) {
75
- const off = appendStaticSlots(slots)
124
+ // Reorder into schema-slot order before laying out the static segment.
125
+ const ordered = emitted.map(() => null), orderedBits = emitted.map(() => null)
126
+ for (let i = 0; i < values.length; i++) { ordered[slotOf(i)] = emitted[i]; orderedBits[slotOf(i)] = slots[i] }
127
+ const off = appendStaticSlots(orderedBits)
76
128
  const staticPtr = mkPtrIR(PTR.OBJECT, schemaId, off)
77
129
  if (!shadow) return staticPtr
78
130
  inc('__dyn_set')
79
131
  const body = [['local.set', `$${ptr}`, staticPtr]]
80
132
  for (let i = 0; i < schema.length; i++)
81
133
  body.push(['drop', ['call', '$__dyn_set', ['i64.reinterpret_f64', ['local.get', `$${ptr}`]],
82
- asI64(emit(['str', String(schema[i])])), asI64(emitted[i])]])
134
+ asI64(emit(['str', String(schema[i])])), asI64(ordered[i])]])
83
135
  body.push(['local.get', `$${ptr}`])
84
136
  return typed(['block', ['result', 'f64'], ...body], 'f64')
85
137
  }
@@ -89,7 +141,7 @@ export default (ctx) => {
89
141
  ['local.set', `$${t}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', ctx.abi.object.ops.allocSlots(schema.length)]]],
90
142
  ]
91
143
  for (let i = 0; i < values.length; i++)
92
- body.push(ctx.abi.object.ops.store(['local.get', `$${t}`], i, asF64(emit(values[i]))))
144
+ body.push(ctx.abi.object.ops.store(['local.get', `$${t}`], slotOf(i), storedValue(values[i])))
93
145
  body.push(['local.set', `$${ptr}`, mkPtrIR(PTR.OBJECT, schemaId, ['local.get', `$${t}`])])
94
146
  if (shadow) {
95
147
  inc('__dyn_set')
@@ -106,6 +158,15 @@ export default (ctx) => {
106
158
 
107
159
  ctx.core.emit['Object.freeze'] = (obj) => asF64(emit(obj))
108
160
 
161
+ // Object.is(a, b) — SameValue, which on NaN-boxed f64 values is exact bit
162
+ // equality. That is precisely why it diverges from `===`: +0 and -0 carry
163
+ // distinct bit patterns (→ false), and a NaN equals itself bit-for-bit
164
+ // (→ true). Objects/booleans/null/undefined compare by their fixed boxed
165
+ // bits, i.e. reference identity, as SameValue requires. (Two distinct heap
166
+ // strings would compare by pointer rather than content; jz only uses numeric
167
+ // Object.is — overwhelmingly `Object.is(x, -0)` — so that path never arises.)
168
+ ctx.core.emit['Object.is'] = (a, b) => typed(['i64.eq', asI64(emit(a)), asI64(emit(b))], 'i32')
169
+
109
170
  // Object.isExtensible / isSealed / isFrozen.
110
171
  // jz fixes an object's schema at construction: a `{…}` literal can
111
172
  // neither grow nor lose keys, so an OBJECT value is non-extensible and
@@ -147,7 +208,7 @@ export default (ctx) => {
147
208
  const arrayValType = (obj) => (typeof obj === 'string' ? lookupValType(obj) : valTypeOf(obj)) === VAL.ARRAY
148
209
  // Index-string key array for an array-like receiver. `lenCall` is the
149
210
  // length builtin: __len for jz arrays, __str_len for strings.
150
- const emitIndexKeys = (obj, lenCall) => {
211
+ const idxKeys = (obj, lenCall) => {
151
212
  inc(lenCall, '__to_str')
152
213
  const v = temp('ik'), i = tempI32('iki'), len = tempI32('ikl')
153
214
  const vPtr = () => ['i64.reinterpret_f64', ['local.get', `$${v}`]]
@@ -171,12 +232,16 @@ export default (ctx) => {
171
232
  const nullish = requireCoercible(obj)
172
233
  if (nullish) return nullish
173
234
  if (isHashTyped(obj)) return emitHashKeys(obj)
174
- if (arrayValType(obj)) return emitIndexKeys(obj, '__len')
175
- if (stringValType(obj)) return emitIndexKeys(obj, '__str_len')
235
+ if (arrayValType(obj)) return idxKeys(obj, '__len')
236
+ if (stringValType(obj)) return idxKeys(obj, '__str_len')
176
237
  const schema = resolveSchema(obj)
177
- if (schema) return emitStringArray(schema)
178
- // Receiver type unknown at compile time. Dispatch on ptr-type at
179
- // runtime: HASH walks the probe table, anything else returns [].
238
+ // A static schema lists only the literal-known keys; a var that takes
239
+ // computed writes (`o[k]=v`) also carries props in its per-object dyn HASH
240
+ // that the schema omits. Enumerate those live (schema dyn props) — the gap
241
+ // that blocked metacircularity (the compiler grows dicts then enumerates).
242
+ if (schema && !mayHaveDynProps(obj)) return emitStringArray(schema)
243
+ // Unknown receiver, or schema with possible dyn props: dispatch on ptr-type
244
+ // at runtime (HASH probe table / OBJECT schema+dyn merge / else []).
180
245
  return emitRuntimeKeys(obj)
181
246
  }
182
247
  ctx.core.emit['Object.getOwnPropertyNames'] = ctx.core.emit['Object.keys']
@@ -194,7 +259,7 @@ export default (ctx) => {
194
259
  ['drop', asF64(emit(obj))],
195
260
  ['f64.const', has ? 1 : 0]], 'f64')
196
261
  }
197
- if (typeof obj === 'string' && ctx.schema.find?.(obj, litKey) >= 0)
262
+ if (typeof obj === 'string' && ctx.schema.slotOf?.(obj, litKey) >= 0)
198
263
  return typed(['f64.const', 1], 'f64')
199
264
  }
200
265
  return typed(['f64.convert_i32_s', emit(['in', key, obj])], 'f64')
@@ -208,6 +273,41 @@ export default (ctx) => {
208
273
  // Reuses the same own-property emitter; receiver-type variants above apply.
209
274
  ctx.core.emit['Object.hasOwn'] = (obj, key) => ctx.core.emit['.hasOwnProperty'](obj, key)
210
275
 
276
+ // __object_toString(value) — canonicalized from `Object.prototype.toString.call(value)`
277
+ // by jzify. Returns the spec-defined "[object Tag]" string. When the value's category
278
+ // is known at compile time the tag folds to a static string load; otherwise the
279
+ // runtime path dispatches on NaN-box bits (NaN→Number, NULL/UNDEF, then PTR type).
280
+ ctx.core.emit['__object_toString'] = (obj) => {
281
+ const emitTag = value => asF64(emit(['str', value]))
282
+ const tag = objectToStringTagForVal(obj)
283
+ if (tag) return block64(['drop', asF64(emit(obj))], emitTag(tag))
284
+
285
+ const value = temp('otag'), type = tempI32('otagt')
286
+ const bits = ['i64.reinterpret_f64', ['local.get', `$${value}`]]
287
+ const byType = dispatchByPtrType(type, [
288
+ [PTR.STRING, emitTag('[object String]')],
289
+ [PTR.ARRAY, emitTag('[object Array]')],
290
+ [PTR.BUFFER, emitTag('[object ArrayBuffer]')],
291
+ [PTR.CLOSURE, emitTag('[object Function]')],
292
+ [PTR.SET, emitTag('[object Set]')],
293
+ [PTR.MAP, emitTag('[object Map]')],
294
+ ], emitTag('[object Object]'))
295
+ const pointerTag = block64(['local.set', `$${type}`, ['call', '$__ptr_type', bits]], byType)
296
+ const nonNumericTag = ['if', ['result', 'f64'],
297
+ ['i64.eq', bits, ['i64.const', NULL_NAN]],
298
+ ['then', emitTag('[object Null]')],
299
+ ['else', ['if', ['result', 'f64'],
300
+ ['i64.eq', bits, ['i64.const', UNDEF_NAN]],
301
+ ['then', emitTag('[object Undefined]')],
302
+ ['else', pointerTag]]]]
303
+ return block64(
304
+ ['local.set', `$${value}`, asF64(emit(obj))],
305
+ ['if', ['result', 'f64'],
306
+ ['f64.eq', ['local.get', `$${value}`], ['local.get', `$${value}`]],
307
+ ['then', emitTag('[object Number]')],
308
+ ['else', nonNumericTag]])
309
+ }
310
+
211
311
  // String primitives are coerced to exotic String objects whose own enumerable
212
312
  // properties are the indexed characters. Object.values/entries iterate them.
213
313
  const stringValType = (obj) => (typeof obj === 'string' ? lookupValType(obj) : valTypeOf(obj)) === VAL.STRING
@@ -236,7 +336,7 @@ export default (ctx) => {
236
336
  if (arrayValType(obj)) { inc('__arr_from'); return typed(['call', '$__arr_from', asI64(emit(obj))], 'f64') }
237
337
  if (isHashTyped(obj)) return emitHashValues(obj)
238
338
  const schema = resolveSchema(obj)
239
- if (!schema) return emitRuntimeValues(obj)
339
+ if (!schema || mayHaveDynProps(obj)) return emitRuntimeValues(obj)
240
340
  const va = asF64(emit(obj))
241
341
  const n = schema.length
242
342
  const t = temp('ov'), base = tempI32('vb')
@@ -299,7 +399,7 @@ export default (ctx) => {
299
399
  }
300
400
  if (isHashTyped(obj)) return emitHashEntries(obj)
301
401
  const schema = resolveSchema(obj)
302
- if (!schema) return emitRuntimeEntries(obj)
402
+ if (!schema || mayHaveDynProps(obj)) return emitRuntimeEntries(obj)
303
403
  const va = asF64(emit(obj))
304
404
  const n = schema.length
305
405
  const t = temp('oe'), pair = tempI32('op'), base = tempI32('eb')
@@ -361,10 +461,18 @@ export default (ctx) => {
361
461
  }
362
462
  const tSchema = resolveSchema(target)
363
463
  const sourceSchemas = sources.map(resolveSchema)
364
- if (!tSchema) err('Object.assign: target needs known schema')
464
+ if (!tSchema) return emitObjectAssignDynamic(target, sources)
365
465
  if (sourceSchemas.some(s => !s)) return emitDynamicAssign(target, sources, sourceSchemas)
366
466
  const t = temp('at'), s = temp('as')
367
467
  const tBase = tempI32('tb'), sBase2 = tempI32('sb')
468
+ // When the target carries a dynamic-props shadow (needsDynShadow), reads of an
469
+ // unknown-schema alias (`let r = Object.assign(t, …); r.a`) dispatch through
470
+ // __dyn_get_any → the hash, not the schema slot. A slot-only write would leave
471
+ // the hash stale, so mirror each store into __dyn_set, exactly as the object
472
+ // literal emit does (above). False unless a collection/dyn-key module is live,
473
+ // so the common fixed-schema assign keeps its slot-only fast path.
474
+ const shadow = needsDynShadow(target)
475
+ if (shadow) inc('__dyn_set')
368
476
  const body = [['local.set', `$${t}`, asF64(emit(target))],
369
477
  ['local.set', `$${tBase}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]]]
370
478
  for (let i = 0; i < sources.length; i++) {
@@ -376,6 +484,9 @@ export default (ctx) => {
376
484
  const ti = tSchema.indexOf(sSchema[si])
377
485
  if (ti < 0) continue
378
486
  body.push(ctx.abi.object.ops.store(['local.get', `$${tBase}`], ti, ctx.abi.object.ops.load(['local.get', `$${sBase2}`], si)))
487
+ if (shadow)
488
+ body.push(['drop', ['call', '$__dyn_set', ['i64.reinterpret_f64', ['local.get', `$${t}`]],
489
+ asI64(emit(['str', String(tSchema[ti])])), ctx.abi.object.ops.loadBits(['local.get', `$${tBase}`], ti)]])
379
490
  }
380
491
  }
381
492
  body.push(['local.get', `$${t}`])
@@ -419,6 +530,14 @@ export default (ctx) => {
419
530
 
420
531
  // Object.create(proto) → shallow copy of object (same schema, copied properties)
421
532
  ctx.core.emit['Object.create'] = (proto) => {
533
+ // Object.create(null) → a fresh, empty, extensible object (no prototype). Without
534
+ // this it falls to the `protoType == null` runtime path below, which returns the
535
+ // proto value (null) itself; property writes on null then land in the GLOBAL
536
+ // __dyn_props table keyed by name, so two such objects collide on same-named keys
537
+ // (`a=Object.create(null); b=Object.create(null); a.x=1; b.x=2` left a.x===2). Reuse
538
+ // the empty-`{}` path, whose per-object hash keeps dynamic keys independent. (Native
539
+ // never hit this — its compiler runs Object.create on the host JS engine.)
540
+ if (isNullishLiteral(proto)) return ctx.core.emit['{}']()
422
541
  const protoType = typeof proto === 'string' ? lookupValType(proto) : valTypeOf(proto)
423
542
  if (protoType === VAL.ARRAY) {
424
543
  // Clone array data + link named-prop sidecar so for-in/bracket-name lookups
@@ -426,7 +545,7 @@ export default (ctx) => {
426
545
  // Header propsPtr lives at $off-16 (current ARRAY layout). We alias src's hash
427
546
  // by copying the slot; __dyn_move covers the shifted-array case where props
428
547
  // were migrated to the global __dyn_props.
429
- includeModule('array')
548
+ ctx.module.include('array')
430
549
  inc('__arr_from', '__dyn_move', '__ptr_offset')
431
550
  const src = temp('ocs')
432
551
  const dst = temp('ocd')
@@ -449,7 +568,7 @@ export default (ctx) => {
449
568
  if (!schema) {
450
569
  if (protoType == null) {
451
570
  const value = temp('ocr')
452
- includeModule('array')
571
+ ctx.module.include('array')
453
572
  inc('__arr_from', '__dyn_move', '__ptr_offset')
454
573
  const dst2 = temp('ocd')
455
574
  const srcOff2 = tempI32('ocso')
@@ -457,7 +576,7 @@ export default (ctx) => {
457
576
  return typed(['block', ['result', 'f64'],
458
577
  ['local.set', `$${value}`, asF64(emit(proto))],
459
578
  ['if', ['result', 'f64'],
460
- ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${value}`]]], ['i32.const', PTR.ARRAY]],
579
+ ptrTypeEq(['local.get', `$${value}`], PTR.ARRAY),
461
580
  ['then', ['block', ['result', 'f64'],
462
581
  ['local.set', `$${dst2}`, ['call', '$__arr_from', ['i64.reinterpret_f64', ['local.get', `$${value}`]]]],
463
582
  ['local.set', `$${srcOff2}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${value}`]]]],
@@ -495,7 +614,7 @@ export default (ctx) => {
495
614
  // Used only after the target schema is known. Unknown HASH targets can grow by
496
615
  // returning a new pointer, which would not preserve aliases to the old value.
497
616
  function emitDynamicAssign(target, sources, sourceSchemas = sources.map(resolveSchema)) {
498
- includeModule('collection')
617
+ ctx.module.include('collection')
499
618
  inc('__hash_set', '__dyn_get_any', '__ptr_offset', '__len')
500
619
  const t = temp('adt'), s = temp('ads'), sBase = tempI32('adsb')
501
620
  const keys = temp('adk'), keysBase = tempI32('adkb'), len = tempI32('adn')
@@ -543,6 +662,56 @@ function emitDynamicAssign(target, sources, sourceSchemas = sources.map(resolveS
543
662
  return typed(['block', ['result', 'f64'], ...body], 'f64')
544
663
  }
545
664
 
665
+ // Object.assign into a target whose schema is unknown at compile time (e.g.
666
+ // `ctx.core.stdlibDeps` — an empty `{}` grown dynamically). Copy every source
667
+ // key into the target's dynamic props via __dyn_set, which updates the
668
+ // per-object hash in place: the target pointer stays stable, so no write-back
669
+ // to the (possibly member-access) lvalue is needed. Returns the target.
670
+ function emitObjectAssignDynamic(target, sources) {
671
+ ctx.module.include('collection')
672
+ inc('__dyn_set', '__dyn_get_any', '__ptr_offset', '__len')
673
+ const t = temp('oat'), s = temp('oas'), sBase = tempI32('oasb')
674
+ const keys = temp('oak'), keysBase = tempI32('oakb'), len = tempI32('oan')
675
+ const i = tempI32('oai'), key = temp('oakey')
676
+ const id = ctx.func.uniq++
677
+ const setKey = (keyBits, valBits) =>
678
+ ['drop', ['call', '$__dyn_set', ['i64.reinterpret_f64', ['local.get', `$${t}`]], keyBits, valBits]]
679
+ const body = [['local.set', `$${t}`, asF64(emit(target))]]
680
+
681
+ for (let si = 0; si < sources.length; si++) {
682
+ const source = sources[si]
683
+ const sSchema = resolveSchema(source)
684
+ body.push(['local.set', `$${s}`, asF64(emit(source))])
685
+ if (sSchema) {
686
+ body.push(['local.set', `$${sBase}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${s}`]]]])
687
+ for (let pi = 0; pi < sSchema.length; pi++)
688
+ body.push(setKey(asI64(emit(['str', String(sSchema[pi])])), ctx.abi.object.ops.loadBits(['local.get', `$${sBase}`], pi)))
689
+ continue
690
+ }
691
+ body.push(
692
+ ['local.set', `$${keys}`, runtimeKeysFromTemp(s, 'oak')],
693
+ ['local.set', `$${keysBase}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${keys}`]]]],
694
+ ['local.set', `$${len}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${keys}`]]]],
695
+ ['local.set', `$${i}`, ['i32.const', 0]],
696
+ ['block', `$oabrk${id}_${si}`, ['loop', `$oaloop${id}_${si}`,
697
+ ['br_if', `$oabrk${id}_${si}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${len}`]]],
698
+ ['local.set', `$${key}`, ['f64.load',
699
+ ['i32.add', ['local.get', `$${keysBase}`], ['i32.shl', ['local.get', `$${i}`], ['i32.const', 3]]]]],
700
+ setKey(['i64.reinterpret_f64', ['local.get', `$${key}`]],
701
+ ['call', '$__dyn_get_any', ['i64.reinterpret_f64', ['local.get', `$${s}`]], ['i64.reinterpret_f64', ['local.get', `$${key}`]]]),
702
+ ['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
703
+ ['br', `$oaloop${id}_${si}`]]])
704
+ }
705
+ body.push(['local.get', `$${t}`])
706
+ return typed(['block', ['result', 'f64'], ...body], 'f64')
707
+ }
708
+
709
+ // A bound var is dynamically keyed when some `obj[k]=v` (non-literal key) wrote
710
+ // to it — program-facts records these in `ctx.types.dynKeyVars`. Such an object
711
+ // can hold props beyond its static schema, so schema-only enumeration would drop
712
+ // them; callers route it through the runtime schema∪dyn-props merge instead.
713
+ const mayHaveDynProps = (obj) => typeof obj === 'string' && !!ctx.types.dynKeyVars?.has(obj)
714
+
546
715
  function resolveSchema(obj) {
547
716
  if (typeof obj === 'string') return ctx.schema.resolve(obj)
548
717
  if (Array.isArray(obj) && obj[0] === '{}')
@@ -554,6 +723,18 @@ function resolveSchema(obj) {
554
723
  return null
555
724
  }
556
725
 
726
+ // Schema of a spread SOURCE, for the OBJECT-vs-HASH decision. A function
727
+ // parameter's runtime shape is caller-determined — its `resolveSchema` is an
728
+ // inferred/union guess bound only by emit (analysis sees it as unknown). Trusting
729
+ // it would (a) slot-index-copy from a layout the actual argument need not have and
730
+ // (b) make emit build an OBJECT while analysis HASH-typed the binding, so reads
731
+ // misdispatch. Treat params as unknown → dynamic runtime-key spread (always sound),
732
+ // mirroring spreadSchema in src/kind.js so both phases agree.
733
+ function spreadSourceSchema(obj) {
734
+ if (typeof obj === 'string' && ctx.func.current?.params?.some(p => p.name === obj)) return null
735
+ return resolveSchema(obj)
736
+ }
737
+
557
738
  /**
558
739
  * Emit object literal with spread: {...a, x: 1, ...b, y: 2}
559
740
  * Merges schemas from all sources, allocates result, copies in order.
@@ -568,22 +749,27 @@ function takeLiteralTarget() {
568
749
  }
569
750
 
570
751
  function emitObjectSpread(props, spreadTarget = takeLiteralTarget()) {
571
- // Collect merged schema: union of all spread source schemas + explicit props
752
+ // Resolve every spread source's schema. A source with no static schema means
753
+ // its full key set is unknown at compile time, so the merge result must be a
754
+ // HASH (dynamic dict) — a fixed schema would silently drop the source's keys
755
+ // it doesn't list. Only when EVERY source is known do we build the fixed-shape
756
+ // OBJECT below.
572
757
  const allNames = []
573
758
  const addName = n => { if (!allNames.includes(n)) allNames.push(n) }
759
+ let allKnown = true
574
760
  for (const p of props) {
575
761
  if (Array.isArray(p) && p[0] === '...') {
576
- const s = resolveSchema(p[1])
762
+ const s = spreadSourceSchema(p[1])
577
763
  if (s) for (const n of s) addName(n)
764
+ else allKnown = false
578
765
  } else if (Array.isArray(p) && p[0] === ':') addName(p[1])
579
766
  }
580
- // Pragmatic fallback: single spread source with no resolvable schema
581
- // (e.g. `{ ...opts }` where opts is a parameter). Emit source directly.
582
- // Alias rather than clone — safe for read-only use; mutation would affect source.
583
- if (!allNames.length && props.length === 1 && Array.isArray(props[0]) && props[0][0] === '...') {
767
+ // Single unknown spread `{ ...opts }` → alias the source directly. Safe for
768
+ // read-only use; mutation would affect the source (a true clone takes the
769
+ // dynamic-merge path below).
770
+ if (!allKnown && props.length === 1 && Array.isArray(props[0]) && props[0][0] === '...')
584
771
  return typed(asF64(emit(props[0][1])), 'f64')
585
- }
586
- if (!allNames.length) err('Object spread: cannot resolve source schema')
772
+ if (!allKnown) return emitDynamicSpread(props)
587
773
 
588
774
  const schemaId = ctx.schema.register(allNames)
589
775
  const schema = ctx.schema.list[schemaId]
@@ -594,26 +780,9 @@ function emitObjectSpread(props, spreadTarget = takeLiteralTarget()) {
594
780
  const body = [['local.set', `$${t}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', ctx.abi.object.ops.allocSlots(schema.length)]]]]
595
781
 
596
782
  // Process props in order — later props override earlier (JS semantics)
597
- let srcF
598
783
  for (const p of props) {
599
784
  if (Array.isArray(p) && p[0] === '...') {
600
785
  const sSchema = resolveSchema(p[1])
601
- if (!sSchema) {
602
- // Unknown-schema source (e.g. parameter). Override each slot via runtime
603
- // __dyn_get_or using existing value as fallback.
604
- includeModule('collection')
605
- inc('__dyn_get_or')
606
- srcF ??= temp('ospf')
607
- body.push(['local.set', `$${srcF}`, asF64(emit(p[1]))])
608
- for (let i = 0; i < schema.length; i++) {
609
- const base = ['local.get', `$${t}`]
610
- body.push(ctx.abi.object.ops.store(base, i,
611
- ['f64.reinterpret_i64', ['call', '$__dyn_get_or', ['i64.reinterpret_f64', ['local.get', `$${srcF}`]],
612
- asI64(emit(['str', String(schema[i])])),
613
- ctx.abi.object.ops.loadBits(base, i)]]))
614
- }
615
- continue
616
- }
617
786
  body.push(['local.set', `$${src}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', asF64(emit(p[1]))]]])
618
787
  for (let si = 0; si < sSchema.length; si++) {
619
788
  const ti = schema.indexOf(sSchema[si])
@@ -622,7 +791,7 @@ function emitObjectSpread(props, spreadTarget = takeLiteralTarget()) {
622
791
  }
623
792
  } else if (Array.isArray(p) && p[0] === ':') {
624
793
  const ti = schema.indexOf(p[1])
625
- if (ti >= 0) body.push(ctx.abi.object.ops.store(['local.get', `$${t}`], ti, asF64(emit(p[2]))))
794
+ if (ti >= 0) body.push(ctx.abi.object.ops.store(['local.get', `$${t}`], ti, storedValue(p[2])))
626
795
  }
627
796
  }
628
797
 
@@ -637,6 +806,55 @@ function emitObjectSpread(props, spreadTarget = takeLiteralTarget()) {
637
806
  return typed(['block', ['result', 'f64'], ...body], 'f64')
638
807
  }
639
808
 
809
+ // Spread merge when any source schema is unknown: build a fresh HASH and copy
810
+ // every key of each source in order (later overrides earlier — JS semantics),
811
+ // threading explicit `k: v` props at their source position. Mirrors
812
+ // emitDynamicAssign but seeds an empty HASH instead of an existing target.
813
+ function emitDynamicSpread(props) {
814
+ ctx.module.include('collection')
815
+ inc('__hash_new', '__hash_set', '__dyn_get_any', '__ptr_offset', '__len')
816
+ const t = temp('dst'), s = temp('dss'), sBase = tempI32('dssb')
817
+ const keys = temp('dsk'), keysBase = tempI32('dskb'), len = tempI32('dsn')
818
+ const i = tempI32('dsi'), key = temp('dskey')
819
+ const id = ctx.func.uniq++
820
+ // `__hash_set` may rehash and return a new pointer, so thread it back into $t.
821
+ const setKey = (keyBits, valBits) =>
822
+ ['local.set', `$${t}`, ['f64.reinterpret_i64',
823
+ ['call', '$__hash_set', ['i64.reinterpret_f64', ['local.get', `$${t}`]], keyBits, valBits]]]
824
+ const body = [['local.set', `$${t}`, ['call', '$__hash_new']]]
825
+
826
+ for (let pi = 0; pi < props.length; pi++) {
827
+ const p = props[pi]
828
+ if (Array.isArray(p) && p[0] === ':') {
829
+ body.push(setKey(asI64(emit(['str', String(p[1])])), asI64(emit(p[2]))))
830
+ continue
831
+ }
832
+ const sSchema = spreadSourceSchema(p[1])
833
+ body.push(['local.set', `$${s}`, asF64(emit(p[1]))])
834
+ if (sSchema) {
835
+ body.push(['local.set', `$${sBase}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${s}`]]]])
836
+ for (let si = 0; si < sSchema.length; si++)
837
+ body.push(setKey(asI64(emit(['str', String(sSchema[si])])), ctx.abi.object.ops.loadBits(['local.get', `$${sBase}`], si)))
838
+ continue
839
+ }
840
+ body.push(
841
+ ['local.set', `$${keys}`, runtimeKeysFromTemp(s, 'dsk')],
842
+ ['local.set', `$${keysBase}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${keys}`]]]],
843
+ ['local.set', `$${len}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${keys}`]]]],
844
+ ['local.set', `$${i}`, ['i32.const', 0]],
845
+ ['block', `$dsbrk${id}_${pi}`, ['loop', `$dsloop${id}_${pi}`,
846
+ ['br_if', `$dsbrk${id}_${pi}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${len}`]]],
847
+ ['local.set', `$${key}`, ['f64.load',
848
+ ['i32.add', ['local.get', `$${keysBase}`], ['i32.shl', ['local.get', `$${i}`], ['i32.const', 3]]]]],
849
+ setKey(['i64.reinterpret_f64', ['local.get', `$${key}`]],
850
+ ['call', '$__dyn_get_any', ['i64.reinterpret_f64', ['local.get', `$${s}`]], ['i64.reinterpret_f64', ['local.get', `$${key}`]]]),
851
+ ['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
852
+ ['br', `$dsloop${id}_${pi}`]]])
853
+ }
854
+ body.push(['local.get', `$${t}`])
855
+ return typed(['block', ['result', 'f64'], ...body], 'f64')
856
+ }
857
+
640
858
  function emitStringArray(names) {
641
859
  const n = names.length
642
860
  const out = allocPtr({ type: PTR.ARRAY, len: n, tag: 'sa' })
@@ -687,9 +905,9 @@ function emitHashEntries(obj) {
687
905
  // IR shape from the same source — only difference is whether they enter from
688
906
  // a static type guard or a runtime ptr-type check.
689
907
  function hashKeysFromTemp(t) {
690
- inc('__ptr_offset', '__cap', '__len')
908
+ inc('__ptr_offset', '__cap', '__len', '__coll_order')
691
909
  const off = tempI32('hko'), cap = tempI32('hkc'), n = tempI32('hkn')
692
- const i = tempI32('hki'), o = tempI32('hkj'), slot = tempI32('hks')
910
+ const i = tempI32('hki'), ord = tempI32('hkr'), slot = tempI32('hks')
693
911
  const out = allocPtr({ type: PTR.ARRAY, len: ['local.get', `$${n}`], tag: 'hka' })
694
912
  const id = ctx.func.uniq++
695
913
  return ['block', ['result', 'f64'],
@@ -697,26 +915,23 @@ function hashKeysFromTemp(t) {
697
915
  out.init,
698
916
  ['local.set', `$${off}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
699
917
  ['local.set', `$${cap}`, ['call', '$__cap', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
918
+ ['local.set', `$${ord}`, ['call', '$__coll_order', ['local.get', `$${off}`], ['local.get', `$${cap}`], ['i32.const', 24]]],
700
919
  ['local.set', `$${i}`, ['i32.const', 0]],
701
- ['local.set', `$${o}`, ['i32.const', 0]],
702
920
  ['block', `$brk${id}`, ['loop', `$loop${id}`,
703
- ['br_if', `$brk${id}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${cap}`]]],
704
- ['local.set', `$${slot}`, ['i32.add', ['local.get', `$${off}`],
705
- ['i32.mul', ['local.get', `$${i}`], ['i32.const', 24]]]],
706
- ['if', ['f64.ne', ['f64.load', ['local.get', `$${slot}`]], ['f64.const', 0]],
707
- ['then',
708
- elemStore(out.local, o,
709
- ['f64.load', ['i32.add', ['local.get', `$${slot}`], ['i32.const', 8]]]),
710
- ['local.set', `$${o}`, ['i32.add', ['local.get', `$${o}`], ['i32.const', 1]]]]],
921
+ ['br_if', `$brk${id}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${n}`]]],
922
+ ['local.set', `$${slot}`, ['i32.load', ['i32.add', ['local.get', `$${ord}`],
923
+ ['i32.shl', ['local.get', `$${i}`], ['i32.const', 2]]]]],
924
+ elemStore(out.local, i,
925
+ ['f64.load', ['i32.add', ['local.get', `$${slot}`], ['i32.const', 8]]]),
711
926
  ['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
712
927
  ['br', `$loop${id}`]]],
713
928
  out.ptr]
714
929
  }
715
930
 
716
931
  function hashValuesFromTemp(t) {
717
- inc('__ptr_offset', '__cap', '__len')
932
+ inc('__ptr_offset', '__cap', '__len', '__coll_order')
718
933
  const off = tempI32('hvo'), cap = tempI32('hvc'), n = tempI32('hvn')
719
- const i = tempI32('hvi'), o = tempI32('hvj'), slot = tempI32('hvs')
934
+ const i = tempI32('hvi'), ord = tempI32('hvr'), slot = tempI32('hvs')
720
935
  const out = allocPtr({ type: PTR.ARRAY, len: ['local.get', `$${n}`], tag: 'hva' })
721
936
  const id = ctx.func.uniq++
722
937
  return ['block', ['result', 'f64'],
@@ -724,26 +939,23 @@ function hashValuesFromTemp(t) {
724
939
  out.init,
725
940
  ['local.set', `$${off}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
726
941
  ['local.set', `$${cap}`, ['call', '$__cap', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
942
+ ['local.set', `$${ord}`, ['call', '$__coll_order', ['local.get', `$${off}`], ['local.get', `$${cap}`], ['i32.const', 24]]],
727
943
  ['local.set', `$${i}`, ['i32.const', 0]],
728
- ['local.set', `$${o}`, ['i32.const', 0]],
729
944
  ['block', `$vbrk${id}`, ['loop', `$vloop${id}`,
730
- ['br_if', `$vbrk${id}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${cap}`]]],
731
- ['local.set', `$${slot}`, ['i32.add', ['local.get', `$${off}`],
732
- ['i32.mul', ['local.get', `$${i}`], ['i32.const', 24]]]],
733
- ['if', ['f64.ne', ['f64.load', ['local.get', `$${slot}`]], ['f64.const', 0]],
734
- ['then',
735
- elemStore(out.local, o,
736
- ['f64.load', ['i32.add', ['local.get', `$${slot}`], ['i32.const', 16]]]),
737
- ['local.set', `$${o}`, ['i32.add', ['local.get', `$${o}`], ['i32.const', 1]]]]],
945
+ ['br_if', `$vbrk${id}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${n}`]]],
946
+ ['local.set', `$${slot}`, ['i32.load', ['i32.add', ['local.get', `$${ord}`],
947
+ ['i32.shl', ['local.get', `$${i}`], ['i32.const', 2]]]]],
948
+ elemStore(out.local, i,
949
+ ['f64.load', ['i32.add', ['local.get', `$${slot}`], ['i32.const', 16]]]),
738
950
  ['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
739
951
  ['br', `$vloop${id}`]]],
740
952
  out.ptr]
741
953
  }
742
954
 
743
955
  function hashEntriesFromTemp(t) {
744
- inc('__ptr_offset', '__cap', '__len', '__alloc_hdr')
956
+ inc('__ptr_offset', '__cap', '__len', '__alloc_hdr', '__coll_order')
745
957
  const off = tempI32('heo'), cap = tempI32('hec'), n = tempI32('hen')
746
- const i = tempI32('hei'), o = tempI32('hej'), slot = tempI32('hes'), pair = tempI32('hep')
958
+ const i = tempI32('hei'), ord = tempI32('her'), slot = tempI32('hes'), pair = tempI32('hep')
747
959
  const out = allocPtr({ type: PTR.ARRAY, len: ['local.get', `$${n}`], tag: 'hea' })
748
960
  const id = ctx.func.uniq++
749
961
  return ['block', ['result', 'f64'],
@@ -751,21 +963,18 @@ function hashEntriesFromTemp(t) {
751
963
  out.init,
752
964
  ['local.set', `$${off}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
753
965
  ['local.set', `$${cap}`, ['call', '$__cap', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
966
+ ['local.set', `$${ord}`, ['call', '$__coll_order', ['local.get', `$${off}`], ['local.get', `$${cap}`], ['i32.const', 24]]],
754
967
  ['local.set', `$${i}`, ['i32.const', 0]],
755
- ['local.set', `$${o}`, ['i32.const', 0]],
756
968
  ['block', `$ebrk${id}`, ['loop', `$eloop${id}`,
757
- ['br_if', `$ebrk${id}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${cap}`]]],
758
- ['local.set', `$${slot}`, ['i32.add', ['local.get', `$${off}`],
759
- ['i32.mul', ['local.get', `$${i}`], ['i32.const', 24]]]],
760
- ['if', ['f64.ne', ['f64.load', ['local.get', `$${slot}`]], ['f64.const', 0]],
761
- ['then',
762
- ['local.set', `$${pair}`, ['call', '$__alloc_hdr', ['i32.const', 2], ['i32.const', 2]]],
763
- ['f64.store', ['local.get', `$${pair}`],
764
- ['f64.load', ['i32.add', ['local.get', `$${slot}`], ['i32.const', 8]]]],
765
- ['f64.store', ['i32.add', ['local.get', `$${pair}`], ['i32.const', 8]],
766
- ['f64.load', ['i32.add', ['local.get', `$${slot}`], ['i32.const', 16]]]],
767
- elemStore(out.local, o, mkPtrIR(PTR.ARRAY, 0, ['local.get', `$${pair}`])),
768
- ['local.set', `$${o}`, ['i32.add', ['local.get', `$${o}`], ['i32.const', 1]]]]],
969
+ ['br_if', `$ebrk${id}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${n}`]]],
970
+ ['local.set', `$${slot}`, ['i32.load', ['i32.add', ['local.get', `$${ord}`],
971
+ ['i32.shl', ['local.get', `$${i}`], ['i32.const', 2]]]]],
972
+ ['local.set', `$${pair}`, ['call', '$__alloc_hdr', ['i32.const', 2], ['i32.const', 2]]],
973
+ ['f64.store', ['local.get', `$${pair}`],
974
+ ['f64.load', ['i32.add', ['local.get', `$${slot}`], ['i32.const', 8]]]],
975
+ ['f64.store', ['i32.add', ['local.get', `$${pair}`], ['i32.const', 8]],
976
+ ['f64.load', ['i32.add', ['local.get', `$${slot}`], ['i32.const', 16]]]],
977
+ elemStore(out.local, i, mkPtrIR(PTR.ARRAY, 0, ['local.get', `$${pair}`])),
769
978
  ['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
770
979
  ['br', `$eloop${id}`]]],
771
980
  out.ptr]
@@ -789,7 +998,7 @@ function runtimeKeysFromTemp(t, tag) {
789
998
  // JSON.parse or compile-time schemas — the OBJECT arm reads it at runtime
790
999
  // and the watr resolver requires the symbol to be declared.
791
1000
  if (!ctx.scope.globals.has('__schema_tbl'))
792
- ctx.scope.globals.set('__schema_tbl', '(global $__schema_tbl (mut i32) (i32.const 0))')
1001
+ declGlobal('__schema_tbl', 'i32')
793
1002
  const tt = tempI32(`${tag}t`)
794
1003
  const empty = allocPtr({ type: PTR.ARRAY, len: 0, tag: `${tag}e` })
795
1004
  return ['block', ['result', 'f64'],
@@ -798,9 +1007,7 @@ function runtimeKeysFromTemp(t, tag) {
798
1007
  ['i32.eq', ['local.get', `$${tt}`], ['i32.const', PTR.HASH]],
799
1008
  ['then', hashKeysFromTemp(t)],
800
1009
  ['else', ['if', ['result', 'f64'],
801
- ['i32.and',
802
- ['i32.eq', ['local.get', `$${tt}`], ['i32.const', PTR.OBJECT]],
803
- ['i32.ne', ['global.get', '$__schema_tbl'], ['i32.const', 0]]],
1010
+ ['i32.eq', ['local.get', `$${tt}`], ['i32.const', PTR.OBJECT]],
804
1011
  ['then', objectKeysFromTemp(t)],
805
1012
  ['else', ['block', ['result', 'f64'], empty.init, empty.ptr]]]]]]
806
1013
  }
@@ -808,7 +1015,7 @@ function runtimeKeysFromTemp(t, tag) {
808
1015
  function emitRuntimeValues(obj) {
809
1016
  inc('__ptr_type')
810
1017
  if (!ctx.scope.globals.has('__schema_tbl'))
811
- ctx.scope.globals.set('__schema_tbl', '(global $__schema_tbl (mut i32) (i32.const 0))')
1018
+ declGlobal('__schema_tbl', 'i32')
812
1019
  const t = temp('rv'), tt = tempI32('rvt')
813
1020
  const empty = allocPtr({ type: PTR.ARRAY, len: 0, tag: 'rve' })
814
1021
  return typed(['block', ['result', 'f64'],
@@ -818,9 +1025,7 @@ function emitRuntimeValues(obj) {
818
1025
  ['i32.eq', ['local.get', `$${tt}`], ['i32.const', PTR.HASH]],
819
1026
  ['then', hashValuesFromTemp(t)],
820
1027
  ['else', ['if', ['result', 'f64'],
821
- ['i32.and',
822
- ['i32.eq', ['local.get', `$${tt}`], ['i32.const', PTR.OBJECT]],
823
- ['i32.ne', ['global.get', '$__schema_tbl'], ['i32.const', 0]]],
1028
+ ['i32.eq', ['local.get', `$${tt}`], ['i32.const', PTR.OBJECT]],
824
1029
  ['then', objectValuesFromTemp(t)],
825
1030
  ['else', ['block', ['result', 'f64'], empty.init, empty.ptr]]]]]], 'f64')
826
1031
  }
@@ -828,7 +1033,7 @@ function emitRuntimeValues(obj) {
828
1033
  function emitRuntimeEntries(obj) {
829
1034
  inc('__ptr_type')
830
1035
  if (!ctx.scope.globals.has('__schema_tbl'))
831
- ctx.scope.globals.set('__schema_tbl', '(global $__schema_tbl (mut i32) (i32.const 0))')
1036
+ declGlobal('__schema_tbl', 'i32')
832
1037
  const t = temp('re'), tt = tempI32('ret')
833
1038
  const empty = allocPtr({ type: PTR.ARRAY, len: 0, tag: 'ree' })
834
1039
  return typed(['block', ['result', 'f64'],
@@ -838,102 +1043,161 @@ function emitRuntimeEntries(obj) {
838
1043
  ['i32.eq', ['local.get', `$${tt}`], ['i32.const', PTR.HASH]],
839
1044
  ['then', hashEntriesFromTemp(t)],
840
1045
  ['else', ['if', ['result', 'f64'],
841
- ['i32.and',
842
- ['i32.eq', ['local.get', `$${tt}`], ['i32.const', PTR.OBJECT]],
843
- ['i32.ne', ['global.get', '$__schema_tbl'], ['i32.const', 0]]],
1046
+ ['i32.eq', ['local.get', `$${tt}`], ['i32.const', PTR.OBJECT]],
844
1047
  ['then', objectEntriesFromTemp(t)],
845
1048
  ['else', ['block', ['result', 'f64'], empty.init, empty.ptr]]]]]], 'f64')
846
1049
  }
847
1050
 
848
- // Schema-keyed Object.keys: copy the schema's keys array (a jz Array of
849
- // STRINGs registered in __schema_tbl[sid]) into a fresh ARRAY so callers can
850
- // mutate without aliasing the schema substrate.
851
- function objectKeysFromTemp(t) {
852
- inc('__alloc_hdr')
853
- const sid = tempI32('oks'), src = tempI32('oksrc'), n = tempI32('okn'), out = tempI32('oko'), i = tempI32('oki')
1051
+ // Shared scaffold for Object.{keys,values,entries} on a runtime OBJECT.
1052
+ //
1053
+ // A plain JS object reports ALL its own keys at enumeration time. jz objects
1054
+ // split that surface in two: a static SCHEMA (jz Array of key STRINGs registered
1055
+ // in __schema_tbl[sid] + field values inline at base+i*8) and a per-instance
1056
+ // HASH of dyn props at base-16 added by computed writes `o[k]=v`. Enumerating
1057
+ // only the schema would silently drop dyn keys — the gap that blocked
1058
+ // metacircularity (kernel dicts grow via `o[k]=v` then enumerate via Object.keys).
1059
+ //
1060
+ // All three variants share the entire scaffold — schema lookup, dyn discovery,
1061
+ // over-alloc output, two iteration loops, shadow-mirror dedup, length patch,
1062
+ // ARRAY ptr boxing. They differ ONLY in per-slot stores:
1063
+ // - keys: write i64 key
1064
+ // - values: write f64 value
1065
+ // - entries: alloc 2-slot pair + write boxed ptr
1066
+ //
1067
+ // Callbacks receive the active locals as named fields so each variant can
1068
+ // reference what it needs without knowing the scaffold's layout.
1069
+ function emitEnumerateObject(t, emitStaticStore, emitDynStore) {
1070
+ inc('__alloc_hdr', '__ptr_offset', '__coll_order')
1071
+ const sid = tempI32('oes'), src = tempI32('oesrc'), sn = tempI32('oen')
1072
+ const base = tempI32('oebase'), props = tempI64('oepr'), poff = tempI32('oepo')
1073
+ const pcap = tempI32('oepc'), dn = tempI32('oedn'), total = tempI32('oetot')
1074
+ const out = tempI32('oeo'), i = tempI32('oei'), o = tempI32('oej')
1075
+ const slot = tempI32('oesl'), ord = tempI32('oeord')
1076
+ const j = tempI32('oej2'), skip = tempI32('oesk'), pair = tempI32('oep')
854
1077
  const id = ctx.func.uniq++
1078
+ const env = { out, o, src, base, i, slot, pair }
855
1079
  return ['block', ['result', 'f64'],
1080
+ // Static schema row: sid (AUX bits) → __schema_tbl[sid] → src offset; n@src-8.
1081
+ // __schema_tbl is omitted when every program schema is empty (dyn-only dicts);
1082
+ // guard the read so empty-table programs see sn=0 here and still enumerate
1083
+ // dyn-props below.
856
1084
  ['local.set', `$${sid}`, ['i32.wrap_i64', ['i64.and',
857
1085
  ['i64.shr_u', ['i64.reinterpret_f64', ['local.get', `$${t}`]], ['i64.const', LAYOUT.AUX_SHIFT]],
858
1086
  ['i64.const', LAYOUT.AUX_MASK]]]],
859
- ['local.set', `$${src}`, ['i32.wrap_i64', ['i64.and',
860
- ['i64.load', ['i32.add', ['global.get', '$__schema_tbl'], ['i32.shl', ['local.get', `$${sid}`], ['i32.const', 3]]]],
861
- ['i64.const', LAYOUT.OFFSET_MASK]]]],
862
- ['local.set', `$${n}`, ['i32.load', ['i32.sub', ['local.get', `$${src}`], ['i32.const', 8]]]],
863
- ['local.set', `$${out}`, ['call', '$__alloc_hdr',
864
- ['local.get', `$${n}`], ['local.get', `$${n}`]]],
865
- ['local.set', `$${i}`, ['i32.const', 0]],
866
- ['block', `$kbrk${id}`, ['loop', `$kloop${id}`,
867
- ['br_if', `$kbrk${id}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${n}`]]],
868
- ['i64.store',
869
- ['i32.add', ['local.get', `$${out}`], ['i32.shl', ['local.get', `$${i}`], ['i32.const', 3]]],
870
- ['i64.load',
871
- ['i32.add', ['local.get', `$${src}`], ['i32.shl', ['local.get', `$${i}`], ['i32.const', 3]]]]],
872
- ['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
873
- ['br', `$kloop${id}`]]],
874
- mkPtrIR(PTR.ARRAY, 0, ['local.get', `$${out}`])]
875
- }
876
-
877
- function objectValuesFromTemp(t) {
878
- inc('__alloc_hdr', '__ptr_offset')
879
- const sid = tempI32('ovs'), src = tempI32('ovsrc'), n = tempI32('ovn')
880
- const base = tempI32('ovbase'), out = tempI32('ovo'), i = tempI32('ovi')
881
- const id = ctx.func.uniq++
882
- return ['block', ['result', 'f64'],
883
- ['local.set', `$${sid}`, ['i32.wrap_i64', ['i64.and',
884
- ['i64.shr_u', ['i64.reinterpret_f64', ['local.get', `$${t}`]], ['i64.const', LAYOUT.AUX_SHIFT]],
885
- ['i64.const', LAYOUT.AUX_MASK]]]],
886
- ['local.set', `$${src}`, ['i32.wrap_i64', ['i64.and',
887
- ['i64.load', ['i32.add', ['global.get', '$__schema_tbl'], ['i32.shl', ['local.get', `$${sid}`], ['i32.const', 3]]]],
888
- ['i64.const', LAYOUT.OFFSET_MASK]]]],
889
- ['local.set', `$${n}`, ['i32.load', ['i32.sub', ['local.get', `$${src}`], ['i32.const', 8]]]],
1087
+ ['local.set', `$${sn}`, ['i32.const', 0]],
1088
+ ['local.set', `$${src}`, ['i32.const', 0]],
1089
+ ['if', ['i32.ne', ['global.get', '$__schema_tbl'], ['i32.const', 0]],
1090
+ ['then',
1091
+ ['local.set', `$${src}`, ['i32.wrap_i64', ['i64.and',
1092
+ ['i64.load', ['i32.add', ['global.get', '$__schema_tbl'], ['i32.shl', ['local.get', `$${sid}`], ['i32.const', 3]]]],
1093
+ ['i64.const', LAYOUT.OFFSET_MASK]]]],
1094
+ ['local.set', `$${sn}`, ['i32.load', ['i32.sub', ['local.get', `$${src}`], ['i32.const', 8]]]]]],
1095
+ // Dyn-props: heap OBJECTs (base >= __heap_start) carry a HASH propsPtr at
1096
+ // base-16 (0 when none). Static-segment objects have no header, so they
1097
+ // contribute no dyn keys (poff stays 0).
890
1098
  ['local.set', `$${base}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
891
- ['local.set', `$${out}`, ['call', '$__alloc_hdr',
892
- ['local.get', `$${n}`], ['local.get', `$${n}`]]],
1099
+ ['local.set', `$${dn}`, ['i32.const', 0]],
1100
+ ['local.set', `$${poff}`, ['i32.const', 0]],
1101
+ ['if', ['i32.ge_u', ['local.get', `$${base}`], ['global.get', '$__heap_start']],
1102
+ ['then',
1103
+ ['local.set', `$${props}`, ['i64.load', ['i32.sub', ['local.get', `$${base}`], ['i32.const', 16]]]],
1104
+ ['if', ['i32.eq',
1105
+ ['i32.wrap_i64', ['i64.and', ['i64.shr_u', ['local.get', `$${props}`], ['i64.const', LAYOUT.TAG_SHIFT]], ['i64.const', LAYOUT.TAG_MASK]]],
1106
+ ['i32.const', PTR.HASH]],
1107
+ ['then',
1108
+ // Resolve forward chain — HASH may have forwarded on grow; the raw
1109
+ // propsPtr offset would point at the forward record, not live slots.
1110
+ ['local.set', `$${poff}`, ['call', '$__ptr_offset', ['local.get', `$${props}`]]],
1111
+ ['local.set', `$${pcap}`, ['i32.load', ['i32.sub', ['local.get', `$${poff}`], ['i32.const', 4]]]],
1112
+ ['local.set', `$${dn}`, ['i32.load', ['i32.sub', ['local.get', `$${poff}`], ['i32.const', 8]]]]]]]],
1113
+ // Over-allocate sn+dn; patch length to actual `o` post-dedup so removed
1114
+ // shadow-mirror slots never expose garbage tails.
1115
+ ['local.set', `$${total}`, ['i32.add', ['local.get', `$${sn}`], ['local.get', `$${dn}`]]],
1116
+ ['local.set', `$${out}`, ['call', '$__alloc_hdr', ['local.get', `$${total}`], ['local.get', `$${total}`]]],
1117
+ ['local.set', `$${o}`, ['i32.const', 0]],
1118
+ // Static schema slots — no skip, schema keys are unique by construction.
893
1119
  ['local.set', `$${i}`, ['i32.const', 0]],
894
- ['block', `$ovbrk${id}`, ['loop', `$ovloop${id}`,
895
- ['br_if', `$ovbrk${id}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${n}`]]],
896
- ['f64.store',
897
- ['i32.add', ['local.get', `$${out}`], ['i32.shl', ['local.get', `$${i}`], ['i32.const', 3]]],
898
- ['f64.load',
899
- ['i32.add', ['local.get', `$${base}`], ['i32.shl', ['local.get', `$${i}`], ['i32.const', 3]]]]],
1120
+ ['block', `$sbrk${id}`, ['loop', `$sloop${id}`,
1121
+ ['br_if', `$sbrk${id}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${sn}`]]],
1122
+ ...emitStaticStore(env),
1123
+ ['local.set', `$${o}`, ['i32.add', ['local.get', `$${o}`], ['i32.const', 1]]],
900
1124
  ['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
901
- ['br', `$ovloop${id}`]]],
1125
+ ['br', `$sloop${id}`]]],
1126
+ // Dyn-prop slots in insertion order (__coll_order sorts the dn live 24-byte
1127
+ // slots by packed seq; hash@+0, key@+8, value@+16). Skip entries whose key
1128
+ // is already in the schema — when an object literal has shadow=true (per
1129
+ // needsDynShadow), each schema key is mirrored into propsPtr at construction
1130
+ // so dyn-key reads hit the hash fast path; the mirror is not an enumeration
1131
+ // entity, so we must not emit it twice.
1132
+ ['if', ['i32.ne', ['local.get', `$${poff}`], ['i32.const', 0]],
1133
+ ['then',
1134
+ ['local.set', `$${ord}`, ['call', '$__coll_order', ['local.get', `$${poff}`], ['local.get', `$${pcap}`], ['i32.const', 24]]],
1135
+ ['local.set', `$${i}`, ['i32.const', 0]],
1136
+ ['block', `$dbrk${id}`, ['loop', `$dloop${id}`,
1137
+ ['br_if', `$dbrk${id}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${dn}`]]],
1138
+ ['local.set', `$${slot}`, ['i32.load', ['i32.add', ['local.get', `$${ord}`],
1139
+ ['i32.shl', ['local.get', `$${i}`], ['i32.const', 2]]]]],
1140
+ ['local.set', `$${skip}`, ['i32.const', 0]],
1141
+ ['local.set', `$${j}`, ['i32.const', 0]],
1142
+ ['block', `$skbrk${id}`, ['loop', `$skloop${id}`,
1143
+ ['br_if', `$skbrk${id}`, ['i32.ge_s', ['local.get', `$${j}`], ['local.get', `$${sn}`]]],
1144
+ ['if', ['i64.eq',
1145
+ ['i64.load', ['i32.add', ['local.get', `$${slot}`], ['i32.const', 8]]],
1146
+ ['i64.load', ['i32.add', ['local.get', `$${src}`], ['i32.shl', ['local.get', `$${j}`], ['i32.const', 3]]]]],
1147
+ ['then', ['local.set', `$${skip}`, ['i32.const', 1]], ['br', `$skbrk${id}`]]],
1148
+ ['local.set', `$${j}`, ['i32.add', ['local.get', `$${j}`], ['i32.const', 1]]],
1149
+ ['br', `$skloop${id}`]]],
1150
+ ['if', ['i32.eqz', ['local.get', `$${skip}`]],
1151
+ ['then',
1152
+ ...emitDynStore(env),
1153
+ ['local.set', `$${o}`, ['i32.add', ['local.get', `$${o}`], ['i32.const', 1]]]]],
1154
+ ['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
1155
+ ['br', `$dloop${id}`]]]]],
1156
+ ['i32.store', ['i32.sub', ['local.get', `$${out}`], ['i32.const', 8]], ['local.get', `$${o}`]],
902
1157
  mkPtrIR(PTR.ARRAY, 0, ['local.get', `$${out}`])]
903
1158
  }
904
1159
 
905
- function objectEntriesFromTemp(t) {
906
- inc('__alloc_hdr', '__ptr_offset')
907
- const sid = tempI32('oes'), src = tempI32('oesrc'), n = tempI32('oen')
908
- const base = tempI32('oebase'), out = tempI32('oeo'), i = tempI32('oei'), pair = tempI32('oep')
909
- const id = ctx.func.uniq++
910
- return ['block', ['result', 'f64'],
911
- ['local.set', `$${sid}`, ['i32.wrap_i64', ['i64.and',
912
- ['i64.shr_u', ['i64.reinterpret_f64', ['local.get', `$${t}`]], ['i64.const', LAYOUT.AUX_SHIFT]],
913
- ['i64.const', LAYOUT.AUX_MASK]]]],
914
- ['local.set', `$${src}`, ['i32.wrap_i64', ['i64.and',
915
- ['i64.load', ['i32.add', ['global.get', '$__schema_tbl'], ['i32.shl', ['local.get', `$${sid}`], ['i32.const', 3]]]],
916
- ['i64.const', LAYOUT.OFFSET_MASK]]]],
917
- ['local.set', `$${n}`, ['i32.load', ['i32.sub', ['local.get', `$${src}`], ['i32.const', 8]]]],
918
- ['local.set', `$${base}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
919
- ['local.set', `$${out}`, ['call', '$__alloc_hdr',
920
- ['local.get', `$${n}`], ['local.get', `$${n}`]]],
921
- ['local.set', `$${i}`, ['i32.const', 0]],
922
- ['block', `$oebrk${id}`, ['loop', `$oeloop${id}`,
923
- ['br_if', `$oebrk${id}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${n}`]]],
924
- ['local.set', `$${pair}`, ['call', '$__alloc_hdr', ['i32.const', 2], ['i32.const', 2]]],
925
- ['i64.store',
926
- ['local.get', `$${pair}`],
927
- ['i64.load',
928
- ['i32.add', ['local.get', `$${src}`], ['i32.shl', ['local.get', `$${i}`], ['i32.const', 3]]]]],
929
- ['f64.store',
930
- ['i32.add', ['local.get', `$${pair}`], ['i32.const', 8]],
931
- ['f64.load',
932
- ['i32.add', ['local.get', `$${base}`], ['i32.shl', ['local.get', `$${i}`], ['i32.const', 3]]]]],
933
- ['f64.store',
934
- ['i32.add', ['local.get', `$${out}`], ['i32.shl', ['local.get', `$${i}`], ['i32.const', 3]]],
935
- mkPtrIR(PTR.ARRAY, 0, ['local.get', `$${pair}`])],
936
- ['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
937
- ['br', `$oeloop${id}`]]],
938
- mkPtrIR(PTR.ARRAY, 0, ['local.get', `$${out}`])]
939
- }
1160
+ // Object.keys for an OBJECT — copy schema key (i64@src+i*8) then dyn key (i64@slot+8).
1161
+ const objectKeysFromTemp = (t) => emitEnumerateObject(t,
1162
+ ({ out, o, src, i }) => [
1163
+ ['i64.store',
1164
+ ['i32.add', ['local.get', `$${out}`], ['i32.shl', ['local.get', `$${o}`], ['i32.const', 3]]],
1165
+ ['i64.load', ['i32.add', ['local.get', `$${src}`], ['i32.shl', ['local.get', `$${i}`], ['i32.const', 3]]]]]],
1166
+ ({ out, o, slot }) => [
1167
+ ['i64.store',
1168
+ ['i32.add', ['local.get', `$${out}`], ['i32.shl', ['local.get', `$${o}`], ['i32.const', 3]]],
1169
+ ['i64.load', ['i32.add', ['local.get', `$${slot}`], ['i32.const', 8]]]]])
1170
+
1171
+ // Object.values for an OBJECT — copy schema value (f64@base+i*8) then dyn value (f64@slot+16).
1172
+ const objectValuesFromTemp = (t) => emitEnumerateObject(t,
1173
+ ({ out, o, base, i }) => [
1174
+ ['f64.store',
1175
+ ['i32.add', ['local.get', `$${out}`], ['i32.shl', ['local.get', `$${o}`], ['i32.const', 3]]],
1176
+ ['f64.load', ['i32.add', ['local.get', `$${base}`], ['i32.shl', ['local.get', `$${i}`], ['i32.const', 3]]]]]],
1177
+ ({ out, o, slot }) => [
1178
+ ['f64.store',
1179
+ ['i32.add', ['local.get', `$${out}`], ['i32.shl', ['local.get', `$${o}`], ['i32.const', 3]]],
1180
+ ['f64.load', ['i32.add', ['local.get', `$${slot}`], ['i32.const', 16]]]]])
1181
+
1182
+ // Object.entries for an OBJECT — alloc 2-slot ARRAY pair {key, value} for each
1183
+ // schema slot (key from src+i*8, value from base+i*8) then each dyn slot
1184
+ // (key@slot+8, value@slot+16) and box the pair into out[o*8].
1185
+ const objectEntriesFromTemp = (t) => emitEnumerateObject(t,
1186
+ ({ out, o, src, base, i, pair }) => [
1187
+ ['local.set', `$${pair}`, ['call', '$__alloc_hdr', ['i32.const', 2], ['i32.const', 2]]],
1188
+ ['i64.store', ['local.get', `$${pair}`],
1189
+ ['i64.load', ['i32.add', ['local.get', `$${src}`], ['i32.shl', ['local.get', `$${i}`], ['i32.const', 3]]]]],
1190
+ ['f64.store', ['i32.add', ['local.get', `$${pair}`], ['i32.const', 8]],
1191
+ ['f64.load', ['i32.add', ['local.get', `$${base}`], ['i32.shl', ['local.get', `$${i}`], ['i32.const', 3]]]]],
1192
+ ['f64.store',
1193
+ ['i32.add', ['local.get', `$${out}`], ['i32.shl', ['local.get', `$${o}`], ['i32.const', 3]]],
1194
+ mkPtrIR(PTR.ARRAY, 0, ['local.get', `$${pair}`])]],
1195
+ ({ out, o, slot, pair }) => [
1196
+ ['local.set', `$${pair}`, ['call', '$__alloc_hdr', ['i32.const', 2], ['i32.const', 2]]],
1197
+ ['i64.store', ['local.get', `$${pair}`],
1198
+ ['i64.load', ['i32.add', ['local.get', `$${slot}`], ['i32.const', 8]]]],
1199
+ ['f64.store', ['i32.add', ['local.get', `$${pair}`], ['i32.const', 8]],
1200
+ ['f64.load', ['i32.add', ['local.get', `$${slot}`], ['i32.const', 16]]]],
1201
+ ['f64.store',
1202
+ ['i32.add', ['local.get', `$${out}`], ['i32.shl', ['local.get', `$${o}`], ['i32.const', 3]]],
1203
+ mkPtrIR(PTR.ARRAY, 0, ['local.get', `$${pair}`])]])