jz 0.1.0 → 0.2.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,34 +7,51 @@
7
7
  * @module object
8
8
  */
9
9
 
10
- import { emit, typed, asF64, valTypeOf, lookupValType, VAL, temp, tempI32, allocPtr, needsDynShadow, mkPtrIR, extractF64Bits, appendStaticSlots, slotAddr, repOf, updateRep } from '../src/compile.js'
11
- import { ctx, err, inc, PTR } from '../src/ctx.js'
10
+ import { typed, asF64, asI64, temp, tempI32, allocPtr, needsDynShadow, mkPtrIR, extractF64Bits, appendStaticSlots, slotAddr, 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'
12
14
 
13
15
 
14
16
  export default (ctx) => {
15
17
  inc('__mkptr', '__alloc', '__alloc_hdr', '__ptr_offset', '__len', '__ptr_type')
16
18
 
17
- // Object literal: {x: 1, y: 2} → allocate, fill, return pointer with schemaId
19
+ // Object literal: {x: 1, y: 2} → allocate, fill, return pointer with schemaId.
20
+ // OBJECT alloc uses __alloc_hdr (16-byte header at off-16) to enable per-object
21
+ // propsPtr — dyn property writes (e.g. `ctx.metadata = {}` in watr) hit the
22
+ // per-object hash directly, skipping the global __dyn_props probe. The
23
+ // header gate `off >= __heap_start` keeps static-segment objects on the
24
+ // global-hash path (their off-16 belongs to neighboring static slots).
18
25
  ctx.core.emit['{}'] = (...rawProps) => {
19
- if (rawProps.length === 0)
20
- return mkPtrIR(PTR.OBJECT, 0, ['call', '$__alloc', ['i32.const', 8]])
26
+ if (rawProps.length === 0) {
27
+ // Honor the literal target's autobox/merged schema so `let ctx = {}` followed
28
+ // by `ctx.meta = ...` allocates with the right cap. Otherwise the default
29
+ // cap=1 alloc overwrites the autobox preamble's wrapper, and subsequent
30
+ // schema-slot writes to offsets >= 8 land out-of-bounds.
31
+ const target = takeLiteralTarget()
32
+ 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]])
36
+ }
21
37
 
22
38
  // Flatten comma-grouped props: [',', p1, p2] → [p1, p2]
23
39
  const props = rawProps.length === 1 && Array.isArray(rawProps[0]) && rawProps[0][0] === ','
24
40
  ? rawProps[0].slice(1) : rawProps
25
41
 
42
+ const target = takeLiteralTarget()
43
+
26
44
  // Object spread: {...a, x: 1, ...b} — merge schemas, copy props from sources
27
45
  const hasSpreads = props.some(p => Array.isArray(p) && p[0] === '...')
28
- if (hasSpreads) return emitObjectSpread(props)
46
+ if (hasSpreads) return emitObjectSpread(props, target)
29
47
 
30
48
  const names = [], values = []
31
49
  for (const p of props) {
32
50
  if (Array.isArray(p) && p[0] === ':') { names.push(p[1]); values.push(p[2]) }
33
51
  }
34
52
 
35
- // Use variable's merged schema if available (from Object.assign inference), else register literal schema
53
+ // Use variable's merged schema if available (from Object.assign inference), else register literal schema.
36
54
  let schemaId = ctx.schema.register(names)
37
- const target = ctx.schema.targetStack.at(-1)
38
55
  if (target) {
39
56
  const merged = ctx.schema.resolve(target)
40
57
  if (merged) schemaId = ctx.schema.idOf(target)
@@ -57,15 +74,15 @@ export default (ctx) => {
57
74
  inc('__dyn_set')
58
75
  const body = [['local.set', `$${ptr}`, staticPtr]]
59
76
  for (let i = 0; i < schema.length; i++)
60
- body.push(['drop', ['call', '$__dyn_set', ['local.get', `$${ptr}`],
61
- emit(['str', String(schema[i])]), asF64(emitted[i])]])
77
+ body.push(['drop', ['call', '$__dyn_set', ['i64.reinterpret_f64', ['local.get', `$${ptr}`]],
78
+ asI64(emit(['str', String(schema[i])])), asI64(emitted[i])]])
62
79
  body.push(['local.get', `$${ptr}`])
63
80
  return typed(['block', ['result', 'f64'], ...body], 'f64')
64
81
  }
65
82
  }
66
83
 
