jz 0.1.1 → 0.2.1

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,36 +7,51 @@
7
7
  * @module object
8
8
  */
9
9
 
10
- import { typed, asF64, temp, tempI32, allocPtr, needsDynShadow, mkPtrIR, extractF64Bits, appendStaticSlots, slotAddr } from '../src/ir.js'
10
+ import { typed, asF64, asI64, temp, tempI32, allocPtr, needsDynShadow, mkPtrIR, extractF64Bits, appendStaticSlots, slotAddr, elemStore } from '../src/ir.js'
11
11
  import { emit } from '../src/emit.js'
12
- import { valTypeOf, lookupValType, VAL, repOf, updateRep } from '../src/analyze.js'
13
- import { ctx, err, inc, PTR } from '../src/ctx.js'
12
+ import { valTypeOf, lookupValType, VAL, repOf, updateRep, shapeOf } from '../src/analyze.js'
13
+ import { ctx, err, inc, PTR, LAYOUT } from '../src/ctx.js'
14
14
 
15
15
 
16
16
  export default (ctx) => {
17
17
  inc('__mkptr', '__alloc', '__alloc_hdr', '__ptr_offset', '__len', '__ptr_type')
18
18
 
19
- // 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).
20
25
  ctx.core.emit['{}'] = (...rawProps) => {
21
- if (rawProps.length === 0)
22
- 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
+ }
23
37
 
24
38
  // Flatten comma-grouped props: [',', p1, p2] → [p1, p2]
25
39
  const props = rawProps.length === 1 && Array.isArray(rawProps[0]) && rawProps[0][0] === ','
26
40
  ? rawProps[0].slice(1) : rawProps
27
41
 
42
+ const target = takeLiteralTarget()
43
+
28
44
  // Object spread: {...a, x: 1, ...b} — merge schemas, copy props from sources
29
45
  const hasSpreads = props.some(p => Array.isArray(p) && p[0] === '...')
30
- if (hasSpreads) return emitObjectSpread(props)
46
+ if (hasSpreads) return emitObjectSpread(props, target)
31
47
 
32
48
  const names = [], values = []
33
49
  for (const p of props) {
34
50
  if (Array.isArray(p) && p[0] === ':') { names.push(p[1]); values.push(p[2]) }
35
51
  }
36
52
 
37
- // 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.
38
54
  let schemaId = ctx.schema.register(names)
39
- const target = ctx.schema.targetStack.at(-1)
40
55
  if (target) {
41
56
  const merged = ctx.schema.resolve(target)
42
57
  if (merged) schemaId = ctx.schema.idOf(target)
@@ -59,15 +74,15 @@ export default (ctx) => {
59
74
  inc('__dyn_set')
60
75
  const body = [['local.set', `$${ptr}`, staticPtr]]
61
76
  for (let i = 0; i < schema.length; i++)
62
- body.push(['drop', ['call', '$__dyn_set', ['local.get', `$${ptr}`],
63
- 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])]])
64
79
  body.push(['local.get', `$${ptr}`])
65
80
  return typed(['block', ['result', 'f64'], ...body], 'f64')
66
81
  }
67
82
  }
68
83
 
