jz 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +288 -314
  2. package/bench/README.md +319 -0
  3. package/bench/bench.svg +112 -0
  4. package/cli.js +32 -24
  5. package/index.js +177 -55
  6. package/interop.js +88 -159
  7. package/jz.svg +5 -0
  8. package/jzify/arguments.js +97 -0
  9. package/jzify/bundler.js +382 -0
  10. package/jzify/classes.js +328 -0
  11. package/jzify/hoist-vars.js +177 -0
  12. package/jzify/index.js +51 -0
  13. package/jzify/names.js +37 -0
  14. package/jzify/switch.js +106 -0
  15. package/jzify/transform.js +349 -0
  16. package/layout.js +179 -0
  17. package/module/array.js +322 -153
  18. package/module/collection.js +603 -145
  19. package/module/console.js +55 -43
  20. package/module/core.js +266 -153
  21. package/module/date.js +15 -3
  22. package/module/function.js +73 -6
  23. package/module/index.js +2 -1
  24. package/module/json.js +226 -61
  25. package/module/math.js +414 -186
  26. package/module/number.js +306 -60
  27. package/module/object.js +448 -184
  28. package/module/regex.js +255 -25
  29. package/module/schema.js +24 -6
  30. package/module/simd.js +85 -0
  31. package/module/string.js +586 -220
  32. package/module/symbol.js +1 -1
  33. package/module/timer.js +9 -14
  34. package/module/typedarray.js +45 -48
  35. package/package.json +41 -12
  36. package/src/abi/index.js +39 -23
  37. package/src/abi/string.js +38 -41
  38. package/src/ast.js +460 -0
  39. package/src/autoload.js +26 -24
  40. package/src/bridge.js +111 -0
  41. package/src/compile/analyze-scans.js +661 -0
  42. package/src/compile/analyze.js +1565 -0
  43. package/src/compile/emit-assign.js +408 -0
  44. package/src/compile/emit.js +3201 -0
  45. package/src/compile/flow-types.js +103 -0
  46. package/src/{compile.js → compile/index.js} +497 -125
  47. package/src/{infer.js → compile/infer.js} +27 -98
  48. package/src/{narrow.js → compile/narrow.js} +302 -96
  49. package/src/compile/plan/advise.js +316 -0
  50. package/src/compile/plan/common.js +150 -0
  51. package/src/compile/plan/index.js +118 -0
  52. package/src/compile/plan/inline.js +679 -0
  53. package/src/compile/plan/literals.js +984 -0
  54. package/src/compile/plan/loops.js +472 -0
  55. package/src/compile/plan/scope.js +573 -0
  56. package/src/compile/program-facts.js +404 -0
  57. package/src/ctx.js +176 -58
  58. package/src/ir.js +540 -171
  59. package/src/kind-traits.js +105 -0
  60. package/src/kind.js +462 -0
  61. package/src/op-policy.js +57 -0
  62. package/src/{optimize.js → optimize/index.js} +1106 -446
  63. package/src/optimize/vectorize.js +1874 -0
  64. package/src/param-reps.js +65 -0
  65. package/src/parse.js +44 -0
  66. package/src/{prepare.js → prepare/index.js} +589 -202
  67. package/src/reps.js +115 -0
  68. package/src/resolve.js +12 -3
  69. package/src/static.js +199 -0
  70. package/src/type.js +647 -0
  71. package/src/{assemble.js → wat/assemble.js} +86 -48
  72. package/src/wat/optimize.js +3760 -0
  73. package/transform.js +21 -0
  74. package/wasi.js +47 -5
  75. package/src/analyze.js +0 -3818
  76. package/src/emit.js +0 -3040
  77. package/src/jzify.js +0 -1580
  78. package/src/plan.js +0 -2132
  79. package/src/vectorize.js +0 -1088
  80. /package/src/{codegen.js → wat/codegen.js} +0 -0
