jz 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/module/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,9 +31,12 @@ 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
35
- return mkPtrIR(PTR.OBJECT, schemaId, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', cap], ['i32.const', 8]])
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)
39
+ return mkPtrIR(PTR.OBJECT, schemaId, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', cap]])
36
40
  }
37
41
 
38
42
  // Flatten comma-grouped props: [',', p1, p2] → [p1, p2]
@@ -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)], ['i32.const', 8]]],
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,14 +244,62 @@ 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
+ }
300
+ if (isHashTyped(obj)) return emitHashEntries(obj)
153
301
  const schema = resolveSchema(obj)
154
- if (!schema) err('Object.entries requires object with known schema')
302
+ if (!schema) return emitRuntimeEntries(obj)
155
303
  const va = asF64(emit(obj))
156
304
  const n = schema.length
157
305
  const t = temp('oe'), pair = tempI32('op'), base = tempI32('eb')
@@ -160,9 +308,9 @@ export default (ctx) => {
160
308
  ['local.set', `$${base}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]]]
161
309
  for (let i = 0; i < n; i++) {
162
310
  body.push(
163
- ['local.set', `$${pair}`, ['call', '$__alloc_hdr', ['i32.const', 2], ['i32.const', 2], ['i32.const', 8]]],
311
+ ['local.set', `$${pair}`, ['call', '$__alloc_hdr', ['i32.const', 2], ['i32.const', 2]]],
164
312
  ['f64.store', slotAddr(pair, 0), emit(['str', schema[i]])],
165
- ['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)],
166
314
  ['f64.store', slotAddr(out.local, i), mkPtrIR(PTR.ARRAY, 0, ['local.get', `$${pair}`])])
167
315
  }
168
316
  body.push(out.ptr)
@@ -170,6 +318,9 @@ export default (ctx) => {
170
318
  }
171
319
 
172
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
173
324
  if (typeof target === 'string') {
174
325
  const vt = repOf(target)?.val
175
326
  if (vt && vt !== VAL.OBJECT) {
@@ -182,11 +333,14 @@ export default (ctx) => {
182
333
  const boxedSchema = ['__inner__', ...allProps]
183
334
  const schemaId = ctx.schema.register(boxedSchema)
184
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.
185
339
  updateRep(target, { schemaId })
186
340
  const t = tempI32('bx'), s = temp('bs')
187
341
  const body = [
188
- ['local.set', `$${t}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', Math.max(1, boxedSchema.length)], ['i32.const', 8]]],
189
- ['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))),
190
344
  ]
191
345
  const sBase = tempI32('sb')
192
346
  for (const source of sources) {
@@ -196,7 +350,7 @@ export default (ctx) => {
196
350
  for (let si = 0; si < sSchema.length; si++) {
197
351
  const ti = boxedSchema.indexOf(sSchema[si])
198
352
  if (ti < 0) continue
199
- 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)))
200
354
  }
201
355
  }