69
84
  const body = [
70
- ['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]]],
71
86
  ]
72
87
  for (let i = 0; i < values.length; i++)
73
88
  body.push(['f64.store', slotAddr(t, i), asF64(emit(values[i]))])
@@ -75,8 +90,8 @@ export default (ctx) => {
75
90
  if (shadow) {
76
91
  inc('__dyn_set')
77
92
  for (let i = 0; i < schema.length; i++)
78
- body.push(['drop', ['call', '$__dyn_set', ['local.get', `$${ptr}`], emit(['str', String(schema[i])]),
79
- ['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)]]])
80
95
  }
81
96
  body.push(['local.get', `$${ptr}`])
82
97
 
@@ -86,10 +101,37 @@ export default (ctx) => {
86
101
  // === Object static methods ===
87
102
 
88
103
  ctx.core.emit['Object.keys'] = (obj) => {
104
+ if (isHashTyped(obj)) return emitHashKeys(obj)
89
105
  const schema = resolveSchema(obj)
90
- if (!schema) err('Object.keys requires object with known schema')
91
- 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')
92
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']
93
135
 
94
136
  ctx.core.emit['Object.values'] = (obj) => {
95
137
  const schema = resolveSchema(obj)
@@ -99,7 +141,7 @@ export default (ctx) => {
99
141
  const t = temp('ov'), base = tempI32('vb')
100
142
  const out = allocPtr({ type: PTR.ARRAY, len: n, tag: 'oa' })
101
143
  const body = [['local.set', `$${t}`, va], out.init,
102
- ['local.set', `$${base}`, ['call', '$__ptr_offset', ['local.get', `$${t}`]]]]
144
+ ['local.set', `$${base}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]]]
103
145
  for (let i = 0; i < n; i++)
104
146
  body.push(['f64.store', slotAddr(out.local, i), ['f64.load', slotAddr(base, i)]])
105
147
  body.push(out.ptr)
@@ -114,7 +156,7 @@ export default (ctx) => {
114
156
  const t = temp('oe'), pair = tempI32('op'), base = tempI32('eb')
115
157
  const out = allocPtr({ type: PTR.ARRAY, len: n, tag: 'oa' })
116
158
  const body = [['local.set', `$${t}`, va], out.init,
117
- ['local.set', `$${base}`, ['call', '$__ptr_offset', ['local.get', `$${t}`]]]]
159
+ ['local.set', `$${base}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]]]
118
160
  for (let i = 0; i < n; i++) {
119
161
  body.push(
120
162
  ['local.set', `$${pair}`, ['call', '$__alloc_hdr', ['i32.const', 2], ['i32.const', 2], ['i32.const', 8]]],
@@ -142,14 +184,14 @@ export default (ctx) => {
142
184
  updateRep(target, { schemaId })
143
185
  const t = tempI32('bx'), s = temp('bs')
144
186
  const body = [
145
- ['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]]],
146
188
  ['f64.store', ['local.get', `$${t}`], asF64(emit(target))],
147
189
  ]
148
190
  const sBase = tempI32('sb')
149
191
  for (const source of sources) {
150
192
  const sSchema = resolveSchema(source)
151
193
  body.push(['local.set', `$${s}`, asF64(emit(source))])
152
- 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}`]]]])
153
195
  for (let si = 0; si < sSchema.length; si++) {
154
196
  const ti = boxedSchema.indexOf(sSchema[si])
155
197
  if (ti < 0) continue
@@ -167,12 +209,12 @@ export default (ctx) => {
167
209
  const t = temp('at'), s = temp('as')
168
210
  const tBase = tempI32('tb'), sBase2 = tempI32('sb')
169
211
  const body = [['local.set', `$${t}`, asF64(emit(target))],
170
- ['local.set', `$${tBase}`, ['call', '$__ptr_offset', ['local.get', `$${t}`]]]]
212
+ ['local.set', `$${tBase}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]]]
171
213
  for (const source of sources) {
172
214
  const sSchema = resolveSchema(source)
173
215
  if (!sSchema) err('Object.assign: source needs known schema')
174
216
  body.push(['local.set', `$${s}`, asF64(emit(source))])
175
- 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}`]]]])
176
218
  for (let si = 0; si < sSchema.length; si++) {
177
219
  const ti = tSchema.indexOf(sSchema[si])
178
220
  if (ti < 0) continue
@@ -193,18 +235,18 @@ export default (ctx) => {
193
235
  const id = ctx.func.uniq++
194
236
  return typed(['block', ['result', 'f64'],
195
237
  ['local.set', `$${t}`, ['call', '$__hash_new']],
196
- ['local.set', `$${ptr}`, ['call', '$__ptr_offset', va]],
197
- ['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]]],
198
240
  ['local.set', `$${i}`, ['i32.const', 0]],
199
241
  ['block', `$brk${id}`, ['loop', `$loop${id}`,
200
242
  ['br_if', `$brk${id}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${len}`]]],
201
243
  // Load pair (array of 2): pair = ptr_offset(arr[i])
202
- ['local.set', `$${pair}`, ['call', '$__ptr_offset',
203
- ['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]]]]]]],
204
246
  // hash_set(result, pair[0], pair[1])
205
- ['local.set', `$${t}`, ['call', '$__hash_set', ['local.get', `$${t}`],
206
- ['f64.load', ['local.get', `$${pair}`]],
207
- ['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]]]]]],
208
250
  ['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
209
251
  ['br', `$loop${id}`]]],
210
252
  ['local.get', `$${t}`]], 'f64')
@@ -216,15 +258,25 @@ export default (ctx) => {
216
258
  if (protoType === VAL.ARRAY) {
217
259
  // Clone array data + link named-prop sidecar so for-in/bracket-name lookups
218
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.
219
264
  inc('__arr_from', '__dyn_move', '__ptr_offset')
220
265
  const src = temp('ocs')
221
266
  const dst = temp('ocd')
267
+ const srcOff = tempI32('ocso')
268
+ const dstOff = tempI32('ocdo')
222
269
  return typed(['block', ['result', 'f64'],
223
270
  ['local.set', `$${src}`, asF64(emit(proto))],
224
- ['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]]]],
225
277
  ['call', '$__dyn_move',
226
- ['call', '$__ptr_offset', ['local.get', `$${src}`]],
227
- ['call', '$__ptr_offset', ['local.get', `$${dst}`]]],
278
+ ['local.get', `$${srcOff}`],
279
+ ['local.get', `$${dstOff}`]],
228
280
  ['local.get', `$${dst}`]], 'f64')
229
281
  }
230
282
  const schema = resolveSchema(proto)
@@ -233,15 +285,22 @@ export default (ctx) => {
233
285
  const value = temp('ocr')
234
286
  inc('__arr_from', '__dyn_move', '__ptr_offset')
235
287
  const dst2 = temp('ocd')
288
+ const srcOff2 = tempI32('ocso')
289
+ const dstOff2 = tempI32('ocdo')
236
290
  return typed(['block', ['result', 'f64'],
237
291
  ['local.set', `$${value}`, asF64(emit(proto))],
238
292
  ['if', ['result', 'f64'],
239
- ['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]],
240
294
  ['then', ['block', ['result', 'f64'],
241
- ['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]]]],
242
301
  ['call', '$__dyn_move',
243
- ['call', '$__ptr_offset', ['local.get', `$${value}`]],
244
- ['call', '$__ptr_offset', ['local.get', `$${dst2}`]]],
302
+ ['local.get', `$${srcOff2}`],
303
+ ['local.get', `$${dstOff2}`]],
245
304
  ['local.get', `$${dst2}`]]],
246
305
  ['else', ['local.get', `$${value}`]]]] , 'f64')
247
306
  }
@@ -253,8 +312,8 @@ export default (ctx) => {
253
312
  const srcBase = tempI32('cb')
254
313
  const body = [
255
314
  ['local.set', `$${s}`, asF64(emit(proto))],
256
- ['local.set', `$${t}`, ['call', '$__alloc', ['i32.const', n * 8]]],
257
- ['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}`]]]],
258
317
  ]
259
318
  // Copy all properties from proto
260
319
  for (let i = 0; i < n; i++)
@@ -270,6 +329,10 @@ function resolveSchema(obj) {
270
329
  if (typeof obj === 'string') return ctx.schema.resolve(obj)
271
330
  if (Array.isArray(obj) && obj[0] === '{}')
272
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
273
336
  return null
274
337
  }
275
338
 
@@ -277,7 +340,16 @@ function resolveSchema(obj) {
277
340
  * Emit object literal with spread: {...a, x: 1, ...b, y: 2}
278
341
  * Merges schemas from all sources, allocates result, copies in order.
279
342
  */
280
- 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()) {
281
353
  // Collect merged schema: union of all spread source schemas + explicit props
282
354
  const allNames = []
283
355
  const addName = n => { if (!allNames.includes(n)) allNames.push(n) }
@@ -301,7 +373,7 @@ function emitObjectSpread(props) {
301
373
  const ptr = temp('objp')
302
374
  const src = tempI32('osp')
303
375
 
304
- 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]]]]
305
377
 
306
378
  // Process props in order — later props override earlier (JS semantics)
307
379
  let srcF
@@ -318,13 +390,13 @@ function emitObjectSpread(props) {
318
390
  for (let i = 0; i < schema.length; i++) {
319
391
  const slot = slotAddr(t, i)
320
392
  body.push(['f64.store', slot,
321
- ['call', '$__dyn_get_or', ['local.get', `$${srcF}`],
322
- emit(['str', String(schema[i])]),
323
- ['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]]]])
324
396
  }
325
397
  continue
326
398
  }
327
- 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]))]]])
328
400
  for (let si = 0; si < sSchema.length; si++) {
329
401
  const ti = schema.indexOf(sSchema[si])
330
402
  if (ti < 0) continue
@@ -337,12 +409,11 @@ function emitObjectSpread(props) {
337
409
  }
338
410
 
339
411
  body.push(['local.set', `$${ptr}`, mkPtrIR(PTR.OBJECT, schemaId, ['local.get', `$${t}`])])
340
- const spreadTarget = ctx.schema.targetStack.at(-1)
341
412
  if (needsDynShadow(spreadTarget)) {
342
413
  inc('__dyn_set')
343
414
  for (let i = 0; i < schema.length; i++)
344
- body.push(['drop', ['call', '$__dyn_set', ['local.get', `$${ptr}`], emit(['str', String(schema[i])]),
345
- ['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)]]])
346
417
  }
347
418
  body.push(['local.get', `$${ptr}`])
348
419
  return typed(['block', ['result', 'f64'], ...body], 'f64')
@@ -357,3 +428,112 @@ function emitStringArray(names) {
357
428
  body.push(out.ptr)
358
429
  return typed(['block', ['result', 'f64'], ...body], 'f64')
359
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,9 +7,9 @@
7
7
  * @module regex
8
8
  */
9
9
 
10
- import { typed, asF64, UNDEF_NAN, mkPtrIR, temp, tempI32 } from '../src/ir.js'
10
+ import { typed, asF64, asI64, UNDEF_NAN, mkPtrIR, temp, tempI32 } from '../src/ir.js'
11
11
  import { emit } from '../src/emit.js'
12
- import { err, inc, PTR } from '../src/ctx.js'
12
+ import { err, inc, PTR, LAYOUT } from '../src/ctx.js'
13
13
 
14
14
  // Build IR that constructs a match array: [full, cap1, cap2, ...]
15
15
  // strLocal, msLocal, meLocal are local names (i32 for ms/me, f64 for str).
@@ -23,7 +23,7 @@ const buildMatchArr = (strLocal, msLocal, meLocal, nGroups) => {
23
23
  ['i32.store', ['local.get', `$${arr}`], ['i32.const', N]],
24
24
  ['i32.store', ['i32.add', ['local.get', `$${arr}`], ['i32.const', 4]], ['i32.const', N]],
25
25
  ['f64.store', ['i32.add', ['local.get', `$${arr}`], ['i32.const', 8]],
26
- ['call', '$__str_slice', ['local.get', `$${strLocal}`],
26
+ ['call', '$__str_slice', ['i64.reinterpret_f64', ['local.get', `$${strLocal}`]],
27
27
  ['local.get', `$${msLocal}`], ['local.get', `$${meLocal}`]]],
28
28
  ]
29
29
  for (let i = 1; i <= nGroups; i++) {
@@ -31,7 +31,7 @@ const buildMatchArr = (strLocal, msLocal, meLocal, nGroups) => {
31
31
  ['if', ['result', 'f64'],
32
32
  ['i32.lt_s', ['global.get', `$__re_g${i}_start`], ['i32.const', 0]],
33
33
  ['then', ['f64.const', `nan:${UNDEF_NAN}`]],
34
- ['else', ['call', '$__str_slice', ['local.get', `$${strLocal}`],
34
+ ['else', ['call', '$__str_slice', ['i64.reinterpret_f64', ['local.get', `$${strLocal}`]],
35
35
  ['global.get', `$__re_g${i}_start`], ['global.get', `$__re_g${i}_end`]]]]])
36
36
  }
37
37
  stmts.push(mkPtrIR(PTR.ARRAY, 0, ['i32.add', ['local.get', `$${arr}`], ['i32.const', 8]]))
@@ -654,14 +654,16 @@ export default (ctx) => {
654
654
 
655
655
  ctx.runtime.regex = { count: 0, vars: new Map(), compiled: new Map(), groups: new Map() }
656
656
 
657
- // SSO → heap normalizer: returns data offset (i32) for direct byte access
658
- ctx.core.stdlib['__str_to_buf'] = `(func $__str_to_buf (param $ptr f64) (result i32)
659
- (local $type i32) (local $off i32) (local $len i32) (local $buf i32) (local $i i32)
660
- (local.set $type (call $__ptr_type (local.get $ptr)))
661
- (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})))
662
664
  (then (return (call $__ptr_offset (local.get $ptr)))))
663
665
  (local.set $off (call $__ptr_offset (local.get $ptr)))
664
- (local.set $len (call $__ptr_aux (local.get $ptr)))
666
+ (local.set $len (i32.and (local.get $aux) (i32.const 7)))
665
667
  (local.set $buf (call $__alloc (local.get $len)))
666
668
  (local.set $i (i32.const 0))
667
669
  (block $done (loop $next
@@ -691,7 +693,7 @@ export default (ctx) => {
691
693
 
692
694
  // Search wrapper: tries match at each position, returns (match_start, match_end) via locals
693
695
  const searchName = `__regex_search_${id}`
694
- 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)
695
697
  (local $off i32) (local $len i32) (local $pos i32) (local $result i32)
696
698
  (local.set $off (call $__str_to_buf (local.get $str)))
697
699
  (local.set $len (call $__str_byteLen (local.get $str)))
@@ -735,7 +737,7 @@ export default (ctx) => {
735
737
  return typed(['block', ['result', 'f64'],
736
738
  ['local.set', `$${s}`, asF64(emit(str))],
737
739
  ['local.set', `$${mstart}`, ['local.set', `$${mend}`,
738
- ['call', `$__regex_search_${id}`, ['local.get', `$${s}`]]]],
740
+ ['call', `$__regex_search_${id}`, ['i64.reinterpret_f64', ['local.get', `$${s}`]]]]],
739
741
  // search returns (start, end) multi-value; capture both
740
742
  ['if', ['result', 'f64'], ['i32.ge_s', ['local.get', `$${mstart}`], ['i32.const', 0]],
741
743
  ['then', ['f64.const', 1]],
@@ -751,7 +753,7 @@ export default (ctx) => {
751
753
  return typed(['block', ['result', 'f64'],
752
754
  ['local.set', `$${s}`, asF64(emit(str))],
753
755
  ['local.set', `$${ms}`, ['local.set', `$${me}`,
754
- ['call', `$__regex_search_${id}`, ['local.get', `$${s}`]]]],
756
+ ['call', `$__regex_search_${id}`, ['i64.reinterpret_f64', ['local.get', `$${s}`]]]]],
755
757
  ['if', ['result', 'f64'], ['i32.lt_s', ['local.get', `$${ms}`], ['i32.const', 0]],
756
758
  ['then', ['f64.const', 0]],
757
759
  ['else', buildMatchArr(s, ms, me, nGroups)]]], 'f64')
@@ -763,13 +765,13 @@ export default (ctx) => {
763
765
  if (id == null) {
764
766
  // Fall back to string search (indexOf)
765
767
  inc('__str_indexof')
766
- 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')
767
769
  }
768
770
  const s = temp('ss'), ms = tempI32('ssms'), me = tempI32('ssme')
769
771
  return typed(['block', ['result', 'f64'],
770
772
  ['local.set', `$${s}`, asF64(emit(str))],
771
773
  ['local.set', `$${ms}`, ['local.set', `$${me}`,
772
- ['call', `$__regex_search_${id}`, ['local.get', `$${s}`]]]],
774
+ ['call', `$__regex_search_${id}`, ['i64.reinterpret_f64', ['local.get', `$${s}`]]]]],
773
775
  ['f64.convert_i32_s', ['local.get', `$${ms}`]]], 'f64')
774
776
  }
775
777
 
@@ -783,21 +785,22 @@ export default (ctx) => {
783
785
  return typed(['block', ['result', 'f64'],
784
786
  ['local.set', `$${s}`, asF64(emit(str))],
785
787
  ['local.set', `$${q}`, asF64(emit(search))],
786
- ['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]]],
787
789
  ['if', ['result', 'f64'], ['i32.lt_s', ['local.get', `$${idx}`], ['i32.const', 0]],
788
790
  ['then', ['f64.const', 0]],
789
791
  ['else',
790
792
  ['call', '$__wrap1',
791
- ['call', '$__str_slice', ['local.get', `$${s}`],
792
- ['local.get', `$${idx}`],
793
- ['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')
794
797
  }
795
798
  const nGroups = ctx.runtime.regex.groups.get(id) || 0
796
799
  const s = temp('sm'), ms = tempI32('smms'), me = tempI32('smme')
797
800
  return typed(['block', ['result', 'f64'],
798
801
  ['local.set', `$${s}`, asF64(emit(str))],
799
802
  ['local.set', `$${ms}`, ['local.set', `$${me}`,
800
- ['call', `$__regex_search_${id}`, ['local.get', `$${s}`]]]],
803
+ ['call', `$__regex_search_${id}`, ['i64.reinterpret_f64', ['local.get', `$${s}`]]]]],
801
804
  ['if', ['result', 'f64'], ['i32.lt_s', ['local.get', `$${ms}`], ['i32.const', 0]],
802
805
  ['then', ['f64.const', 0]],
803
806
  ['else', buildMatchArr(s, ms, me, nGroups)]]], 'f64')
@@ -809,7 +812,7 @@ export default (ctx) => {
809
812
  if (id == null) {
810
813
  // Fall back to string replace
811
814
  inc('__str_replace')
812
- 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')
813
816
  }
814
817
  inc('__str_slice', '__str_concat', '__str_byteLen')
815
818
  const s = temp('sr'), r = temp('srr'), ms = tempI32('srms'), me = tempI32('srme')
@@ -817,16 +820,16 @@ export default (ctx) => {
817
820
  ['local.set', `$${s}`, asF64(emit(str))],
818
821
  ['local.set', `$${r}`, asF64(emit(repl))],
819
822
  ['local.set', `$${ms}`, ['local.set', `$${me}`,
820
- ['call', `$__regex_search_${id}`, ['local.get', `$${s}`]]]],
823
+ ['call', `$__regex_search_${id}`, ['i64.reinterpret_f64', ['local.get', `$${s}`]]]]],
821
824
  ['if', ['result', 'f64'], ['i32.lt_s', ['local.get', `$${ms}`], ['i32.const', 0]],
822
825
  ['then', ['local.get', `$${s}`]],
823
826
  ['else',
824
827
  ['call', '$__str_concat',
825
- ['call', '$__str_concat',
826
- ['call', '$__str_slice', ['local.get', `$${s}`], ['i32.const', 0], ['local.get', `$${ms}`]],
827
- ['local.get', `$${r}`]],
828
- ['call', '$__str_slice', ['local.get', `$${s}`], ['local.get', `$${me}`],
829
- ['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')
830
833
  }
831
834
 
832
835
  // str.split(/re/) → array of substrings
@@ -835,14 +838,14 @@ export default (ctx) => {
835
838
  if (id == null) {
836
839
  // Fall back to string split
837
840
  inc('__str_split')
838
- 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')
839
842
  }
840
843
 
841
844
  // Generate a split-by-regex WAT function for this regex
842
845
  const splitName = `__regex_split_${id}`
843
846
  if (!ctx.core.stdlib[splitName]) {
844
847
  inc('__str_to_buf', '__str_slice', '__alloc')
845
- ctx.core.stdlib[splitName] = `(func $${splitName} (param $str f64) (result f64)
848
+ ctx.core.stdlib[splitName] = `(func $${splitName} (param $str i64) (result f64)
846
849
  (local $off i32) (local $len i32) (local $pos i32) (local $result i32)
847
850
  (local $mstart i32) (local $mend i32) (local $prevEnd i32)
848
851
  (local $arrOff i32) (local $count i32) (local $cap i32)
@@ -909,6 +912,6 @@ export default (ctx) => {
909
912
  inc(splitName)
910
913
  }
911
914
 
912
- return typed(['call', `$${splitName}`, asF64(emit(str))], 'f64')
915
+ return typed(['call', `$${splitName}`, asI64(emit(str))], 'f64')
913
916
  }
914
917
  }