jz 0.8.0 → 0.9.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 +56 -9
- package/bench/README.md +121 -50
- package/bench/bench.svg +27 -27
- package/cli.js +3 -1
- package/dist/interop.js +1 -1
- package/dist/jz.js +7541 -6178
- package/index.js +165 -74
- package/interop.js +189 -17
- package/jzify/arguments.js +32 -1
- package/jzify/async.js +409 -0
- package/jzify/classes.js +136 -18
- package/jzify/generators.js +639 -0
- package/jzify/hoist-vars.js +73 -1
- package/jzify/index.js +123 -4
- package/jzify/names.js +1 -0
- package/jzify/transform.js +239 -1
- package/layout.js +48 -3
- package/module/array.js +318 -43
- package/module/atomics.js +144 -0
- package/module/collection.js +1429 -155
- package/module/core.js +431 -40
- package/module/date.js +142 -120
- package/module/fs.js +144 -0
- package/module/function.js +6 -3
- package/module/index.js +4 -1
- package/module/json.js +270 -49
- package/module/math.js +1032 -145
- package/module/number.js +532 -163
- package/module/object.js +353 -95
- package/module/regex.js +157 -7
- package/module/schema.js +100 -2
- package/module/string.js +428 -93
- package/module/typedarray.js +264 -72
- package/module/web.js +36 -0
- package/package.json +5 -5
- package/src/abi/string.js +82 -11
- package/src/ast.js +11 -5
- package/src/autoload.js +28 -1
- package/src/bridge.js +7 -2
- package/src/compile/analyze-scans.js +22 -7
- package/src/compile/analyze.js +136 -30
- package/src/compile/dyn-closure-tables.js +277 -0
- package/src/compile/emit-assign.js +281 -15
- package/src/compile/emit.js +1055 -70
- package/src/compile/index.js +277 -29
- package/src/compile/infer.js +42 -4
- package/src/compile/inplace-store.js +329 -0
- package/src/compile/loop-recurrence.js +167 -0
- package/src/compile/narrow.js +555 -18
- package/src/compile/peel-stencil.js +5 -0
- package/src/compile/plan/index.js +45 -3
- package/src/compile/plan/inline.js +8 -1
- package/src/compile/plan/literals.js +39 -0
- package/src/compile/plan/scope.js +430 -23
- package/src/compile/program-facts.js +732 -38
- package/src/ctx.js +64 -9
- package/src/helper-counters.js +7 -1
- package/src/ir.js +113 -5
- package/src/kind-traits.js +66 -3
- package/src/kind.js +84 -12
- package/src/op-policy.js +7 -4
- package/src/optimize/index.js +1179 -704
- package/src/optimize/recurse.js +2 -2
- package/src/optimize/vectorize.js +982 -67
- package/src/prepare/index.js +809 -67
- package/src/prepare/math-kernel.js +331 -0
- package/src/prepare/pre-eval.js +714 -0
- package/src/snapshot.js +194 -0
- package/src/type.js +1170 -56
- package/src/wat/assemble.js +403 -65
- package/src/wat/codegen.js +74 -14
- package/transform.js +113 -4
- package/wasi.js +3 -0
package/module/object.js
CHANGED
|
@@ -7,12 +7,13 @@
|
|
|
7
7
|
* @module object
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { typed, asF64, asI64, NULL_NAN, UNDEF_NAN, temp, tempI32, tempI64, block64, ptrTypeEq, dispatchByPtrType, allocPtr, needsDynShadow, mkPtrIR, extractF64Bits, appendStaticSlots, slotAddr, elemLoad, elemStore, boolBoxIR } from '../src/ir.js'
|
|
10
|
+
import { typed, asF64, asI64, NULL_NAN, UNDEF_NAN, temp, tempI32, tempI64, block64, ptrTypeEq, dispatchByPtrType, allocPtr, needsDynShadow, mkPtrIR, extractF64Bits, appendStaticSlots, slotAddr, elemLoad, elemStore, boolBoxIR, carrierF64 } from '../src/ir.js'
|
|
11
11
|
import { emit } from '../src/bridge.js'
|
|
12
12
|
import { staticArrayPtr } from './array.js'
|
|
13
13
|
import { valTypeOf, shapeOf } from '../src/kind.js'
|
|
14
14
|
import { VAL, lookupValType, repOf, updateRep } from '../src/reps.js'
|
|
15
15
|
import { ctx, err, inc, PTR, LAYOUT, declGlobal } from '../src/ctx.js'
|
|
16
|
+
import { isReassigned } from '../src/ast.js'
|
|
16
17
|
|
|
17
18
|
// Object.prototype.toString tag per value category. Matches what JS engines
|
|
18
19
|
// return for primitive/built-in types; canonicalized from
|
|
@@ -39,7 +40,18 @@ const objectToStringTagForVal = (obj) => {
|
|
|
39
40
|
return val ? OBJECT_TO_STRING_TAGS[val] : null
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
|
|
43
|
+
// emit(node) ONCE, before branching — same self-host miscompile class as emit.js's
|
|
44
|
+
// 'return' handler (src/compile/emit.js): emit(node) called separately inline per
|
|
45
|
+
// ternary arm, wrapped by a DIFFERENT coercion (boolBoxIR vs asF64) per arm, is
|
|
46
|
+
// behaviorally identical in JS but self-host-fragile. See .work/todo.md (groundtruth archive).
|
|
47
|
+
const storedValue = (node) => carrierF64(node, emit(node))
|
|
48
|
+
|
|
49
|
+
// Array-IR twin of collection.js's heapResetWat (WAT-string form) — both MUST
|
|
50
|
+
// gate identically. True when `off` is ephemeral (>= the post-init high-water
|
|
51
|
+
// mark): only then does a receiver's off-16 header sidecar hold the live
|
|
52
|
+
// dyn-prop truth. See collection.js's heapResetWat for the full durable-
|
|
53
|
+
// receiver policy rationale.
|
|
54
|
+
const heapResetIR = () => ctx.scope.globals.has('__heap_reset') ? ['global.get', '$__heap_reset'] : ['i32.const', 0]
|
|
43
55
|
|
|
44
56
|
export default (ctx) => {
|
|
45
57
|
inc('__mkptr', '__alloc', '__alloc_hdr', '__ptr_offset', '__len', '__ptr_type')
|
|
@@ -58,6 +70,20 @@ export default (ctx) => {
|
|
|
58
70
|
// schema-slot writes to offsets >= 8 land out-of-bounds.
|
|
59
71
|
const target = takeLiteralTarget()
|
|
60
72
|
const merged = target ? ctx.schema.resolve(target) : null
|
|
73
|
+
// Dictionary mode: a `{}` whose binding takes ONLY computed-key access (no
|
|
74
|
+
// static prop ever — merged schema empty) is a string-keyed dictionary, not
|
|
75
|
+
// a fixed-shape record. Represent it as a real HASH so every get/set is one
|
|
76
|
+
// strict probe instead of the OBJECT dyn-sidecar detour, and the RMW fusion
|
|
77
|
+
// (`o[k] = f(o[k])`, emit-assign.js) can hold a slot address. The same
|
|
78
|
+
// heuristic V8 uses to send such objects to dictionary mode. Sound: all dyn
|
|
79
|
+
// paths dispatch on the runtime tag, and mem.Hash marshals it as a plain
|
|
80
|
+
// object at the boundary.
|
|
81
|
+
if (target && !merged?.length && ctx.types.dynWriteVars?.has(target)) {
|
|
82
|
+
ctx.module.include('collection')
|
|
83
|
+
inc('__hash_new_small')
|
|
84
|
+
updateRep(target, { val: VAL.HASH })
|
|
85
|
+
return typed(['call', '$__hash_new_small'], 'f64')
|
|
86
|
+
}
|
|
61
87
|
// Register the empty schema so schemaId always indexes a real schema.list
|
|
62
88
|
// entry — __json_obj and dyn-get load keys via $__schema_tbl[sid] and would
|
|
63
89
|
// crash on an unregistered id 0 (table left uninitialized when list empty).
|
|
@@ -117,24 +143,38 @@ export default (ctx) => {
|
|
|
117
143
|
// `map.get(k).n++` that no alias analysis could attribute — so a literal
|
|
118
144
|
// whose schema intersects it allocates per-evaluation instead.
|
|
119
145
|
const neverWritten = names.every(n => !ctx.module.writtenProps?.has(n))
|
|
120
|
-
|
|
146
|
+
// `!shadow`: a computed-key write on the target (`o[k]=v`) mutates the object —
|
|
147
|
+
// a shared static instance would leak call N's writes into call N+1. The old
|
|
148
|
+
// shadow-mirror masked this by re-storing literal values through __dyn_set's
|
|
149
|
+
// schema-arm on every evaluation (an accidental reset that still leaked
|
|
150
|
+
// runtime-ADDED keys); with the mirror gone (tier 2), mutable literals must
|
|
151
|
+
// allocate fresh per evaluation — the runtime path below.
|
|
152
|
+
if (neverWritten && !shadow && values.length >= 2 && values.length === schema.length && !ctx.memory.shared) {
|
|
121
153
|
const emitted = values.map(storedValue)
|
|
122
154
|
// asF64 folds i32.const → f64.const so int-literal values also qualify.
|
|
123
155
|
const slots = emitted.map(v => extractF64Bits(v))
|
|
124
156
|
if (slots.every(b => b !== null)) {
|
|
125
157
|
// Reorder into schema-slot order before laying out the static segment.
|
|
126
|
-
const
|
|
127
|
-
for (let i = 0; i < values.length; i++)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
158
|
+
const orderedBits = emitted.map(() => null)
|
|
159
|
+
for (let i = 0; i < values.length; i++) orderedBits[slotOf(i)] = slots[i]
|
|
160
|
+
// Full 16-byte __alloc_hdr-shaped header ([props:8]=0, len=0, cap=slots):
|
|
161
|
+
// dyn machinery reads the off-16 props word — a headerless static object
|
|
162
|
+
// aliased whatever data preceded it (the durable-dangler garbage class),
|
|
163
|
+
// and a runtime dyn-set/delete on the shared instance now has a real,
|
|
164
|
+
// writable slot to install a sidecar into. No runtime shadow mirror
|
|
165
|
+
// either way (tier 2): dyn READS of schema props resolve through the
|
|
166
|
+
// schema-arm (__schema_tbl — itself static data now), so the old
|
|
167
|
+
// per-prop __dyn_set mirror was pure init cost — the block it emitted
|
|
168
|
+
// also made the literal non-const, forcing every ENCLOSING literal
|
|
169
|
+
// (`const A = [{…}, {…}]`) to build at runtime.
|
|
170
|
+
if (!ctx.runtime.data) ctx.runtime.data = ''
|
|
171
|
+
while (ctx.runtime.data.length % 8 !== 0) ctx.runtime.data += '\0'
|
|
172
|
+
const hdrOff = ctx.runtime.data.length
|
|
173
|
+
const hdr = new Uint8Array(16); const hdv = new DataView(hdr.buffer)
|
|
174
|
+
hdv.setInt32(12, ctx.abi.object.ops.allocSlots ? ctx.abi.object.ops.allocSlots(schema.length) : schema.length, true)
|
|
175
|
+
for (let i = 0; i < 16; i++) ctx.runtime.data += String.fromCharCode(hdr[i])
|
|
176
|
+
appendStaticSlots(orderedBits)
|
|
177
|
+
return mkPtrIR(PTR.OBJECT, schemaId, hdrOff + 16)
|
|
138
178
|
}
|
|
139
179
|
}
|
|
140
180
|
|
|
@@ -157,6 +197,11 @@ export default (ctx) => {
|
|
|
157
197
|
|
|
158
198
|
// === Object static methods ===
|
|
159
199
|
|
|
200
|
+
// Object.freeze: identity passthrough — jz objects have no per-property
|
|
201
|
+
// metadata, so write-protection is not enforced (documented divergence).
|
|
202
|
+
// Call forms are folded away in prepare (which records the binding in
|
|
203
|
+
// ctx.runtime.frozenVars so isFrozen answers true for it); this emitter
|
|
204
|
+
// survives only for freeze-as-a-value (`arr.map(Object.freeze)`).
|
|
160
205
|
ctx.core.emit['Object.freeze'] = (obj) => asF64(emit(obj))
|
|
161
206
|
|
|
162
207
|
// Object.is(a, b) — SameValue, which on NaN-boxed f64 values is exact bit
|
|
@@ -180,14 +225,21 @@ export default (ctx) => {
|
|
|
180
225
|
if (t === VAL.NUMBER || t === VAL.STRING || t === VAL.BIGINT) return { ext: 0, sealed: 1, frozen: 1 }
|
|
181
226
|
return { ext: 1, sealed: 0, frozen: 0 }
|
|
182
227
|
}
|
|
183
|
-
const objQuery = (pick) => (obj) => {
|
|
184
|
-
|
|
228
|
+
const objQuery = (pick, frozenAware = false) => (obj) => {
|
|
229
|
+
let v = pick(extKind(obj))
|
|
230
|
+
if (frozenAware) {
|
|
231
|
+
// A binding freeze() marked in prepare (composition unwraps to the same
|
|
232
|
+
// name there). Trustworthy only while never reassigned — a rebind points
|
|
233
|
+
// at a fresh, unfrozen object.
|
|
234
|
+
if (typeof obj === 'string' && ctx.runtime.frozenVars?.has(obj) &&
|
|
235
|
+
!isReassigned(ctx.func.body, obj)) v = 1
|
|
236
|
+
}
|
|
185
237
|
if (obj == null) return typed(['f64.const', v], 'f64')
|
|
186
238
|
return typed(['block', ['result', 'f64'], ['drop', asF64(emit(obj))], ['f64.const', v]], 'f64')
|
|
187
239
|
}
|
|
188
240
|
ctx.core.emit['Object.isExtensible'] = objQuery((k) => k.ext)
|
|
189
241
|
ctx.core.emit['Object.isSealed'] = objQuery((k) => k.sealed)
|
|
190
|
-
ctx.core.emit['Object.isFrozen'] = objQuery((k) => k.frozen)
|
|
242
|
+
ctx.core.emit['Object.isFrozen'] = objQuery((k) => k.frozen, true)
|
|
191
243
|
|
|
192
244
|
// RequireObjectCoercible: Object.keys/values/entries reject null & undefined
|
|
193
245
|
// with a TypeError. A literal lowers to a [null, value] node — so `null` is
|
|
@@ -229,10 +281,18 @@ export default (ctx) => {
|
|
|
229
281
|
out.ptr], 'f64')
|
|
230
282
|
}
|
|
231
283
|
|
|
232
|
-
|
|
284
|
+
// Shared by Object.keys and __keys_ro. `ro` marks the for-in path: its result
|
|
285
|
+
// is read-only by construction (the lowering only reads ks[i]/ks.length), so
|
|
286
|
+
// the HASH arms may serve the shared enum-cache array (core.js __hash_keys_ro)
|
|
287
|
+
// instead of a fresh copy. Object.keys stays fresh — callers may mutate it.
|
|
288
|
+
const emitKeysGeneric = (obj, ro) => {
|
|
289
|
+
// Shared memory: cache globals are per-instance but the table is cross-thread,
|
|
290
|
+
// and shared's `__clear` (plain rewind, core.js) takes no reset injections —
|
|
291
|
+
// both break the cache's invalidation story. Serve the uncached path there.
|
|
292
|
+
if (ctx.memory.shared) ro = false
|
|
233
293
|
const nullish = requireCoercible(obj)
|
|
234
294
|
if (nullish) return nullish
|
|
235
|
-
if (isHashTyped(obj)) return emitHashKeys(obj)
|
|
295
|
+
if (isHashTyped(obj)) return ro ? emitHashKeysRO(obj) : emitHashKeys(obj)
|
|
236
296
|
if (arrayValType(obj)) return idxKeys(obj, '__len')
|
|
237
297
|
if (stringValType(obj)) return idxKeys(obj, '__str_len')
|
|
238
298
|
const schema = resolveSchema(obj)
|
|
@@ -240,11 +300,12 @@ export default (ctx) => {
|
|
|
240
300
|
// computed writes (`o[k]=v`) also carries props in its per-object dyn HASH
|
|
241
301
|
// that the schema omits. Enumerate those live (schema ∪ dyn props) — the gap
|
|
242
302
|
// that blocked metacircularity (the compiler grows dicts then enumerates).
|
|
243
|
-
if (schema && !mayHaveDynProps(obj)) return emitStringArray(schema)
|
|
303
|
+
if (schema && !mayHaveDynProps(obj) && !hasOutOfSchemaWrites(obj, schema)) return emitStringArray(schema)
|
|
244
304
|
// Unknown receiver, or schema with possible dyn props: dispatch on ptr-type
|
|
245
305
|
// at runtime (HASH probe table / OBJECT schema+dyn merge / else []).
|
|
246
|
-
return emitRuntimeKeys(obj)
|
|
306
|
+
return emitRuntimeKeys(obj, ro)
|
|
247
307
|
}
|
|
308
|
+
ctx.core.emit['Object.keys'] = (obj) => emitKeysGeneric(obj, false)
|
|
248
309
|
ctx.core.emit['Object.getOwnPropertyNames'] = ctx.core.emit['Object.keys']
|
|
249
310
|
|
|
250
311
|
// for-in's read-only key enumeration (src/prepare for…in lowering). Identical to
|
|
@@ -258,18 +319,20 @@ export default (ctx) => {
|
|
|
258
319
|
// Object.keys (evaluates the receiver, full runtime enumeration).
|
|
259
320
|
ctx.core.emit['__keys_ro'] = (obj) => {
|
|
260
321
|
// Pool only when the receiver's enumerable key set is provably the static
|
|
261
|
-
// schema: a bare var with NO computed-key writes (`o[k]=v`)
|
|
262
|
-
//
|
|
322
|
+
// schema: a bare var with NO computed-key writes (`o[k]=v`) and no literal
|
|
323
|
+
// writes outside the schema — either kind adds enumerable keys the pool
|
|
324
|
+
// would drop (computed writes via dynWriteVars; out-of-schema literal
|
|
325
|
+
// writes land in the dyn sidecar — see hasOutOfSchemaWrites).
|
|
263
326
|
// `mayHaveDynProps` is too coarse here — it also flags computed-READ receivers,
|
|
264
327
|
// and for-in's own `o[k]` read would otherwise veto its own pooling.
|
|
265
328
|
if (typeof obj === 'string' && !ctx.types.dynWriteVars?.has(obj) && !isHashTyped(obj) && !arrayValType(obj) && !stringValType(obj)) {
|
|
266
329
|
const schema = resolveSchema(obj)
|
|
267
|
-
if (schema) {
|
|
330
|
+
if (schema && !hasOutOfSchemaWrites(obj, schema)) {
|
|
268
331
|
const slots = schema.map(name => extractF64Bits(asF64(emit(['str', name]))))
|
|
269
332
|
if (slots.every(b => b !== null)) return staticArrayPtr(slots)
|
|
270
333
|
}
|
|
271
334
|
}
|
|
272
|
-
return
|
|
335
|
+
return emitKeysGeneric(obj, true)
|
|
273
336
|
}
|
|
274
337
|
|
|
275
338
|
// Object.prototype.hasOwnProperty(key) — own-property presence check.
|
|
@@ -362,7 +425,7 @@ export default (ctx) => {
|
|
|
362
425
|
if (arrayValType(obj)) { inc('__arr_from'); return typed(['call', '$__arr_from', asI64(emit(obj))], 'f64') }
|
|
363
426
|
if (isHashTyped(obj)) return emitHashValues(obj)
|
|
364
427
|
const schema = resolveSchema(obj)
|
|
365
|
-
if (!schema || mayHaveDynProps(obj)) return emitRuntimeValues(obj)
|
|
428
|
+
if (!schema || mayHaveDynProps(obj) || hasOutOfSchemaWrites(obj, schema)) return emitRuntimeValues(obj)
|
|
366
429
|
const va = asF64(emit(obj))
|
|
367
430
|
const n = schema.length
|
|
368
431
|
const t = temp('ov'), base = tempI32('vb')
|
|
@@ -425,7 +488,7 @@ export default (ctx) => {
|
|
|
425
488
|
}
|
|
426
489
|
if (isHashTyped(obj)) return emitHashEntries(obj)
|
|
427
490
|
const schema = resolveSchema(obj)
|
|
428
|
-
if (!schema || mayHaveDynProps(obj)) return emitRuntimeEntries(obj)
|
|
491
|
+
if (!schema || mayHaveDynProps(obj) || hasOutOfSchemaWrites(obj, schema)) return emitRuntimeEntries(obj)
|
|
429
492
|
const va = asF64(emit(obj))
|
|
430
493
|
const n = schema.length
|
|
431
494
|
const t = temp('oe'), pair = tempI32('op'), base = tempI32('eb')
|
|
@@ -458,6 +521,8 @@ export default (ctx) => {
|
|
|
458
521
|
}
|
|
459
522
|
const boxedSchema = ['__inner__', ...allProps]
|
|
460
523
|
const schemaId = ctx.schema.register(boxedSchema)
|
|
524
|
+
// Extern-write belt: source slot values copied in below, unseen by the censuses.
|
|
525
|
+
ctx.schema.externSlotSids?.add(schemaId)
|
|
461
526
|
ctx.schema.vars.set(target, schemaId)
|
|
462
527
|
// Emit-time rep mutation: Object.assign's target gains a freshly-registered
|
|
463
528
|
// boxed-schema binding here; downstream `.prop` reads in the same emit pass
|
|
@@ -489,6 +554,11 @@ export default (ctx) => {
|
|
|
489
554
|
const sourceSchemas = sources.map(resolveSchema)
|
|
490
555
|
if (!tSchema) return emitObjectAssignDynamic(target, sources)
|
|
491
556
|
if (sourceSchemas.some(s => !s)) return emitDynamicAssign(target, sources, sourceSchemas)
|
|
557
|
+
// Extern-write belt: cross-schema slot copies into the TARGET's sid below
|
|
558
|
+
// (plan's hazard scan marks the same target when it resolves it).
|
|
559
|
+
const tSid = typeof target === 'string'
|
|
560
|
+
? (repOf(target)?.schemaId ?? ctx.schema.vars.get(target)) : null
|
|
561
|
+
if (tSid != null) ctx.schema.externSlotSids?.add(tSid)
|
|
492
562
|
const t = temp('at'), s = temp('as')
|
|
493
563
|
const tBase = tempI32('tb'), sBase2 = tempI32('sb')
|
|
494
564
|
// When the target carries a dynamic-props shadow (needsDynShadow), reads of an
|
|
@@ -585,9 +655,9 @@ export default (ctx) => {
|
|
|
585
655
|
['f64.store',
|
|
586
656
|
['i32.sub', ['local.get', `$${dstOff}`], ['i32.const', 16]],
|
|
587
657
|
['f64.load', ['i32.sub', ['local.get', `$${srcOff}`], ['i32.const', 16]]]],
|
|
588
|
-
['call', '$__dyn_move',
|
|
658
|
+
['drop', ['call', '$__dyn_move',
|
|
589
659
|
['local.get', `$${srcOff}`],
|
|
590
|
-
['local.get', `$${dstOff}`]],
|
|
660
|
+
['local.get', `$${dstOff}`]]],
|
|
591
661
|
['local.get', `$${dst}`]], 'f64')
|
|
592
662
|
}
|
|
593
663
|
const schema = resolveSchema(proto)
|
|
@@ -610,9 +680,9 @@ export default (ctx) => {
|
|
|
610
680
|
['f64.store',
|
|
611
681
|
['i32.sub', ['local.get', `$${dstOff2}`], ['i32.const', 16]],
|
|
612
682
|
['f64.load', ['i32.sub', ['local.get', `$${srcOff2}`], ['i32.const', 16]]]],
|
|
613
|
-
['call', '$__dyn_move',
|
|
683
|
+
['drop', ['call', '$__dyn_move',
|
|
614
684
|
['local.get', `$${srcOff2}`],
|
|
615
|
-
['local.get', `$${dstOff2}`]],
|
|
685
|
+
['local.get', `$${dstOff2}`]]],
|
|
616
686
|
['local.get', `$${dst2}`]]],
|
|
617
687
|
['else', ['local.get', `$${value}`]]]] , 'f64')
|
|
618
688
|
}
|
|
@@ -738,10 +808,35 @@ function emitObjectAssignDynamic(target, sources) {
|
|
|
738
808
|
// them; callers route it through the runtime schema∪dyn-props merge instead.
|
|
739
809
|
const mayHaveDynProps = (obj) => typeof obj === 'string' && !!ctx.types.dynKeyVars?.has(obj)
|
|
740
810
|
|
|
811
|
+
// A literal-key write of a key OUTSIDE the receiver's schema lands in the
|
|
812
|
+
// dyn-props sidecar (locals get no propMap/autoBox merge) — the static schema
|
|
813
|
+
// fold would silently drop it from enumeration, so such receivers must take
|
|
814
|
+
// the runtime merge path. Bare-var receivers only, mirroring dynWriteVars'
|
|
815
|
+
// per-name precision (expression receivers are runtime-dispatched anyway).
|
|
816
|
+
const hasOutOfSchemaWrites = (obj, schema) => {
|
|
817
|
+
if (typeof obj !== 'string') return false
|
|
818
|
+
const w = ctx.types.literalWriteKeys?.get(obj)
|
|
819
|
+
if (!w) return false
|
|
820
|
+
for (const k of w) if (!schema.includes(k)) return true
|
|
821
|
+
return false
|
|
822
|
+
}
|
|
823
|
+
|
|
741
824
|
function resolveSchema(obj) {
|
|
742
825
|
if (typeof obj === 'string') return ctx.schema.resolve(obj)
|
|
743
|
-
if (Array.isArray(obj) && obj[0] === '{}')
|
|
744
|
-
|
|
826
|
+
if (Array.isArray(obj) && obj[0] === '{}') {
|
|
827
|
+
// Comma-grouped children arrive as ['{}', [',', p1, p2, …]] — same unwrap
|
|
828
|
+
// as the '{}' emitter's, or a grouped literal resolves to zero keys.
|
|
829
|
+
const props = obj.length === 2 && Array.isArray(obj[1]) && obj[1][0] === ','
|
|
830
|
+
? obj[1].slice(1) : obj.slice(1)
|
|
831
|
+
// A spread-bearing literal's schema is the MERGE emitObjectSpread builds
|
|
832
|
+
// (or null when any source is unknown → HASH result, runtime enumeration).
|
|
833
|
+
// Filtering to ':'-entries here made Object.keys({ ...S, z: 9 }) fold to
|
|
834
|
+
// ['z'] while the object's real slot layout carried S's keys too — the
|
|
835
|
+
// spread's keys vanished from keys/values/entries/for-in/JSON while
|
|
836
|
+
// remaining readable as properties (watr-in-kernel's normalize() cfg).
|
|
837
|
+
if (props.some(p => Array.isArray(p) && p[0] === '...')) return spreadLiteralSchema(props)
|
|
838
|
+
return props.filter(p => Array.isArray(p) && p[0] === ':').map(p => p[1])
|
|
839
|
+
}
|
|
745
840
|
// JSON-shape inferred: JSON.parse(constStr) call or `.prop`/`[i]` chain
|
|
746
841
|
// resolving to a known OBJECT shape carries its key list as `names`.
|
|
747
842
|
const sh = shapeOf(obj)
|
|
@@ -774,22 +869,32 @@ function takeLiteralTarget() {
|
|
|
774
869
|
return frame.name
|
|
775
870
|
}
|
|
776
871
|
|
|
872
|
+
// Merged static schema of a spread-bearing literal, or null when any spread
|
|
873
|
+
// source's key set is unknown at compile time (→ HASH result). The SINGLE
|
|
874
|
+
// source of truth for emitObjectSpread (which builds the object with exactly
|
|
875
|
+
// this slot layout) and resolveSchema (which keys/values/entries/for-in fold
|
|
876
|
+
// against) — the two MUST agree or enumeration drops the spread's keys.
|
|
877
|
+
function spreadLiteralSchema(props) {
|
|
878
|
+
const names = []
|
|
879
|
+
const add = n => { if (!names.includes(n)) names.push(n) }
|
|
880
|
+
for (const p of props) {
|
|
881
|
+
if (Array.isArray(p) && p[0] === '...') {
|
|
882
|
+
const s = spreadSourceSchema(p[1])
|
|
883
|
+
if (!s) return null
|
|
884
|
+
for (const n of s) add(n)
|
|
885
|
+
} else if (Array.isArray(p) && p[0] === ':') add(p[1])
|
|
886
|
+
}
|
|
887
|
+
return names
|
|
888
|
+
}
|
|
889
|
+
|
|
777
890
|
function emitObjectSpread(props, spreadTarget = takeLiteralTarget()) {
|
|
778
891
|
// Resolve every spread source's schema. A source with no static schema means
|
|
779
892
|
// its full key set is unknown at compile time, so the merge result must be a
|
|
780
893
|
// HASH (dynamic dict) — a fixed schema would silently drop the source's keys
|
|
781
894
|
// it doesn't list. Only when EVERY source is known do we build the fixed-shape
|
|
782
895
|
// OBJECT below.
|
|
783
|
-
const allNames =
|
|
784
|
-
const
|
|
785
|
-
let allKnown = true
|
|
786
|
-
for (const p of props) {
|
|
787
|
-
if (Array.isArray(p) && p[0] === '...') {
|
|
788
|
-
const s = spreadSourceSchema(p[1])
|
|
789
|
-
if (s) for (const n of s) addName(n)
|
|
790
|
-
else allKnown = false
|
|
791
|
-
} else if (Array.isArray(p) && p[0] === ':') addName(p[1])
|
|
792
|
-
}
|
|
896
|
+
const allNames = spreadLiteralSchema(props)
|
|
897
|
+
const allKnown = allNames != null
|
|
793
898
|
// Single unknown spread `{ ...src }` → shallow-clone src at runtime, preserving
|
|
794
899
|
// its type (OBJECT→OBJECT, HASH→HASH). Aliasing src (the old shortcut) leaked
|
|
795
900
|
// every later write to the result back into the source — a real correctness bug
|
|
@@ -804,6 +909,10 @@ function emitObjectSpread(props, spreadTarget = takeLiteralTarget()) {
|
|
|
804
909
|
if (!allKnown) return emitDynamicSpread(props)
|
|
805
910
|
|
|
806
911
|
const schemaId = ctx.schema.register(allNames)
|
|
912
|
+
// Extern-write belt: the spread slot-copies below write source-schema values
|
|
913
|
+
// into this sid outside the write censuses' view (collectSlotWriteHazards
|
|
914
|
+
// normally resolves the same merge at plan time; the belt covers divergence).
|
|
915
|
+
ctx.schema.externSlotSids?.add(schemaId)
|
|
807
916
|
const schema = ctx.schema.list[schemaId]
|
|
808
917
|
const t = tempI32('obj')
|
|
809
918
|
const ptr = temp('objp')
|
|
@@ -918,6 +1027,28 @@ function emitHashKeys(obj) {
|
|
|
918
1027
|
hashKeysFromTemp(t)], 'f64')
|
|
919
1028
|
}
|
|
920
1029
|
|
|
1030
|
+
// for-in over a statically-HASH receiver: serve keys through the shared enum
|
|
1031
|
+
// cache (core.js __hash_keys_ro) — read-only by the for-in lowering's contract.
|
|
1032
|
+
function emitHashKeysRO(obj) {
|
|
1033
|
+
inc('__hash_keys_ro')
|
|
1034
|
+
declEnumcGlobals()
|
|
1035
|
+
return typed(['call', '$__hash_keys_ro', ['i64.reinterpret_f64', asF64(emit(obj))]], 'f64')
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// __hash_keys_ro's cache globals — declared at emit time so the helper's text
|
|
1039
|
+
// resolves even in builds where collection.js (which also declares them for its
|
|
1040
|
+
// delete-hook) never loads. enumcConsumed marks that some enumeration site can
|
|
1041
|
+
// FILL the cache this build (the OBJECT arm is inline IR — reachability can't
|
|
1042
|
+
// see it), so assemble.js knows to reset it in `__clear` (ABA guard).
|
|
1043
|
+
function declEnumcGlobals() {
|
|
1044
|
+
ctx.runtime.enumcConsumed = true
|
|
1045
|
+
if (!ctx.scope.globals.has('__enumc_off')) {
|
|
1046
|
+
declGlobal('__enumc_off', 'i32')
|
|
1047
|
+
declGlobal('__enumc_len', 'i32')
|
|
1048
|
+
declGlobal('__enumc_arr', 'f64')
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
|
|
921
1052
|
function emitHashValues(obj) {
|
|
922
1053
|
const t = temp('hv')
|
|
923
1054
|
return typed(['block', ['result', 'f64'],
|
|
@@ -1017,30 +1148,39 @@ function hashEntriesFromTemp(t) {
|
|
|
1017
1148
|
// compile time or lazily at runtime by JSON.parse via __jp_schema_get); other
|
|
1018
1149
|
// types (ARRAY, nullish, primitives) return an empty array. The empty-array
|
|
1019
1150
|
// fallback is allocated in all arms for type uniformity at the if boundary.
|
|
1020
|
-
function emitRuntimeKeys(obj) {
|
|
1151
|
+
function emitRuntimeKeys(obj, ro) {
|
|
1021
1152
|
const t = temp('rk')
|
|
1022
1153
|
return typed(['block', ['result', 'f64'],
|
|
1023
1154
|
['local.set', `$${t}`, asF64(emit(obj))],
|
|
1024
|
-
runtimeKeysFromTemp(t, 'rk')], 'f64')
|
|
1155
|
+
runtimeKeysFromTemp(t, 'rk', ro)], 'f64')
|
|
1025
1156
|
}
|
|
1026
1157
|
|
|
1027
|
-
function runtimeKeysFromTemp(t, tag) {
|
|
1158
|
+
function runtimeKeysFromTemp(t, tag, ro) {
|
|
1159
|
+
if (ctx.memory.shared) ro = false // see emitKeysGeneric — no enum cache under shared memory
|
|
1028
1160
|
inc('__ptr_type')
|
|
1029
1161
|
// Ensure the schema table global exists even in programs that never use
|
|
1030
1162
|
// JSON.parse or compile-time schemas — the OBJECT arm reads it at runtime
|
|
1031
|
-
// and the watr resolver requires the symbol to be declared.
|
|
1163
|
+
// and the watr resolver requires the symbol to be declared. Declaring is not
|
|
1164
|
+
// enough: the OBJECT arm READS the table inline (no named helper to count),
|
|
1165
|
+
// so mark consumption for assemble.js's needsSchemaTbl gate or the table
|
|
1166
|
+
// stays 0 and enumeration silently yields zero schema keys.
|
|
1032
1167
|
if (!ctx.scope.globals.has('__schema_tbl'))
|
|
1033
1168
|
declGlobal('__schema_tbl', 'i32')
|
|
1169
|
+
ctx.runtime.schemaTblConsumed = true
|
|
1034
1170
|
const tt = tempI32(`${tag}t`)
|
|
1035
1171
|
const empty = allocPtr({ type: PTR.ARRAY, len: 0, tag: `${tag}e` })
|
|
1036
1172
|
return ['block', ['result', 'f64'],
|
|
1037
1173
|
['local.set', `$${tt}`, ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
|
|
1038
1174
|
['if', ['result', 'f64'],
|
|
1039
1175
|
['i32.eq', ['local.get', `$${tt}`], ['i32.const', PTR.HASH]],
|
|
1040
|
-
|
|
1176
|
+
// for-in (ro): serve the shared enum-cache array — see emitHashKeysRO.
|
|
1177
|
+
['then', ro
|
|
1178
|
+
? (inc('__hash_keys_ro'), declEnumcGlobals(),
|
|
1179
|
+
['call', '$__hash_keys_ro', ['i64.reinterpret_f64', ['local.get', `$${t}`]]])
|
|
1180
|
+
: hashKeysFromTemp(t)],
|
|
1041
1181
|
['else', ['if', ['result', 'f64'],
|
|
1042
1182
|
['i32.eq', ['local.get', `$${tt}`], ['i32.const', PTR.OBJECT]],
|
|
1043
|
-
['then', objectKeysFromTemp(t)],
|
|
1183
|
+
['then', objectKeysFromTemp(t, ro)],
|
|
1044
1184
|
['else', ['block', ['result', 'f64'], empty.init, empty.ptr]]]]]]
|
|
1045
1185
|
}
|
|
1046
1186
|
|
|
@@ -1048,6 +1188,7 @@ function emitRuntimeValues(obj) {
|
|
|
1048
1188
|
inc('__ptr_type')
|
|
1049
1189
|
if (!ctx.scope.globals.has('__schema_tbl'))
|
|
1050
1190
|
declGlobal('__schema_tbl', 'i32')
|
|
1191
|
+
ctx.runtime.schemaTblConsumed = true
|
|
1051
1192
|
const t = temp('rv'), tt = tempI32('rvt')
|
|
1052
1193
|
const empty = allocPtr({ type: PTR.ARRAY, len: 0, tag: 'rve' })
|
|
1053
1194
|
return typed(['block', ['result', 'f64'],
|
|
@@ -1066,6 +1207,7 @@ function emitRuntimeEntries(obj) {
|
|
|
1066
1207
|
inc('__ptr_type')
|
|
1067
1208
|
if (!ctx.scope.globals.has('__schema_tbl'))
|
|
1068
1209
|
declGlobal('__schema_tbl', 'i32')
|
|
1210
|
+
ctx.runtime.schemaTblConsumed = true
|
|
1069
1211
|
const t = temp('re'), tt = tempI32('ret')
|
|
1070
1212
|
const empty = allocPtr({ type: PTR.ARRAY, len: 0, tag: 'ree' })
|
|
1071
1213
|
return typed(['block', ['result', 'f64'],
|
|
@@ -1098,17 +1240,88 @@ function emitRuntimeEntries(obj) {
|
|
|
1098
1240
|
//
|
|
1099
1241
|
// Callbacks receive the active locals as named fields so each variant can
|
|
1100
1242
|
// reference what it needs without knowing the scaffold's layout.
|
|
1101
|
-
function emitEnumerateObject(t, emitStaticStore, emitDynStore) {
|
|
1243
|
+
function emitEnumerateObject(t, emitStaticStore, emitDynStore, ro) {
|
|
1102
1244
|
inc('__alloc_hdr', '__ptr_offset', '__coll_order')
|
|
1245
|
+
if (ro) declEnumcGlobals()
|
|
1246
|
+
// Durable-receiver global-table merge (see below) only when collection.js's
|
|
1247
|
+
// dyn-props machinery is actually part of this build — a program that never
|
|
1248
|
+
// writes a dynamic prop anywhere never loads collection.js, so __dyn_props/
|
|
1249
|
+
// __ihash_get_local wouldn't exist to reference.
|
|
1250
|
+
const hasDynProps = ctx.scope.globals.has('__dyn_props')
|
|
1251
|
+
if (hasDynProps) inc('__ihash_get_local', '__is_nullish')
|
|
1103
1252
|
const sid = tempI32('oes'), src = tempI32('oesrc'), sn = tempI32('oen')
|
|
1104
|
-
const base = tempI32('oebase'), props = tempI64('oepr')
|
|
1105
|
-
|
|
1253
|
+
const base = tempI32('oebase'), props = tempI64('oepr')
|
|
1254
|
+
// TWO dyn-prop sources for a DURABLE receiver: the off-16 sidecar (populated
|
|
1255
|
+
// if this receiver got dyn props DURING init, while __heap_reset was still
|
|
1256
|
+
// low) and the global __dyn_props table (populated by RUNTIME/post-init
|
|
1257
|
+
// writes — see collection.js's heapResetWat). Either, both, or neither may
|
|
1258
|
+
// be non-empty; enumerate whichever exist, deduping across schema ∪ global
|
|
1259
|
+
// ∪ sidecar (in that priority — global shadows a sidecar entry with the
|
|
1260
|
+
// same key, matching __dyn_get_t_h's read priority). An EPHEMERAL receiver
|
|
1261
|
+
// only ever uses the sidecar slot (poffG stays 0 for it).
|
|
1262
|
+
const poffG = tempI32('oepoG'), pcapG = tempI32('oepcG'), dnG = tempI32('oednG'), ordG = tempI32('oeordG')
|
|
1263
|
+
const poffS = tempI32('oepoS'), pcapS = tempI32('oepcS'), dnS = tempI32('oednS'), ordS = tempI32('oeordS')
|
|
1264
|
+
const total = tempI32('oetot')
|
|
1106
1265
|
const out = tempI32('oeo'), i = tempI32('oei'), o = tempI32('oej')
|
|
1107
|
-
const slot = tempI32('oesl')
|
|
1266
|
+
const slot = tempI32('oesl')
|
|
1108
1267
|
const j = tempI32('oej2'), skip = tempI32('oesk'), pair = tempI32('oep')
|
|
1109
1268
|
const id = ctx.func.uniq++
|
|
1110
1269
|
const env = { out, o, src, base, i, slot, pair }
|
|
1111
|
-
|
|
1270
|
+
// for-in enum cache, OBJECT arm (see core.js __hash_keys_ro for the scheme).
|
|
1271
|
+
// Key = (sidecar off, sidecar len): the sidecar identifies the object (one
|
|
1272
|
+
// sidecar per object, offs unique), sid/schema are immutable per object, and
|
|
1273
|
+
// every other key-set change clears the cache — sidecar inserts change dnS
|
|
1274
|
+
// (natural miss), sidecar/global deletes and global dyn-prop inserts clear
|
|
1275
|
+
// __enumc_off at their (cold) sites. Checked BEFORE the global __dyn_props
|
|
1276
|
+
// probe, so a hit skips the ihash lookup too — sound because any global-side
|
|
1277
|
+
// structural change since fill cleared the cache. poffS≠0 guard: an empty
|
|
1278
|
+
// cache (off 0) must not match a sidecar-less object.
|
|
1279
|
+
const roHit = ro ? [['if', ['i32.and',
|
|
1280
|
+
['i32.and',
|
|
1281
|
+
['i32.ne', ['local.get', `$${poffS}`], ['i32.const', 0]],
|
|
1282
|
+
['i32.eq', ['local.get', `$${poffS}`], ['global.get', '$__enumc_off']]],
|
|
1283
|
+
['i32.eq', ['local.get', `$${dnS}`], ['global.get', '$__enumc_len']]],
|
|
1284
|
+
['then', ['br', `$oed${id}`, ['global.get', '$__enumc_arr']]]]] : []
|
|
1285
|
+
// Dedup-and-store one dyn source's dn live slots (at poff/pcap, ord already
|
|
1286
|
+
// computed) against the schema (0..sn @ src) and, when `against` is given,
|
|
1287
|
+
// a second dyn source's already-walked ord array (0..dn2 @ ord2).
|
|
1288
|
+
const walkDyn = (label, dn, ord, against) => ['if', ['i32.ne', ['local.get', `$${dn}`], ['i32.const', 0]],
|
|
1289
|
+
['then',
|
|
1290
|
+
['local.set', `$${i}`, ['i32.const', 0]],
|
|
1291
|
+
['block', `$${label}brk${id}`, ['loop', `$${label}loop${id}`,
|
|
1292
|
+
['br_if', `$${label}brk${id}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${dn}`]]],
|
|
1293
|
+
['local.set', `$${slot}`, ['i32.load', ['i32.add', ['local.get', `$${ord}`],
|
|
1294
|
+
['i32.shl', ['local.get', `$${i}`], ['i32.const', 2]]]]],
|
|
1295
|
+
['local.set', `$${skip}`, ['i32.const', 0]],
|
|
1296
|
+
['local.set', `$${j}`, ['i32.const', 0]],
|
|
1297
|
+
['block', `$${label}skbrk${id}`, ['loop', `$${label}skloop${id}`,
|
|
1298
|
+
['br_if', `$${label}skbrk${id}`, ['i32.ge_s', ['local.get', `$${j}`], ['local.get', `$${sn}`]]],
|
|
1299
|
+
['if', ['i64.eq',
|
|
1300
|
+
['i64.load', ['i32.add', ['local.get', `$${slot}`], ['i32.const', 8]]],
|
|
1301
|
+
['i64.load', ['i32.add', ['local.get', `$${src}`], ['i32.shl', ['local.get', `$${j}`], ['i32.const', 3]]]]],
|
|
1302
|
+
['then', ['local.set', `$${skip}`, ['i32.const', 1]], ['br', `$${label}skbrk${id}`]]],
|
|
1303
|
+
['local.set', `$${j}`, ['i32.add', ['local.get', `$${j}`], ['i32.const', 1]]],
|
|
1304
|
+
['br', `$${label}skloop${id}`]]],
|
|
1305
|
+
...(against ? [
|
|
1306
|
+
['if', ['i32.eqz', ['local.get', `$${skip}`]],
|
|
1307
|
+
['then',
|
|
1308
|
+
['local.set', `$${j}`, ['i32.const', 0]],
|
|
1309
|
+
['block', `$${label}gdbrk${id}`, ['loop', `$${label}gdloop${id}`,
|
|
1310
|
+
['br_if', `$${label}gdbrk${id}`, ['i32.ge_s', ['local.get', `$${j}`], ['local.get', `$${against.dn}`]]],
|
|
1311
|
+
['if', ['i64.eq',
|
|
1312
|
+
['i64.load', ['i32.add', ['local.get', `$${slot}`], ['i32.const', 8]]],
|
|
1313
|
+
['i64.load', ['i32.add', ['i32.load', ['i32.add', ['local.get', `$${against.ord}`],
|
|
1314
|
+
['i32.shl', ['local.get', `$${j}`], ['i32.const', 2]]]], ['i32.const', 8]]]],
|
|
1315
|
+
['then', ['local.set', `$${skip}`, ['i32.const', 1]], ['br', `$${label}gdbrk${id}`]]],
|
|
1316
|
+
['local.set', `$${j}`, ['i32.add', ['local.get', `$${j}`], ['i32.const', 1]]],
|
|
1317
|
+
['br', `$${label}gdloop${id}`]]]]]] : []),
|
|
1318
|
+
['if', ['i32.eqz', ['local.get', `$${skip}`]],
|
|
1319
|
+
['then',
|
|
1320
|
+
...emitDynStore(env),
|
|
1321
|
+
['local.set', `$${o}`, ['i32.add', ['local.get', `$${o}`], ['i32.const', 1]]]]],
|
|
1322
|
+
['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
|
|
1323
|
+
['br', `$${label}loop${id}`]]]]]
|
|
1324
|
+
return ['block', `$oed${id}`, ['result', 'f64'],
|
|
1112
1325
|
// Static schema row: sid (AUX bits) → __schema_tbl[sid] → src offset; n@src-8.
|
|
1113
1326
|
// __schema_tbl is omitted when every program schema is empty (dyn-only dicts);
|
|
1114
1327
|
// guard the read so empty-table programs see sn=0 here and still enumerate
|
|
@@ -1124,13 +1337,20 @@ function emitEnumerateObject(t, emitStaticStore, emitDynStore) {
|
|
|
1124
1337
|
['i64.load', ['i32.add', ['global.get', '$__schema_tbl'], ['i32.shl', ['local.get', `$${sid}`], ['i32.const', 3]]]],
|
|
1125
1338
|
['i64.const', LAYOUT.OFFSET_MASK]]]],
|
|
1126
1339
|
['local.set', `$${sn}`, ['i32.load', ['i32.sub', ['local.get', `$${src}`], ['i32.const', 8]]]]]],
|
|
1127
|
-
// Dyn-props: heap OBJECTs
|
|
1128
|
-
//
|
|
1129
|
-
//
|
|
1340
|
+
// Dyn-props: heap OBJECTs carry a HASH propsPtr either at base-16
|
|
1341
|
+
// (populated by an init-time write, or by any write at all on an
|
|
1342
|
+
// EPHEMERAL receiver — one allocated after the post-init high-water
|
|
1343
|
+
// mark, per the durable-receiver policy) or in the global __dyn_props
|
|
1344
|
+
// table keyed by offset (populated by a RUNTIME/post-init write on a
|
|
1345
|
+
// DURABLE receiver; see collection.js's heapResetWat). Static-segment
|
|
1346
|
+
// objects (base < __heap_start) have no header at all and predate any
|
|
1347
|
+
// warm-reuse machinery, so they contribute no dyn keys either way.
|
|
1130
1348
|
['local.set', `$${base}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
|
|
1131
|
-
['local.set', `$${
|
|
1132
|
-
['local.set', `$${
|
|
1133
|
-
['
|
|
1349
|
+
['local.set', `$${dnG}`, ['i32.const', 0]],
|
|
1350
|
+
['local.set', `$${poffG}`, ['i32.const', 0]],
|
|
1351
|
+
['local.set', `$${dnS}`, ['i32.const', 0]],
|
|
1352
|
+
['local.set', `$${poffS}`, ['i32.const', 0]],
|
|
1353
|
+
['if', ['i32.ge_u', ['local.get', `$${base}`], heapResetIR()],
|
|
1134
1354
|
['then',
|
|
1135
1355
|
['local.set', `$${props}`, ['i64.load', ['i32.sub', ['local.get', `$${base}`], ['i32.const', 16]]]],
|
|
1136
1356
|
['if', ['i32.eq',
|
|
@@ -1139,12 +1359,56 @@ function emitEnumerateObject(t, emitStaticStore, emitDynStore) {
|
|
|
1139
1359
|
['then',
|
|
1140
1360
|
// Resolve forward chain — HASH may have forwarded on grow; the raw
|
|
1141
1361
|
// propsPtr offset would point at the forward record, not live slots.
|
|
1142
|
-
['local.set', `$${
|
|
1143
|
-
['local.set', `$${
|
|
1144
|
-
['local.set', `$${
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1362
|
+
['local.set', `$${poffS}`, ['call', '$__ptr_offset', ['local.get', `$${props}`]]],
|
|
1363
|
+
['local.set', `$${pcapS}`, ['i32.load', ['i32.sub', ['local.get', `$${poffS}`], ['i32.const', 4]]]],
|
|
1364
|
+
['local.set', `$${dnS}`, ['i32.load', ['i32.sub', ['local.get', `$${poffS}`], ['i32.const', 8]]]]]],
|
|
1365
|
+
...roHit],
|
|
1366
|
+
...(hasDynProps ? [['else',
|
|
1367
|
+
// Sidecar (init-time keys, if any) — only for genuinely heap-allocated
|
|
1368
|
+
// receivers (base >= __heap_start): static-segment objects have no
|
|
1369
|
+
// header at all, so the off-16 read would hit neighboring static data.
|
|
1370
|
+
// Durable words may carry the runtime-shadowed bit0 marker
|
|
1371
|
+
// (collection.js __dyn_set) — mask it out or the resolved sidecar off
|
|
1372
|
+
// is misaligned.
|
|
1373
|
+
['if', ['i32.ge_u', ['local.get', `$${base}`], ['global.get', '$__heap_start']],
|
|
1374
|
+
['then',
|
|
1375
|
+
['local.set', `$${props}`, ['i64.and',
|
|
1376
|
+
['i64.load', ['i32.sub', ['local.get', `$${base}`], ['i32.const', 16]]], ['i64.const', -2]]],
|
|
1377
|
+
['if', ['i32.eq',
|
|
1378
|
+
['i32.wrap_i64', ['i64.and', ['i64.shr_u', ['local.get', `$${props}`], ['i64.const', LAYOUT.TAG_SHIFT]], ['i64.const', LAYOUT.TAG_MASK]]],
|
|
1379
|
+
['i32.const', PTR.HASH]],
|
|
1380
|
+
['then',
|
|
1381
|
+
['local.set', `$${poffS}`, ['call', '$__ptr_offset', ['local.get', `$${props}`]]],
|
|
1382
|
+
['local.set', `$${pcapS}`, ['i32.load', ['i32.sub', ['local.get', `$${poffS}`], ['i32.const', 4]]]],
|
|
1383
|
+
['local.set', `$${dnS}`, ['i32.load', ['i32.sub', ['local.get', `$${poffS}`], ['i32.const', 8]]]]]]]],
|
|
1384
|
+
...roHit,
|
|
1385
|
+
// Global (runtime-written keys, if any) — NO heap_start gate: __dyn_set
|
|
1386
|
+
// routes writes on STATIC-SEGMENT receivers here too (they have no
|
|
1387
|
+
// header, so the global table is their only storage), and the probe is
|
|
1388
|
+
// keyed by offset, needing no header. Gating it on heap_start silently
|
|
1389
|
+
// dropped `o.zz = 3` on a data-segment literal from enumeration.
|
|
1390
|
+
['if', ['f64.ne', ['global.get', '$__dyn_props'], ['f64.const', 0]],
|
|
1391
|
+
['then',
|
|
1392
|
+
['local.set', `$${props}`, ['call', '$__ihash_get_local',
|
|
1393
|
+
['i64.reinterpret_f64', ['global.get', '$__dyn_props']],
|
|
1394
|
+
['i64.reinterpret_f64', ['f64.convert_i32_s', ['local.get', `$${base}`]]]]],
|
|
1395
|
+
['if', ['i32.eqz', ['call', '$__is_nullish', ['local.get', `$${props}`]]],
|
|
1396
|
+
['then',
|
|
1397
|
+
['local.set', `$${poffG}`, ['call', '$__ptr_offset', ['local.get', `$${props}`]]],
|
|
1398
|
+
['local.set', `$${pcapG}`, ['i32.load', ['i32.sub', ['local.get', `$${poffG}`], ['i32.const', 4]]]],
|
|
1399
|
+
['local.set', `$${dnG}`, ['i32.load', ['i32.sub', ['local.get', `$${poffG}`], ['i32.const', 8]]]]]]]]]] : [])],
|
|
1400
|
+
// for-in with no dyn sources at all: the enumeration IS the schema key
|
|
1401
|
+
// array, and __schema_tbl[sid] already holds it as a static jz array —
|
|
1402
|
+
// return it boxed directly. Read-only by for-in's contract, static by
|
|
1403
|
+
// construction: no alloc, no cache, no invalidation.
|
|
1404
|
+
...(ro ? [['if', ['i32.and',
|
|
1405
|
+
['i32.and', ['i32.eqz', ['local.get', `$${dnG}`]], ['i32.eqz', ['local.get', `$${dnS}`]]],
|
|
1406
|
+
['i32.ne', ['local.get', `$${src}`], ['i32.const', 0]]],
|
|
1407
|
+
['then', ['br', `$oed${id}`, mkPtrIR(PTR.ARRAY, 0, ['local.get', `$${src}`])]]]] : []),
|
|
1408
|
+
// Over-allocate sn+dnG+dnS; patch length to actual `o` post-dedup so
|
|
1409
|
+
// removed shadow-mirror/cross-source-duplicate slots never expose
|
|
1410
|
+
// garbage tails.
|
|
1411
|
+
['local.set', `$${total}`, ['i32.add', ['local.get', `$${sn}`], ['i32.add', ['local.get', `$${dnG}`], ['local.get', `$${dnS}`]]]],
|
|
1148
1412
|
['local.set', `$${out}`, ['call', '$__alloc_hdr', ['local.get', `$${total}`], ['local.get', `$${total}`]]],
|
|
1149
1413
|
['local.set', `$${o}`, ['i32.const', 0]],
|
|
1150
1414
|
// Static schema slots — no skip, schema keys are unique by construction.
|
|
@@ -1155,42 +1419,36 @@ function emitEnumerateObject(t, emitStaticStore, emitDynStore) {
|
|
|
1155
1419
|
['local.set', `$${o}`, ['i32.add', ['local.get', `$${o}`], ['i32.const', 1]]],
|
|
1156
1420
|
['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
|
|
1157
1421
|
['br', `$sloop${id}`]]],
|
|
1158
|
-
// Dyn-prop slots in insertion order (__coll_order sorts the
|
|
1422
|
+
// Dyn-prop slots in insertion order (__coll_order sorts the live 24-byte
|
|
1159
1423
|
// slots by packed seq; hash@+0, key@+8, value@+16). Skip entries whose key
|
|
1160
1424
|
// is already in the schema — when an object literal has shadow=true (per
|
|
1161
1425
|
// needsDynShadow), each schema key is mirrored into propsPtr at construction
|
|
1162
1426
|
// so dyn-key reads hit the hash fast path; the mirror is not an enumeration
|
|
1163
|
-
// entity, so we must not emit it twice.
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
['local.set', `$${j}`, ['i32.const', 0]],
|
|
1174
|
-
['block', `$skbrk${id}`, ['loop', `$skloop${id}`,
|
|
1175
|
-
['br_if', `$skbrk${id}`, ['i32.ge_s', ['local.get', `$${j}`], ['local.get', `$${sn}`]]],
|
|
1176
|
-
['if', ['i64.eq',
|
|
1177
|
-
['i64.load', ['i32.add', ['local.get', `$${slot}`], ['i32.const', 8]]],
|
|
1178
|
-
['i64.load', ['i32.add', ['local.get', `$${src}`], ['i32.shl', ['local.get', `$${j}`], ['i32.const', 3]]]]],
|
|
1179
|
-
['then', ['local.set', `$${skip}`, ['i32.const', 1]], ['br', `$skbrk${id}`]]],
|
|
1180
|
-
['local.set', `$${j}`, ['i32.add', ['local.get', `$${j}`], ['i32.const', 1]]],
|
|
1181
|
-
['br', `$skloop${id}`]]],
|
|
1182
|
-
['if', ['i32.eqz', ['local.get', `$${skip}`]],
|
|
1183
|
-
['then',
|
|
1184
|
-
...emitDynStore(env),
|
|
1185
|
-
['local.set', `$${o}`, ['i32.add', ['local.get', `$${o}`], ['i32.const', 1]]]]],
|
|
1186
|
-
['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
|
|
1187
|
-
['br', `$dloop${id}`]]]]],
|
|
1427
|
+
// entity, so we must not emit it twice. Global walks first (schema-dedup
|
|
1428
|
+
// only); sidecar walks second (schema-dedup AND global-dedup, so a key
|
|
1429
|
+
// present in both — reassigned at runtime after being set at init — is
|
|
1430
|
+
// emitted once, from the authoritative global copy).
|
|
1431
|
+
['if', ['i32.ne', ['local.get', `$${poffG}`], ['i32.const', 0]],
|
|
1432
|
+
['then', ['local.set', `$${ordG}`, ['call', '$__coll_order', ['local.get', `$${poffG}`], ['local.get', `$${pcapG}`], ['i32.const', 24]]]]],
|
|
1433
|
+
['if', ['i32.ne', ['local.get', `$${poffS}`], ['i32.const', 0]],
|
|
1434
|
+
['then', ['local.set', `$${ordS}`, ['call', '$__coll_order', ['local.get', `$${poffS}`], ['local.get', `$${pcapS}`], ['i32.const', 24]]]]],
|
|
1435
|
+
walkDyn('oeg', dnG, ordG, null),
|
|
1436
|
+
walkDyn('oes', dnS, ordS, { dn: dnG, ord: ordG }),
|
|
1188
1437
|
['i32.store', ['i32.sub', ['local.get', `$${out}`], ['i32.const', 8]], ['local.get', `$${o}`]],
|
|
1438
|
+
// Fill the enum cache (keyed by sidecar — see roHit above). Objects without
|
|
1439
|
+
// a sidecar are either tier-1 (returned above) or global-only (rare; a 0 key
|
|
1440
|
+
// would collide across objects, so leave them uncached).
|
|
1441
|
+
...(ro ? [['if', ['i32.ne', ['local.get', `$${poffS}`], ['i32.const', 0]],
|
|
1442
|
+
['then',
|
|
1443
|
+
['global.set', '$__enumc_off', ['local.get', `$${poffS}`]],
|
|
1444
|
+
['global.set', '$__enumc_len', ['local.get', `$${dnS}`]],
|
|
1445
|
+
['global.set', '$__enumc_arr', mkPtrIR(PTR.ARRAY, 0, ['local.get', `$${out}`])]]]] : []),
|
|
1189
1446
|
mkPtrIR(PTR.ARRAY, 0, ['local.get', `$${out}`])]
|
|
1190
1447
|
}
|
|
1191
1448
|
|
|
1192
1449
|
// Object.keys for an OBJECT — copy schema key (i64@src+i*8) then dyn key (i64@slot+8).
|
|
1193
|
-
|
|
1450
|
+
// ro (for-in): serve the static schema array / enum cache — see emitEnumerateObject.
|
|
1451
|
+
const objectKeysFromTemp = (t, ro) => emitEnumerateObject(t,
|
|
1194
1452
|
({ out, o, src, i }) => [
|
|
1195
1453
|
['i64.store',
|
|
1196
1454
|
['i32.add', ['local.get', `$${out}`], ['i32.shl', ['local.get', `$${o}`], ['i32.const', 3]]],
|
|
@@ -1198,7 +1456,7 @@ const objectKeysFromTemp = (t) => emitEnumerateObject(t,
|
|
|
1198
1456
|
({ out, o, slot }) => [
|
|
1199
1457
|
['i64.store',
|
|
1200
1458
|
['i32.add', ['local.get', `$${out}`], ['i32.shl', ['local.get', `$${o}`], ['i32.const', 3]]],
|
|
1201
|
-
['i64.load', ['i32.add', ['local.get', `$${slot}`], ['i32.const', 8]]]]])
|
|
1459
|
+
['i64.load', ['i32.add', ['local.get', `$${slot}`], ['i32.const', 8]]]]], ro)
|
|
1202
1460
|
|
|
1203
1461
|
// Object.values for an OBJECT — copy schema value (f64@base+i*8) then dyn value (f64@slot+16).
|
|
1204
1462
|
const objectValuesFromTemp = (t) => emitEnumerateObject(t,
|