jz 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/module/object.js CHANGED
@@ -7,10 +7,11 @@
7
7
  * @module object
8
8
  */
9
9
 
10
- import { typed, asF64, asI64, temp, tempI32, allocPtr, needsDynShadow, mkPtrIR, extractF64Bits, appendStaticSlots, slotAddr, elemStore } from '../src/ir.js'
10
+ import { typed, asF64, asI64, temp, tempI32, allocPtr, needsDynShadow, mkPtrIR, extractF64Bits, appendStaticSlots, slotAddr, elemLoad, elemStore } from '../src/ir.js'
11
11
  import { emit } from '../src/emit.js'
12
12
  import { valTypeOf, lookupValType, VAL, repOf, updateRep, shapeOf } from '../src/analyze.js'
13
13
  import { ctx, err, inc, PTR, LAYOUT } from '../src/ctx.js'
14
+ import { includeModule } from '../src/autoload.js'
14
15
 
15
16
 
16
17
  export default (ctx) => {
@@ -30,8 +31,11 @@ export default (ctx) => {
30
31
  // schema-slot writes to offsets >= 8 land out-of-bounds.
31
32
  const target = takeLiteralTarget()
32
33
  const merged = target ? ctx.schema.resolve(target) : null
33
- const schemaId = merged ? ctx.schema.idOf(target) : 0
34
- const cap = merged ? Math.max(1, merged.length) : 1
34
+ // Register the empty schema so schemaId always indexes a real schema.list
35
+ // entry __json_obj and dyn-get load keys via $__schema_tbl[sid] and would
36
+ // crash on an unregistered id 0 (table left uninitialized when list empty).
37
+ const schemaId = merged ? ctx.schema.idOf(target) : ctx.schema.register([])
38
+ const cap = ctx.abi.object.ops.allocSlots(merged ? merged.length : 0)
35
39
  return mkPtrIR(PTR.OBJECT, schemaId, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', cap]])
36
40
  }
37
41
 
@@ -63,7 +67,7 @@ export default (ctx) => {
63
67
  // R: Static data segment for objects of pure-literal property values (own-memory only).
64
68
  // Even with shadow needed, we can skip alloc + N stores; just feed literal values to __dyn_set.
65
69
  const shadow = needsDynShadow(target)
66
- if (values.length >= 2 && !ctx.memory.shared) {
70
+ if (values.length >= 2 && values.length === schema.length && !ctx.memory.shared) {
67
71
  const emitted = values.map(emit)
68
72
  // asF64 folds i32.const → f64.const so int-literal values also qualify.
69
73
  const slots = emitted.map(v => extractF64Bits(asF64(v)))
@@ -82,16 +86,16 @@ export default (ctx) => {
82
86
  }
83
87
 
84
88
  const body = [
85
- ['local.set', `$${t}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', Math.max(1, schema.length)]]],
89
+ ['local.set', `$${t}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', ctx.abi.object.ops.allocSlots(schema.length)]]],
86
90
  ]
87
91
  for (let i = 0; i < values.length; i++)
88
- body.push(['f64.store', slotAddr(t, i), asF64(emit(values[i]))])
92
+ body.push(ctx.abi.object.ops.store(['local.get', `$${t}`], i, asF64(emit(values[i]))))
89
93
  body.push(['local.set', `$${ptr}`, mkPtrIR(PTR.OBJECT, schemaId, ['local.get', `$${t}`])])
90
94
  if (shadow) {
91
95
  inc('__dyn_set')
92
96
  for (let i = 0; i < schema.length; i++)
93
97
  body.push(['drop', ['call', '$__dyn_set', ['i64.reinterpret_f64', ['local.get', `$${ptr}`]], asI64(emit(['str', String(schema[i])])),
94
- ['i64.load', slotAddr(t, i)]]])
98
+ ctx.abi.object.ops.loadBits(['local.get', `$${t}`], i)]])
95
99
  }
96
100
  body.push(['local.get', `$${ptr}`])
97
101
 
@@ -100,14 +104,82 @@ export default (ctx) => {
100
104
 
101
105
  // === Object static methods ===
102
106
 
107
+ ctx.core.emit['Object.freeze'] = (obj) => asF64(emit(obj))
108
+
109
+ // Object.isExtensible / isSealed / isFrozen.
110
+ // jz fixes an object's schema at construction: a `{…}` literal can
111
+ // neither grow nor lose keys, so an OBJECT value is non-extensible and
112
+ // sealed; its slots stay writable, so it is not frozen. Arrays, maps,
113
+ // sets and hashes grow dynamically → extensible. Primitives are
114
+ // non-objects → ES2015 reports them sealed & frozen, not extensible.
115
+ const extKind = (obj) => {
116
+ const t = typeof obj === 'string' ? lookupValType(obj) : valTypeOf(obj)
117
+ if (t === VAL.OBJECT) return { ext: 0, sealed: 1, frozen: 0 }
118
+ if (t === VAL.NUMBER || t === VAL.STRING || t === VAL.BIGINT) return { ext: 0, sealed: 1, frozen: 1 }
119
+ return { ext: 1, sealed: 0, frozen: 0 }
120
+ }
121
+ const objQuery = (pick) => (obj) => {
122
+ const v = pick(extKind(obj))
123
+ if (obj == null) return typed(['f64.const', v], 'f64')
124
+ return typed(['block', ['result', 'f64'], ['drop', asF64(emit(obj))], ['f64.const', v]], 'f64')
125
+ }
126
+ ctx.core.emit['Object.isExtensible'] = objQuery((k) => k.ext)
127
+ ctx.core.emit['Object.isSealed'] = objQuery((k) => k.sealed)
128
+ ctx.core.emit['Object.isFrozen'] = objQuery((k) => k.frozen)
129
+
130
+ // RequireObjectCoercible: Object.keys/values/entries reject null & undefined
131
+ // with a TypeError. A literal lowers to a [null, value] node — so `null` is
132
+ // [null, null] and `undefined` is [null, undefined] (both JSON-print alike);
133
+ // a missing argument arrives as JS undefined. Anything else (incl.
134
+ // booleans/numbers, which JS boxes) is left to the normal path.
135
+ const isNullishLiteral = (node) => node === undefined
136
+ || (Array.isArray(node) && node.length === 2 && node[0] == null && node[1] == null)
137
+ const requireCoercible = (node) => {
138
+ if (!isNullishLiteral(node)) return null
139
+ ctx.runtime.throws = true
140
+ return typed(['block', ['result', 'f64'], ['throw', '$__jz_err', ['f64.const', 0]]], 'f64')
141
+ }
142
+
143
+ // Arrays and (coerced) strings expose their indices as own enumerable
144
+ // keys — Object.keys/entries iterate "0".."n-1". `arrayValType` mirrors
145
+ // `stringValType` below: a string arg is a variable name, anything else
146
+ // an AST node.
147
+ const arrayValType = (obj) => (typeof obj === 'string' ? lookupValType(obj) : valTypeOf(obj)) === VAL.ARRAY
148
+ // Index-string key array for an array-like receiver. `lenCall` is the
149
+ // length builtin: __len for jz arrays, __str_len for strings.
150
+ const emitIndexKeys = (obj, lenCall) => {
151
+ inc(lenCall, '__to_str')
152
+ const v = temp('ik'), i = tempI32('iki'), len = tempI32('ikl')
153
+ const vPtr = () => ['i64.reinterpret_f64', ['local.get', `$${v}`]]
154
+ const out = allocPtr({ type: PTR.ARRAY, len: ['local.get', `$${len}`], tag: 'ik' })
155
+ const id = ctx.func.uniq++
156
+ return typed(['block', ['result', 'f64'],
157
+ ['local.set', `$${v}`, asF64(emit(obj))],
158
+ ['local.set', `$${len}`, ['call', `$${lenCall}`, vPtr()]],
159
+ out.init,
160
+ ['local.set', `$${i}`, ['i32.const', 0]],
161
+ ['block', `$brk${id}`, ['loop', `$loop${id}`,
162
+ ['br_if', `$brk${id}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${len}`]]],
163
+ elemStore(out.local, i, ['f64.reinterpret_i64',
164
+ ['call', '$__to_str', ['i64.reinterpret_f64', ['f64.convert_i32_s', ['local.get', `$${i}`]]]]]),
165
+ ['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
166
+ ['br', `$loop${id}`]]],
167
+ out.ptr], 'f64')
168
+ }
169
+
103
170
  ctx.core.emit['Object.keys'] = (obj) => {
171
+ const nullish = requireCoercible(obj)
172
+ if (nullish) return nullish
104
173
  if (isHashTyped(obj)) return emitHashKeys(obj)
174
+ if (arrayValType(obj)) return emitIndexKeys(obj, '__len')
175
+ if (stringValType(obj)) return emitIndexKeys(obj, '__str_len')
105
176
  const schema = resolveSchema(obj)
106
177
  if (schema) return emitStringArray(schema)
107
178
  // Receiver type unknown at compile time. Dispatch on ptr-type at
108
179
  // runtime: HASH walks the probe table, anything else returns [].
109
180
  return emitRuntimeKeys(obj)
110
181
  }
182
+ ctx.core.emit['Object.getOwnPropertyNames'] = ctx.core.emit['Object.keys']
111
183
 
112
184
  // Object.prototype.hasOwnProperty(key) — own-property presence check.
113
185
  // Compile-time fold for literal keys against object literals or variables
@@ -132,8 +204,36 @@ export default (ctx) => {
132
204
  ctx.core.emit[`.${VAL.ARRAY}:hasOwnProperty`] = ctx.core.emit['.hasOwnProperty']
133
205
  ctx.core.emit[`.${VAL.STRING}:hasOwnProperty`] = ctx.core.emit['.hasOwnProperty']
134
206
  ctx.core.emit[`.${VAL.CLOSURE}:hasOwnProperty`] = ctx.core.emit['.hasOwnProperty']
207
+ // Object.hasOwn(o, k) — ES2022 static equivalent of o.hasOwnProperty(k).
208
+ // Reuses the same own-property emitter; receiver-type variants above apply.
209
+ ctx.core.emit['Object.hasOwn'] = (obj, key) => ctx.core.emit['.hasOwnProperty'](obj, key)
210
+
211
+ // String primitives are coerced to exotic String objects whose own enumerable
212
+ // properties are the indexed characters. Object.values/entries iterate them.
213
+ const stringValType = (obj) => (typeof obj === 'string' ? lookupValType(obj) : valTypeOf(obj)) === VAL.STRING
135
214
 
136
215
  ctx.core.emit['Object.values'] = (obj) => {
216
+ const nullish = requireCoercible(obj)
217
+ if (nullish) return nullish
218
+ if (stringValType(obj)) {
219
+ inc('__str_idx', '__str_len')
220
+ const s = temp('osv'), i = tempI32('osvi'), len = tempI32('osvl')
221
+ const sPtr = () => ['i64.reinterpret_f64', ['local.get', `$${s}`]]
222
+ const out = allocPtr({ type: PTR.ARRAY, len: ['local.get', `$${len}`], tag: 'osv' })
223
+ const id = ctx.func.uniq++
224
+ return typed(['block', ['result', 'f64'],
225
+ ['local.set', `$${s}`, asF64(emit(obj))],
226
+ ['local.set', `$${len}`, ['call', '$__str_len', sPtr()]],
227
+ out.init,
228
+ ['local.set', `$${i}`, ['i32.const', 0]],
229
+ ['block', `$brk${id}`, ['loop', `$loop${id}`,
230
+ ['br_if', `$brk${id}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${len}`]]],
231
+ elemStore(out.local, i, ['call', '$__str_idx', sPtr(), ['local.get', `$${i}`]]),
232
+ ['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
233
+ ['br', `$loop${id}`]]],
234
+ out.ptr], 'f64')
235
+ }
236
+ if (arrayValType(obj)) { inc('__arr_from'); return typed(['call', '$__arr_from', asI64(emit(obj))], 'f64') }
137
237
  if (isHashTyped(obj)) return emitHashValues(obj)
138
238
  const schema = resolveSchema(obj)
139
239
  if (!schema) return emitRuntimeValues(obj)
@@ -144,12 +244,59 @@ export default (ctx) => {
144
244
  const body = [['local.set', `$${t}`, va], out.init,
145
245
  ['local.set', `$${base}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]]]
146
246
  for (let i = 0; i < n; i++)
147
- body.push(['f64.store', slotAddr(out.local, i), ['f64.load', slotAddr(base, i)]])
247
+ body.push(['f64.store', slotAddr(out.local, i), ctx.abi.object.ops.load(['local.get', `$${base}`], i)])
148
248
  body.push(out.ptr)
149
249
  return typed(['block', ['result', 'f64'], ...body], 'f64')
150
250
  }
151
251
 
152
252
  ctx.core.emit['Object.entries'] = (obj) => {
253
+ const nullish = requireCoercible(obj)
254
+ if (nullish) return nullish
255
+ if (stringValType(obj)) {
256
+ inc('__str_idx', '__str_len', '__to_str')
257
+ const s = temp('oes'), i = tempI32('oesi'), len = tempI32('oesl'), pair = tempI32('oep')
258
+ const sPtr = () => ['i64.reinterpret_f64', ['local.get', `$${s}`]]
259
+ const out = allocPtr({ type: PTR.ARRAY, len: ['local.get', `$${len}`], tag: 'oes' })
260
+ const id = ctx.func.uniq++
261
+ return typed(['block', ['result', 'f64'],
262
+ ['local.set', `$${s}`, asF64(emit(obj))],
263
+ ['local.set', `$${len}`, ['call', '$__str_len', sPtr()]],
264
+ out.init,
265
+ ['local.set', `$${i}`, ['i32.const', 0]],
266
+ ['block', `$brk${id}`, ['loop', `$loop${id}`,
267
+ ['br_if', `$brk${id}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${len}`]]],
268
+ ['local.set', `$${pair}`, ['call', '$__alloc_hdr', ['i32.const', 2], ['i32.const', 2]]],
269
+ ['f64.store', slotAddr(pair, 0), ['f64.reinterpret_i64',
270
+ ['call', '$__to_str', ['i64.reinterpret_f64', ['f64.convert_i32_s', ['local.get', `$${i}`]]]]]],
271
+ ['f64.store', slotAddr(pair, 1), ['call', '$__str_idx', sPtr(), ['local.get', `$${i}`]]],
272
+ elemStore(out.local, i, mkPtrIR(PTR.ARRAY, 0, ['local.get', `$${pair}`])),
273
+ ['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
274
+ ['br', `$loop${id}`]]],
275
+ out.ptr], 'f64')
276
+ }
277
+ if (arrayValType(obj)) {
278
+ inc('__len', '__to_str', '__ptr_offset', '__alloc_hdr')
279
+ const v = temp('oea'), i = tempI32('oeai'), len = tempI32('oeal'), base = tempI32('oeab'), pair = tempI32('oeap')
280
+ const vPtr = () => ['i64.reinterpret_f64', ['local.get', `$${v}`]]
281
+ const out = allocPtr({ type: PTR.ARRAY, len: ['local.get', `$${len}`], tag: 'oea' })
282
+ const id = ctx.func.uniq++
283
+ return typed(['block', ['result', 'f64'],
284
+ ['local.set', `$${v}`, asF64(emit(obj))],
285
+ ['local.set', `$${len}`, ['call', '$__len', vPtr()]],
286
+ out.init,
287
+ ['local.set', `$${base}`, ['call', '$__ptr_offset', vPtr()]],
288
+ ['local.set', `$${i}`, ['i32.const', 0]],
289
+ ['block', `$brk${id}`, ['loop', `$loop${id}`,
290
+ ['br_if', `$brk${id}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${len}`]]],
291
+ ['local.set', `$${pair}`, ['call', '$__alloc_hdr', ['i32.const', 2], ['i32.const', 2]]],
292
+ ['f64.store', slotAddr(pair, 0), ['f64.reinterpret_i64',
293
+ ['call', '$__to_str', ['i64.reinterpret_f64', ['f64.convert_i32_s', ['local.get', `$${i}`]]]]]],
294
+ ['f64.store', slotAddr(pair, 1), elemLoad(base, i)],
295
+ elemStore(out.local, i, mkPtrIR(PTR.ARRAY, 0, ['local.get', `$${pair}`])),
296
+ ['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
297
+ ['br', `$loop${id}`]]],
298
+ out.ptr], 'f64')
299
+ }
153
300
  if (isHashTyped(obj)) return emitHashEntries(obj)
154
301
  const schema = resolveSchema(obj)
155
302
  if (!schema) return emitRuntimeEntries(obj)
@@ -163,7 +310,7 @@ export default (ctx) => {
163
310
  body.push(
164
311
  ['local.set', `$${pair}`, ['call', '$__alloc_hdr', ['i32.const', 2], ['i32.const', 2]]],
165
312
  ['f64.store', slotAddr(pair, 0), emit(['str', schema[i]])],
166
- ['f64.store', slotAddr(pair, 1), ['f64.load', slotAddr(base, i)]],
313
+ ['f64.store', slotAddr(pair, 1), ctx.abi.object.ops.load(['local.get', `$${base}`], i)],
167
314
  ['f64.store', slotAddr(out.local, i), mkPtrIR(PTR.ARRAY, 0, ['local.get', `$${pair}`])])
168
315
  }
169
316
  body.push(out.ptr)
@@ -171,6 +318,9 @@ export default (ctx) => {
171
318
  }
172
319
 
173
320
  ctx.core.emit['Object.assign'] = (target, ...sources) => {
321
+ // RequireObjectCoercible(target) — null/undefined is a TypeError.
322
+ const nullish = requireCoercible(target)
323
+ if (nullish) return nullish
174
324
  if (typeof target === 'string') {
175
325
  const vt = repOf(target)?.val
176
326
  if (vt && vt !== VAL.OBJECT) {
@@ -183,11 +333,14 @@ export default (ctx) => {
183
333
  const boxedSchema = ['__inner__', ...allProps]
184
334
  const schemaId = ctx.schema.register(boxedSchema)
185
335
  ctx.schema.vars.set(target, schemaId)
336
+ // Emit-time rep mutation: Object.assign's target gains a freshly-registered
337
+ // boxed-schema binding here; downstream `.prop` reads in the same emit pass
338
+ // depend on schemaId being live on the rep, not just in ctx.schema.vars.
186
339
  updateRep(target, { schemaId })
187
340
  const t = tempI32('bx'), s = temp('bs')
188
341
  const body = [
189
- ['local.set', `$${t}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', Math.max(1, boxedSchema.length)]]],
190
- ['f64.store', ['local.get', `$${t}`], asF64(emit(target))],
342
+ ['local.set', `$${t}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', ctx.abi.object.ops.allocSlots(boxedSchema.length)]]],
343
+ ctx.abi.object.ops.store(['local.get', `$${t}`], 0, asF64(emit(target))),
191
344
  ]
192
345
  const sBase = tempI32('sb')
193
346
  for (const source of sources) {
@@ -197,7 +350,7 @@ export default (ctx) => {
197
350
  for (let si = 0; si < sSchema.length; si++) {
198
351
  const ti = boxedSchema.indexOf(sSchema[si])
199
352
  if (ti < 0) continue
200
- body.push(['f64.store', slotAddr(t, ti), ['f64.load', slotAddr(sBase, si)]])
353
+ body.push(ctx.abi.object.ops.store(['local.get', `$${t}`], ti, ctx.abi.object.ops.load(['local.get', `$${sBase}`], si)))
201
354
  }
202
355
  }
203
356
  body.push(['local.set', `$${target}`,
@@ -207,20 +360,22 @@ export default (ctx) => {
207
360
  }
208
361
  }
209
362
  const tSchema = resolveSchema(target)
363
+ const sourceSchemas = sources.map(resolveSchema)
210
364
  if (!tSchema) err('Object.assign: target needs known schema')
365
+ if (sourceSchemas.some(s => !s)) return emitDynamicAssign(target, sources, sourceSchemas)
211
366
  const t = temp('at'), s = temp('as')
212
367
  const tBase = tempI32('tb'), sBase2 = tempI32('sb')
213
368
  const body = [['local.set', `$${t}`, asF64(emit(target))],
214
369
  ['local.set', `$${tBase}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]]]
215
- for (const source of sources) {
216
- const sSchema = resolveSchema(source)
217
- if (!sSchema) err('Object.assign: source needs known schema')
370
+ for (let i = 0; i < sources.length; i++) {
371
+ const source = sources[i]
372
+ const sSchema = sourceSchemas[i]
218
373
  body.push(['local.set', `$${s}`, asF64(emit(source))])
219
374
  body.push(['local.set', `$${sBase2}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${s}`]]]])
220
375
  for (let si = 0; si < sSchema.length; si++) {
221
376
  const ti = tSchema.indexOf(sSchema[si])
222
377
  if (ti < 0) continue
223
- body.push(['f64.store', slotAddr(tBase, ti), ['f64.load', slotAddr(sBase2, si)]])
378
+ body.push(ctx.abi.object.ops.store(['local.get', `$${tBase}`], ti, ctx.abi.object.ops.load(['local.get', `$${sBase2}`], si)))
224
379
  }
225
380
  }
226
381
  body.push(['local.get', `$${t}`])
@@ -231,8 +386,12 @@ export default (ctx) => {
231
386
  err('Object.defineProperty descriptor semantics are outside jz scope; jzify only folds static bundler export helpers')
232
387
  }
233
388
 
234
- // Object.fromEntries(arr) → creates HASH from array of [key, value] pairs
389
+ // Object.fromEntries(arr) → creates HASH from array of [key, value] pairs.
390
+ // Spec step 1 is RequireObjectCoercible(iterable): a missing/nullish argument
391
+ // is a TypeError, not an `emit(undefined)` compiler crash.
235
392
  ctx.core.emit['Object.fromEntries'] = (arr) => {
393
+ const nullishThrow = requireCoercible(arr)
394
+ if (nullishThrow) return nullishThrow
236
395
  inc('__hash_new', '__hash_set')
237
396
  inc('__str_hash', '__str_eq')
238
397
  const va = asF64(emit(arr))
@@ -267,6 +426,7 @@ export default (ctx) => {
267
426
  // Header propsPtr lives at $off-16 (current ARRAY layout). We alias src's hash
268
427
  // by copying the slot; __dyn_move covers the shifted-array case where props
269
428
  // were migrated to the global __dyn_props.
429
+ includeModule('array')
270
430
  inc('__arr_from', '__dyn_move', '__ptr_offset')
271
431
  const src = temp('ocs')
272
432
  const dst = temp('ocd')
@@ -289,6 +449,7 @@ export default (ctx) => {
289
449
  if (!schema) {
290
450
  if (protoType == null) {
291
451
  const value = temp('ocr')
452
+ includeModule('array')
292
453
  inc('__arr_from', '__dyn_move', '__ptr_offset')
293
454
  const dst2 = temp('ocd')
294
455
  const srcOff2 = tempI32('ocso')
@@ -318,12 +479,12 @@ export default (ctx) => {
318
479
  const srcBase = tempI32('cb')
319
480
  const body = [
320
481
  ['local.set', `$${s}`, asF64(emit(proto))],
321
- ['local.set', `$${t}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', Math.max(1, n)]]],
482
+ ['local.set', `$${t}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', ctx.abi.object.ops.allocSlots(n)]]],
322
483
  ['local.set', `$${srcBase}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${s}`]]]],
323
484
  ]
324
485
  // Copy all properties from proto
325
486
  for (let i = 0; i < n; i++)
326
- body.push(['f64.store', slotAddr(t, i), ['f64.load', slotAddr(srcBase, i)]])
487
+ body.push(ctx.abi.object.ops.store(['local.get', `$${t}`], i, ctx.abi.object.ops.load(['local.get', `$${srcBase}`], i)))
327
488
  body.push(mkPtrIR(PTR.OBJECT, schemaId, ['local.get', `$${t}`]))
328
489
  return typed(['block', ['result', 'f64'], ...body], 'f64')
329
490
  }
@@ -331,6 +492,57 @@ export default (ctx) => {
331
492
 
332
493
  // --- Helpers ---
333
494
 
495
+ // Used only after the target schema is known. Unknown HASH targets can grow by
496
+ // returning a new pointer, which would not preserve aliases to the old value.
497
+ function emitDynamicAssign(target, sources, sourceSchemas = sources.map(resolveSchema)) {
498
+ includeModule('collection')
499
+ inc('__hash_set', '__dyn_get_any', '__ptr_offset', '__len')
500
+ const t = temp('adt'), s = temp('ads'), sBase = tempI32('adsb')
501
+ const keys = temp('adk'), keysBase = tempI32('adkb'), len = tempI32('adn')
502
+ const i = tempI32('adi'), key = temp('adkey')
503
+ const id = ctx.func.uniq++
504
+ const body = [['local.set', `$${t}`, asF64(emit(target))]]
505
+
506
+ for (let si = 0; si < sources.length; si++) {
507
+ const source = sources[si]
508
+ const sSchema = sourceSchemas[si]
509
+ body.push(['local.set', `$${s}`, asF64(emit(source))])
510
+ if (sSchema) {
511
+ body.push(['local.set', `$${sBase}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${s}`]]]])
512
+ for (let pi = 0; pi < sSchema.length; pi++)
513
+ body.push(['local.set', `$${t}`, ['f64.reinterpret_i64',
514
+ ['call', '$__hash_set', ['i64.reinterpret_f64', ['local.get', `$${t}`]],
515
+ asI64(emit(['str', String(sSchema[pi])])),
516
+ ctx.abi.object.ops.loadBits(['local.get', `$${sBase}`], pi)]]])
517
+ continue
518
+ }
519
+
520
+ body.push(
521
+ ['local.set', `$${keys}`, runtimeKeysFromTemp(s, 'adk')],
522
+ ['local.set', `$${keysBase}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${keys}`]]]],
523
+ ['local.set', `$${len}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${keys}`]]]],
524
+ ['local.set', `$${i}`, ['i32.const', 0]],
525
+ ['block', `$adbrk${id}_${si}`, ['loop', `$adloop${id}_${si}`,
526
+ ['br_if', `$adbrk${id}_${si}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${len}`]]],
527
+ ['local.set', `$${key}`, ['f64.load',
528
+ ['i32.add', ['local.get', `$${keysBase}`], ['i32.shl', ['local.get', `$${i}`], ['i32.const', 3]]]]],
529
+ ['local.set', `$${t}`, ['f64.reinterpret_i64',
530
+ ['call', '$__hash_set',
531
+ ['i64.reinterpret_f64', ['local.get', `$${t}`]],
532
+ ['i64.reinterpret_f64', ['local.get', `$${key}`]],
533
+ ['call', '$__dyn_get_any',
534
+ ['i64.reinterpret_f64', ['local.get', `$${s}`]],
535
+ ['i64.reinterpret_f64', ['local.get', `$${key}`]]]]]],
536
+ ['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
537
+ ['br', `$adloop${id}_${si}`]]])
538
+ }
539
+
540
+ if (typeof target === 'string' && ctx.func.locals.get(target) === 'f64')
541
+ body.push(['local.set', `$${target}`, ['local.get', `$${t}`]])
542
+ body.push(['local.get', `$${t}`])
543
+ return typed(['block', ['result', 'f64'], ...body], 'f64')
544
+ }
545
+
334
546
  function resolveSchema(obj) {
335
547
  if (typeof obj === 'string') return ctx.schema.resolve(obj)
336
548
  if (Array.isArray(obj) && obj[0] === '{}')
@@ -338,7 +550,7 @@ function resolveSchema(obj) {
338
550
  // JSON-shape inferred: JSON.parse(constStr) call or `.prop`/`[i]` chain
339
551
  // resolving to a known OBJECT shape carries its key list as `names`.
340
552
  const sh = shapeOf(obj)
341
- if (sh?.vt === VAL.OBJECT && sh.names) return sh.names
553
+ if (sh?.val === VAL.OBJECT && sh.names) return sh.names
342
554
  return null
343
555
  }
344
556
 
@@ -379,7 +591,7 @@ function emitObjectSpread(props, spreadTarget = takeLiteralTarget()) {
379
591
  const ptr = temp('objp')
380
592
  const src = tempI32('osp')
381
593
 
382
- const body = [['local.set', `$${t}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', Math.max(1, schema.length)]]]]
594
+ const body = [['local.set', `$${t}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', ctx.abi.object.ops.allocSlots(schema.length)]]]]
383
595
 
384
596
  // Process props in order — later props override earlier (JS semantics)
385
597
  let srcF
@@ -388,17 +600,17 @@ function emitObjectSpread(props, spreadTarget = takeLiteralTarget()) {
388
600
  const sSchema = resolveSchema(p[1])
389
601
  if (!sSchema) {
390
602
  // Unknown-schema source (e.g. parameter). Override each slot via runtime
391
- // __dyn_get_or using existing value as fallback. Requires collection module.
392
- if (!ctx.module.modules.collection) err('Object spread: source needs known schema')
603
+ // __dyn_get_or using existing value as fallback.
604
+ includeModule('collection')
393
605
  inc('__dyn_get_or')
394
606
  srcF ??= temp('ospf')
395
607
  body.push(['local.set', `$${srcF}`, asF64(emit(p[1]))])
396
608
  for (let i = 0; i < schema.length; i++) {
397
- const slot = slotAddr(t, i)
398
- body.push(['f64.store', slot,
609
+ const base = ['local.get', `$${t}`]
610
+ body.push(ctx.abi.object.ops.store(base, i,
399
611
  ['f64.reinterpret_i64', ['call', '$__dyn_get_or', ['i64.reinterpret_f64', ['local.get', `$${srcF}`]],
400
612
  asI64(emit(['str', String(schema[i])])),
401
- ['i64.load', slot]]]])
613
+ ctx.abi.object.ops.loadBits(base, i)]]))
402
614
  }
403
615
  continue
404
616
  }
@@ -406,11 +618,11 @@ function emitObjectSpread(props, spreadTarget = takeLiteralTarget()) {
406
618
  for (let si = 0; si < sSchema.length; si++) {
407
619
  const ti = schema.indexOf(sSchema[si])
408
620
  if (ti < 0) continue
409
- body.push(['f64.store', slotAddr(t, ti), ['f64.load', slotAddr(src, si)]])
621
+ body.push(ctx.abi.object.ops.store(['local.get', `$${t}`], ti, ctx.abi.object.ops.load(['local.get', `$${src}`], si)))
410
622
  }
411
623
  } else if (Array.isArray(p) && p[0] === ':') {
412
624
  const ti = schema.indexOf(p[1])
413
- if (ti >= 0) body.push(['f64.store', slotAddr(t, ti), asF64(emit(p[2]))])
625
+ if (ti >= 0) body.push(ctx.abi.object.ops.store(['local.get', `$${t}`], ti, asF64(emit(p[2]))))
414
626
  }
415
627
  }
416
628
 
@@ -419,7 +631,7 @@ function emitObjectSpread(props, spreadTarget = takeLiteralTarget()) {
419
631
  inc('__dyn_set')
420
632
  for (let i = 0; i < schema.length; i++)
421
633
  body.push(['drop', ['call', '$__dyn_set', ['i64.reinterpret_f64', ['local.get', `$${ptr}`]], asI64(emit(['str', String(schema[i])])),
422
- ['i64.load', slotAddr(t, i)]]])
634
+ ctx.abi.object.ops.loadBits(['local.get', `$${t}`], i)]])
423
635
  }