67
84
  const body = [
68
- ['local.set', `$${t}`, ['call', '$__alloc', ['i32.const', schema.length * 8]]],
85
+ ['local.set', `$${t}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', Math.max(1, schema.length)], ['i32.const', 8]]],
69
86
  ]
70
87
  for (let i = 0; i < values.length; i++)
71
88
  body.push(['f64.store', slotAddr(t, i), asF64(emit(values[i]))])
@@ -73,8 +90,8 @@ export default (ctx) => {
73
90
  if (shadow) {
74
91
  inc('__dyn_set')
75
92
  for (let i = 0; i < schema.length; i++)
76
- body.push(['drop', ['call', '$__dyn_set', ['local.get', `$${ptr}`], emit(['str', String(schema[i])]),
77
- ['f64.load', slotAddr(t, i)]]])
93
+ body.push(['drop', ['call', '$__dyn_set', ['i64.reinterpret_f64', ['local.get', `$${ptr}`]], asI64(emit(['str', String(schema[i])])),
94
+ ['i64.load', slotAddr(t, i)]]])
78
95
  }
79
96
  body.push(['local.get', `$${ptr}`])
80
97
 
@@ -84,10 +101,37 @@ export default (ctx) => {
84
101
  // === Object static methods ===
85
102
 
86
103
  ctx.core.emit['Object.keys'] = (obj) => {
104
+ if (isHashTyped(obj)) return emitHashKeys(obj)
87
105
  const schema = resolveSchema(obj)
88
- if (!schema) err('Object.keys requires object with known schema')
89
- return emitStringArray(schema)
106
+ if (schema) return emitStringArray(schema)
107
+ // Receiver type unknown at compile time. Dispatch on ptr-type at
108
+ // runtime: HASH walks the probe table, anything else returns [].
109
+ return emitRuntimeKeys(obj)
110
+ }
111
+
112
+ // Object.prototype.hasOwnProperty(key) — own-property presence check.
113
+ // Compile-time fold for literal keys against object literals or variables
114
+ // with known schemas; runtime path delegates to the `in` operator (same
115
+ // ptr-type dispatch + __hash_has for HASH, dyn_props probe for OBJECT).
116
+ ctx.core.emit['.hasOwnProperty'] = (obj, key) => {
117
+ const litKey = Array.isArray(key) && key[0] === 'str' ? String(key[1]) : null
118
+ if (litKey != null) {
119
+ if (Array.isArray(obj) && obj[0] === '{}') {
120
+ const has = obj.slice(1).some(p => Array.isArray(p) && p[0] === ':' && String(p[1]) === litKey)
121
+ return typed(['block', ['result', 'f64'],
122
+ ['drop', asF64(emit(obj))],
123
+ ['f64.const', has ? 1 : 0]], 'f64')
124
+ }
125
+ if (typeof obj === 'string' && ctx.schema.find?.(obj, litKey) >= 0)
126
+ return typed(['f64.const', 1], 'f64')
127
+ }
128
+ return typed(['f64.convert_i32_s', emit(['in', key, obj])], 'f64')
90
129
  }
130
+ ctx.core.emit[`.${VAL.HASH}:hasOwnProperty`] = ctx.core.emit['.hasOwnProperty']
131
+ ctx.core.emit[`.${VAL.OBJECT}:hasOwnProperty`] = ctx.core.emit['.hasOwnProperty']
132
+ ctx.core.emit[`.${VAL.ARRAY}:hasOwnProperty`] = ctx.core.emit['.hasOwnProperty']
133
+ ctx.core.emit[`.${VAL.STRING}:hasOwnProperty`] = ctx.core.emit['.hasOwnProperty']
134
+ ctx.core.emit[`.${VAL.CLOSURE}:hasOwnProperty`] = ctx.core.emit['.hasOwnProperty']
91
135
 
92
136
  ctx.core.emit['Object.values'] = (obj) => {
93
137
  const schema = resolveSchema(obj)
@@ -97,7 +141,7 @@ export default (ctx) => {
97
141
  const t = temp('ov'), base = tempI32('vb')
98
142
  const out = allocPtr({ type: PTR.ARRAY, len: n, tag: 'oa' })
99
143
  const body = [['local.set', `$${t}`, va], out.init,
100
- ['local.set', `$${base}`, ['call', '$__ptr_offset', ['local.get', `$${t}`]]]]
144
+ ['local.set', `$${base}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]]]
101
145
  for (let i = 0; i < n; i++)
102
146
  body.push(['f64.store', slotAddr(out.local, i), ['f64.load', slotAddr(base, i)]])
103
147
  body.push(out.ptr)