202
356
  body.push(['local.set', `$${target}`,
@@ -206,20 +360,22 @@ export default (ctx) => {
206
360
  }
207
361
  }
208
362
  const tSchema = resolveSchema(target)
363
+ const sourceSchemas = sources.map(resolveSchema)
209
364
  if (!tSchema) err('Object.assign: target needs known schema')
365
+ if (sourceSchemas.some(s => !s)) return emitDynamicAssign(target, sources, sourceSchemas)
210
366
  const t = temp('at'), s = temp('as')
211
367
  const tBase = tempI32('tb'), sBase2 = tempI32('sb')
212
368
  const body = [['local.set', `$${t}`, asF64(emit(target))],
213
369
  ['local.set', `$${tBase}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]]]
214
- for (const source of sources) {
215
- const sSchema = resolveSchema(source)
216
- 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]
217
373
  body.push(['local.set', `$${s}`, asF64(emit(source))])
218
374
  body.push(['local.set', `$${sBase2}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${s}`]]]])
219
375
  for (let si = 0; si < sSchema.length; si++) {
220
376
  const ti = tSchema.indexOf(sSchema[si])
221
377
  if (ti < 0) continue
222
- 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)))
223
379
  }
224
380
  }
225
381
  body.push(['local.get', `$${t}`])
@@ -230,8 +386,12 @@ export default (ctx) => {
230
386
  err('Object.defineProperty descriptor semantics are outside jz scope; jzify only folds static bundler export helpers')
231
387
  }
232
388
 
233
- // 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.
234
392
  ctx.core.emit['Object.fromEntries'] = (arr) => {
393
+ const nullishThrow = requireCoercible(arr)
394
+ if (nullishThrow) return nullishThrow
235
395
  inc('__hash_new', '__hash_set')
236
396
  inc('__str_hash', '__str_eq')
237
397
  const va = asF64(emit(arr))
@@ -266,6 +426,7 @@ export default (ctx) => {
266
426
  // Header propsPtr lives at $off-16 (current ARRAY layout). We alias src's hash
267
427
  // by copying the slot; __dyn_move covers the shifted-array case where props
268
428
  // were migrated to the global __dyn_props.
429
+ includeModule('array')
269
430
  inc('__arr_from', '__dyn_move', '__ptr_offset')
270
431
  const src = temp('ocs')
271
432
  const dst = temp('ocd')
@@ -288,6 +449,7 @@ export default (ctx) => {
288
449
  if (!schema) {
289
450
  if (protoType == null) {
290
451
  const value = temp('ocr')
452
+ includeModule('array')
291
453
  inc('__arr_from', '__dyn_move', '__ptr_offset')
292
454
  const dst2 = temp('ocd')
293
455
  const srcOff2 = tempI32('ocso')
@@ -317,12 +479,12 @@ export default (ctx) => {
317
479
  const srcBase = tempI32('cb')
318
480
  const body = [
319
481
  ['local.set', `$${s}`, asF64(emit(proto))],
320
- ['local.set', `$${t}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', Math.max(1, n)], ['i32.const', 8]]],
482
+ ['local.set', `$${t}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', ctx.abi.object.ops.allocSlots(n)]]],
321
483
  ['local.set', `$${srcBase}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${s}`]]]],
322
484
  ]
323
485
  // Copy all properties from proto
324
486
  for (let i = 0; i < n; i++)
325
- 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)))
326
488
  body.push(mkPtrIR(PTR.OBJECT, schemaId, ['local.get', `$${t}`]))
327
489
  return typed(['block', ['result', 'f64'], ...body], 'f64')
328
490
  }
@@ -330,6 +492,57 @@ export default (ctx) => {
330
492
 
331
493
  // --- Helpers ---
332
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
+
333
546
  function resolveSchema(obj) {
334
547
  if (typeof obj === 'string') return ctx.schema.resolve(obj)
335
548
  if (Array.isArray(obj) && obj[0] === '{}')
@@ -337,7 +550,7 @@ function resolveSchema(obj) {
337
550
  // JSON-shape inferred: JSON.parse(constStr) call or `.prop`/`[i]` chain
338
551
  // resolving to a known OBJECT shape carries its key list as `names`.
339
552
  const sh = shapeOf(obj)
340
- if (sh?.vt === VAL.OBJECT && sh.names) return sh.names
553
+ if (sh?.val === VAL.OBJECT && sh.names) return sh.names
341
554
  return null
342
555
  }
343
556
 
@@ -378,7 +591,7 @@ function emitObjectSpread(props, spreadTarget = takeLiteralTarget()) {
378
591
  const ptr = temp('objp')
379
592
  const src = tempI32('osp')
380
593
 
381
- const body = [['local.set', `$${t}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', Math.max(1, schema.length)], ['i32.const', 8]]]]
594
+ const body = [['local.set', `$${t}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', ctx.abi.object.ops.allocSlots(schema.length)]]]]
382
595
 
383
596
  // Process props in order — later props override earlier (JS semantics)
384
597
  let srcF
@@ -387,17 +600,17 @@ function emitObjectSpread(props, spreadTarget = takeLiteralTarget()) {
387
600
  const sSchema = resolveSchema(p[1])
388
601
  if (!sSchema) {
389
602
  // Unknown-schema source (e.g. parameter). Override each slot via runtime
390
- // __dyn_get_or using existing value as fallback. Requires collection module.
391
- if (!ctx.module.modules.collection) err('Object spread: source needs known schema')
603
+ // __dyn_get_or using existing value as fallback.
604
+ includeModule('collection')
392
605
  inc('__dyn_get_or')
393
606
  srcF ??= temp('ospf')
394
607
  body.push(['local.set', `$${srcF}`, asF64(emit(p[1]))])
395
608
  for (let i = 0; i < schema.length; i++) {
396
- const slot = slotAddr(t, i)
397
- body.push(['f64.store', slot,
609
+ const base = ['local.get', `$${t}`]
610
+ body.push(ctx.abi.object.ops.store(base, i,
398
611
  ['f64.reinterpret_i64', ['call', '$__dyn_get_or', ['i64.reinterpret_f64', ['local.get', `$${srcF}`]],
399
612
  asI64(emit(['str', String(schema[i])])),
400
- ['i64.load', slot]]]])
613
+ ctx.abi.object.ops.loadBits(base, i)]]))
401
614
  }
402
615
  continue
403
616
  }
@@ -405,11 +618,11 @@ function emitObjectSpread(props, spreadTarget = takeLiteralTarget()) {
405
618
  for (let si = 0; si < sSchema.length; si++) {
406
619
  const ti = schema.indexOf(sSchema[si])
407
620
  if (ti < 0) continue
408
- 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)))
409
622
  }
410
623
  } else if (Array.isArray(p) && p[0] === ':') {
411
624
  const ti = schema.indexOf(p[1])
412
- 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]))))
413
626
  }
414
627
  }
415
628
 
@@ -418,7 +631,7 @@ function emitObjectSpread(props, spreadTarget = takeLiteralTarget()) {
418
631
  inc('__dyn_set')
419
632
  for (let i = 0; i < schema.length; i++)
420
633
  body.push(['drop', ['call', '$__dyn_set', ['i64.reinterpret_f64', ['local.get', `$${ptr}`]], asI64(emit(['str', String(schema[i])])),
421
- ['i64.load', slotAddr(t, i)]]])
634
+ ctx.abi.object.ops.loadBits(['local.get', `$${t}`], i)]])
422
635
  }
423
636
  body.push(['local.get', `$${ptr}`])
424
637
  return typed(['block', ['result', 'f64'], ...body], 'f64')
@@ -462,6 +675,13 @@ function emitHashValues(obj) {
462
675
  hashValuesFromTemp(t)], 'f64')
463
676
  }
464
677
 
678
+ function emitHashEntries(obj) {
679
+ const t = temp('he')
680
+ return typed(['block', ['result', 'f64'],
681
+ ['local.set', `$${t}`, asF64(emit(obj))],
682
+ hashEntriesFromTemp(t)], 'f64')
683
+ }
684
+
465
685
  // Inline body of the HASH walk against an already-bound f64 local. Shared by
466
686
  // the static-HASH path and the runtime-dispatch path so both produce the same
467
687
  // IR shape from the same source — only difference is whether they enter from
@@ -520,22 +740,59 @@ function hashValuesFromTemp(t) {
520
740
  out.ptr]
521
741
  }
522
742
 
743
+ function hashEntriesFromTemp(t) {
744
+ inc('__ptr_offset', '__cap', '__len', '__alloc_hdr')
745
+ const off = tempI32('heo'), cap = tempI32('hec'), n = tempI32('hen')
746
+ const i = tempI32('hei'), o = tempI32('hej'), slot = tempI32('hes'), pair = tempI32('hep')
747
+ const out = allocPtr({ type: PTR.ARRAY, len: ['local.get', `$${n}`], tag: 'hea' })
748
+ const id = ctx.func.uniq++
749
+ return ['block', ['result', 'f64'],
750
+ ['local.set', `$${n}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
751
+ out.init,
752
+ ['local.set', `$${off}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
753
+ ['local.set', `$${cap}`, ['call', '$__cap', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
754
+ ['local.set', `$${i}`, ['i32.const', 0]],
755
+ ['local.set', `$${o}`, ['i32.const', 0]],
756
+ ['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]]]]],
769
+ ['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
770
+ ['br', `$eloop${id}`]]],
771
+ out.ptr]
772
+ }
773
+
523
774
  // Type-unknown receiver: bind the value, branch on ptr-type. HASH walks the
524
775
  // probe table; OBJECT loads the schema's key array (registered statically at
525
776
  // compile time or lazily at runtime by JSON.parse via __jp_schema_get); other
526
777
  // types (ARRAY, nullish, primitives) return an empty array. The empty-array
527
778
  // fallback is allocated in all arms for type uniformity at the if boundary.
528
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) {
529
787
  inc('__ptr_type')
530
788
  // Ensure the schema table global exists even in programs that never use
531
789
  // JSON.parse or compile-time schemas — the OBJECT arm reads it at runtime
532
790
  // and the watr resolver requires the symbol to be declared.
533
791
  if (!ctx.scope.globals.has('__schema_tbl'))
534
792
  ctx.scope.globals.set('__schema_tbl', '(global $__schema_tbl (mut i32) (i32.const 0))')
535
- const t = temp('rk'), tt = tempI32('rkt')
536
- const empty = allocPtr({ type: PTR.ARRAY, len: 0, tag: 'rke' })
537
- return typed(['block', ['result', 'f64'],
538
- ['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'],
539
796
  ['local.set', `$${tt}`, ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
540
797
  ['if', ['result', 'f64'],
541
798
  ['i32.eq', ['local.get', `$${tt}`], ['i32.const', PTR.HASH]],
@@ -545,7 +802,7 @@ function emitRuntimeKeys(obj) {
545
802
  ['i32.eq', ['local.get', `$${tt}`], ['i32.const', PTR.OBJECT]],
546
803
  ['i32.ne', ['global.get', '$__schema_tbl'], ['i32.const', 0]]],
547
804
  ['then', objectKeysFromTemp(t)],
548
- ['else', ['block', ['result', 'f64'], empty.init, empty.ptr]]]]]], 'f64')
805
+ ['else', ['block', ['result', 'f64'], empty.init, empty.ptr]]]]]]
549
806
  }
550
807
 
551
808
  function emitRuntimeValues(obj) {
@@ -568,6 +825,26 @@ function emitRuntimeValues(obj) {
568
825
  ['else', ['block', ['result', 'f64'], empty.init, empty.ptr]]]]]], 'f64')
569
826
  }
570
827
 
828
+ function emitRuntimeEntries(obj) {
829
+ inc('__ptr_type')
830
+ if (!ctx.scope.globals.has('__schema_tbl'))
831
+ ctx.scope.globals.set('__schema_tbl', '(global $__schema_tbl (mut i32) (i32.const 0))')
832
+ const t = temp('re'), tt = tempI32('ret')
833
+ const empty = allocPtr({ type: PTR.ARRAY, len: 0, tag: 'ree' })
834
+ return typed(['block', ['result', 'f64'],
835
+ ['local.set', `$${t}`, asF64(emit(obj))],
836
+ ['local.set', `$${tt}`, ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
837
+ ['if', ['result', 'f64'],
838
+ ['i32.eq', ['local.get', `$${tt}`], ['i32.const', PTR.HASH]],
839
+ ['then', hashEntriesFromTemp(t)],
840
+ ['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]]],
844
+ ['then', objectEntriesFromTemp(t)],
845
+ ['else', ['block', ['result', 'f64'], empty.init, empty.ptr]]]]]], 'f64')
846
+ }
847
+
571
848
  // Schema-keyed Object.keys: copy the schema's keys array (a jz Array of
572
849
  // STRINGs registered in __schema_tbl[sid]) into a fresh ARRAY so callers can
573
850
  // mutate without aliasing the schema substrate.
@@ -584,7 +861,7 @@ function objectKeysFromTemp(t) {
584
861
  ['i64.const', LAYOUT.OFFSET_MASK]]]],
585
862
  ['local.set', `$${n}`, ['i32.load', ['i32.sub', ['local.get', `$${src}`], ['i32.const', 8]]]],
586
863
  ['local.set', `$${out}`, ['call', '$__alloc_hdr',
587
- ['local.get', `$${n}`], ['local.get', `$${n}`], ['i32.const', 8]]],
864
+ ['local.get', `$${n}`], ['local.get', `$${n}`]]],
588
865
  ['local.set', `$${i}`, ['i32.const', 0]],
589
866
  ['block', `$kbrk${id}`, ['loop', `$kloop${id}`,
590
867
  ['br_if', `$kbrk${id}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${n}`]]],
@@ -612,7 +889,7 @@ function objectValuesFromTemp(t) {
612
889
  ['local.set', `$${n}`, ['i32.load', ['i32.sub', ['local.get', `$${src}`], ['i32.const', 8]]]],
613
890
  ['local.set', `$${base}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
614
891
  ['local.set', `$${out}`, ['call', '$__alloc_hdr',
615
- ['local.get', `$${n}`], ['local.get', `$${n}`], ['i32.const', 8]]],
892
+ ['local.get', `$${n}`], ['local.get', `$${n}`]]],
616
893
  ['local.set', `$${i}`, ['i32.const', 0]],
617
894
  ['block', `$ovbrk${id}`, ['loop', `$ovloop${id}`,
618
895
  ['br_if', `$ovbrk${id}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${n}`]]],
@@ -624,3 +901,39 @@ function objectValuesFromTemp(t) {
624
901
  ['br', `$ovloop${id}`]]],
625
902
  mkPtrIR(PTR.ARRAY, 0, ['local.get', `$${out}`])]
626
903
  }
904
+
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
+ }