424
636
  body.push(['local.get', `$${ptr}`])
425
637
  return typed(['block', ['result', 'f64'], ...body], 'f64')
@@ -565,16 +777,22 @@ function hashEntriesFromTemp(t) {
565
777
  // types (ARRAY, nullish, primitives) return an empty array. The empty-array
566
778
  // fallback is allocated in all arms for type uniformity at the if boundary.
567
779
  function emitRuntimeKeys(obj) {
780
+ const t = temp('rk')
781
+ return typed(['block', ['result', 'f64'],
782
+ ['local.set', `$${t}`, asF64(emit(obj))],
783
+ runtimeKeysFromTemp(t, 'rk')], 'f64')
784
+ }
785
+
786
+ function runtimeKeysFromTemp(t, tag) {
568
787
  inc('__ptr_type')
569
788
  // Ensure the schema table global exists even in programs that never use
570
789
  // JSON.parse or compile-time schemas — the OBJECT arm reads it at runtime
571
790
  // and the watr resolver requires the symbol to be declared.
572
791
  if (!ctx.scope.globals.has('__schema_tbl'))
573
792
  ctx.scope.globals.set('__schema_tbl', '(global $__schema_tbl (mut i32) (i32.const 0))')
574
- const t = temp('rk'), tt = tempI32('rkt')
575
- const empty = allocPtr({ type: PTR.ARRAY, len: 0, tag: 'rke' })
576
- return typed(['block', ['result', 'f64'],
577
- ['local.set', `$${t}`, asF64(emit(obj))],
793
+ const tt = tempI32(`${tag}t`)
794
+ const empty = allocPtr({ type: PTR.ARRAY, len: 0, tag: `${tag}e` })
795
+ return ['block', ['result', 'f64'],
578
796
  ['local.set', `$${tt}`, ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
579
797
  ['if', ['result', 'f64'],
580
798
  ['i32.eq', ['local.get', `$${tt}`], ['i32.const', PTR.HASH]],
@@ -584,7 +802,7 @@ function emitRuntimeKeys(obj) {
584
802
  ['i32.eq', ['local.get', `$${tt}`], ['i32.const', PTR.OBJECT]],
585
803
  ['i32.ne', ['global.get', '$__schema_tbl'], ['i32.const', 0]]],
586
804
  ['then', objectKeysFromTemp(t)],
587
- ['else', ['block', ['result', 'f64'], empty.init, empty.ptr]]]]]], 'f64')
805
+ ['else', ['block', ['result', 'f64'], empty.init, empty.ptr]]]]]]
588
806
  }
589
807
 
590
808
  function emitRuntimeValues(obj) {