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