@@ -0,0 +1,408 @@
1
+ /**
2
+ * Assignment IR helpers — element writes (`arr[i] = v`), property writes
3
+ * (`obj.p = v`), and the binding-persist discipline for helpers that may
4
+ * relocate a header (`__arr_set_idx_ptr`, `__arr_grow`, `__hash_set`).
5
+ *
6
+ * Extracted from emit.js along its section banner; calls back into the
7
+ * dispatcher through bridge.js (the same pattern module/*.js uses), so the
8
+ * import graph stays acyclic: emit.js → emit-assign.js → bridge → emit.
9
+ *
10
+ * @module compile/emit-assign
11
+ */
12
+
13
+ import { ctx, err, inc, PTR } from '../ctx.js'
14
+ import { T } from '../ast.js'
15
+ import { staticPropertyKey } from '../static.js'
16
+ import { valTypeOf, shapeOf } from '../kind.js'
17
+ import { VAL, lookupValType, repOf } from '../reps.js'
18
+ import {
19
+ typed, asF64, asI32, asI64, temp, tempI32, withTemp, block64,
20
+ ptrOffsetIR, ptrTypeEq, boxedAddr, writeVar, isGlobal, isBoundName, isLiteralStr,
21
+ usesDynProps, needsDynShadow, boolBoxIR,
22
+ } from '../ir.js'
23
+ import { emit } from '../bridge.js'
24
+
25
+
26
+ // Boxed-bool-aware store value: booleans persist as their tagged atom.
27
+ const storedValue = (node) => valTypeOf(node) === VAL.BOOL ? boolBoxIR(emit(node)) : asF64(emit(node))
28
+
29
+ // Integer array-index key: '3' → 3; rejects non-canonical and 2³²−1.
30
+ function arrayIndexKey(key) {
31
+ const n = Number(key)
32
+ const u = n >>> 0
33
+ return String(u) === key && u !== 0xffffffff ? u : null
34
+ }
35
+
36
+ /** Write a (possibly relocated) f64 pointer back to its binding, honoring the
37
+ * same cell-vs-global-vs-local discipline as writeVar. Returns the store IR
38
+ * for the given f64 `ptr` expression; used as a callback by helpers that may
39
+ * relocate the array header (`__arr_set_idx_ptr`, `__arr_set_length`,
40
+ * `__arr_grow`, `__hash_set`). */
41
+ function persistBindingPtr(name, ptr) {
42
+ // A NaN-boxed pointer never lands in an i32-narrowed cell: cellTypes requires
43
+ // intCertain (integer-valued defs only), and a binding that receives array/hash
44
+ // pointers is never integer-certain — so the f64 width is always right here.
45
+ if (ctx.func.boxed?.has(name)) return ['f64.store', boxedAddr(name), ptr]
46
+ if (isGlobal(name)) return ['global.set', `$${name}`, ptr]
47
+ return ['local.set', `$${name}`, ptr]
48
+ }
49
+ /** Curried form for call sites that pass a persist callback. */
50
+ const persistBinding = name => ptr => persistBindingPtr(name, ptr)
51
+
52
+ /** Emit an ARRAY element write via `__arr_set_idx_ptr`. The helper may relocate
53
+ * the array header (capacity grow); `persist` writes the new pointer back to
54
+ * the receiver binding. Returns the stored value as the block result. */
55
+ function storeArrayPayload(arrExpr, idxNode, valueExpr, persist) {
56
+ const arrTmp = `${T}asi${ctx.func.uniq++}`
57
+ const idxTmp = `${T}asj${ctx.func.uniq++}`
58
+ const valTmp = `${T}asv${ctx.func.uniq++}`
59
+ ctx.func.locals.set(arrTmp, 'f64')
60
+ ctx.func.locals.set(idxTmp, 'i32')
61
+ ctx.func.locals.set(valTmp, 'f64')
62
+ inc('__arr_set_idx_ptr')
63
+ const body = [
64
+ ['local.set', `$${arrTmp}`, arrExpr],
65
+ ['local.set', `$${idxTmp}`, asI32(typed(idxNode, 'f64'))],
66
+ ['local.set', `$${valTmp}`, valueExpr],
67
+ ['local.set', `$${arrTmp}`, ['call', '$__arr_set_idx_ptr', ['i64.reinterpret_f64', ['local.get', `$${arrTmp}`]], ['local.get', `$${idxTmp}`], ['local.get', `$${valTmp}`]]],
68
+ ]
69
+ if (persist) body.push(persist(['local.get', `$${arrTmp}`]))
70
+ body.push(['local.get', `$${valTmp}`])
71
+ return block64(...body)
72
+ }
73
+
74
+ /** Strict-mode guard for dynamic property writes — emitted in branches that
75
+ * fall through to `__dyn_set` or its key-kind dispatch. */
76
+ function ensureDynSetAllowed(arr) {
77
+ if (!ctx.transform.strict) return
78
+ const arrLabel = typeof arr === 'string' ? arr : '<expr>'
79
+ err(`strict mode: dynamic property assignment \`${arrLabel}[<expr>] = ...\` falls back to __dyn_set. Use a literal key or known array/typed-array numeric index, or pass { strict: false }.`)
80
+ }
81
+
82
+ /** Last-resort dynamic property write through `__dyn_set`. */
83
+ function dynSetCall(arr, keyExpr, valueExpr) {
84
+ ensureDynSetAllowed(arr)
85
+ inc('__dyn_set')
86
+ return typed(['f64.reinterpret_i64', ['call', '$__dyn_set', asI64(emit(arr)), asI64(keyExpr), asI64(valueExpr)]], 'f64')
87
+ }
88
+
89
+ /** Runtime fork by key kind: string keys go to `__dyn_set`, numeric keys go through
90
+ * `numericIR(keyExpr)`. Used when key type is unknown at compile time. */
91
+ function dispatchByKeyKind(arr, keyExpr, valueExpr, numericIR) {
92
+ ensureDynSetAllowed(arr)
93
+ const keyTmp = temp()
94
+ return block64(
95
+ ['local.set', `$${keyTmp}`, keyExpr],
96
+ ['if', ['result', 'f64'], ['call', '$__is_str_key', ['i64.reinterpret_f64', ['local.get', `$${keyTmp}`]]],
97
+ ['then', ['f64.reinterpret_i64', ['call', '$__dyn_set', asI64(emit(arr)), ['i64.reinterpret_f64', ['local.get', `$${keyTmp}`]], asI64(valueExpr)]]],
98
+ ['else', numericIR(['local.get', `$${keyTmp}`])]])
99
+ }
100
+
101
+ /** Build a `__ptr_type`-fork IR for `arr[idx] = val` when receiver is opaque
102
+ * (non-string expr, or string-named binding of unknown VAL). Forks on
103
+ * ARRAY → `__arr_set_idx_ptr` (+ optional persist), TYPED → `__typed_set_idx`,
104
+ * else → raw f64.store at the OBJECT/HASH payload offset. */
105
+ function emitPolymorphicElementStore(arrExpr, idxI32, valueExpr, arrVT, persist) {
106
+ const objTmp = temp('asu')
107
+ const idxTmp = tempI32('asi')
108
+ const ptrTmp = temp('asp')
109
+ const valTmp = temp()
110
+ const hasTypedSet = !!ctx.core.stdlib['__typed_set_idx']
111
+ inc('__ptr_type', '__arr_set_idx_ptr')
112
+ if (hasTypedSet) inc('__typed_set_idx')
113
+ const arrSetCall = ['call', '$__arr_set_idx_ptr', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]], ['local.get', `$${idxTmp}`], ['local.get', `$${valTmp}`]]
114
+ const arrayBranch = ['block', ['result', 'f64'],
115
+ ['local.set', `$${ptrTmp}`, arrSetCall],
116
+ ...(persist ? [persist(['local.get', `$${ptrTmp}`])] : []),
117
+ ['local.get', `$${valTmp}`]]
118
+ const fallbackStore = ['block', ['result', 'f64'],
119
+ ['f64.store', ['i32.add', ptrOffsetIR(['local.get', `$${objTmp}`], arrVT), ['i32.shl', ['local.get', `$${idxTmp}`], ['i32.const', 3]]], ['local.get', `$${valTmp}`]],
120
+ ['local.get', `$${valTmp}`]]
121
+ const elseBranch = hasTypedSet
122
+ ? ['if', ['result', 'f64'],
123
+ ptrTypeEq(['local.get', `$${objTmp}`], PTR.TYPED),
124
+ ['then', ['call', '$__typed_set_idx', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]], ['local.get', `$${idxTmp}`], ['local.get', `$${valTmp}`]]],
125
+ ['else', fallbackStore]]
126
+ : fallbackStore
127
+ return block64(
128
+ ['local.set', `$${objTmp}`, asF64(arrExpr)],
129
+ ['local.set', `$${idxTmp}`, idxI32],
130
+ ['local.set', `$${valTmp}`, valueExpr],
131
+ ['if', ['result', 'f64'],
132
+ ptrTypeEq(['local.get', `$${objTmp}`], PTR.ARRAY),
133
+ ['then', arrayBranch],
134
+ ['else', elseBranch]])
135
+ }
136
+
137
+ /** Element assignment: `arr[idx] = val`. Linear strategy chain — first match wins.
138
+ * Order matters: literal-key fast paths shadow generic stores; SRoA shadow before
139
+ * schema; typed-array element write before generic f64.store. */
140
+ export { persistBindingPtr }
141
+
142
+ export function emitElementAssign(arr, idx, val) {
143
+ // _expect is clobbered by every sub-emit() — capture statement-position hint
144
+ // up front so the typed-array element-write path can elide the value materialize.
145
+ const void_ = ctx.func._expect === 'void'
146
+ const keyType = valTypeOf(idx)
147
+ // A provably-numeric index name — an int-certain loop counter or a NUMBER-typed
148
+ // local — can never be a string key, so the runtime `__is_str_key` → `__dyn_set`
149
+ // dispatch is dead. Mirrors the index *read* path (`intIndexIR`), closing the
150
+ // read/write asymmetry on `arr[i] = …` inside refined-array loops.
151
+ const idxNumericName = typeof idx === 'string' &&
152
+ (repOf(idx)?.intCertain === true || repOf(idx)?.val === VAL.NUMBER)
153
+ const useRuntimeKeyDispatch = !idxNumericName &&
154
+ (keyType == null || (typeof idx === 'string' && keyType !== VAL.STRING))
155
+ const keyExpr = asF64(emit(idx))
156
+ const valueExpr = asF64(emit(val))
157
+ // Literal string key, or schema-known object receiver with a static key expression.
158
+ const litKey = isLiteralStr(idx) ? idx[1]
159
+ : typeof arr === 'string' && lookupValType(arr) === VAL.OBJECT ? staticPropertyKey(idx)
160
+ : null
161
+
162
+ // 1. SRoA flat object: `o['k'] = x` → `local.set $o#i` (no heap store).
163
+ if (litKey != null && typeof arr === 'string' && ctx.func.flatObjects?.has(arr)) {
164
+ const fo = ctx.func.flatObjects.get(arr)
165
+ const fi = fo.names.indexOf(litKey)
166
+ if (fi >= 0) return withTemp(valueExpr, t => [
167
+ ['local.set', `$${arr}#${fi}`, ['local.get', `$${t}`]],
168
+ ['local.get', `$${t}`]])
169
+ }
170
+ // 2. Schema field literal key → direct payload-slot write.
171
+ if (litKey != null && typeof arr === 'string' && ctx.schema.slotOf) {
172
+ const slot = ctx.schema.slotOf(arr, litKey)
173
+ if (slot >= 0) return withTemp(valueExpr, t => [
174
+ ctx.abi.object.ops.store(ptrOffsetIR(asF64(emit(arr)), lookupValType(arr) || VAL.OBJECT), slot, ['local.get', `$${t}`]),
175
+ ['local.get', `$${t}`]])
176
+ }
177
+ // 3. Known-ARRAY receiver + literal numeric key → __arr_set_idx_ptr.
178
+ const arrIndex = litKey != null ? arrayIndexKey(litKey) : null
179
+ if (arrIndex != null && typeof arr === 'string' && valTypeOf(arr) === VAL.ARRAY)
180
+ return storeArrayPayload(asF64(emit(arr)), typed(['f64.const', arrIndex], 'f64'), valueExpr, persistBinding(arr))
181
+
182
+ // 4. Known-STRING key → __dyn_set (after schema/SRoA literal-key paths).
183
+ if (keyType === VAL.STRING) return dynSetCall(arr, keyExpr, valueExpr)
184
+
185
+ // 5. Typed-array receiver → __typed_set_idx (or per-ctor element write).
186
+ if (typeof arr === 'string' && ctx.core.emit['.typed:[]='] &&
187
+ lookupValType(arr) === 'typed') {
188
+ const r = ctx.core.emit['.typed:[]=']?.(arr, idx, val, void_)
189
+ if (r) return r
190
+ // Element ctor unknown — runtime aux-byte dispatch. __typed_set_idx
191
+ // returns the stored value as f64, used directly as the expr result.
192
+ inc('__typed_set_idx')
193
+ return typed(['call', '$__typed_set_idx',
194
+ asI64(emit(arr)), asI32(emit(idx)), valueExpr], 'f64')
195
+ }
196
+
197
+ // 6. Boxed schema array — payload pointer is stored at the receiver's payload offset.
198
+ if (typeof arr === 'string' && ctx.schema.isBoxed?.(arr)) {
199
+ const inner = ctx.schema.emitInner(arr)
200
+ const arrVT = lookupValType(arr) || VAL.OBJECT
201
+ const storeNumeric = keyNode => storeArrayPayload(inner, keyNode, valueExpr, ptr =>
202
+ ['f64.store', ptrOffsetIR(asF64(emit(arr)), arrVT), ptr])
203
+ if (useRuntimeKeyDispatch) {
204
+ inc('__dyn_set', '__is_str_key')
205
+ return dispatchByKeyKind(arr, keyExpr, valueExpr, storeNumeric)
206
+ }
207
+ return typed(storeNumeric(keyExpr), 'f64')
208
+ }
209
+
210
+ // 7. Known-ARRAY receiver, generic key.
211
+ if (typeof arr === 'string' && valTypeOf(arr) === VAL.ARRAY) {
212
+ const persist = persistBinding(arr)
213
+ const arrExpr = asF64(emit(arr))
214
+ if (useRuntimeKeyDispatch) {
215
+ inc('__dyn_set', '__is_str_key')
216
+ return dispatchByKeyKind(arr, keyExpr, valueExpr, keyNode => storeArrayPayload(arrExpr, keyNode, valueExpr, persist))
217
+ }
218
+ return storeArrayPayload(arrExpr, keyExpr, valueExpr, persist)
219
+ }
220
+
221
+ const knownArrVT = typeof arr === 'string' ? lookupValType(arr) : null
222
+ const arrVT = knownArrVT || VAL.OBJECT
223
+
224
+ // 8. Polymorphic + runtime key dispatch — key kind unknown AND receiver shape
225
+ // possibly TypedArray (or fully opaque). Numeric branch forks on __ptr_type.
226
+ // Deliberately a 2-fork (TYPED vs else) rather than reusing
227
+ // emitPolymorphicElementStore's 3-fork: dynamic-key dispatch only fires when
228
+ // receiver isn't statically ARRAY (Step 7 already caught that), so the
229
+ // ARRAY branch would be dead code that bloats every unknown-key write.
230
+ if (useRuntimeKeyDispatch) {
231
+ inc('__dyn_set', '__is_str_key')
232
+ const hasTypedSet = !!ctx.core.stdlib['__typed_set_idx']
233
+ if (knownArrVT == null && hasTypedSet) {
234
+ const objTmp = temp('asu')
235
+ const idxTmp = tempI32('asi')
236
+ const valTmp = temp()
237
+ inc('__ptr_type', '__typed_set_idx')
238
+ // When arr type is unknown (could be TypedArray) and __typed_set_idx is
239
+ // available, dispatch the numeric branch through __ptr_type so TypedArray
240
+ // writes go by element type. Without this, ternary-typed arrays (e.g.
241
+ // `num === 4 ? new Uint32Array(4) : new Uint8Array(16)`) would silently
242
+ // f64.store boxed bytes regardless of element width.
243
+ return dispatchByKeyKind(arr, keyExpr, valueExpr, keyNode => ['block', ['result', 'f64'],
244
+ ['local.set', `$${objTmp}`, asF64(emit(arr))],
245
+ ['local.set', `$${idxTmp}`, asI32(typed(keyNode, 'f64'))],
246
+ ['local.set', `$${valTmp}`, valueExpr],
247
+ ['if', ['result', 'f64'],
248
+ ptrTypeEq(['local.get', `$${objTmp}`], PTR.TYPED),
249
+ ['then', ['call', '$__typed_set_idx', ['i64.reinterpret_f64', ['local.get', `$${objTmp}`]], ['local.get', `$${idxTmp}`], ['local.get', `$${valTmp}`]]],
250
+ ['else', ['block', ['result', 'f64'],
251
+ ['f64.store', ['i32.add', ptrOffsetIR(['local.get', `$${objTmp}`], arrVT), ['i32.shl', ['local.get', `$${idxTmp}`], ['i32.const', 3]]], ['local.get', `$${valTmp}`]],
252
+ ['local.get', `$${valTmp}`]]]]])
253
+ }
254
+ const valTmp = temp()
255
+ return dispatchByKeyKind(arr, keyExpr, valueExpr, keyNode => ['block', ['result', 'f64'],
256
+ ['local.set', `$${valTmp}`, valueExpr],
257
+ ['f64.store', ['i32.add', ptrOffsetIR(asF64(emit(arr)), arrVT), ['i32.shl', asI32(typed(keyNode, 'f64')), ['i32.const', 3]]], ['local.get', `$${valTmp}`]],
258
+ ['local.get', `$${valTmp}`]])
259
+ }
260
+
261
+ // 9. Opaque receiver (non-string expr) or string-named with unknown VT — pure
262
+ // __ptr_type dispatch (no key-kind fork: key is provably numeric here).
263
+ if (typeof arr !== 'string')
264
+ return emitPolymorphicElementStore(emit(arr), asI32(emit(idx)), valueExpr, arrVT, null)
265
+ if (knownArrVT == null)
266
+ return emitPolymorphicElementStore(emit(arr), asI32(emit(idx)), valueExpr, arrVT, persistBinding(arr))
267
+
268
+ // Default: known-VT receiver that isn't ARRAY/TYPED/OBJECT special — raw f64.store.
269
+ return withTemp(valueExpr, t => [
270
+ ['f64.store', ['i32.add', ptrOffsetIR(asF64(emit(arr)), arrVT), ['i32.shl', asI32(emit(idx)), ['i32.const', 3]]], ['local.get', `$${t}`]],
271
+ ['local.get', `$${t}`]])
272
+ }
273
+
274
+ /** Property assignment: `obj.prop = val`. Strategies (first match wins):
275
+ * - `arr.length = N` resize (ARRAY or unknown receiver)
276
+ * - SRoA flat-object property
277
+ * - Schema-known field (with dyn shadow if needed)
278
+ * - OBJECT / dyn-props receiver → __dyn_set
279
+ * - Hoisted-but-not-declared binding (treat as dyn)
280
+ * - Non-string receiver expr → __dyn_set
281
+ * Default: __hash_set on a string-named receiver. */
282
+ export function emitPropertyAssign(obj, prop, val) {
283
+ // arr.length = N — array resize. Intercept before the schema/object paths
284
+ // (`length` is never a schema field). Only ARRAY (or unknown — the runtime
285
+ // helper guards non-arrays) receivers resize; known OBJECT/Map/etc. keep
286
+ // `.length =` as a plain property write below. The expression value is N.
287
+ if (prop === 'length') {
288
+ const recvVt = valTypeOf(obj)
289
+ if (recvVt === VAL.TYPED) err(`Typed arrays are fixed-size — cannot assign to \`${typeof obj === 'string' ? obj : '<expr>'}.length\``)
290
+ if (recvVt === VAL.ARRAY || recvVt == null) {
291
+ inc('__arr_set_length')
292
+ const arrTmp = `${T}aln${ctx.func.uniq++}`
293
+ const nTmp = `${T}alv${ctx.func.uniq++}`
294
+ ctx.func.locals.set(arrTmp, 'f64')
295
+ ctx.func.locals.set(nTmp, 'i32')
296
+ // Write the relocated pointer back to a simple var receiver so later
297
+ // reads skip the forwarding hop; complex receivers stay correct via it.
298
+ const persist = recvVt === VAL.ARRAY && typeof obj === 'string'
299
+ ? persistBindingPtr(obj, ['local.get', `$${arrTmp}`])
300
+ : null
301
+ const body = [
302
+ ['local.set', `$${arrTmp}`, asF64(emit(obj))],
303
+ ['local.set', `$${nTmp}`, asI32(emit(val))],
304
+ ['local.set', `$${arrTmp}`, ['call', '$__arr_set_length', ['i64.reinterpret_f64', ['local.get', `$${arrTmp}`]], ['local.get', `$${nTmp}`]]],
305
+ ]
306
+ if (persist) body.push(persist)
307
+ body.push(['f64.convert_i32_s', ['local.get', `$${nTmp}`]])
308
+ return block64(...body)
309
+ }
310
+ }
311
+ // SRoA flat object: `o.prop = x` → `local.set $o#i` (no heap store).
312
+ const flatW = typeof obj === 'string' ? ctx.func.flatObjects?.get(obj) : null
313
+ if (flatW) {
314
+ const fi = flatW.names.indexOf(prop)
315
+ if (fi >= 0) return withTemp(storedValue(val), t => [
316
+ ['local.set', `$${obj}#${fi}`, ['local.get', `$${t}`]],
317
+ ['local.get', `$${t}`]])
318
+ }
319
+ // Unboxed OBJECT-pointer receiver carrying its own schema on the value (ptrAux):
320
+ // resolve the field slot directly, mirroring emitPropAccess (core.js). A param /
321
+ // struct cell narrowed to an unboxed OBJECT ptr keeps its schema as `ptrAux`, not
322
+ // in ctx.schema.vars under the name — so schema.slotOf(name) misses and the write
323
+ // would fall to __dyn_set (propsPtr) while the READ resolves the slot via ptrAux,
324
+ // targeting different memory (write lost). Match the read.
325
+ {
326
+ const vaProbe = emit(obj)
327
+ if (vaProbe?.ptrKind === VAL.OBJECT && vaProbe.ptrAux != null) {
328
+ const sch = ctx.schema.list[vaProbe.ptrAux]
329
+ const si = sch ? sch.indexOf(prop) : -1
330
+ if (si >= 0) return withTemp(storedValue(val), t => [
331
+ ctx.abi.object.ops.store(ptrOffsetIR(asF64(emit(obj)), VAL.OBJECT), si, ['local.get', `$${t}`]),
332
+ ['local.get', `$${t}`]])
333
+ }
334
+ }
335
+ // Schema-based object → f64.store at fixed offset.
336
+ if (typeof obj === 'string' && ctx.schema.slotOf) {
337
+ const idx = ctx.schema.slotOf(obj, prop)
338
+ if (idx >= 0) {
339
+ const va = emit(obj), vv = storedValue(val), t = temp()
340
+ const shadow = needsDynShadow(obj)
341
+ if (shadow) inc('__dyn_set')
342
+ const stmts = [
343
+ ['local.set', `$${t}`, vv],
344
+ ctx.abi.object.ops.store(ptrOffsetIR(asF64(va), lookupValType(obj) || VAL.OBJECT), idx, ['local.get', `$${t}`]),
345
+ ]
346
+ if (shadow)
347
+ stmts.push(['drop', ['call', '$__dyn_set', asI64(va), asI64(emit(['str', prop])), ['i64.reinterpret_f64', ['local.get', `$${t}`]]]])
348
+ stmts.push(['local.get', `$${t}`])
349
+ return block64(...stmts)
350
+ }
351
+ }
352
+ // Chained receiver (`a.b.c = v`): resolve the holder's static shape so the
353
+ // write targets the SAME fixed slot the READ path uses (emitPropAccess →
354
+ // shapeOf, core.js). Without this the write fell to __dyn_set (per-OBJECT
355
+ // propsPtr) while `a.b.c` reads the schema slot — different memory, so the
356
+ // value was lost (read returned the stale slot). This is what corrupted the
357
+ // self-host `ctx.func.X = …` writes (e.g. finallyStack), dropping try/finally.
358
+ if (typeof obj !== 'string') {
359
+ const sh = shapeOf(obj)
360
+ if (sh?.val === VAL.OBJECT && sh.names) {
361
+ const i = sh.names.indexOf(prop)
362
+ if (i >= 0) return withTemp(storedValue(val), t => [
363
+ ctx.abi.object.ops.store(ptrOffsetIR(asF64(emit(obj)), VAL.OBJECT), i, ['local.get', `$${t}`]),
364
+ ['local.get', `$${t}`]])
365
+ }
366
+ }
367
+ if (typeof obj === 'string') {
368
+ const objType = valTypeOf(obj)
369
+ // OBJECT receivers (incl. JSON.parse-derived bindings) with off-schema
370
+ // properties go through __dyn_set, which writes to the per-OBJECT
371
+ // propsPtr at off-16 — same path as object-literal dyn shadow writes
372
+ // (module/object.js). __hash_set assumes HASH bucket layout and would
373
+ // corrupt OBJECT memory.
374
+ if (usesDynProps(objType) || objType === VAL.OBJECT) {
375
+ inc('__dyn_set')
376
+ return typed(['f64.reinterpret_i64', ['call', '$__dyn_set', asI64(emit(obj)), asI64(emit(['str', prop])), asI64(storedValue(val))]], 'f64')
377
+ }
378
+ if (ctx.func.names.has(obj) && !isBoundName(obj)) {
379
+ inc('__dyn_set')
380
+ return typed(['f64.reinterpret_i64', ['call', '$__dyn_set', asI64(emit(obj)), asI64(emit(['str', prop])), asI64(storedValue(val))]], 'f64')
381
+ }
382
+ if (objType == null && ctx.transform.host !== 'wasi') {
383
+ ctx.features.external = true
384
+ }
385
+ inc('__hash_set')
386
+ // `__hash_set` returns the (possibly reallocated) HASH pointer, which must be
387
+ // written back into `obj`. But JS `(obj.prop = v)` evaluates to `v`, not the
388
+ // object — so capture the value and return it. This only diverges in value
389
+ // position: postfix `o.p++` lowers to `(o.p = o.p+1) - 1`, `a = (o.p = v)`,
390
+ // `f(o.p = v)`. Returning the pointer there computes `object - 1` → garbage
391
+ // (the self-host regex `c.labelId++` bug). Void position discards the tail.
392
+ const keyBits = asI64(emit(['str', prop]))
393
+ return withTemp(storedValue(val), t => {
394
+ const tget = ['local.get', `$${t}`]
395
+ const setCall = typed(['f64.reinterpret_i64',
396
+ ['call', '$__hash_set', asI64(emit(obj)), keyBits, ['i64.reinterpret_f64', tget]]], 'f64')
397
+ const writeback = isGlobal(obj) ? ['global.set', `$${obj}`, setCall]
398
+ // Closure-captured (boxed) locals store at the cell address, not the slot.
399
+ : ctx.func.boxed?.has(obj) ? writeVar(obj, setCall, true)
400
+ : ['local.set', `$${obj}`, setCall]
401
+ return [writeback, tget]
402
+ })
403
+ }
404
+ if (ctx.transform.host !== 'wasi') ctx.features.external = true
405
+ inc('__dyn_set')
406
+ return typed(['f64.reinterpret_i64', ['call', '$__dyn_set', asI64(emit(obj)), asI64(emit(['str', prop])), asI64(storedValue(val))]], 'f64')
407
+ }
408
+