jz 0.8.1 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -9
- package/bench/README.md +121 -50
- package/bench/bench.svg +23 -23
- package/cli.js +3 -1
- package/dist/interop.js +1 -1
- package/dist/jz.js +7541 -6271
- 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 +299 -44
- 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 +892 -32
- 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 +301 -84
- package/module/typedarray.js +264 -72
- package/module/web.js +36 -0
- package/package.json +5 -5
- package/src/abi/string.js +71 -5
- 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 +979 -54
- package/src/compile/index.js +271 -29
- package/src/compile/infer.js +34 -3
- package/src/compile/inplace-store.js +329 -0
- package/src/compile/narrow.js +543 -15
- 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 +1060 -750
- package/src/optimize/recurse.js +2 -2
- package/src/optimize/vectorize.js +972 -67
- package/src/prepare/index.js +792 -63
- 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/array.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* @module array
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { typed, asF64, asI64, asI32, NULL_NAN, UNDEF_NAN, temp, tempI32, allocPtr, multiCount, arrayLoop, elemLoad, elemStore, truthyIR, extractF64Bits, appendStaticSlots, mkPtrIR, slotAddr, isLiteralStr, resolveValType, undefExpr, ptrTypeEq } from '../src/ir.js'
|
|
11
|
+
import { typed, asF64, asI64, asI32, NULL_NAN, UNDEF_NAN, temp, tempI32, allocPtr, multiCount, arrayLoop, elemLoad, elemStore, truthyIR, extractF64Bits, appendStaticSlots, mkPtrIR, slotAddr, isLiteralStr, resolveValType, undefExpr, ptrTypeEq, carrierF64, isPureIR } from '../src/ir.js'
|
|
12
12
|
import { inBoundsArrIdx } from '../src/type.js'
|
|
13
13
|
import { emit, spread, deps, idx as emitIndex } from '../src/bridge.js'
|
|
14
14
|
import { valTypeOf } from '../src/kind.js'
|
|
@@ -17,7 +17,7 @@ import { staticPropertyKey, staticObjectProps, inlineArraySid, staticIndexKey, i
|
|
|
17
17
|
import { VAL, lookupValType, lookupNotString, updateRep } from '../src/reps.js'
|
|
18
18
|
import { structInline } from '../src/abi/index.js'
|
|
19
19
|
import { ctx, inc, err, warnDeopt, PTR, LAYOUT, followForwardingWat } from '../src/ctx.js'
|
|
20
|
-
import { strHashLiteral } from './collection.js'
|
|
20
|
+
import { strHashLiteral, dynPropsFilterSetIR, durableFwdLogIR } from './collection.js'
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
/** Allocate ARRAY (type=1): header + n*8 data. Returns { local, setup, ptr } where local is data offset. */
|
|
@@ -42,7 +42,14 @@ export function staticArrayPtr(slots) {
|
|
|
42
42
|
dv.setInt32(8, len, true); dv.setInt32(12, len, true) // off-8: len, off-4: cap (props word at 0..7 stays 0)
|
|
43
43
|
for (let i = 0; i < 16; i++) ctx.runtime.data += String.fromCharCode(hdr[i])
|
|
44
44
|
appendStaticSlots(slots)
|
|
45
|
-
|
|
45
|
+
const ptr = mkPtrIR(PTR.ARRAY, 0, headerOff + 16)
|
|
46
|
+
// Compile-time identity for the static base/len read fold (see the saArr tag in
|
|
47
|
+
// the '[]' handler + optimize's foldStaticConstArrayReads): a const global bound
|
|
48
|
+
// to this literal reads elements with literal base/len instead of the
|
|
49
|
+
// __ptr_offset call + header load.
|
|
50
|
+
ptr.staticOff = headerOff + 16
|
|
51
|
+
ptr.staticLen = len
|
|
52
|
+
return ptr
|
|
46
53
|
}
|
|
47
54
|
|
|
48
55
|
function hoistArrayValue(arr) {
|
|
@@ -152,7 +159,13 @@ function makeCallback(fn, argReps) {
|
|
|
152
159
|
const result = emit(subst)
|
|
153
160
|
// Preserve i32 result type so callers (truthyIR, etc.) can skip f64↔i32 round-trips.
|
|
154
161
|
const ty = result.type === 'i32' ? 'i32' : 'f64'
|
|
155
|
-
|
|
162
|
+
const wrapped = typed(['block', ['result', ty], ...stmts, result], ty)
|
|
163
|
+
// An i32 result carrying ptrKind is an UNBOXED POINTER (a narrowed-return
|
|
164
|
+
// callee: the caller must rebox via this metadata) — the block wrapper must
|
|
165
|
+
// carry it through or downstream asF64 numeric-converts the raw offset
|
|
166
|
+
// (map stored [1104,1128] instead of the objects a named ctor fn returned).
|
|
167
|
+
if (result.ptrKind != null) { wrapped.ptrKind = result.ptrKind; wrapped.ptrAux = result.ptrAux }
|
|
168
|
+
return wrapped
|
|
156
169
|
},
|
|
157
170
|
}
|
|
158
171
|
}
|
|
@@ -198,26 +211,61 @@ const arrMethod = (name, nArgs = 0) => (...args) => {
|
|
|
198
211
|
}
|
|
199
212
|
|
|
200
213
|
const needsArrayDynMove = () => ctx.core.includes.has('__dyn_set')
|
|
214
|
+
// Whether durableFwdLogIR emits a real call (vs '' — see its own comment): only when
|
|
215
|
+
// __heap_reset exists (owned-memory builds; shared memory never declares it — core.js).
|
|
216
|
+
// Gates the deps() edge below the SAME way, so a shared-memory build (where core.js
|
|
217
|
+
// never registers __durable_fwd_log/__durable_fwd_heal) never requests a name nothing
|
|
218
|
+
// delivers — see collection.js's durableFwdLogIR comment for the full rationale.
|
|
219
|
+
const needsDurableFwdLog = () => ctx.scope.globals.has('__heap_reset')
|
|
220
|
+
// knownArray=true (__arr_grow_known): the raw offset + inline forwarding chase (see
|
|
221
|
+
// arrGrow below) calls $__ptr_offset_fwd directly, not the generic $__ptr_offset.
|
|
222
|
+
// '__durable_fwd_log' is an EXPLICIT edge (not left to the auto-dep scan): arrGrow's
|
|
223
|
+
// body always contains a durableFwdLogIR() call, but self-host's realize/regex-scan
|
|
224
|
+
// auto-deps path silently drops a helper reachable only that way (the exact
|
|
225
|
+
// "Unknown func $__clamp_idx" shape documented in test/selfhost-includes.js) — that
|
|
226
|
+
// test would fail (and the kernel would trap) without this line.
|
|
201
227
|
const arrayGrowDeps = (knownArray = false) => () => [
|
|
202
|
-
...(knownArray ? [] : ['__ptr_type']),
|
|
203
|
-
'
|
|
228
|
+
...(knownArray ? ['__ptr_offset_fwd'] : ['__ptr_type', '__ptr_offset']),
|
|
229
|
+
'__alloc_hdr', '__mkptr',
|
|
230
|
+
...(needsDurableFwdLog() ? ['__durable_fwd_log'] : []),
|
|
204
231
|
...(needsArrayDynMove() ? ['__dyn_move'] : []),
|
|
205
232
|
]
|
|
206
233
|
|
|
234
|
+
// Marks an ARRAY header slot ($off-16) as "props live in the global __dyn_props
|
|
235
|
+
// table, not here" — written whenever a shift/grow migrates or rekeys an entry
|
|
236
|
+
// there. Any nonzero, non-HASH-tagged i64 works: __dyn_get_t_h / __dyn_set /
|
|
237
|
+
// __dyn_del's ARRAY arms already treat "off-16 nonzero and not HASH-tagged" as
|
|
238
|
+
// "fall through to the global hash" (their only fast-accept is a HASH tag; their
|
|
239
|
+
// only fast-reject is exact zero). -1 decodes to tag 15, which is never PTR.HASH.
|
|
240
|
+
// Without this, a shift migrates props to the global table leaving nonzero
|
|
241
|
+
// leftover header bytes (which happens to satisfy "not zero, so check global") —
|
|
242
|
+
// but a *subsequent grow* allocates a fresh, zeroed header block, erasing that
|
|
243
|
+
// accidental signal and making __dyn_get_t_h wrongly conclude "no props" without
|
|
244
|
+
// ever consulting the global table. Writing this sentinel explicitly (instead of
|
|
245
|
+
// relying on incidental nonzero garbage) closes that gap for both shift and grow.
|
|
246
|
+
const DYN_PROPS_GLOBAL_SENTINEL = '(i64.const -1)'
|
|
247
|
+
|
|
207
248
|
// Arrays keep dynamic props in the global table because old/new array storage can
|
|
208
|
-
// be forwarded. Relocate that entry when growth moves the backing store
|
|
249
|
+
// be forwarded. Relocate that entry when growth moves the backing store; mark the
|
|
250
|
+
// new header so a later read/write/delete knows to consult the global table (the
|
|
251
|
+
// fresh __alloc_hdr block zeroes $newOff-16, which alone would read as "no props").
|
|
209
252
|
const maybeDynMoveIR = () => needsArrayDynMove()
|
|
210
|
-
?
|
|
253
|
+
? `(if (i32.eq (call $__dyn_move (local.get $off) (local.get $newOff)) (i32.const 1))
|
|
254
|
+
(then (i64.store (i32.sub (local.get $newOff) (i32.const 16)) ${DYN_PROPS_GLOBAL_SENTINEL})))`
|
|
211
255
|
: ''
|
|
212
256
|
|
|
213
257
|
// Per-object propsPtr lives in the 16-byte header at $off-16. On grow we copy it
|
|
214
258
|
// from old to new header (still HASH-tagged → unshifted ARRAY case). On shift we
|
|
215
259
|
// migrate it to the global __dyn_props because the forwarding writes overwrite
|
|
216
|
-
// the destination's $newOff-16 slot
|
|
217
|
-
//
|
|
218
|
-
//
|
|
260
|
+
// the destination's $newOff-16 slot — headerPropsToGlobalIR marks that slot with
|
|
261
|
+
// DYN_PROPS_GLOBAL_SENTINEL so a later grow's fresh (zeroed) header doesn't lose
|
|
262
|
+
// the signal; maybeDynMoveIR marks it again on every subsequent grow/rekey.
|
|
219
263
|
const headerPropsCopyIR = () => needsArrayDynMove() ? `
|
|
220
264
|
(local.set $oldProps (f64.load (i32.sub (local.get $off) (i32.const 16))))
|
|
265
|
+
;; strip the runtime-shadowed marker (collection.js bit0) — the relocated
|
|
266
|
+
;; header is ephemeral, where markers are meaningless and an odd props
|
|
267
|
+
;; offset would corrupt the sidecar probe
|
|
268
|
+
(local.set $oldProps (f64.reinterpret_i64 (i64.and (i64.reinterpret_f64 (local.get $oldProps)) (i64.const -2))))
|
|
221
269
|
(if (i32.eq
|
|
222
270
|
(i32.wrap_i64 (i64.and (i64.shr_u (i64.reinterpret_f64 (local.get $oldProps)) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK})))
|
|
223
271
|
(i32.const ${PTR.HASH}))
|
|
@@ -227,6 +275,7 @@ const headerPropsToGlobalIR = () => needsArrayDynMove() ? `
|
|
|
227
275
|
(if (i32.ge_u (local.get $off) (i32.const 16))
|
|
228
276
|
(then
|
|
229
277
|
(local.set $oldProps (f64.load (i32.sub (local.get $off) (i32.const 16))))
|
|
278
|
+
(local.set $oldProps (f64.reinterpret_i64 (i64.and (i64.reinterpret_f64 (local.get $oldProps)) (i64.const -2))))
|
|
230
279
|
(if (i32.eq
|
|
231
280
|
(i32.wrap_i64 (i64.and (i64.shr_u (i64.reinterpret_f64 (local.get $oldProps)) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK})))
|
|
232
281
|
(i32.const ${PTR.HASH}))
|
|
@@ -238,7 +287,12 @@ const headerPropsToGlobalIR = () => needsArrayDynMove() ? `
|
|
|
238
287
|
(i64.reinterpret_f64 (local.get $root))
|
|
239
288
|
(i64.reinterpret_f64 (f64.convert_i32_s (local.get $newOff)))
|
|
240
289
|
(i64.reinterpret_f64 (local.get $oldProps)))))
|
|
241
|
-
(global.set $__dyn_props (local.get $root))
|
|
290
|
+
(global.set $__dyn_props (local.get $root))
|
|
291
|
+
${dynPropsFilterSetIR('(local.get $newOff)')}
|
|
292
|
+
;; for-in enum cache: sidecar props moved into the global table —
|
|
293
|
+
;; enumeration state changed off the cache's key. Clear (see collection.js).
|
|
294
|
+
(global.set $__enumc_off (i32.const 0))
|
|
295
|
+
(i64.store (i32.sub (local.get $newOff) (i32.const 16)) ${DYN_PROPS_GLOBAL_SENTINEL}))))) ` : ''
|
|
242
296
|
|
|
243
297
|
export default (ctx) => {
|
|
244
298
|
deps({
|
|
@@ -247,9 +301,11 @@ export default (ctx) => {
|
|
|
247
301
|
__arr_grow_known: arrayGrowDeps(true),
|
|
248
302
|
__arr_shift: () => [
|
|
249
303
|
'__ptr_offset',
|
|
304
|
+
...(needsDurableFwdLog() ? ['__durable_fwd_log'] : []), // explicit edge — see arrayGrowDeps's comment
|
|
250
305
|
...(needsArrayDynMove() ? ['__dyn_move', '__hash_new', '__ihash_set_local'] : []),
|
|
251
306
|
],
|
|
252
307
|
__arr_fill: ['__ptr_offset', '__clamp_idx'], // body-calls __clamp_idx; declare it (self-host auto-scan can't be relied on — see test/selfhost-includes.js)
|
|
308
|
+
__arr_copyWithin: ['__ptr_type', '__ptr_offset', '__clamp_idx'],
|
|
253
309
|
__arr_set_idx_ptr: ['__arr_grow', '__ptr_offset'],
|
|
254
310
|
__arr_push1: ['__arr_grow_known', '__ptr_offset_fwd'],
|
|
255
311
|
__arr_set_length: ['__arr_grow_known', '__ptr_offset', '__ptr_type'],
|
|
@@ -270,8 +326,17 @@ export default (ctx) => {
|
|
|
270
326
|
|
|
271
327
|
inc('__ptr_offset', '__ptr_type', '__len', '__set_len', '__typed_idx', '__is_truthy')
|
|
272
328
|
|
|
273
|
-
// Array.isArray(x): check ptr_type === PTR.ARRAY
|
|
329
|
+
// Array.isArray(x): check ptr_type === PTR.ARRAY.
|
|
330
|
+
// Statically-known ARRAY values must answer from the FACT, not the carrier —
|
|
331
|
+
// a rep-narrowed array (raw base local, e.g. a slice() result) is not a
|
|
332
|
+
// NaN-box, so the runtime tag test would read a plain number and say false.
|
|
274
333
|
ctx.core.emit['Array.isArray'] = (x) => {
|
|
334
|
+
const vt = valTypeOf(x)
|
|
335
|
+
if (vt === VAL.ARRAY) {
|
|
336
|
+
const v = emit(x)
|
|
337
|
+
return isPureIR(v) ? typed(['i32.const', 1], 'i32')
|
|
338
|
+
: typed(['block', ['result', 'i32'], ['drop', asF64(v)], ['i32.const', 1]], 'i32')
|
|
339
|
+
}
|
|
275
340
|
const v = asF64(emit(x))
|
|
276
341
|
const t = temp('t')
|
|
277
342
|
return typed(['i32.and',
|
|
@@ -449,6 +514,11 @@ export default (ctx) => {
|
|
|
449
514
|
// guard (non-array ptr → fresh 4-cap buffer) for untyped call sites; the `_known`
|
|
450
515
|
// variant skips it for hot paths that already proved the receiver is an array.
|
|
451
516
|
// Single source of truth: both stdlib entries share the grow/relocate tail.
|
|
517
|
+
// `_known` callers (all `deps()`-verified ARRAY-only: __arr_push1, __arr_set_length,
|
|
518
|
+
// the inline-len array store) already proved the tag, so it inlines the raw offset +
|
|
519
|
+
// forwarding chase (mirrors __arr_idx_known) instead of paying __ptr_offset's
|
|
520
|
+
// tag-extract + FORWARDING_MASK dispatch — ARRAY always needs the chase (it's
|
|
521
|
+
// forwarding-capable) but never needs the re-check this variant would otherwise repeat.
|
|
452
522
|
const arrGrow = (name, defensive) => `(func $${name} (param $ptr i64) (param $minCap i32) (result f64)
|
|
453
523
|
${defensive ? '(local $t i32) ' : ''}(local $off i32) (local $oldCap i32) (local $newCap i32) (local $newOff i32) (local $len i32)
|
|
454
524
|
${needsArrayDynMove() ? '(local $oldProps f64)' : ''}
|
|
@@ -462,7 +532,8 @@ export default (ctx) => {
|
|
|
462
532
|
(then
|
|
463
533
|
(local.set $newCap (select (local.get $minCap) (i32.const 4) (i32.gt_s (local.get $minCap) (i32.const 4))))
|
|
464
534
|
(local.set $newOff (call $__alloc_hdr (i32.const 0) (local.get $newCap)))
|
|
465
|
-
(return (call $__mkptr (i32.const ${PTR.ARRAY}) (i32.const 0) (local.get $newOff)))))` : `(local.set $off (
|
|
535
|
+
(return (call $__mkptr (i32.const ${PTR.ARRAY}) (i32.const 0) (local.get $newOff)))))` : `(local.set $off (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ${LAYOUT.OFFSET_MASK}))))
|
|
536
|
+
${followForwardingWat('$off', { lowGuard: true })}`}
|
|
466
537
|
(local.set $oldCap (i32.load (i32.sub (local.get $off) (i32.const 4))))
|
|
467
538
|
(if (i32.ge_s (local.get $oldCap) (local.get $minCap))
|
|
468
539
|
(then (return (f64.reinterpret_i64 (local.get $ptr)))))
|
|
@@ -475,6 +546,7 @@ export default (ctx) => {
|
|
|
475
546
|
(memory.copy (local.get $newOff) (local.get $off) (i32.shl (local.get $len) (i32.const 3)))
|
|
476
547
|
${headerPropsCopyIR()}
|
|
477
548
|
${maybeDynMoveIR()}
|
|
549
|
+
${durableFwdLogIR('off', 'newOff', 'len', 'oldCap')}
|
|
478
550
|
(i32.store (i32.sub (local.get $off) (i32.const 8)) (local.get $newOff))
|
|
479
551
|
(i32.store (i32.sub (local.get $off) (i32.const 4)) (i32.const -1))
|
|
480
552
|
(call $__mkptr (i32.const ${PTR.ARRAY}) (i32.const 0) (local.get $newOff)))`
|
|
@@ -574,15 +646,28 @@ export default (ctx) => {
|
|
|
574
646
|
// so `const x = [1, 2, 3]` is a data segment, not an alloc.
|
|
575
647
|
if (ctx.func.atModuleScope && len >= 1 && !ctx.memory.shared) {
|
|
576
648
|
// asF64 folds i32.const → f64.const literally, so int-literal arrays also qualify.
|
|
577
|
-
|
|
578
|
-
|
|
649
|
+
// carrierF64: a bool literal folds to its TRUE/FALSE atom const — still
|
|
650
|
+
// static-extractable, and the element keeps boolean identity in the segment.
|
|
651
|
+
const vals = elems.map(e => carrierF64(e, emit(e)))
|
|
652
|
+
const slots = vals.map(v => extractF64Bits(v))
|
|
653
|
+
if (slots.every(b => b !== null)) {
|
|
654
|
+
const ptr = staticArrayPtr(slots)
|
|
655
|
+
// Every element a capture-free closure → tag the candidate set (funcIdx +
|
|
656
|
+
// uniform-ABI body name) so a `const ops = [...arrows]` indexed CALL can
|
|
657
|
+
// lower to a br_table of direct calls (emit.js tryConstFnArrayDispatch) —
|
|
658
|
+
// the AOT form of a polymorphic inline cache, with the generic
|
|
659
|
+
// call_indirect as the always-sound default arm.
|
|
660
|
+
if (vals.length && vals.every(v => v.closureBodyName != null && v.closureFuncIdx != null))
|
|
661
|
+
ptr.fnElements = vals.map(v => ({ idx: v.closureFuncIdx, name: v.closureBodyName }))
|
|
662
|
+
return ptr
|
|
663
|
+
}
|
|
579
664
|
}
|
|
580
665
|
// L3/'speed' bumps the cap floor (default 4) to skip more growth cycles.
|
|
581
666
|
const minCap = Math.max(ctx.transform.optimize?.arrayMinCap | 0, 4)
|
|
582
667
|
const a = allocArray(len, Math.max(len, minCap))
|
|
583
668
|
const body = [...a.setup]
|
|
584
669
|
for (let i = 0; i < len; i++)
|
|
585
|
-
body.push(['f64.store', slotAddr(a.local, i),
|
|
670
|
+
body.push(['f64.store', slotAddr(a.local, i), carrierF64(elems[i], emit(elems[i]))])
|
|
586
671
|
body.push(a.ptr)
|
|
587
672
|
return typed(['block', ['result', 'f64'], ...body], 'f64')
|
|
588
673
|
}
|
|
@@ -614,6 +699,14 @@ export default (ctx) => {
|
|
|
614
699
|
// Emit-time rep seed on fresh hoist-temp `h` so the recursive emit
|
|
615
700
|
// below (`ctx.core.emit['[]'](h, idx)`) takes the typed-arr fast path.
|
|
616
701
|
if (vtArr) updateRep(h, { val: vtArr })
|
|
702
|
+
// Inline field-read receiver (`plan.tw[i]`): carry the schema slot's
|
|
703
|
+
// program-wide typed kind onto the temp — VAL.TYPED alone has no element
|
|
704
|
+
// width, so without the ctor the read decays to the dynamic path.
|
|
705
|
+
if (vtArr === VAL.TYPED && Array.isArray(arr) && (arr[0] === '.' || arr[0] === '?.') &&
|
|
706
|
+
typeof arr[1] === 'string' && typeof arr[2] === 'string' && ctx.schema?.slotTypedCtorAt) {
|
|
707
|
+
const fc = ctx.schema.slotTypedCtorAt(arr[1], arr[2])
|
|
708
|
+
if (fc) (ctx.types.typedElem ||= new Map()).set(h, fc)
|
|
709
|
+
}
|
|
617
710
|
const setup = ['local.set', `$${h}`, asF64(emit(arr))]
|
|
618
711
|
const result = ctx.core.emit['[]'](h, idx)
|
|
619
712
|
return typed(['block', ['result', 'f64'], setup, asF64(result)], 'f64')
|
|
@@ -708,23 +801,34 @@ export default (ctx) => {
|
|
|
708
801
|
}
|
|
709
802
|
// HASH receiver with runtime string key: probe the HASH directly via
|
|
710
803
|
// __hash_get_local. Mirrors the literal-key path above but defers the
|
|
711
|
-
// hash computation to runtime.
|
|
712
|
-
//
|
|
713
|
-
//
|
|
804
|
+
// hash computation to runtime. Non-string keys (known-numeric, or the
|
|
805
|
+
// runtime-dispatch else-arm) go through __dyn_get_expr, whose entry
|
|
806
|
+
// ToPropertyKey-normalizes — `h[97]` reads the '97' slot the (also
|
|
807
|
+
// normalizing) write stored. A known HASH is never an array, so there is
|
|
808
|
+
// no hot array-index path to protect here.
|
|
714
809
|
if (vt === VAL.HASH) {
|
|
715
810
|
if (keyType === VAL.STRING) {
|
|
716
811
|
inc('__hash_get_local')
|
|
717
812
|
return typed(['f64.reinterpret_i64', ['call', '$__hash_get_local', ['i64.reinterpret_f64', ptrExpr], asI64(emit(idx))]], 'f64')
|
|
718
813
|
}
|
|
719
814
|
if (useRuntimeKeyDispatch) {
|
|
720
|
-
inc('__hash_get_local', '__is_str_key')
|
|
815
|
+
inc('__hash_get_local', '__is_str_key', '__dyn_get_expr')
|
|
721
816
|
const keyTmp = temp()
|
|
722
817
|
return typed(['block', ['result', 'f64'],
|
|
723
818
|
['local.set', `$${keyTmp}`, asF64(emit(idx))],
|
|
724
819
|
['if', ['result', 'f64'], ['call', '$__is_str_key', ['i64.reinterpret_f64', ['local.get', `$${keyTmp}`]]],
|
|
725
820
|
['then', ['f64.reinterpret_i64', ['call', '$__hash_get_local', ['i64.reinterpret_f64', ptrExpr], ['i64.reinterpret_f64', ['local.get', `$${keyTmp}`]]]]],
|
|
726
|
-
['else',
|
|
821
|
+
['else', ['f64.reinterpret_i64', ['call', '$__dyn_get_expr', ['i64.reinterpret_f64', ptrExpr], ['i64.reinterpret_f64', ['local.get', `$${keyTmp}`]]]]]]], 'f64')
|
|
727
822
|
}
|
|
823
|
+
inc('__dyn_get_expr')
|
|
824
|
+
return typed(['f64.reinterpret_i64', ['call', '$__dyn_get_expr', ['i64.reinterpret_f64', ptrExpr], asI64(emit(idx))]], 'f64')
|
|
825
|
+
}
|
|
826
|
+
// OBJECT receiver with a non-string key: never an array element — route to the
|
|
827
|
+
// ToPropertyKey-normalizing dyn read (the WRITE side already goes to __dyn_set;
|
|
828
|
+
// see the numeric-index design note below, which stays scoped to UNKNOWN receivers).
|
|
829
|
+
if (vt === VAL.OBJECT && keyType !== VAL.STRING) {
|
|
830
|
+
inc('__dyn_get_expr')
|
|
831
|
+
return typed(['f64.reinterpret_i64', ['call', '$__dyn_get_expr', ['i64.reinterpret_f64', ptrExpr], asI64(emit(idx))]], 'f64')
|
|
728
832
|
}
|
|
729
833
|
// Known array → direct f64 element load, skip string check
|
|
730
834
|
if (keyType === VAL.STRING)
|
|
@@ -785,14 +889,33 @@ export default (ctx) => {
|
|
|
785
889
|
const idxProvenInBounds = keyIsNum
|
|
786
890
|
&& typeof arr === 'string' && typeof idx === 'string'
|
|
787
891
|
&& inBoundsArrIdx(ctx).has(arr + '\x00' + idx)
|
|
892
|
+
// Tag reads whose receiver folded to a compile-time constant box: when the
|
|
893
|
+
// decl registers the same bits as a STATIC array (ctx.scope.staticArrs) and
|
|
894
|
+
// the program never resizes/aliases the name, optimize's
|
|
895
|
+
// foldStaticConstArrayReads collapses base+len to literals (the decl's
|
|
896
|
+
// data-segment offset isn't known until module init emits, so emit can
|
|
897
|
+
// only tag — same phasing as the constFnArrays devirt).
|
|
898
|
+
const saTag = (node) => {
|
|
899
|
+
if (typeof arr !== 'string') return node
|
|
900
|
+
// Identity proof, either form: the receiver resolved to a DIRECT read of the
|
|
901
|
+
// module global `arr` (global names are unique — same name the decl registers),
|
|
902
|
+
// or it already folded to a constant box whose bits the decl records. A local
|
|
903
|
+
// shadowing `arr` emits a local.get receiver — neither form matches, no tag.
|
|
904
|
+
if (Array.isArray(ptrExpr) && ptrExpr[0] === 'global.get' && ptrExpr[1] === `$${arr}`) node.saArr = arr
|
|
905
|
+
else {
|
|
906
|
+
const bits = extractF64Bits(ptrExpr)
|
|
907
|
+
if (bits !== null) { node.saArr = arr; node.saBits = bits }
|
|
908
|
+
}
|
|
909
|
+
return node
|
|
910
|
+
}
|
|
788
911
|
if (idxProvenInBounds) {
|
|
789
912
|
// base local must be i32. Flat tee form so downstream peepholes can fold
|
|
790
913
|
// `i32.wrap_i64 (i64.reinterpret_f64 (f64.load …))` → `i32.load …`
|
|
791
914
|
// when this load feeds a ptrUnboxed OBJECT field.
|
|
792
915
|
const baseI32 = tempI32('ab')
|
|
793
|
-
return typed(ctx.abi.array.ops.load(
|
|
916
|
+
return typed(saTag(ctx.abi.array.ops.load(
|
|
794
917
|
['local.tee', `$${baseI32}`, arrBase()],
|
|
795
|
-
vi), 'f64')
|
|
918
|
+
vi)), 'f64')
|
|
796
919
|
}
|
|
797
920
|
// Known plain array, numeric key, NOT proven in-bounds → inline bounds-checked
|
|
798
921
|
// load: `idx < len ? load : undefined`. Same semantics as __arr_idx_known but
|
|
@@ -801,14 +924,14 @@ export default (ctx) => {
|
|
|
801
924
|
// the check would read raw memory for OOB indices (e.g. `a[1]` on a length-1 array).
|
|
802
925
|
if (keyIsNum) {
|
|
803
926
|
const baseI32 = tempI32('ab'), idxI32 = tempI32('ai')
|
|
804
|
-
return typed(['if', ['result', 'f64'],
|
|
927
|
+
return typed(saTag(['if', ['result', 'f64'],
|
|
805
928
|
['i32.lt_u',
|
|
806
929
|
['local.tee', `$${idxI32}`, vi],
|
|
807
930
|
['i32.load', ['i32.sub',
|
|
808
931
|
['local.tee', `$${baseI32}`, arrBase()],
|
|
809
932
|
['i32.const', 8]]]],
|
|
810
933
|
['then', ctx.abi.array.ops.load(['local.get', `$${baseI32}`], ['local.get', `$${idxI32}`])],
|
|
811
|
-
['else', undefExpr()]], 'f64')
|
|
934
|
+
['else', undefExpr()]]), 'f64')
|
|
812
935
|
}
|
|
813
936
|
const baseTmp = temp()
|
|
814
937
|
// Numeric key (literal or known-NUMBER name) → skip __is_str_key dispatch;
|
|
@@ -913,7 +1036,7 @@ export default (ctx) => {
|
|
|
913
1036
|
const isGlobal = !box && ctx.scope.globals.has(arr) && !ctx.func.locals?.has(arr)
|
|
914
1037
|
const readVar = box ? ['f64.load', ['local.get', `$${box}`]] : isGlobal ? ['global.get', `$${arr}`] : ['local.get', `$${arr}`]
|
|
915
1038
|
const writeVar = v => box ? ['f64.store', ['local.get', `$${box}`], v] : isGlobal ? ['global.set', `$${arr}`, v] : ['local.set', `$${arr}`, v]
|
|
916
|
-
const vv =
|
|
1039
|
+
const vv = carrierF64(vals[0], emit(vals[0]))
|
|
917
1040
|
const pushed = ['call', '$__arr_push1', ['i64.reinterpret_f64', readVar], vv]
|
|
918
1041
|
if (void_) return typed(['block', writeVar(pushed)], 'void')
|
|
919
1042
|
return typed(['block', ['result', 'f64'],
|
|
@@ -966,7 +1089,7 @@ export default (ctx) => {
|
|
|
966
1089
|
|
|
967
1090
|
// Store each value and increment len
|
|
968
1091
|
for (const val of vals) {
|
|
969
|
-
const vv =
|
|
1092
|
+
const vv = carrierF64(val, emit(val))
|
|
970
1093
|
body.push(
|
|
971
1094
|
['f64.store',
|
|
972
1095
|
['i32.add', ['local.get', `$${pushBase}`], ['i32.shl', ['local.get', `$${len}`], ['i32.const', 3]]],
|
|
@@ -1019,6 +1142,21 @@ export default (ctx) => {
|
|
|
1019
1142
|
ctx.core.emit['.shift'] = (arr) => (inc('__arr_shift'),
|
|
1020
1143
|
typed(['call', '$__arr_shift', asI64(emit(arr))], 'f64'))
|
|
1021
1144
|
|
|
1145
|
+
// durableFwdLogIR on the off->newOff mark below: unlike grow (whose newOff is always
|
|
1146
|
+
// a FRESH allocation, unconditionally ephemeral whenever off reads durable), shift's
|
|
1147
|
+
// newOff is just off+8 — normally still in the SAME durability class as off, so a
|
|
1148
|
+
// shift of a durable array is ordinarily legitimate persistent state that must survive
|
|
1149
|
+
// `_clear`. It only crosses into ephemeral in the one-in-8-bytes edge case where off
|
|
1150
|
+
// sits exactly at __heap_reset-8, which durableFwdLogIR's two-sided check (source
|
|
1151
|
+
// durable AND target ephemeral) catches without misfiring on the common case.
|
|
1152
|
+
// rawOff's OWN path-compression rewrite (off->newOff becomes rawOff->newOff two lines
|
|
1153
|
+
// below the mark) needs no separate log call: if rawOff is durable, its header no
|
|
1154
|
+
// longer holds real (len, cap) at this point — it already holds a forward — so
|
|
1155
|
+
// whichever EARLIER relocation first turned rawOff from real data into a forward
|
|
1156
|
+
// record (the only place a durable rawOff's true pre-relocation state could still be
|
|
1157
|
+
// read) already logged it then, under its own off/newOff names. Healing restores
|
|
1158
|
+
// rawOff's header wholesale from that entry, independent of how many times its
|
|
1159
|
+
// forward target is rewritten afterward by path compression.
|
|
1022
1160
|
ctx.core.stdlib['__arr_shift'] = () => `(func $__arr_shift (param $arr i64) (result f64)
|
|
1023
1161
|
(local $rawOff i32) (local $off i32) (local $newOff i32) (local $len i32) (local $cap i32) (local $val f64)
|
|
1024
1162
|
${needsArrayDynMove() ? '(local $oldProps f64) (local $root f64)' : ''}
|
|
@@ -1039,8 +1177,11 @@ export default (ctx) => {
|
|
|
1039
1177
|
(i32.store (i32.add (local.get $off) (i32.const 4))
|
|
1040
1178
|
(select (i32.sub (local.get $cap) (i32.const 1)) (i32.const 0) (i32.gt_s (local.get $cap) (i32.const 0))))
|
|
1041
1179
|
${maybeDynMoveIR()}
|
|
1180
|
+
${durableFwdLogIR('off', 'newOff', 'len', 'cap')}
|
|
1042
1181
|
(i32.store (i32.sub (local.get $off) (i32.const 8)) (local.get $newOff))
|
|
1043
1182
|
(i32.store (i32.sub (local.get $off) (i32.const 4)) (i32.const -1))
|
|
1183
|
+
;; rawOff path-compression below needs no durableFwdLogIR call of its own —
|
|
1184
|
+
;; see the comment above this function.
|
|
1044
1185
|
(if (i32.and (i32.ne (local.get $rawOff) (local.get $off)) (i32.ge_u (local.get $rawOff) (i32.const 8)))
|
|
1045
1186
|
(then
|
|
1046
1187
|
(i32.store (i32.sub (local.get $rawOff) (i32.const 8)) (local.get $newOff))
|
|
@@ -1054,7 +1195,7 @@ export default (ctx) => {
|
|
|
1054
1195
|
inc('__arr_fill')
|
|
1055
1196
|
return typed(['call', '$__arr_fill',
|
|
1056
1197
|
asI64(emit(arr)),
|
|
1057
|
-
val == null ? undefExpr() :
|
|
1198
|
+
val == null ? undefExpr() : carrierF64(val, emit(val)),
|
|
1058
1199
|
start == null ? ['i32.const', 0] : asI32(emit(start)),
|
|
1059
1200
|
end == null ? ['i32.const', 0x7FFFFFFF] : asI32(emit(end))], 'f64')
|
|
1060
1201
|
}
|
|
@@ -1139,9 +1280,36 @@ export default (ctx) => {
|
|
|
1139
1280
|
return typed(['block', ['result', 'f64'], ...body], 'f64')
|
|
1140
1281
|
}
|
|
1141
1282
|
|
|
1142
|
-
// .unshift(
|
|
1143
|
-
|
|
1144
|
-
|
|
1283
|
+
// .unshift(...vals) → prepend elements, shift existing right. Multi-arg per ES:
|
|
1284
|
+
// `a.unshift(1, 2, 3)` yields [1, 2, 3, …existing] and returns the NEW LENGTH
|
|
1285
|
+
// (the helper's contract — it mutates in place; a grow leaves a forwarding
|
|
1286
|
+
// pointer so the receiver box stays valid, no write-back). Args EVALUATE
|
|
1287
|
+
// left-to-right (spilled to temps in source order) but INSERT last-to-first
|
|
1288
|
+
// through the single-value helper so the block lands in argument order; the
|
|
1289
|
+
// receiver is evaluated ONCE. (The old emitter silently DROPPED every argument
|
|
1290
|
+
// past the first — in the self-host kernel that broke assemble.js's own
|
|
1291
|
+
// `inject.unshift(setBase, ...stores)`, the last byte-parity ordering
|
|
1292
|
+
// divergence.)
|
|
1293
|
+
ctx.core.emit['.unshift'] = (arr, ...rawVals) => {
|
|
1294
|
+
inc('__arr_unshift')
|
|
1295
|
+
// Flatten comma-grouped args: [',', v1, v2] → [v1, v2] (same unwrap as '{}')
|
|
1296
|
+
const vals = rawVals.length === 1 && Array.isArray(rawVals[0]) && rawVals[0][0] === ','
|
|
1297
|
+
? rawVals[0].slice(1) : rawVals
|
|
1298
|
+
if (vals.length <= 1) {
|
|
1299
|
+
const val = vals[0]
|
|
1300
|
+
return typed(['call', '$__arr_unshift', asI64(emit(arr)), val === undefined ? undefExpr() : carrierF64(val, emit(val))], 'f64')
|
|
1301
|
+
}
|
|
1302
|
+
const recv = temp('usr')
|
|
1303
|
+
const temps = vals.map(() => temp('us'))
|
|
1304
|
+
const body = [
|
|
1305
|
+
['local.set', `$${recv}`, asF64(emit(arr))],
|
|
1306
|
+
...vals.map((v, i) => ['local.set', `$${temps[i]}`, carrierF64(v, emit(v))]),
|
|
1307
|
+
]
|
|
1308
|
+
for (let i = vals.length - 1; i >= 1; i--)
|
|
1309
|
+
body.push(['drop', ['call', '$__arr_unshift', ['i64.reinterpret_f64', ['local.get', `$${recv}`]], ['local.get', `$${temps[i]}`]]])
|
|
1310
|
+
body.push(['call', '$__arr_unshift', ['i64.reinterpret_f64', ['local.get', `$${recv}`]], ['local.get', `$${temps[0]}`]])
|
|
1311
|
+
return typed(['block', ['result', 'f64'], ...body], 'f64')
|
|
1312
|
+
}
|
|
1145
1313
|
|
|
1146
1314
|
ctx.core.stdlib['__arr_unshift'] = `(func $__arr_unshift (param $arr i64) (param $val f64) (result f64)
|
|
1147
1315
|
(local $off i32) (local $len i32) (local $a f64)
|
|
@@ -1480,8 +1648,13 @@ export default (ctx) => {
|
|
|
1480
1648
|
const recv = hoistArrayValue(arr)
|
|
1481
1649
|
const acc = temp('ra')
|
|
1482
1650
|
// reduce cb signature: (acc, item, idx). Item rep mirrors recv's elem val type.
|
|
1651
|
+
// A BIGINT init seeds the acc's kind: the fold can't silently change type
|
|
1652
|
+
// (mixed BigInt arithmetic rejects at compile), and without the seed the
|
|
1653
|
+
// acc param reads as unknown/number and trips the mix guard on jz's own
|
|
1654
|
+
// SWAR packing idiom (`arr.reduce((a, b, k) => a | (BigInt(b) << …), 0n)`).
|
|
1483
1655
|
const reps = callbackArgReps(arr)
|
|
1484
|
-
const
|
|
1656
|
+
const accRep = init !== undefined && valTypeOf(init) === VAL.BIGINT ? { val: VAL.BIGINT } : null
|
|
1657
|
+
const cb = makeCallback(fn, [accRep, reps[0], { val: VAL.NUMBER }])
|
|
1485
1658
|
// No initial value: JS seeds the accumulator with element 0 and folds from
|
|
1486
1659
|
// index 1 — NOT a 0 seed folded over every element. A 0 seed is invisible
|
|
1487
1660
|
// for `+` (additive identity) but wrong for `*` (→0) and corrupts non-numeric
|
|
@@ -1562,8 +1735,10 @@ export default (ctx) => {
|
|
|
1562
1735
|
}
|
|
1563
1736
|
|
|
1564
1737
|
// .reverse() → in-place swap arr[i] ↔ arr[len-1-i], returns the array.
|
|
1565
|
-
|
|
1566
|
-
|
|
1738
|
+
// Reverse an array VALUE in place, returning it. `.reverse` mutates the
|
|
1739
|
+
// receiver (setup hoists it once); `.toReversed` (ES2023) reverses a fresh
|
|
1740
|
+
// __arr_from copy so the receiver is untouched.
|
|
1741
|
+
function emitArrayReverseInPlace(setup, value) {
|
|
1567
1742
|
const arrTmp = temp('rv')
|
|
1568
1743
|
const base = tempI32('rb')
|
|
1569
1744
|
const len = tempI32('rl')
|
|
@@ -1578,8 +1753,8 @@ export default (ctx) => {
|
|
|
1578
1753
|
const addr = (idxIR) => ['i32.add', ['local.get', `$${base}`], ['i32.shl', idxIR, ['i32.const', 3]]]
|
|
1579
1754
|
|
|
1580
1755
|
return typed(['block', ['result', 'f64'],
|
|
1581
|
-
|
|
1582
|
-
['local.set', `$${arrTmp}`,
|
|
1756
|
+
setup,
|
|
1757
|
+
['local.set', `$${arrTmp}`, value],
|
|
1583
1758
|
['local.set', `$${base}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${arrTmp}`]]]],
|
|
1584
1759
|
['local.set', `$${len}`, ['i32.load', ['i32.sub', ['local.get', `$${base}`], ['i32.const', 8]]]],
|
|
1585
1760
|
['local.set', `$${i}`, ['i32.const', 0]],
|
|
@@ -1596,14 +1771,25 @@ export default (ctx) => {
|
|
|
1596
1771
|
['local.get', `$${arrTmp}`]
|
|
1597
1772
|
], 'f64')
|
|
1598
1773
|
}
|
|
1774
|
+
ctx.core.emit['.reverse'] = (arr) => {
|
|
1775
|
+
const recv = hoistArrayValue(arr)
|
|
1776
|
+
return emitArrayReverseInPlace(recv.setup, recv.value)
|
|
1777
|
+
}
|
|
1778
|
+
ctx.core.emit['.toReversed'] = (arr) => {
|
|
1779
|
+
inc('__arr_from')
|
|
1780
|
+
return emitArrayReverseInPlace(['nop'], typed(['call', '$__arr_from', asI64(emit(arr))], 'f64'))
|
|
1781
|
+
}
|
|
1599
1782
|
|
|
1600
1783
|
// Insertion sort — stable, in-place, O(n²). The comparator is called per
|
|
1601
1784
|
// shift; positive return → swap. NaN returns become "no swap" via f64.gt's
|
|
1602
1785
|
// IEEE 754 semantics (NaN compares false), matching the spec's NaN-as-0
|
|
1603
1786
|
// behavior. When fn is omitted, elements are compared as strings via
|
|
1604
1787
|
// __to_str → __str_cmp (byte-wise; NOT locale-aware).
|
|
1605
|
-
|
|
1606
|
-
|
|
1788
|
+
// Insertion-sort an array VALUE in place, returning it. `.sort` mutates the
|
|
1789
|
+
// receiver; `.toSorted` (ES2023) sorts a fresh __arr_from copy. Default (no
|
|
1790
|
+
// comparator) is lexicographic-string order per spec — NOT the numeric
|
|
1791
|
+
// default typed arrays use (see .typed:sort).
|
|
1792
|
+
function emitArraySortInPlace(setup, value, fn) {
|
|
1607
1793
|
const arrTmp = temp('sr')
|
|
1608
1794
|
const base = tempI32('sb')
|
|
1609
1795
|
const len = tempI32('sl')
|
|
@@ -1617,6 +1803,9 @@ export default (ctx) => {
|
|
|
1617
1803
|
|
|
1618
1804
|
let cmpExpr, cmpSetup
|
|
1619
1805
|
if (fn == null) {
|
|
1806
|
+
// default comparator is ToString + byte compare — both live in the string
|
|
1807
|
+
// module, which an all-numeric program hasn't loaded (dangling inc otherwise)
|
|
1808
|
+
ctx.module.include('string')
|
|
1620
1809
|
inc('__to_str', '__str_cmp')
|
|
1621
1810
|
cmpExpr = (aIR, bIR) => typed(['f64.convert_i32_s',
|
|
1622
1811
|
['call', '$__str_cmp',
|
|
@@ -1640,9 +1829,9 @@ export default (ctx) => {
|
|
|
1640
1829
|
const jPlus1 = ['i32.add', ['local.get', `$${j}`], ['i32.const', 1]]
|
|
1641
1830
|
|
|
1642
1831
|
return typed(['block', ['result', 'f64'],
|
|
1643
|
-
|
|
1832
|
+
setup,
|
|
1644
1833
|
cmpSetup,
|
|
1645
|
-
['local.set', `$${arrTmp}`,
|
|
1834
|
+
['local.set', `$${arrTmp}`, value],
|
|
1646
1835
|
['local.set', `$${base}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${arrTmp}`]]]],
|
|
1647
1836
|
['local.set', `$${len}`, ['i32.load', ['i32.sub', ['local.get', `$${base}`], ['i32.const', 8]]]],
|
|
1648
1837
|
|
|
@@ -1673,6 +1862,72 @@ export default (ctx) => {
|
|
|
1673
1862
|
['local.get', `$${arrTmp}`]
|
|
1674
1863
|
], 'f64')
|
|
1675
1864
|
}
|
|
1865
|
+
ctx.core.emit['.sort'] = (arr, fn) => {
|
|
1866
|
+
const recv = hoistArrayValue(arr)
|
|
1867
|
+
return emitArraySortInPlace(recv.setup, recv.value, fn)
|
|
1868
|
+
}
|
|
1869
|
+
ctx.core.emit['.toSorted'] = (arr, fn) => {
|
|
1870
|
+
inc('__arr_from')
|
|
1871
|
+
return emitArraySortInPlace(['nop'], typed(['call', '$__arr_from', asI64(emit(arr))], 'f64'), fn)
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
// .with(index, value) (ES2023) — a COPY with one element replaced. Negative
|
|
1875
|
+
// index counts from the end; an out-of-range index throws (RangeError in JS —
|
|
1876
|
+
// jz collapses Error subclasses to one generic throw, like .typed:with).
|
|
1877
|
+
ctx.core.emit['.with'] = (arr, index, value) => {
|
|
1878
|
+
inc('__arr_from', '__ptr_offset')
|
|
1879
|
+
ctx.runtime.throws = true
|
|
1880
|
+
const c = temp('awc'), base = tempI32('awb'), len = tempI32('awl'), idx = tempI32('awi')
|
|
1881
|
+
return typed(['block', ['result', 'f64'],
|
|
1882
|
+
['local.set', `$${c}`, typed(['call', '$__arr_from', asI64(emit(arr))], 'f64')],
|
|
1883
|
+
['local.set', `$${base}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${c}`]]]],
|
|
1884
|
+
['local.set', `$${len}`, ['i32.load', ['i32.sub', ['local.get', `$${base}`], ['i32.const', 8]]]],
|
|
1885
|
+
['local.set', `$${idx}`, asI32(emit(index))],
|
|
1886
|
+
['if', ['i32.lt_s', ['local.get', `$${idx}`], ['i32.const', 0]],
|
|
1887
|
+
['then', ['local.set', `$${idx}`, ['i32.add', ['local.get', `$${idx}`], ['local.get', `$${len}`]]]]],
|
|
1888
|
+
['if', ['i32.or',
|
|
1889
|
+
['i32.lt_s', ['local.get', `$${idx}`], ['i32.const', 0]],
|
|
1890
|
+
['i32.ge_s', ['local.get', `$${idx}`], ['local.get', `$${len}`]]],
|
|
1891
|
+
['then', ['throw', '$__jz_err', ['f64.const', 0]]]],
|
|
1892
|
+
['f64.store',
|
|
1893
|
+
['i32.add', ['local.get', `$${base}`], ['i32.shl', ['local.get', `$${idx}`], ['i32.const', 3]]],
|
|
1894
|
+
carrierF64(value, emit(value))],
|
|
1895
|
+
['local.get', `$${c}`]], 'f64')
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
// .copyWithin(target, start, end?) — in-place overlap-safe move, returns the
|
|
1899
|
+
// receiver. memory.copy is memmove-semantic (bulk-memory), so unlike the
|
|
1900
|
+
// element-kind-aware __typed_copyWithin, plain arrays need no direction loop.
|
|
1901
|
+
ctx.core.emit['.copyWithin'] = (arr, target, start, end) => {
|
|
1902
|
+
inc('__arr_copyWithin')
|
|
1903
|
+
return typed(['call', '$__arr_copyWithin', asI64(emit(arr)),
|
|
1904
|
+
target == null ? ['i32.const', 0] : asI32(emit(target)),
|
|
1905
|
+
start == null ? ['i32.const', 0] : asI32(emit(start)),
|
|
1906
|
+
end == null ? ['i32.const', 0x7FFFFFFF] : asI32(emit(end))], 'f64')
|
|
1907
|
+
}
|
|
1908
|
+
ctx.core.stdlib['__arr_copyWithin'] = `(func $__arr_copyWithin (param $arr i64) (param $target i32) (param $start i32) (param $end i32) (result f64)
|
|
1909
|
+
(local $off i32) (local $len i32) (local $count i32)
|
|
1910
|
+
(if (i32.eq (call $__ptr_type (local.get $arr)) (i32.const ${PTR.ARRAY}))
|
|
1911
|
+
(then
|
|
1912
|
+
(local.set $off (call $__ptr_offset (local.get $arr)))
|
|
1913
|
+
(local.set $len (i32.load (i32.sub (local.get $off) (i32.const 8))))
|
|
1914
|
+
(local.set $target (call $__clamp_idx (local.get $target) (local.get $len)))
|
|
1915
|
+
(local.set $start (call $__clamp_idx (local.get $start) (local.get $len)))
|
|
1916
|
+
(local.set $end (call $__clamp_idx (local.get $end) (local.get $len)))
|
|
1917
|
+
(local.set $count (i32.sub (local.get $end) (local.get $start)))
|
|
1918
|
+
(if (i32.gt_s (local.get $count) (i32.sub (local.get $len) (local.get $target)))
|
|
1919
|
+
(then (local.set $count (i32.sub (local.get $len) (local.get $target)))))
|
|
1920
|
+
(if (i32.gt_s (local.get $count) (i32.const 0))
|
|
1921
|
+
(then (memory.copy
|
|
1922
|
+
(i32.add (local.get $off) (i32.shl (local.get $target) (i32.const 3)))
|
|
1923
|
+
(i32.add (local.get $off) (i32.shl (local.get $start) (i32.const 3)))
|
|
1924
|
+
(i32.shl (local.get $count) (i32.const 3)))))))
|
|
1925
|
+
(f64.reinterpret_i64 (local.get $arr)))`
|
|
1926
|
+
|
|
1927
|
+
// Array.of(...items) — spec-identical to the array literal `[...items]`; the
|
|
1928
|
+
// `[` emitter already handles spread-tagged args and the static-data path.
|
|
1929
|
+
// (Distinct from `Array(n)`, which makes a length-n hole array.)
|
|
1930
|
+
ctx.core.emit['Array.of'] = (...items) => ctx.core.emit['['](...items)
|
|
1676
1931
|
|
|
1677
1932
|
// Boxed pointer values (strings/objects/etc.) carry NaN payloads, and
|
|
1678
1933
|
// f64.eq treats NaN as not-equal to anything — even bit-identical NaN —
|
|
@@ -1688,7 +1943,7 @@ export default (ctx) => {
|
|
|
1688
1943
|
|
|
1689
1944
|
ctx.core.emit['.indexOf'] = (arr, val) => {
|
|
1690
1945
|
const recv = hoistArrayValue(arr)
|
|
1691
|
-
const vv =
|
|
1946
|
+
const vv = carrierF64(val, emit(val))
|
|
1692
1947
|
const eq = arrEqIR(val)
|
|
1693
1948
|
const result = tempI32('ix')
|
|
1694
1949
|
const exit = `$exit${ctx.func.uniq++}`
|
|
@@ -1705,7 +1960,7 @@ export default (ctx) => {
|
|
|
1705
1960
|
|
|
1706
1961
|
ctx.core.emit['.includes'] = (arr, val) => {
|
|
1707
1962
|
const recv = hoistArrayValue(arr)
|
|
1708
|
-
const vv =
|
|
1963
|
+
const vv = carrierF64(val, emit(val))
|
|
1709
1964
|
const eq = arrEqIR(val)
|
|
1710
1965
|
const result = tempI32('ic')
|
|
1711
1966
|
const exit = `$exit${ctx.func.uniq++}`
|
|
@@ -1726,7 +1981,7 @@ export default (ctx) => {
|
|
|
1726
1981
|
// (which returned -1 for every array). fromIndex is unsupported, matching .indexOf's array path.
|
|
1727
1982
|
ctx.core.emit['.lastIndexOf'] = (arr, val) => {
|
|
1728
1983
|
const recv = hoistArrayValue(arr)
|
|
1729
|
-
const vv =
|
|
1984
|
+
const vv = carrierF64(val, emit(val))
|
|
1730
1985
|
const eq = arrEqIR(val)
|
|
1731
1986
|
const result = tempI32('lx')
|
|
1732
1987
|
const loop = arrayLoop(recv.value, (_ptr, _len, i, item) => [
|