@@ -112,7 +156,7 @@ export default (ctx) => {
112
156
  const t = temp('oe'), pair = tempI32('op'), base = tempI32('eb')
113
157
  const out = allocPtr({ type: PTR.ARRAY, len: n, tag: 'oa' })
114
158
  const body = [['local.set', `$${t}`, va], out.init,
115
- ['local.set', `$${base}`, ['call', '$__ptr_offset', ['local.get', `$${t}`]]]]
159
+ ['local.set', `$${base}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]]]
116
160
  for (let i = 0; i < n; i++) {
117
161
  body.push(
118
162
  ['local.set', `$${pair}`, ['call', '$__alloc_hdr', ['i32.const', 2], ['i32.const', 2], ['i32.const', 8]]],
@@ -140,14 +184,14 @@ export default (ctx) => {
140
184
  updateRep(target, { schemaId })
141
185
  const t = tempI32('bx'), s = temp('bs')
142
186
  const body = [
143
- ['local.set', `$${t}`, ['call', '$__alloc', ['i32.const', boxedSchema.length * 8]]],
187
+ ['local.set', `$${t}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', Math.max(1, boxedSchema.length)], ['i32.const', 8]]],
144
188
  ['f64.store', ['local.get', `$${t}`], asF64(emit(target))],
145
189
  ]
146
190
  const sBase = tempI32('sb')
147
191
  for (const source of sources) {
148
192
  const sSchema = resolveSchema(source)
149
193
  body.push(['local.set', `$${s}`, asF64(emit(source))])
150
- body.push(['local.set', `$${sBase}`, ['call', '$__ptr_offset', ['local.get', `$${s}`]]])
194
+ body.push(['local.set', `$${sBase}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${s}`]]]])
151
195
  for (let si = 0; si < sSchema.length; si++) {
152
196
  const ti = boxedSchema.indexOf(sSchema[si])
153
197
  if (ti < 0) continue
@@ -165,12 +209,12 @@ export default (ctx) => {
165
209
  const t = temp('at'), s = temp('as')
166
210
  const tBase = tempI32('tb'), sBase2 = tempI32('sb')
167
211
  const body = [['local.set', `$${t}`, asF64(emit(target))],
168
- ['local.set', `$${tBase}`, ['call', '$__ptr_offset', ['local.get', `$${t}`]]]]
212
+ ['local.set', `$${tBase}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]]]
169
213
  for (const source of sources) {
170
214
  const sSchema = resolveSchema(source)
171
215
  if (!sSchema) err('Object.assign: source needs known schema')
172
216
  body.push(['local.set', `$${s}`, asF64(emit(source))])
173
- body.push(['local.set', `$${sBase2}`, ['call', '$__ptr_offset', ['local.get', `$${s}`]]])
217
+ body.push(['local.set', `$${sBase2}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${s}`]]]])
174
218
  for (let si = 0; si < sSchema.length; si++) {
175
219
  const ti = tSchema.indexOf(sSchema[si])
176
220
  if (ti < 0) continue
@@ -191,18 +235,18 @@ export default (ctx) => {
191
235
  const id = ctx.func.uniq++
192
236
  return typed(['block', ['result', 'f64'],
193
237
  ['local.set', `$${t}`, ['call', '$__hash_new']],
194
- ['local.set', `$${ptr}`, ['call', '$__ptr_offset', va]],
195
- ['local.set', `$${len}`, ['call', '$__len', va]],
238
+ ['local.set', `$${ptr}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', va]]],
239
+ ['local.set', `$${len}`, ['call', '$__len', ['i64.reinterpret_f64', va]]],
196
240
  ['local.set', `$${i}`, ['i32.const', 0]],
197
241
  ['block', `$brk${id}`, ['loop', `$loop${id}`,
198
242
  ['br_if', `$brk${id}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${len}`]]],
199
243
  // Load pair (array of 2): pair = ptr_offset(arr[i])
200
- ['local.set', `$${pair}`, ['call', '$__ptr_offset',
201
- ['f64.load', ['i32.add', ['local.get', `$${ptr}`], ['i32.shl', ['local.get', `$${i}`], ['i32.const', 3]]]]]],
244
+ ['local.set', `$${pair}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64',
245
+ ['f64.load', ['i32.add', ['local.get', `$${ptr}`], ['i32.shl', ['local.get', `$${i}`], ['i32.const', 3]]]]]]],
202
246
  // hash_set(result, pair[0], pair[1])
203
- ['local.set', `$${t}`, ['call', '$__hash_set', ['local.get', `$${t}`],
204
- ['f64.load', ['local.get', `$${pair}`]],
205
- ['f64.load', ['i32.add', ['local.get', `$${pair}`], ['i32.const', 8]]]]],
247
+ ['local.set', `$${t}`, ['f64.reinterpret_i64', ['call', '$__hash_set', ['i64.reinterpret_f64', ['local.get', `$${t}`]],
248
+ ['i64.load', ['local.get', `$${pair}`]],
249
+ ['i64.load', ['i32.add', ['local.get', `$${pair}`], ['i32.const', 8]]]]]],
206
250
  ['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
207
251
  ['br', `$loop${id}`]]],
208
252
  ['local.get', `$${t}`]], 'f64')
@@ -214,15 +258,25 @@ export default (ctx) => {
214
258
  if (protoType === VAL.ARRAY) {
215
259
  // Clone array data + link named-prop sidecar so for-in/bracket-name lookups
216
260
  // keep working after Object.create (watr's ctx.local = Object.create(param) pattern).
261
+ // Header propsPtr lives at $off-16 (current ARRAY layout). We alias src's hash
262
+ // by copying the slot; __dyn_move covers the shifted-array case where props
263
+ // were migrated to the global __dyn_props.
217
264
  inc('__arr_from', '__dyn_move', '__ptr_offset')
218
265
  const src = temp('ocs')
219
266
  const dst = temp('ocd')
267
+ const srcOff = tempI32('ocso')
268
+ const dstOff = tempI32('ocdo')
220
269
  return typed(['block', ['result', 'f64'],
221
270
  ['local.set', `$${src}`, asF64(emit(proto))],
222
- ['local.set', `$${dst}`, ['call', '$__arr_from', ['local.get', `$${src}`]]],
271
+ ['local.set', `$${dst}`, ['call', '$__arr_from', ['i64.reinterpret_f64', ['local.get', `$${src}`]]]],
272
+ ['local.set', `$${srcOff}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${src}`]]]],
273
+ ['local.set', `$${dstOff}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${dst}`]]]],
274
+ ['f64.store',
275
+ ['i32.sub', ['local.get', `$${dstOff}`], ['i32.const', 16]],
276
+ ['f64.load', ['i32.sub', ['local.get', `$${srcOff}`], ['i32.const', 16]]]],
223
277
  ['call', '$__dyn_move',
224
- ['call', '$__ptr_offset', ['local.get', `$${src}`]],
225
- ['call', '$__ptr_offset', ['local.get', `$${dst}`]]],
278
+ ['local.get', `$${srcOff}`],
279
+ ['local.get', `$${dstOff}`]],
226
280
  ['local.get', `$${dst}`]], 'f64')
227
281
  }
228
282
  const schema = resolveSchema(proto)
@@ -231,15 +285,22 @@ export default (ctx) => {
231
285
  const value = temp('ocr')
232
286
  inc('__arr_from', '__dyn_move', '__ptr_offset')
233
287
  const dst2 = temp('ocd')
288
+ const srcOff2 = tempI32('ocso')
289
+ const dstOff2 = tempI32('ocdo')
234
290
  return typed(['block', ['result', 'f64'],
235
291
  ['local.set', `$${value}`, asF64(emit(proto))],
236
292
  ['if', ['result', 'f64'],
237
- ['i32.eq', ['call', '$__ptr_type', ['local.get', `$${value}`]], ['i32.const', PTR.ARRAY]],
293
+ ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${value}`]]], ['i32.const', PTR.ARRAY]],
238
294
  ['then', ['block', ['result', 'f64'],
239
- ['local.set', `$${dst2}`, ['call', '$__arr_from', ['local.get', `$${value}`]]],
295
+ ['local.set', `$${dst2}`, ['call', '$__arr_from', ['i64.reinterpret_f64', ['local.get', `$${value}`]]]],
296
+ ['local.set', `$${srcOff2}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${value}`]]]],
297
+ ['local.set', `$${dstOff2}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${dst2}`]]]],
298
+ ['f64.store',
299
+ ['i32.sub', ['local.get', `$${dstOff2}`], ['i32.const', 16]],
300
+ ['f64.load', ['i32.sub', ['local.get', `$${srcOff2}`], ['i32.const', 16]]]],
240
301
  ['call', '$__dyn_move',
241
- ['call', '$__ptr_offset', ['local.get', `$${value}`]],
242
- ['call', '$__ptr_offset', ['local.get', `$${dst2}`]]],
302
+ ['local.get', `$${srcOff2}`],
303
+ ['local.get', `$${dstOff2}`]],
243
304
  ['local.get', `$${dst2}`]]],
