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