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.
- package/README.md +315 -318
- package/bench/README.md +369 -0
- package/bench/bench.svg +102 -0
- package/cli.js +104 -30
- package/dist/interop.js +1 -0
- package/dist/jz.js +6024 -0
- package/index.d.ts +126 -0
- package/index.js +362 -84
- package/interop.js +159 -186
- package/jz.svg +5 -0
- package/jzify/arguments.js +97 -0
- package/jzify/bundler.js +382 -0
- package/jzify/classes.js +328 -0
- package/jzify/hoist-vars.js +177 -0
- package/jzify/index.js +51 -0
- package/jzify/names.js +37 -0
- package/jzify/switch.js +106 -0
- package/jzify/transform.js +349 -0
- package/layout.js +184 -0
- package/module/array.js +377 -176
- package/module/collection.js +639 -145
- package/module/console.js +55 -43
- package/module/core.js +264 -153
- package/module/date.js +15 -3
- package/module/function.js +73 -6
- package/module/index.js +2 -1
- package/module/json.js +226 -61
- package/module/math.js +551 -187
- package/module/number.js +327 -60
- package/module/object.js +474 -184
- package/module/regex.js +255 -25
- package/module/schema.js +24 -6
- package/module/simd.js +117 -0
- package/module/string.js +621 -226
- package/module/symbol.js +1 -1
- package/module/timer.js +9 -14
- package/module/typedarray.js +459 -73
- package/package.json +58 -14
- package/src/abi/index.js +39 -23
- package/src/abi/string.js +38 -41
- package/src/ast.js +460 -0
- package/src/autoload.js +29 -24
- package/src/bridge.js +111 -0
- package/src/compile/analyze-scans.js +824 -0
- package/src/compile/analyze.js +1600 -0
- package/src/compile/emit-assign.js +411 -0
- package/src/compile/emit.js +3512 -0
- package/src/compile/flow-types.js +103 -0
- package/src/{compile.js → compile/index.js} +778 -128
- package/src/{infer.js → compile/infer.js} +62 -98
- package/src/compile/loop-divmod.js +155 -0
- package/src/{narrow.js → compile/narrow.js} +409 -106
- package/src/compile/peel-stencil.js +261 -0
- package/src/compile/plan/advise.js +370 -0
- package/src/compile/plan/common.js +150 -0
- package/src/compile/plan/index.js +122 -0
- package/src/compile/plan/inline.js +682 -0
- package/src/compile/plan/literals.js +1199 -0
- package/src/compile/plan/loops.js +472 -0
- package/src/compile/plan/scope.js +649 -0
- package/src/compile/program-facts.js +423 -0
- package/src/ctx.js +220 -64
- package/src/ir.js +589 -172
- package/src/kind-traits.js +132 -0
- package/src/kind.js +524 -0
- package/src/op-policy.js +57 -0
- package/src/{optimize.js → optimize/index.js} +1432 -473
- package/src/optimize/vectorize.js +4660 -0
- package/src/param-reps.js +65 -0
- package/src/parse.js +44 -0
- package/src/{prepare.js → prepare/index.js} +649 -205
- package/src/prepare/lift-iife.js +149 -0
- package/src/reps.js +116 -0
- package/src/resolve.js +12 -3
- package/src/static.js +208 -0
- package/src/type.js +651 -0
- package/src/{assemble.js → wat/assemble.js} +275 -55
- package/src/wat/optimize.js +3938 -0
- package/transform.js +21 -0
- package/wasi.js +47 -5
- package/src/analyze.js +0 -3818
- package/src/emit.js +0 -3040
- package/src/jzify.js +0 -1580
- package/src/plan.js +0 -2132
- package/src/vectorize.js +0 -1088
- /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
|
+
|