244
305
  ['else', ['local.get', `$${value}`]]]] , 'f64')
245
306
  }
@@ -251,8 +312,8 @@ export default (ctx) => {
251
312
  const srcBase = tempI32('cb')
252
313
  const body = [
253
314
  ['local.set', `$${s}`, asF64(emit(proto))],
254
- ['local.set', `$${t}`, ['call', '$__alloc', ['i32.const', n * 8]]],
255
- ['local.set', `$${srcBase}`, ['call', '$__ptr_offset', ['local.get', `$${s}`]]],
315
+ ['local.set', `$${t}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', Math.max(1, n)], ['i32.const', 8]]],
316
+ ['local.set', `$${srcBase}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${s}`]]]],
256
317
  ]
257
318
  // Copy all properties from proto
258
319
  for (let i = 0; i < n; i++)
@@ -268,6 +329,10 @@ function resolveSchema(obj) {
268
329
  if (typeof obj === 'string') return ctx.schema.resolve(obj)
269
330
  if (Array.isArray(obj) && obj[0] === '{}')
270
331
  return obj.slice(1).filter(p => Array.isArray(p) && p[0] === ':').map(p => p[1])
332
+ // JSON-shape inferred: JSON.parse(constStr) call or `.prop`/`[i]` chain
333
+ // resolving to a known OBJECT shape carries its key list as `names`.
334
+ const sh = shapeOf(obj)
335
+ if (sh?.vt === VAL.OBJECT && sh.names) return sh.names
271
336
  return null
272
337
  }
273
338
 
@@ -275,7 +340,16 @@ function resolveSchema(obj) {
275
340
  * Emit object literal with spread: {...a, x: 1, ...b, y: 2}
276
341
  * Merges schemas from all sources, allocates result, copies in order.
277
342
  */
278
- function emitObjectSpread(props) {
343
+ function takeLiteralTarget() {
344
+ const frame = ctx.schema.targetStack.at(-1)
345
+ if (!frame) return null
346
+ if (typeof frame === 'string') return frame
347
+ if (!frame.active) return null
348
+ frame.active = false
349
+ return frame.name
350
+ }
351
+
352
+ function emitObjectSpread(props, spreadTarget = takeLiteralTarget()) {
279
353
  // Collect merged schema: union of all spread source schemas + explicit props
280
354
  const allNames = []
281
355
  const addName = n => { if (!allNames.includes(n)) allNames.push(n) }
@@ -299,7 +373,7 @@ function emitObjectSpread(props) {
299
373
  const ptr = temp('objp')
300
374
  const src = tempI32('osp')
301
375
 
302
- const body = [['local.set', `$${t}`, ['call', '$__alloc', ['i32.const', schema.length * 8]]]]
376
+ const body = [['local.set', `$${t}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', Math.max(1, schema.length)], ['i32.const', 8]]]]
303
377
 
304
378
  // Process props in order — later props override earlier (JS semantics)
305
379
  let srcF
@@ -316,13 +390,13 @@ function emitObjectSpread(props) {
316
390
  for (let i = 0; i < schema.length; i++) {
317
391
  const slot = slotAddr(t, i)
318
392
  body.push(['f64.store', slot,
319
- ['call', '$__dyn_get_or', ['local.get', `$${srcF}`],
320
- emit(['str', String(schema[i])]),
321
- ['f64.load', slot]]])
393
+ ['f64.reinterpret_i64', ['call', '$__dyn_get_or', ['i64.reinterpret_f64', ['local.get', `$${srcF}`]],
394
+ asI64(emit(['str', String(schema[i])])),
395
+ ['i64.load', slot]]]])
322
396
  }
323
397
  continue
324
398
  }
325
- body.push(['local.set', `$${src}`, ['call', '$__ptr_offset', asF64(emit(p[1]))]])
399
+ body.push(['local.set', `$${src}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', asF64(emit(p[1]))]]])
326
400
  for (let si = 0; si < sSchema.length; si++) {
327
401
  const ti = schema.indexOf(sSchema[si])
328
402
  if (ti < 0) continue
@@ -335,12 +409,11 @@ function emitObjectSpread(props) {
335
409
  }
336
410
 
337
411
  body.push(['local.set', `$${ptr}`, mkPtrIR(PTR.OBJECT, schemaId, ['local.get', `$${t}`])])
338
- const spreadTarget = ctx.schema.targetStack.at(-1)
339
412
  if (needsDynShadow(spreadTarget)) {
340
413
  inc('__dyn_set')
341
414
  for (let i = 0; i < schema.length; i++)
342
- body.push(['drop', ['call', '$__dyn_set', ['local.get', `$${ptr}`], emit(['str', String(schema[i])]),
343
- ['f64.load', slotAddr(t, i)]]])
415
+ body.push(['drop', ['call', '$__dyn_set', ['i64.reinterpret_f64', ['local.get', `$${ptr}`]], asI64(emit(['str', String(schema[i])])),
416
+ ['i64.load', slotAddr(t, i)]]])
344
417
  }
345
418
  body.push(['local.get', `$${ptr}`])
346
419
  return typed(['block', ['result', 'f64'], ...body], 'f64')
@@ -355,3 +428,112 @@ function emitStringArray(names) {
355
428
  body.push(out.ptr)
356
429
  return typed(['block', ['result', 'f64'], ...body], 'f64')
357
430
  }
431
+
432
+ // VAL.HASH covers both literal-typed bindings and JSON-shape inferred chains
433
+ // (e.g. JSON.parse('{...}') → walked via shapeOf for nested `.prop` access).
434
+ // Schema fallback only fires when the static path can't classify the receiver.
435
+ function isHashTyped(obj) {
436
+ if (typeof obj === 'string') return lookupValType(obj) === VAL.HASH
437
+ return valTypeOf(obj) === VAL.HASH
438
+ }
439
+
440
+ // HASH layout: open-addressed probe table, each entry 24 bytes —
441
+ // [hash:f64][key:f64][value:f64]. Slot is empty when hash field == 0
442
+ // (tombstone == 1). __len exposes live entry count at off-8; __cap exposes
443
+ // slot count at off-4. Output array is pre-sized to __len; walk all cap
444
+ // slots and append occupied keys. Iteration order is hash-derived, matching
445
+ // jz's `for-in` over HASH — not the JS spec's insertion order.
446
+ function emitHashKeys(obj) {
447
+ const t = temp('hk')
448
+ return typed(['block', ['result', 'f64'],
449
+ ['local.set', `$${t}`, asF64(emit(obj))],
450
+ hashKeysFromTemp(t)], 'f64')
451
+ }
452
+
453
+ // Inline body of the HASH walk against an already-bound f64 local. Shared by
454
+ // the static-HASH path and the runtime-dispatch path so both produce the same
455
+ // IR shape from the same source — only difference is whether they enter from
456
+ // a static type guard or a runtime ptr-type check.
457
+ function hashKeysFromTemp(t) {
458
+ inc('__ptr_offset', '__cap', '__len')
459
+ const off = tempI32('hko'), cap = tempI32('hkc'), n = tempI32('hkn')
460
+ const i = tempI32('hki'), o = tempI32('hkj'), slot = tempI32('hks')
461
+ const out = allocPtr({ type: PTR.ARRAY, len: ['local.get', `$${n}`], tag: 'hka' })
462
+ const id = ctx.func.uniq++
463
+ return ['block', ['result', 'f64'],
464
+ ['local.set', `$${n}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
465
+ out.init,
466
+ ['local.set', `$${off}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
467
+ ['local.set', `$${cap}`, ['call', '$__cap', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
468
+ ['local.set', `$${i}`, ['i32.const', 0]],
469
+ ['local.set', `$${o}`, ['i32.const', 0]],
470
+ ['block', `$brk${id}`, ['loop', `$loop${id}`,
471
+ ['br_if', `$brk${id}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${cap}`]]],
472
+ ['local.set', `$${slot}`, ['i32.add', ['local.get', `$${off}`],
473
+ ['i32.mul', ['local.get', `$${i}`], ['i32.const', 24]]]],
474
+ ['if', ['f64.ne', ['f64.load', ['local.get', `$${slot}`]], ['f64.const', 0]],
475
+ ['then',
476
+ elemStore(out.local, o,
477
+ ['f64.load', ['i32.add', ['local.get', `$${slot}`], ['i32.const', 8]]]),
478
+ ['local.set', `$${o}`, ['i32.add', ['local.get', `$${o}`], ['i32.const', 1]]]]],
479
+ ['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
480
+ ['br', `$loop${id}`]]],
481
+ out.ptr]
482
+ }
483
+
484
+ // Type-unknown receiver: bind the value, branch on ptr-type. HASH walks the
485
+ // probe table; OBJECT loads the schema's key array (registered statically at
486
+ // compile time or lazily at runtime by JSON.parse via __jp_schema_get); other
487
+ // types (ARRAY, nullish, primitives) return an empty array. The empty-array
488
+ // fallback is allocated in all arms for type uniformity at the if boundary.
489
+ function emitRuntimeKeys(obj) {
490
+ inc('__ptr_type')
491
+ // Ensure the schema table global exists even in programs that never use
492
+ // JSON.parse or compile-time schemas — the OBJECT arm reads it at runtime
493
+ // and the watr resolver requires the symbol to be declared.
494
+ if (!ctx.scope.globals.has('__schema_tbl'))
495
+ ctx.scope.globals.set('__schema_tbl', '(global $__schema_tbl (mut i32) (i32.const 0))')
496
+ const t = temp('rk'), tt = tempI32('rkt')
497
+ const empty = allocPtr({ type: PTR.ARRAY, len: 0, tag: 'rke' })
498
+ return typed(['block', ['result', 'f64'],
499
+ ['local.set', `$${t}`, asF64(emit(obj))],
500
+ ['local.set', `$${tt}`, ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
501
+ ['if', ['result', 'f64'],
502
+ ['i32.eq', ['local.get', `$${tt}`], ['i32.const', PTR.HASH]],
503
+ ['then', hashKeysFromTemp(t)],
504
+ ['else', ['if', ['result', 'f64'],
505
+ ['i32.and',
506
+ ['i32.eq', ['local.get', `$${tt}`], ['i32.const', PTR.OBJECT]],
507
+ ['i32.ne', ['global.get', '$__schema_tbl'], ['i32.const', 0]]],
508
+ ['then', objectKeysFromTemp(t)],
509
+ ['else', ['block', ['result', 'f64'], empty.init, empty.ptr]]]]]], 'f64')
510
+ }
511
+
512
+ // Schema-keyed Object.keys: copy the schema's keys array (a jz Array of
513
+ // STRINGs registered in __schema_tbl[sid]) into a fresh ARRAY so callers can
514
+ // mutate without aliasing the schema substrate.
515
+ function objectKeysFromTemp(t) {
516
+ inc('__alloc_hdr')
517
+ const sid = tempI32('oks'), src = tempI32('oksrc'), n = tempI32('okn'), out = tempI32('oko'), i = tempI32('oki')
518
+ const id = ctx.func.uniq++
519
+ return ['block', ['result', 'f64'],
520
+ ['local.set', `$${sid}`, ['i32.wrap_i64', ['i64.and',
521
+ ['i64.shr_u', ['i64.reinterpret_f64', ['local.get', `$${t}`]], ['i64.const', LAYOUT.AUX_SHIFT]],
522
+ ['i64.const', LAYOUT.AUX_MASK]]]],
523
+ ['local.set', `$${src}`, ['i32.wrap_i64', ['i64.and',
524
+ ['i64.load', ['i32.add', ['global.get', '$__schema_tbl'], ['i32.shl', ['local.get', `$${sid}`], ['i32.const', 3]]]],
525
+ ['i64.const', LAYOUT.OFFSET_MASK]]]],
526
+ ['local.set', `$${n}`, ['i32.load', ['i32.sub', ['local.get', `$${src}`], ['i32.const', 8]]]],
527
+ ['local.set', `$${out}`, ['call', '$__alloc_hdr',
528
+ ['local.get', `$${n}`], ['local.get', `$${n}`], ['i32.const', 8]]],
529
+ ['local.set', `$${i}`, ['i32.const', 0]],
530
+ ['block', `$kbrk${id}`, ['loop', `$kloop${id}`,
531
+ ['br_if', `$kbrk${id}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${n}`]]],
532
+ ['i64.store',
533
+ ['i32.add', ['local.get', `$${out}`], ['i32.shl', ['local.get', `$${i}`], ['i32.const', 3]]],
534
+ ['i64.load',
535
+ ['i32.add', ['local.get', `$${src}`], ['i32.shl', ['local.get', `$${i}`], ['i32.const', 3]]]]],
536
+ ['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
537
+ ['br', `$kloop${id}`]]],
538
+ mkPtrIR(PTR.ARRAY, 0, ['local.get', `$${out}`])]
539
+ }
package/module/regex.js CHANGED
@@ -7,8 +7,9 @@
7
7
  * @module regex
8
8
  */
9
9
 
10
- import { emit, typed, asF64, UNDEF_NAN, mkPtrIR, temp, tempI32 } from '../src/compile.js'
11
- import { err, inc, PTR } from '../src/ctx.js'
10
+ import { typed, asF64, asI64, UNDEF_NAN, mkPtrIR, temp, tempI32 } from '../src/ir.js'
11
+ import { emit } from '../src/emit.js'
12
+ import { err, inc, PTR, LAYOUT } from '../src/ctx.js'
12
13
 
13
14
  // Build IR that constructs a match array: [full, cap1, cap2, ...]
14
15
  // strLocal, msLocal, meLocal are local names (i32 for ms/me, f64 for str).
@@ -22,7 +23,7 @@ const buildMatchArr = (strLocal, msLocal, meLocal, nGroups) => {
22
23
  ['i32.store', ['local.get', `$${arr}`], ['i32.const', N]],
23
24
  ['i32.store', ['i32.add', ['local.get', `$${arr}`], ['i32.const', 4]], ['i32.const', N]],
24
25
  ['f64.store', ['i32.add', ['local.get', `$${arr}`], ['i32.const', 8]],
25
- ['call', '$__str_slice', ['local.get', `$${strLocal}`],
26
+ ['call', '$__str_slice', ['i64.reinterpret_f64', ['local.get', `$${strLocal}`]],
26
27
  ['local.get', `$${msLocal}`], ['local.get', `$${meLocal}`]]],
27
28
  ]
28
29
  for (let i = 1; i <= nGroups; i++) {
@@ -30,7 +31,7 @@ const buildMatchArr = (strLocal, msLocal, meLocal, nGroups) => {
30
31
  ['if', ['result', 'f64'],
31
32
  ['i32.lt_s', ['global.get', `$__re_g${i}_start`], ['i32.const', 0]],
32
33
  ['then', ['f64.const', `nan:${UNDEF_NAN}`]],
33
- ['else', ['call', '$__str_slice', ['local.get', `$${strLocal}`],
34
+ ['else', ['call', '$__str_slice', ['i64.reinterpret_f64', ['local.get', `$${strLocal}`]],
34
35
  ['global.get', `$__re_g${i}_start`], ['global.get', `$__re_g${i}_end`]]]]])
35
36
  }
36
37
  stmts.push(mkPtrIR(PTR.ARRAY, 0, ['i32.add', ['local.get', `$${arr}`], ['i32.const', 8]]))
@@ -653,14 +654,16 @@ export default (ctx) => {
653
654
 
654
655
  ctx.runtime.regex = { count: 0, vars: new Map(), compiled: new Map(), groups: new Map() }
655
656
 
656
- // SSO → heap normalizer: returns data offset (i32) for direct byte access
657
- ctx.core.stdlib['__str_to_buf'] = `(func $__str_to_buf (param $ptr f64) (result i32)
658
- (local $type i32) (local $off i32) (local $len i32) (local $buf i32) (local $i i32)
659
- (local.set $type (call $__ptr_type (local.get $ptr)))
660
- (if (i32.eq (local.get $type) (i32.const 4))
657
+ // SSO → heap normalizer: returns data offset (i32) for direct byte access.
658
+ // Heap STRING: aux bit SSO_BIT is 0 → offset already points at bytes.
659
+ // SSO STRING: aux bit SSO_BIT is 1 bytes are packed in offset; spill to heap.
660
+ ctx.core.stdlib['__str_to_buf'] = `(func $__str_to_buf (param $ptr i64) (result i32)
661
+ (local $aux i32) (local $off i32) (local $len i32) (local $buf i32) (local $i i32)
662
+ (local.set $aux (call $__ptr_aux (local.get $ptr)))
663
+ (if (i32.eqz (i32.and (local.get $aux) (i32.const ${LAYOUT.SSO_BIT})))
661
664
  (then (return (call $__ptr_offset (local.get $ptr)))))
662
665
  (local.set $off (call $__ptr_offset (local.get $ptr)))
663
- (local.set $len (call $__ptr_aux (local.get $ptr)))
666
+ (local.set $len (i32.and (local.get $aux) (i32.const 7)))
664
667
  (local.set $buf (call $__alloc (local.get $len)))
665
668
  (local.set $i (i32.const 0))
666
669
  (block $done (loop $next
@@ -690,7 +693,7 @@ export default (ctx) => {
690
693
 
691
694
  // Search wrapper: tries match at each position, returns (match_start, match_end) via locals
692
695
  const searchName = `__regex_search_${id}`
693
- ctx.core.stdlib[searchName] = `(func $${searchName} (param $str f64) (result i32 i32)
696
+ ctx.core.stdlib[searchName] = `(func $${searchName} (param $str i64) (result i32 i32)
694
697
  (local $off i32) (local $len i32) (local $pos i32) (local $result i32)
695
698
  (local.set $off (call $__str_to_buf (local.get $str)))
696
699
  (local.set $len (call $__str_byteLen (local.get $str)))
@@ -734,7 +737,7 @@ export default (ctx) => {
734
737
  return typed(['block', ['result', 'f64'],
735
738
  ['local.set', `$${s}`, asF64(emit(str))],
736
739
  ['local.set', `$${mstart}`, ['local.set', `$${mend}`,
737
- ['call', `$__regex_search_${id}`, ['local.get', `$${s}`]]]],
740
+ ['call', `$__regex_search_${id}`, ['i64.reinterpret_f64', ['local.get', `$${s}`]]]]],
738
741
  // search returns (start, end) multi-value; capture both
739
742
  ['if', ['result', 'f64'], ['i32.ge_s', ['local.get', `$${mstart}`], ['i32.const', 0]],
740
743
  ['then', ['f64.const', 1]],
@@ -750,7 +753,7 @@ export default (ctx) => {
750
753
  return typed(['block', ['result', 'f64'],
751
754
  ['local.set', `$${s}`, asF64(emit(str))],
752
755
  ['local.set', `$${ms}`, ['local.set', `$${me}`,
753
- ['call', `$__regex_search_${id}`, ['local.get', `$${s}`]]]],
756
+ ['call', `$__regex_search_${id}`, ['i64.reinterpret_f64', ['local.get', `$${s}`]]]]],
754
757
  ['if', ['result', 'f64'], ['i32.lt_s', ['local.get', `$${ms}`], ['i32.const', 0]],
755
758
  ['then', ['f64.const', 0]],
756
759
  ['else', buildMatchArr(s, ms, me, nGroups)]]], 'f64')
@@ -762,13 +765,13 @@ export default (ctx) => {
762
765
  if (id == null) {
763
766
  // Fall back to string search (indexOf)
764
767
  inc('__str_indexof')
765
- return typed(['f64.convert_i32_s', ['call', '$__str_indexof', asF64(emit(str)), asF64(emit(search))]], 'f64')
768
+ return typed(['f64.convert_i32_s', ['call', '$__str_indexof', asI64(emit(str)), asI64(emit(search)), ['i32.const', 0]]], 'f64')
766
769
  }
767
770
  const s = temp('ss'), ms = tempI32('ssms'), me = tempI32('ssme')
768
771
  return typed(['block', ['result', 'f64'],
769
772
  ['local.set', `$${s}`, asF64(emit(str))],
770
773
  ['local.set', `$${ms}`, ['local.set', `$${me}`,
771
- ['call', `$__regex_search_${id}`, ['local.get', `$${s}`]]]],
774
+ ['call', `$__regex_search_${id}`, ['i64.reinterpret_f64', ['local.get', `$${s}`]]]]],
772
775
  ['f64.convert_i32_s', ['local.get', `$${ms}`]]], 'f64')
773
776
  }
774
777
 
@@ -782,21 +785,22 @@ export default (ctx) => {
782
785
  return typed(['block', ['result', 'f64'],
783
786
  ['local.set', `$${s}`, asF64(emit(str))],
784
787
  ['local.set', `$${q}`, asF64(emit(search))],
785
- ['local.set', `$${idx}`, ['call', '$__str_indexof', ['local.get', `$${s}`], ['local.get', `$${q}`]]],
788
+ ['local.set', `$${idx}`, ['call', '$__str_indexof', ['i64.reinterpret_f64', ['local.get', `$${s}`]], ['i64.reinterpret_f64', ['local.get', `$${q}`]], ['i32.const', 0]]],
786
789
  ['if', ['result', 'f64'], ['i32.lt_s', ['local.get', `$${idx}`], ['i32.const', 0]],
787
790
  ['then', ['f64.const', 0]],
788
791
  ['else',
789
792
  ['call', '$__wrap1',
790
- ['call', '$__str_slice', ['local.get', `$${s}`],
791
- ['local.get', `$${idx}`],
792
- ['i32.add', ['local.get', `$${idx}`], ['call', '$__str_byteLen', ['local.get', `$${q}`]]]]]]]], 'f64')
793
+ ['i64.reinterpret_f64',
794
+ ['call', '$__str_slice', ['i64.reinterpret_f64', ['local.get', `$${s}`]],
795
+ ['local.get', `$${idx}`],
796
+ ['i32.add', ['local.get', `$${idx}`], ['call', '$__str_byteLen', ['i64.reinterpret_f64', ['local.get', `$${q}`]]]]]]]]]], 'f64')
793
797
  }
794
798
  const nGroups = ctx.runtime.regex.groups.get(id) || 0
795
799
  const s = temp('sm'), ms = tempI32('smms'), me = tempI32('smme')
796
800
  return typed(['block', ['result', 'f64'],
797
801
  ['local.set', `$${s}`, asF64(emit(str))],
798
802
  ['local.set', `$${ms}`, ['local.set', `$${me}`,
799
- ['call', `$__regex_search_${id}`, ['local.get', `$${s}`]]]],
803
+ ['call', `$__regex_search_${id}`, ['i64.reinterpret_f64', ['local.get', `$${s}`]]]]],
800
804
  ['if', ['result', 'f64'], ['i32.lt_s', ['local.get', `$${ms}`], ['i32.const', 0]],
801
805
  ['then', ['f64.const', 0]],
802
806
  ['else', buildMatchArr(s, ms, me, nGroups)]]], 'f64')
@@ -808,7 +812,7 @@ export default (ctx) => {
808
812
  if (id == null) {
809
813
  // Fall back to string replace
810
814
  inc('__str_replace')
811
- return typed(['call', '$__str_replace', asF64(emit(str)), asF64(emit(search)), asF64(emit(repl))], 'f64')
815
+ return typed(['call', '$__str_replace', asI64(emit(str)), asI64(emit(search)), asI64(emit(repl))], 'f64')
812
816
  }
813
817
  inc('__str_slice', '__str_concat', '__str_byteLen')
814
818
  const s = temp('sr'), r = temp('srr'), ms = tempI32('srms'), me = tempI32('srme')
@@ -816,16 +820,16 @@ export default (ctx) => {
816
820
  ['local.set', `$${s}`, asF64(emit(str))],
817
821
  ['local.set', `$${r}`, asF64(emit(repl))],
818
822
  ['local.set', `$${ms}`, ['local.set', `$${me}`,
819
- ['call', `$__regex_search_${id}`, ['local.get', `$${s}`]]]],
823
+ ['call', `$__regex_search_${id}`, ['i64.reinterpret_f64', ['local.get', `$${s}`]]]]],
820
824
  ['if', ['result', 'f64'], ['i32.lt_s', ['local.get', `$${ms}`], ['i32.const', 0]],
821
825
  ['then', ['local.get', `$${s}`]],
822
826
  ['else',
823
827
  ['call', '$__str_concat',
824
- ['call', '$__str_concat',
825
- ['call', '$__str_slice', ['local.get', `$${s}`], ['i32.const', 0], ['local.get', `$${ms}`]],
826
- ['local.get', `$${r}`]],
827
- ['call', '$__str_slice', ['local.get', `$${s}`], ['local.get', `$${me}`],
828
- ['call', '$__str_byteLen', ['local.get', `$${s}`]]]]]]], 'f64')
828
+ ['i64.reinterpret_f64', ['call', '$__str_concat',
829
+ ['i64.reinterpret_f64', ['call', '$__str_slice', ['i64.reinterpret_f64', ['local.get', `$${s}`]], ['i32.const', 0], ['local.get', `$${ms}`]]],
830
+ ['i64.reinterpret_f64', ['local.get', `$${r}`]]]],
831
+ ['i64.reinterpret_f64', ['call', '$__str_slice', ['i64.reinterpret_f64', ['local.get', `$${s}`]], ['local.get', `$${me}`],
832
+ ['call', '$__str_byteLen', ['i64.reinterpret_f64', ['local.get', `$${s}`]]]]]]]]], 'f64')
829
833
  }
830
834
 
831
835
  // str.split(/re/) → array of substrings
@@ -834,14 +838,14 @@ export default (ctx) => {
834
838
  if (id == null) {
835
839
  // Fall back to string split
836
840
  inc('__str_split')
837
- return typed(['call', '$__str_split', asF64(emit(str)), asF64(emit(sep))], 'f64')
841
+ return typed(['call', '$__str_split', asI64(emit(str)), asI64(emit(sep))], 'f64')
838
842
  }
839
843
 
840
844
  // Generate a split-by-regex WAT function for this regex
841
845
  const splitName = `__regex_split_${id}`
842
846
  if (!ctx.core.stdlib[splitName]) {
843
847
  inc('__str_to_buf', '__str_slice', '__alloc')
844
- ctx.core.stdlib[splitName] = `(func $${splitName} (param $str f64) (result f64)
848
+ ctx.core.stdlib[splitName] = `(func $${splitName} (param $str i64) (result f64)
845
849
  (local $off i32) (local $len i32) (local $pos i32) (local $result i32)
846
850
  (local $mstart i32) (local $mend i32) (local $prevEnd i32)
847
851
  (local $arrOff i32) (local $count i32) (local $cap i32)
@@ -908,6 +912,6 @@ export default (ctx) => {
908
912
  inc(splitName)
909
913
  }
910
914
 
911
- return typed(['call', `$${splitName}`, asF64(emit(str))], 'f64')
915
+ return typed(['call', `$${splitName}`, asI64(emit(str))], 'f64')
912
916
  }
913
917
  }