jz 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +315 -318
- package/bench/README.md +369 -0
- package/bench/bench.svg +102 -0
- package/cli.js +104 -30
- package/dist/interop.js +1 -0
- package/dist/jz.js +6024 -0
- package/index.d.ts +126 -0
- package/index.js +362 -84
- package/interop.js +159 -186
- package/jz.svg +5 -0
- package/jzify/arguments.js +97 -0
- package/jzify/bundler.js +382 -0
- package/jzify/classes.js +328 -0
- package/jzify/hoist-vars.js +177 -0
- package/jzify/index.js +51 -0
- package/jzify/names.js +37 -0
- package/jzify/switch.js +106 -0
- package/jzify/transform.js +349 -0
- package/layout.js +184 -0
- package/module/array.js +377 -176
- package/module/collection.js +639 -145
- package/module/console.js +55 -43
- package/module/core.js +264 -153
- package/module/date.js +15 -3
- package/module/function.js +73 -6
- package/module/index.js +2 -1
- package/module/json.js +226 -61
- package/module/math.js +551 -187
- package/module/number.js +327 -60
- package/module/object.js +474 -184
- package/module/regex.js +255 -25
- package/module/schema.js +24 -6
- package/module/simd.js +117 -0
- package/module/string.js +621 -226
- package/module/symbol.js +1 -1
- package/module/timer.js +9 -14
- package/module/typedarray.js +459 -73
- package/package.json +58 -14
- package/src/abi/index.js +39 -23
- package/src/abi/string.js +38 -41
- package/src/ast.js +460 -0
- package/src/autoload.js +29 -24
- package/src/bridge.js +111 -0
- package/src/compile/analyze-scans.js +824 -0
- package/src/compile/analyze.js +1600 -0
- package/src/compile/emit-assign.js +411 -0
- package/src/compile/emit.js +3512 -0
- package/src/compile/flow-types.js +103 -0
- package/src/{compile.js → compile/index.js} +778 -128
- package/src/{infer.js → compile/infer.js} +62 -98
- package/src/compile/loop-divmod.js +155 -0
- package/src/{narrow.js → compile/narrow.js} +409 -106
- package/src/compile/peel-stencil.js +261 -0
- package/src/compile/plan/advise.js +370 -0
- package/src/compile/plan/common.js +150 -0
- package/src/compile/plan/index.js +122 -0
- package/src/compile/plan/inline.js +682 -0
- package/src/compile/plan/literals.js +1199 -0
- package/src/compile/plan/loops.js +472 -0
- package/src/compile/plan/scope.js +649 -0
- package/src/compile/program-facts.js +423 -0
- package/src/ctx.js +220 -64
- package/src/ir.js +589 -172
- package/src/kind-traits.js +132 -0
- package/src/kind.js +524 -0
- package/src/op-policy.js +57 -0
- package/src/{optimize.js → optimize/index.js} +1432 -473
- package/src/optimize/vectorize.js +4660 -0
- package/src/param-reps.js +65 -0
- package/src/parse.js +44 -0
- package/src/{prepare.js → prepare/index.js} +649 -205
- package/src/prepare/lift-iife.js +149 -0
- package/src/reps.js +116 -0
- package/src/resolve.js +12 -3
- package/src/static.js +208 -0
- package/src/type.js +651 -0
- package/src/{assemble.js → wat/assemble.js} +275 -55
- package/src/wat/optimize.js +3938 -0
- package/transform.js +21 -0
- package/wasi.js +47 -5
- package/src/analyze.js +0 -3818
- package/src/emit.js +0 -3040
- package/src/jzify.js +0 -1580
- package/src/plan.js +0 -2132
- package/src/vectorize.js +0 -1088
- /package/src/{codegen.js → wat/codegen.js} +0 -0
package/module/array.js
CHANGED
|
@@ -8,11 +8,15 @@
|
|
|
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 } from '../src/ir.js'
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
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'
|
|
12
|
+
import { inBoundsArrIdx } from '../src/type.js'
|
|
13
|
+
import { emit, spread, deps, idx as emitIndex } from '../src/bridge.js'
|
|
14
|
+
import { valTypeOf } from '../src/kind.js'
|
|
15
|
+
import { extractParams, classifyParam, ASSIGN_OPS, refsName, REFS_IN_EXPR } from '../src/ast.js'
|
|
16
|
+
import { staticPropertyKey, staticObjectProps, inlineArraySid, staticIndexKey } from '../src/static.js'
|
|
17
|
+
import { VAL, lookupValType, lookupNotString, updateRep } from '../src/reps.js'
|
|
14
18
|
import { structInline } from '../src/abi/index.js'
|
|
15
|
-
import { ctx, inc, err, PTR, LAYOUT } from '../src/ctx.js'
|
|
19
|
+
import { ctx, inc, err, warnDeopt, PTR, LAYOUT, followForwardingWat } from '../src/ctx.js'
|
|
16
20
|
import { strHashLiteral } from './collection.js'
|
|
17
21
|
|
|
18
22
|
|
|
@@ -22,17 +26,23 @@ function allocArray(len, cap) {
|
|
|
22
26
|
return { local: a.local, setup: [a.init], ptr: a.ptr }
|
|
23
27
|
}
|
|
24
28
|
|
|
25
|
-
/** Pack literal i64 slots as a static ARRAY
|
|
26
|
-
*
|
|
27
|
-
|
|
29
|
+
/** Pack literal i64 slots as a static ARRAY into the data segment, returning a
|
|
30
|
+
* folded ARRAY pointer to the first slot. The 16-byte header MUST match
|
|
31
|
+
* __alloc_hdr (core.js): a zeroed dyn-props word at off-16, then len/cap at
|
|
32
|
+
* off-8/-4. Heap arrays get that props word zeroed for free; a static array with
|
|
33
|
+
* only an 8-byte header left off-16 pointing at adjacent data-segment bytes, so
|
|
34
|
+
* for-in / named-prop lookup (which read off-16 as the props-sidecar pointer)
|
|
35
|
+
* walked garbage → OOB (test262 built-ins/Object/keys sparse-array). */
|
|
36
|
+
export function staticArrayPtr(slots) {
|
|
28
37
|
if (!ctx.runtime.data) ctx.runtime.data = ''
|
|
29
38
|
while (ctx.runtime.data.length % 8 !== 0) ctx.runtime.data += '\0'
|
|
30
39
|
const headerOff = ctx.runtime.data.length
|
|
31
40
|
const len = slots.length
|
|
32
|
-
const hdr = new Uint8Array(
|
|
33
|
-
|
|
41
|
+
const hdr = new Uint8Array(16); const dv = new DataView(hdr.buffer)
|
|
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
|
+
for (let i = 0; i < 16; i++) ctx.runtime.data += String.fromCharCode(hdr[i])
|
|
34
44
|
appendStaticSlots(slots)
|
|
35
|
-
return mkPtrIR(PTR.ARRAY, 0, headerOff +
|
|
45
|
+
return mkPtrIR(PTR.ARRAY, 0, headerOff + 16)
|
|
36
46
|
}
|
|
37
47
|
|
|
38
48
|
function hoistArrayValue(arr) {
|
|
@@ -94,16 +104,8 @@ function substExpr(node, mapping) {
|
|
|
94
104
|
return out
|
|
95
105
|
}
|
|
96
106
|
|
|
97
|
-
// Check whether a name is referenced inside a pure expression body.
|
|
98
|
-
// Mirrors substExpr's traversal — skips property names on '.'/'?.' and object keys on ':'.
|
|
99
107
|
function exprUses(node, name) {
|
|
100
|
-
|
|
101
|
-
if (!Array.isArray(node)) return false
|
|
102
|
-
const op = node[0]
|
|
103
|
-
if (op === '.' || op === '?.') return exprUses(node[1], name)
|
|
104
|
-
if (op === ':') return exprUses(node[2], name)
|
|
105
|
-
for (let i = 1; i < node.length; i++) if (exprUses(node[i], name)) return true
|
|
106
|
-
return false
|
|
108
|
+
return refsName(node, name, REFS_IN_EXPR)
|
|
107
109
|
}
|
|
108
110
|
|
|
109
111
|
// Callback factory: returns { setup, call, usedParams } where call(argExprs) emits the invocation.
|
|
@@ -239,26 +241,32 @@ const headerPropsToGlobalIR = () => needsArrayDynMove() ? `
|
|
|
239
241
|
(global.set $__dyn_props (local.get $root)))))) ` : ''
|
|
240
242
|
|
|
241
243
|
export default (ctx) => {
|
|
242
|
-
|
|
243
|
-
__arr_idx: [],
|
|
244
|
+
deps({
|
|
245
|
+
__arr_idx: ['__ptr_offset_fwd'],
|
|
244
246
|
__arr_grow: arrayGrowDeps(false),
|
|
245
247
|
__arr_grow_known: arrayGrowDeps(true),
|
|
246
248
|
__arr_shift: () => [
|
|
247
249
|
'__ptr_offset',
|
|
248
250
|
...(needsArrayDynMove() ? ['__dyn_move', '__hash_new', '__ihash_set_local'] : []),
|
|
249
251
|
],
|
|
252
|
+
__arr_fill: ['__ptr_offset'],
|
|
250
253
|
__arr_set_idx_ptr: ['__arr_grow', '__ptr_offset'],
|
|
251
254
|
__arr_push1: ['__arr_grow_known', '__ptr_offset'],
|
|
255
|
+
__arr_set_length: ['__arr_grow_known', '__ptr_offset', '__ptr_type'],
|
|
252
256
|
__arr_unshift: ['__arr_grow', '__len', '__ptr_offset'],
|
|
257
|
+
__arr_splice: ['__arr_grow', '__len', '__ptr_offset', '__alloc_hdr', '__mkptr'],
|
|
253
258
|
__typed_idx: () => ctx.features.typedarray || ctx.features.external
|
|
254
|
-
? ['__len']
|
|
255
|
-
: ['__len', '__ptr_offset'],
|
|
259
|
+
? ['__len', '__ptr_offset_fwd']
|
|
260
|
+
: ['__len', '__ptr_offset', '__ptr_offset_fwd'],
|
|
261
|
+
__arr_idx_known: ['__ptr_offset_fwd'],
|
|
256
262
|
})
|
|
257
263
|
|
|
258
|
-
// Iteration methods
|
|
259
|
-
// (item, idx)
|
|
260
|
-
//
|
|
261
|
-
|
|
264
|
+
// Iteration methods invoke callbacks with an implicit trailing index: .map/
|
|
265
|
+
// .filter/.forEach pass (item, idx); .reduce/.reduceRight pass (acc, item, idx).
|
|
266
|
+
// The uniform closure width must accommodate the widest of these (arity 3) even
|
|
267
|
+
// when no source-level closure declares that arity — otherwise a reduce callback
|
|
268
|
+
// routed through the closure path overflows the call_indirect signature.
|
|
269
|
+
ctx.closure.floor = Math.max(ctx.closure.floor ?? 0, 3)
|
|
262
270
|
|
|
263
271
|
inc('__ptr_offset', '__ptr_type', '__len', '__set_len', '__typed_idx', '__is_truthy')
|
|
264
272
|
|
|
@@ -268,7 +276,7 @@ export default (ctx) => {
|
|
|
268
276
|
const t = temp('t')
|
|
269
277
|
return typed(['i32.and',
|
|
270
278
|
['f64.ne', ['local.tee', `$${t}`, v], ['local.get', `$${t}`]],
|
|
271
|
-
['
|
|
279
|
+
ptrTypeEq(['local.get', `$${t}`], PTR.ARRAY)], 'i32')
|
|
272
280
|
}
|
|
273
281
|
|
|
274
282
|
ctx.core.emit['new.Array'] = (len) => {
|
|
@@ -298,13 +306,7 @@ export default (ctx) => {
|
|
|
298
306
|
(then (f64.const nan:${UNDEF_NAN}))
|
|
299
307
|
(else
|
|
300
308
|
(local.set $off (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ${LAYOUT.OFFSET_MASK}))))
|
|
301
|
-
(
|
|
302
|
-
(loop $follow
|
|
303
|
-
(br_if $done (i32.lt_u (local.get $off) (i32.const 8)))
|
|
304
|
-
(br_if $done (i32.gt_u (local.get $off) (i32.shl (memory.size) (i32.const 16))))
|
|
305
|
-
(br_if $done (i32.ne (i32.load (i32.sub (local.get $off) (i32.const 4))) (i32.const -1)))
|
|
306
|
-
(local.set $off (i32.load (i32.sub (local.get $off) (i32.const 8))))
|
|
307
|
-
(br $follow)))
|
|
309
|
+
${followForwardingWat('$off', { lowGuard: true })}
|
|
308
310
|
(if (result f64)
|
|
309
311
|
(i32.and
|
|
310
312
|
(i32.ge_u (local.get $off) (i32.const 8))
|
|
@@ -317,13 +319,7 @@ export default (ctx) => {
|
|
|
317
319
|
ctx.core.stdlib['__arr_idx_known'] = `(func $__arr_idx_known (param $ptr i64) (param $i i32) (result f64)
|
|
318
320
|
(local $off i32)
|
|
319
321
|
(local.set $off (i32.wrap_i64 (i64.and (local.get $ptr) (i64.const ${LAYOUT.OFFSET_MASK}))))
|
|
320
|
-
(
|
|
321
|
-
(loop $follow
|
|
322
|
-
(br_if $done (i32.lt_u (local.get $off) (i32.const 8)))
|
|
323
|
-
(br_if $done (i32.gt_u (local.get $off) (i32.shl (memory.size) (i32.const 16))))
|
|
324
|
-
(br_if $done (i32.ne (i32.load (i32.sub (local.get $off) (i32.const 4))) (i32.const -1)))
|
|
325
|
-
(local.set $off (i32.load (i32.sub (local.get $off) (i32.const 8))))
|
|
326
|
-
(br $follow)))
|
|
322
|
+
${followForwardingWat('$off', { lowGuard: true })}
|
|
327
323
|
(if (result f64)
|
|
328
324
|
(i32.and
|
|
329
325
|
(i32.ge_u (local.get $off) (i32.const 8))
|
|
@@ -357,12 +353,7 @@ export default (ctx) => {
|
|
|
357
353
|
;; ARRAY fast path: follow forwarding inline, bounds-check against header len, f64.load — no $__len call.
|
|
358
354
|
(if (i32.and (i32.eq (local.get $t) (i32.const ${PTR.ARRAY})) (i32.ge_u (local.get $off) (i32.const 8)))
|
|
359
355
|
(then
|
|
360
|
-
(
|
|
361
|
-
(loop $follow
|
|
362
|
-
(br_if $done (i32.gt_u (local.get $off) (i32.shl (memory.size) (i32.const 16))))
|
|
363
|
-
(br_if $done (i32.ne (i32.load (i32.sub (local.get $off) (i32.const 4))) (i32.const -1)))
|
|
364
|
-
(local.set $off (i32.load (i32.sub (local.get $off) (i32.const 8))))
|
|
365
|
-
(br $follow)))
|
|
356
|
+
${followForwardingWat('$off', { lowGuard: false })}
|
|
366
357
|
(return (if (result f64)
|
|
367
358
|
(i32.and (i32.ge_s (local.get $i) (i32.const 0)) (i32.lt_u (local.get $i) (i32.load (i32.sub (local.get $off) (i32.const 8)))))
|
|
368
359
|
(then (f64.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3)))))
|
|
@@ -433,7 +424,7 @@ export default (ctx) => {
|
|
|
433
424
|
const op = n[0]
|
|
434
425
|
if (op == null) return true // [null,x] literal — null/number/bigint
|
|
435
426
|
if (op === '=>') return false // arrow function — callable
|
|
436
|
-
if (op === '{}' || op === 'str' || op === 'strcat' || op === '//') return true
|
|
427
|
+
if (op === '{}' || op === 'str' || op === 'strcat' || op === '//' || op === 'bool') return true // object/string/regexp/boolean literal
|
|
437
428
|
if (op === '[]' && n.length < 3) return true // array literal
|
|
438
429
|
if (op === '()' && n[1] === 'Symbol') return true // Symbol(...) result
|
|
439
430
|
return false // calls / member access — unknown
|
|
@@ -515,11 +506,14 @@ export default (ctx) => {
|
|
|
515
506
|
|
|
516
507
|
// Grow array if capacity insufficient. Returns (possibly new) NaN-boxed pointer.
|
|
517
508
|
// Old storage is left behind as a forwarding header so existing aliases keep
|
|
518
|
-
// seeing the current backing store after growth.
|
|
519
|
-
|
|
520
|
-
|
|
509
|
+
// seeing the current backing store after growth. `defensive` adds a type/bounds
|
|
510
|
+
// guard (non-array ptr → fresh 4-cap buffer) for untyped call sites; the `_known`
|
|
511
|
+
// variant skips it for hot paths that already proved the receiver is an array.
|
|
512
|
+
// Single source of truth: both stdlib entries share the grow/relocate tail.
|
|
513
|
+
const arrGrow = (name, defensive) => `(func $${name} (param $ptr i64) (param $minCap i32) (result f64)
|
|
514
|
+
${defensive ? '(local $t i32) ' : ''}(local $off i32) (local $oldCap i32) (local $newCap i32) (local $newOff i32) (local $len i32)
|
|
521
515
|
${needsArrayDynMove() ? '(local $oldProps f64)' : ''}
|
|
522
|
-
(local.set $t (call $__ptr_type (local.get $ptr)))
|
|
516
|
+
${defensive ? `(local.set $t (call $__ptr_type (local.get $ptr)))
|
|
523
517
|
(local.set $off (call $__ptr_offset (local.get $ptr)))
|
|
524
518
|
;; Defensive path: invalid/non-array pointer -> create fresh array buffer.
|
|
525
519
|
(if
|
|
@@ -529,7 +523,7 @@ export default (ctx) => {
|
|
|
529
523
|
(then
|
|
530
524
|
(local.set $newCap (select (local.get $minCap) (i32.const 4) (i32.gt_s (local.get $minCap) (i32.const 4))))
|
|
531
525
|
(local.set $newOff (call $__alloc_hdr (i32.const 0) (local.get $newCap)))
|
|
532
|
-
(return (call $__mkptr (i32.const ${PTR.ARRAY}) (i32.const 0) (local.get $newOff)))))
|
|
526
|
+
(return (call $__mkptr (i32.const ${PTR.ARRAY}) (i32.const 0) (local.get $newOff)))))` : `(local.set $off (call $__ptr_offset (local.get $ptr)))`}
|
|
533
527
|
(local.set $oldCap (i32.load (i32.sub (local.get $off) (i32.const 4))))
|
|
534
528
|
(if (i32.ge_s (local.get $oldCap) (local.get $minCap))
|
|
535
529
|
(then (return (f64.reinterpret_i64 (local.get $ptr)))))
|
|
@@ -546,25 +540,8 @@ export default (ctx) => {
|
|
|
546
540
|
(i32.store (i32.sub (local.get $off) (i32.const 4)) (i32.const -1))
|
|
547
541
|
(call $__mkptr (i32.const ${PTR.ARRAY}) (i32.const 0) (local.get $newOff)))`
|
|
548
542
|
|
|
549
|
-
ctx.core.stdlib['
|
|
550
|
-
|
|
551
|
-
${needsArrayDynMove() ? '(local $oldProps f64)' : ''}
|
|
552
|
-
(local.set $off (call $__ptr_offset (local.get $ptr)))
|
|
553
|
-
(local.set $oldCap (i32.load (i32.sub (local.get $off) (i32.const 4))))
|
|
554
|
-
(if (i32.ge_s (local.get $oldCap) (local.get $minCap))
|
|
555
|
-
(then (return (f64.reinterpret_i64 (local.get $ptr)))))
|
|
556
|
-
(local.set $newCap (select
|
|
557
|
-
(local.get $minCap)
|
|
558
|
-
(i32.shl (local.get $oldCap) (i32.const 1))
|
|
559
|
-
(i32.gt_s (local.get $minCap) (i32.shl (local.get $oldCap) (i32.const 1)))))
|
|
560
|
-
(local.set $len (i32.load (i32.sub (local.get $off) (i32.const 8))))
|
|
561
|
-
(local.set $newOff (call $__alloc_hdr (local.get $len) (local.get $newCap)))
|
|
562
|
-
(memory.copy (local.get $newOff) (local.get $off) (i32.shl (local.get $len) (i32.const 3)))
|
|
563
|
-
${headerPropsCopyIR()}
|
|
564
|
-
${maybeDynMoveIR()}
|
|
565
|
-
(i32.store (i32.sub (local.get $off) (i32.const 8)) (local.get $newOff))
|
|
566
|
-
(i32.store (i32.sub (local.get $off) (i32.const 4)) (i32.const -1))
|
|
567
|
-
(call $__mkptr (i32.const ${PTR.ARRAY}) (i32.const 0) (local.get $newOff)))`
|
|
543
|
+
ctx.core.stdlib['__arr_grow'] = () => arrGrow('__arr_grow', true)
|
|
544
|
+
ctx.core.stdlib['__arr_grow_known'] = () => arrGrow('__arr_grow_known', false)
|
|
568
545
|
|
|
569
546
|
// Hot for arr[i] = val (~18M calls in watr self-host). Compute base via __ptr_offset
|
|
570
547
|
// once and read len from the inline header (i32.load base-8) — avoids __len's separate
|
|
@@ -609,6 +586,37 @@ export default (ctx) => {
|
|
|
609
586
|
(i32.store (i32.sub (local.get $base) (i32.const 8)) (i32.add (local.get $len) (i32.const 1)))
|
|
610
587
|
(local.get $p))`
|
|
611
588
|
|
|
589
|
+
// arr.length = N. Truncation (N ≤ len) just rewrites the len word in place — no
|
|
590
|
+
// relocation, so aliases keep their pointer. Growth past capacity relocates via
|
|
591
|
+
// __arr_grow_known (old header forward-marked) and undefined-fills the new slots,
|
|
592
|
+
// matching JS sparse-array semantics. Non-ARRAY receivers are left unchanged so a
|
|
593
|
+
// mistyped `.length =` cannot corrupt object/collection headers. Returns the
|
|
594
|
+
// (possibly relocated) pointer; the assignment's value is N (computed at the call site).
|
|
595
|
+
ctx.core.stdlib['__arr_set_length'] = `(func $__arr_set_length (param $ptr i64) (param $n i32) (result f64)
|
|
596
|
+
(local $base i32) (local $p f64) (local $oldLen i32) (local $cap i32) (local $k i32)
|
|
597
|
+
(local.set $p (f64.reinterpret_i64 (local.get $ptr)))
|
|
598
|
+
(if (i32.ne (call $__ptr_type (local.get $ptr)) (i32.const ${PTR.ARRAY}))
|
|
599
|
+
(then (return (local.get $p))))
|
|
600
|
+
(if (i32.lt_s (local.get $n) (i32.const 0)) (then (local.set $n (i32.const 0))))
|
|
601
|
+
(local.set $base (call $__ptr_offset (local.get $ptr)))
|
|
602
|
+
(if (i32.lt_u (local.get $base) (i32.const 8)) (then (return (local.get $p))))
|
|
603
|
+
(local.set $oldLen (i32.load (i32.sub (local.get $base) (i32.const 8))))
|
|
604
|
+
(local.set $cap (i32.load (i32.sub (local.get $base) (i32.const 4))))
|
|
605
|
+
(if (i32.gt_s (local.get $n) (local.get $cap))
|
|
606
|
+
(then
|
|
607
|
+
(local.set $p (call $__arr_grow_known (local.get $ptr) (local.get $n)))
|
|
608
|
+
(local.set $base (call $__ptr_offset (i64.reinterpret_f64 (local.get $p))))))
|
|
609
|
+
(if (i32.gt_u (local.get $n) (local.get $oldLen))
|
|
610
|
+
(then
|
|
611
|
+
(local.set $k (local.get $oldLen))
|
|
612
|
+
(block $fdone (loop $fill
|
|
613
|
+
(br_if $fdone (i32.ge_u (local.get $k) (local.get $n)))
|
|
614
|
+
(i64.store (i32.add (local.get $base) (i32.shl (local.get $k) (i32.const 3))) (i64.const ${UNDEF_NAN}))
|
|
615
|
+
(local.set $k (i32.add (local.get $k) (i32.const 1)))
|
|
616
|
+
(br $fill)))))
|
|
617
|
+
(i32.store (i32.sub (local.get $base) (i32.const 8)) (local.get $n))
|
|
618
|
+
(local.get $p))`
|
|
619
|
+
|
|
612
620
|
// === Array literal ===
|
|
613
621
|
|
|
614
622
|
ctx.core.emit['['] = (...elems) => {
|
|
@@ -617,8 +625,14 @@ export default (ctx) => {
|
|
|
617
625
|
if (!hasSpread) {
|
|
618
626
|
const len = elems.length
|
|
619
627
|
// R: Static data segment for arrays of pure-literal elements (own-memory only).
|
|
620
|
-
//
|
|
621
|
-
|
|
628
|
+
// Raw f64 bits embedded directly — a constant array becomes a const pointer with no
|
|
629
|
+
// alloc and no per-element store. A static array aliases ONE shared data-segment
|
|
630
|
+
// region, so this is sound only when no caller expects a fresh instance per
|
|
631
|
+
// evaluation: at module scope the literal runs exactly once. A function-local
|
|
632
|
+
// literal (which would leak in-place mutations across calls — a latent bug the old
|
|
633
|
+
// len≥4 gate also had) allocs fresh instead. Module scope lifts the size floor too,
|
|
634
|
+
// so `const x = [1, 2, 3]` is a data segment, not an alloc.
|
|
635
|
+
if (ctx.func.atModuleScope && len >= 1 && !ctx.memory.shared) {
|
|
622
636
|
// asF64 folds i32.const → f64.const literally, so int-literal arrays also qualify.
|
|
623
637
|
const slots = elems.map(e => extractF64Bits(asF64(emit(e))))
|
|
624
638
|
if (slots.every(b => b !== null)) return staticArrayPtr(slots)
|
|
@@ -633,10 +647,10 @@ export default (ctx) => {
|
|
|
633
647
|
return typed(['block', ['result', 'f64'], ...body], 'f64')
|
|
634
648
|
}
|
|
635
649
|
|
|
636
|
-
// Spread literal:
|
|
650
|
+
// Spread literal: spread pre-sums the total length, allocates
|
|
637
651
|
// exact, and bulk-copies ARRAY sources with a single memory.copy — vs the
|
|
638
652
|
// per-element __arr_set_idx_ptr grow loop. Normalise the parser's `['...', x]`.
|
|
639
|
-
return
|
|
653
|
+
return spread(elems.map(e =>
|
|
640
654
|
Array.isArray(e) && e[0] === '...' ? ['__spread', e[1]] : e))
|
|
641
655
|
}
|
|
642
656
|
|
|
@@ -668,14 +682,17 @@ export default (ctx) => {
|
|
|
668
682
|
const litKey = isLiteralStr(idx) ? idx[1]
|
|
669
683
|
: typeof arr === 'string' && lookupValType(arr) === VAL.OBJECT ? staticPropertyKey(idx)
|
|
670
684
|
: null
|
|
671
|
-
// SRoA flat object: `o['k']` → `local.get $o#i` (
|
|
672
|
-
|
|
685
|
+
// SRoA flat object/array: `o['k']` / `a[2]` → `local.get $o#i` (scanFlatObjects).
|
|
686
|
+
// A bare integer index resolves its slot key here (not via `litKey`, which stays
|
|
687
|
+
// null for arrays so the heap-array / schema paths below are untouched).
|
|
688
|
+
if (typeof arr === 'string' && ctx.func.flatObjects?.has(arr)) {
|
|
673
689
|
const fo = ctx.func.flatObjects.get(arr)
|
|
674
|
-
const
|
|
690
|
+
const flatKey = litKey != null ? litKey : staticIndexKey(idx)
|
|
691
|
+
const fi = flatKey != null ? fo.names.indexOf(flatKey) : -1
|
|
675
692
|
if (fi >= 0) return typed(['local.get', `$${arr}#${fi}`], 'f64')
|
|
676
693
|
}
|
|
677
|
-
if (litKey != null && typeof arr === 'string' && ctx.schema.
|
|
678
|
-
const slot = ctx.schema.
|
|
694
|
+
if (litKey != null && typeof arr === 'string' && ctx.schema.slotOf) {
|
|
695
|
+
const slot = ctx.schema.slotOf(arr, litKey)
|
|
679
696
|
if (slot >= 0) {
|
|
680
697
|
inc('__ptr_offset')
|
|
681
698
|
return typed(ctx.abi.object.ops.load(['call', '$__ptr_offset', ['i64.reinterpret_f64', asF64(emit(arr))]], slot), 'f64')
|
|
@@ -692,10 +709,24 @@ export default (ctx) => {
|
|
|
692
709
|
// Multi-value calls are materialized at call site (see '()' handler), so
|
|
693
710
|
// func()[i] works naturally — func() returns a heap array pointer, [i] indexes it.
|
|
694
711
|
const vt = typeof arr === 'string' ? lookupValType(arr) : valTypeOf(arr)
|
|
695
|
-
|
|
712
|
+
// Literal string key on a receiver that isn't a known array/typed/string:
|
|
713
|
+
// `x['a']` IS `x.a` — delegate to the dot emitter so an untyped/OBJECT/external
|
|
714
|
+
// receiver gets the same polymorphic dispatch (__dyn_get_any_t_h, host-external
|
|
715
|
+
// aware). The local `dynLoad` fallback below calls __dyn_get, which only probes
|
|
716
|
+
// the internal HASH layout — so `x['a']` on a host object silently returned
|
|
717
|
+
// undefined while `x.a` worked. (Known ARRAY/TYPED/STRING fall through unchanged:
|
|
718
|
+
// their string-key semantics differ from a HASH/OBJECT property read.)
|
|
719
|
+
if (litKey != null && vt !== VAL.ARRAY && vt !== VAL.TYPED && vt !== VAL.STRING)
|
|
720
|
+
return emit(['.', arr, litKey])
|
|
721
|
+
// emitIndex (not bare asI32(emit)) narrows integer index arithmetic — incl. a
|
|
722
|
+
// literal term like the `+1` of `a[i*W + x + 1]` — to i32 ops instead of the
|
|
723
|
+
// f64 convert/trunc round-trip. Non-i32 keys (string dispatch) fall back to
|
|
724
|
+
// asI32(emit) inside emitIndex, so this is a strict improvement for every branch.
|
|
725
|
+
const va = emit(arr), vi = emitIndex(idx)
|
|
696
726
|
const ptrExpr = asF64(va)
|
|
697
727
|
const dynLoad = (objExpr, keyExpr) => {
|
|
698
728
|
if (ctx.transform.strict) err(`strict mode: dynamic property access \`${typeof arr === 'string' ? arr : '<expr>'}[<expr>]\` falls back to __dyn_get. Use a literal key or known typed-array receiver, or pass { strict: false }.`)
|
|
729
|
+
warnDeopt('deopt-dyn-read', `dynamic property read \`${typeof arr === 'string' ? arr : '<expr>'}[…]\` couldn't resolve a static type — it falls back to a runtime hash lookup (~1.5–2× slower than a typed/slot read, far worse in a hot loop). Use a literal key, a typed-array receiver, or a Map for genuinely dynamic keys.`)
|
|
699
730
|
inc('__dyn_get')
|
|
700
731
|
return ['f64.reinterpret_i64', ['call', '$__dyn_get', ['i64.reinterpret_f64', objExpr], ['i64.reinterpret_f64', keyExpr]]]
|
|
701
732
|
}
|
|
@@ -745,6 +776,16 @@ export default (ctx) => {
|
|
|
745
776
|
if (keyType === VAL.STRING)
|
|
746
777
|
return typed(dynLoad(ptrExpr, asF64(emit(idx))), 'f64')
|
|
747
778
|
if (vt === 'array') {
|
|
779
|
+
// Base offset of the array's data region. A binding proven never relocated
|
|
780
|
+
// (scanNeverGrown — a fresh array literal whose every use is a pure read, so no
|
|
781
|
+
// grow op can ever run) skips the realloc-forwarding follow: its base is the raw
|
|
782
|
+
// post-header offset `wrap(reinterpret(ptr) & OFFSET_MASK)`, no __ptr_offset call.
|
|
783
|
+
// Memory-safe ONLY under that proof — a relocated array read through this stale
|
|
784
|
+
// base would corrupt memory (see scanNeverGrown's default-deny rationale).
|
|
785
|
+
const neverGrown = typeof arr === 'string' && ctx.func.localReps?.get(arr)?.neverGrown === true
|
|
786
|
+
const arrBase = () => neverGrown
|
|
787
|
+
? ['i32.wrap_i64', ['i64.and', ['i64.reinterpret_f64', ptrExpr], ['i64.const', LAYOUT.OFFSET_MASK]]]
|
|
788
|
+
: (inc('__ptr_offset'), ['call', '$__ptr_offset', ['i64.reinterpret_f64', ptrExpr]])
|
|
748
789
|
// structInline Array<S>: element i is K consecutive inline f64 schema
|
|
749
790
|
// cells — no per-row heap object, no stored element pointer. `arr[i]` is
|
|
750
791
|
// the byte address of the element's first cell, returned as a first-class
|
|
@@ -753,11 +794,10 @@ export default (ctx) => {
|
|
|
753
794
|
// structInline handles (src/analyze.js analyzeStructInline).
|
|
754
795
|
const inlSid = inlineArraySid(arr)
|
|
755
796
|
if (inlSid != null) {
|
|
756
|
-
inc('__ptr_offset')
|
|
757
797
|
const baseI32 = tempI32('ab')
|
|
758
798
|
const K = ctx.schema.list[inlSid].length
|
|
759
799
|
const cell = typed(structInline(K).ops.elemAddr(
|
|
760
|
-
['local.tee', `$${baseI32}`,
|
|
800
|
+
['local.tee', `$${baseI32}`, arrBase()],
|
|
761
801
|
vi), 'i32')
|
|
762
802
|
cell.ptrKind = VAL.OBJECT
|
|
763
803
|
cell.ptrAux = inlSid
|
|
@@ -767,29 +807,55 @@ export default (ctx) => {
|
|
|
767
807
|
// not __typed_idx (which does __len + __ptr_offset = two forwarding follows
|
|
768
808
|
// plus type-dispatch overhead irrelevant for plain arrays).
|
|
769
809
|
const keyIsNum = keyType === VAL.NUMBER
|
|
770
|
-
// Inline fast path
|
|
771
|
-
//
|
|
772
|
-
//
|
|
773
|
-
//
|
|
774
|
-
//
|
|
775
|
-
//
|
|
776
|
-
//
|
|
777
|
-
//
|
|
778
|
-
//
|
|
779
|
-
//
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
810
|
+
// Inline fast path for any known plain ARRAY + numeric key: the type-tag
|
|
811
|
+
// dispatch and bounds check inside __arr_idx(_known) are dead weight in hot
|
|
812
|
+
// kernels — most visibly AST walkers doing `node[i]` over heterogeneous
|
|
813
|
+
// arrays, where no element schema/valType is ever inferred. Emit the
|
|
814
|
+
// f64.load directly. base goes through __ptr_offset (still the forwarding
|
|
815
|
+
// follow), and hoistAddrBase CSEs the (base, i) pair across the iteration
|
|
816
|
+
// body. taggedLinear stores every element as one 8-byte f64 cell —
|
|
817
|
+
// Array<NUMBER>/<STRING>/<OBJECT> alike — so a direct f64.load is correct
|
|
818
|
+
// regardless of elem kind (raw f64 for NUMBER, NaN-boxed pointer for
|
|
819
|
+
// OBJECT/STRING; downstream typed() handles both). The load shape is fixed
|
|
820
|
+
// by the carrier, not by any rep fact, so we do NOT gate on a known element
|
|
821
|
+
// schema/valType: a bare `let a = [...]` walked by index gets the same
|
|
822
|
+
// inline load as an Array<{x,y,z}>. (structInline arrays returned above via
|
|
823
|
+
// inlineArraySid; typed arrays are VAL.TYPED, handled below.)
|
|
824
|
+
//
|
|
825
|
+
// Take the UNCHECKED inline load only when the index is proven in-bounds by
|
|
826
|
+
// an enclosing canonical loop `for (let i=C; i<arr.length; i++)` — a pure
|
|
827
|
+
// index<length structural proof (scanBoundedArrIdx, src/type.js), itself
|
|
828
|
+
// element-kind-independent. Skipping the bounds check on an arbitrary numeric
|
|
829
|
+
// index is unsound: `a[1]` on a length-1 array would read the raw cell instead
|
|
830
|
+
// of undefined; those fall through to the inline bounds-checked load below.
|
|
831
|
+
const idxProvenInBounds = keyIsNum
|
|
832
|
+
&& typeof arr === 'string' && typeof idx === 'string'
|
|
833
|
+
&& inBoundsArrIdx(ctx).has(arr + '\x00' + idx)
|
|
834
|
+
if (idxProvenInBounds) {
|
|
835
|
+
// base local must be i32. Flat tee form so downstream peepholes can fold
|
|
786
836
|
// `i32.wrap_i64 (i64.reinterpret_f64 (f64.load …))` → `i32.load …`
|
|
787
837
|
// when this load feeds a ptrUnboxed OBJECT field.
|
|
788
838
|
const baseI32 = tempI32('ab')
|
|
789
839
|
return typed(ctx.abi.array.ops.load(
|
|
790
|
-
['local.tee', `$${baseI32}`,
|
|
840
|
+
['local.tee', `$${baseI32}`, arrBase()],
|
|
791
841
|
vi), 'f64')
|
|
792
842
|
}
|
|
843
|
+
// Known plain array, numeric key, NOT proven in-bounds → inline bounds-checked
|
|
844
|
+
// load: `idx < len ? load : undefined`. Same semantics as __arr_idx_known but
|
|
845
|
+
// inline, so watr hoists the loop-invariant len load and CSEs the base — the
|
|
846
|
+
// residual cost is a single (predictable) compare per access, not a call. Skipping
|
|
847
|
+
// the check would read raw memory for OOB indices (e.g. `a[1]` on a length-1 array).
|
|
848
|
+
if (keyIsNum) {
|
|
849
|
+
const baseI32 = tempI32('ab'), idxI32 = tempI32('ai')
|
|
850
|
+
return typed(['if', ['result', 'f64'],
|
|
851
|
+
['i32.lt_u',
|
|
852
|
+
['local.tee', `$${idxI32}`, vi],
|
|
853
|
+
['i32.load', ['i32.sub',
|
|
854
|
+
['local.tee', `$${baseI32}`, arrBase()],
|
|
855
|
+
['i32.const', 8]]]],
|
|
856
|
+
['then', ctx.abi.array.ops.load(['local.get', `$${baseI32}`], ['local.get', `$${idxI32}`])],
|
|
857
|
+
['else', undefExpr()]], 'f64')
|
|
858
|
+
}
|
|
793
859
|
const baseTmp = temp()
|
|
794
860
|
// Numeric key (literal or known-NUMBER name) → skip __is_str_key dispatch;
|
|
795
861
|
// arrays don't honor string-key access for numeric keys (keys aren't coerced
|
|
@@ -842,7 +908,7 @@ export default (ctx) => {
|
|
|
842
908
|
const keyI32 = asI32(typed(keyExpr, 'f64'))
|
|
843
909
|
if (ctx.module.modules['string'] && !notString) {
|
|
844
910
|
return ['if', ['result', 'f64'],
|
|
845
|
-
|
|
911
|
+
ptrTypeEq(ptrExpr, PTR.STRING),
|
|
846
912
|
['then', (inc('__str_idx'), ['call', '$__str_idx', ['i64.reinterpret_f64', ptrExpr], keyI32])],
|
|
847
913
|
['else', (['call', '$__typed_idx', ['i64.reinterpret_f64', ptrExpr], keyI32])]]
|
|
848
914
|
}
|
|
@@ -852,7 +918,7 @@ export default (ctx) => {
|
|
|
852
918
|
if (ctx.module.modules['string'] && !notString)
|
|
853
919
|
return typed(
|
|
854
920
|
['if', ['result', 'f64'],
|
|
855
|
-
|
|
921
|
+
ptrTypeEq(ptrExpr, PTR.STRING),
|
|
856
922
|
['then', stringLoad()],
|
|
857
923
|
['else', arrayLoad]],
|
|
858
924
|
'f64')
|
|
@@ -1021,6 +1087,42 @@ export default (ctx) => {
|
|
|
1021
1087
|
(i32.store (i32.sub (local.get $rawOff) (i32.const 4)) (i32.const -1))))
|
|
1022
1088
|
(local.get $val))))))`
|
|
1023
1089
|
|
|
1090
|
+
// .fill(value, start?, end?) — overwrite [start, end) with value; mutate + return the
|
|
1091
|
+
// array. start/end default to 0 / length and accept negatives (offset from end). The
|
|
1092
|
+
// INT_MAX end sentinel clamps to length in the helper, which also normalizes negatives.
|
|
1093
|
+
ctx.core.emit['.fill'] = (arr, val, start, end) => {
|
|
1094
|
+
inc('__arr_fill')
|
|
1095
|
+
return typed(['call', '$__arr_fill',
|
|
1096
|
+
asI64(emit(arr)),
|
|
1097
|
+
val == null ? undefExpr() : asF64(emit(val)),
|
|
1098
|
+
start == null ? ['i32.const', 0] : asI32(emit(start)),
|
|
1099
|
+
end == null ? ['i32.const', 0x7FFFFFFF] : asI32(emit(end))], 'f64')
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
ctx.core.stdlib['__arr_fill'] = `(func $__arr_fill (param $arr i64) (param $val f64) (param $start i32) (param $end i32) (result f64)
|
|
1103
|
+
(local $off i32) (local $len i32) (local $i i32)
|
|
1104
|
+
(if (i32.eq
|
|
1105
|
+
(i32.wrap_i64 (i64.and (i64.shr_u (local.get $arr) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK})))
|
|
1106
|
+
(i32.const ${PTR.ARRAY}))
|
|
1107
|
+
(then
|
|
1108
|
+
(local.set $off (call $__ptr_offset (local.get $arr)))
|
|
1109
|
+
(if (i32.ge_u (local.get $off) (i32.const 8))
|
|
1110
|
+
(then
|
|
1111
|
+
(local.set $len (i32.load (i32.sub (local.get $off) (i32.const 8))))
|
|
1112
|
+
(if (i32.lt_s (local.get $start) (i32.const 0)) (then (local.set $start (i32.add (local.get $len) (local.get $start)))))
|
|
1113
|
+
(if (i32.lt_s (local.get $start) (i32.const 0)) (then (local.set $start (i32.const 0))))
|
|
1114
|
+
(if (i32.gt_s (local.get $start) (local.get $len)) (then (local.set $start (local.get $len))))
|
|
1115
|
+
(if (i32.lt_s (local.get $end) (i32.const 0)) (then (local.set $end (i32.add (local.get $len) (local.get $end)))))
|
|
1116
|
+
(if (i32.lt_s (local.get $end) (i32.const 0)) (then (local.set $end (i32.const 0))))
|
|
1117
|
+
(if (i32.gt_s (local.get $end) (local.get $len)) (then (local.set $end (local.get $len))))
|
|
1118
|
+
(local.set $i (local.get $start))
|
|
1119
|
+
(block $done (loop $fill
|
|
1120
|
+
(br_if $done (i32.ge_s (local.get $i) (local.get $end)))
|
|
1121
|
+
(f64.store (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3))) (local.get $val))
|
|
1122
|
+
(local.set $i (i32.add (local.get $i) (i32.const 1)))
|
|
1123
|
+
(br $fill)))))))
|
|
1124
|
+
(f64.reinterpret_i64 (local.get $arr)))`
|
|
1125
|
+
|
|
1024
1126
|
// .splice(start) | .splice(start, deleteCount) → remove range, return removed as new array
|
|
1025
1127
|
ctx.core.emit['.splice'] = (arr, start, deleteCount) => {
|
|
1026
1128
|
const recv = hoistArrayValue(arr)
|
|
@@ -1098,59 +1200,111 @@ export default (ctx) => {
|
|
|
1098
1200
|
(i32.store (i32.sub (local.get $off) (i32.const 8)) (i32.add (local.get $len) (i32.const 1)))
|
|
1099
1201
|
(f64.convert_i32_s (i32.add (local.get $len) (i32.const 1))))`
|
|
1100
1202
|
|
|
1101
|
-
// .
|
|
1102
|
-
|
|
1203
|
+
// .splice(start, deleteCount, ...items) with inserts → __arr_splice. Deletes
|
|
1204
|
+
// `del` elements at `s`, inserts the elements of `ins`, returns the removed
|
|
1205
|
+
// elements as a new array. Mutation is in place: a grow relocates the buffer
|
|
1206
|
+
// but leaves a forwarding pointer, so the caller's NaN-box still resolves —
|
|
1207
|
+
// no write-back needed (mirrors __arr_unshift). memory.copy is memmove, so the
|
|
1208
|
+
// tail shift is correct whether the array grew (shift right) or shrank (left).
|
|
1209
|
+
ctx.core.stdlib['__arr_splice'] = `(func $__arr_splice (param $arr i64) (param $start i32) (param $del i32) (param $ins i64) (result f64)
|
|
1210
|
+
(local $off i32) (local $len i32) (local $s i32) (local $cnt i32)
|
|
1211
|
+
(local $m i32) (local $newLen i32) (local $tail i32) (local $out f64) (local $a f64)
|
|
1212
|
+
(local.set $len (call $__len (local.get $arr)))
|
|
1213
|
+
(local.set $off (call $__ptr_offset (local.get $arr)))
|
|
1214
|
+
(local.set $s (local.get $start))
|
|
1215
|
+
(if (i32.lt_s (local.get $s) (i32.const 0))
|
|
1216
|
+
(then
|
|
1217
|
+
(local.set $s (i32.add (local.get $s) (local.get $len)))
|
|
1218
|
+
(if (i32.lt_s (local.get $s) (i32.const 0)) (then (local.set $s (i32.const 0))))))
|
|
1219
|
+
(if (i32.gt_s (local.get $s) (local.get $len)) (then (local.set $s (local.get $len))))
|
|
1220
|
+
(local.set $cnt (local.get $del))
|
|
1221
|
+
(if (i32.lt_s (local.get $cnt) (i32.const 0)) (then (local.set $cnt (i32.const 0))))
|
|
1222
|
+
(if (i32.gt_s (i32.add (local.get $s) (local.get $cnt)) (local.get $len))
|
|
1223
|
+
(then (local.set $cnt (i32.sub (local.get $len) (local.get $s)))))
|
|
1224
|
+
(local.set $m (call $__len (local.get $ins)))
|
|
1225
|
+
(local.set $newLen (i32.add (i32.sub (local.get $len) (local.get $cnt)) (local.get $m)))
|
|
1226
|
+
(local.set $tail (i32.sub (i32.sub (local.get $len) (local.get $s)) (local.get $cnt)))
|
|
1227
|
+
(local.set $out (call $__mkptr (i32.const ${PTR.ARRAY}) (i32.const 0) (call $__alloc_hdr (local.get $cnt) (local.get $cnt))))
|
|
1228
|
+
(memory.copy
|
|
1229
|
+
(call $__ptr_offset (i64.reinterpret_f64 (local.get $out)))
|
|
1230
|
+
(i32.add (local.get $off) (i32.shl (local.get $s) (i32.const 3)))
|
|
1231
|
+
(i32.shl (local.get $cnt) (i32.const 3)))
|
|
1232
|
+
(local.set $a (f64.reinterpret_i64 (local.get $arr)))
|
|
1233
|
+
(if (i32.gt_s (local.get $newLen) (local.get $len))
|
|
1234
|
+
(then (local.set $a (call $__arr_grow (local.get $arr) (local.get $newLen)))))
|
|
1235
|
+
(local.set $off (call $__ptr_offset (i64.reinterpret_f64 (local.get $a))))
|
|
1236
|
+
(memory.copy
|
|
1237
|
+
(i32.add (local.get $off) (i32.shl (i32.add (local.get $s) (local.get $m)) (i32.const 3)))
|
|
1238
|
+
(i32.add (local.get $off) (i32.shl (i32.add (local.get $s) (local.get $cnt)) (i32.const 3)))
|
|
1239
|
+
(i32.shl (local.get $tail) (i32.const 3)))
|
|
1240
|
+
(memory.copy
|
|
1241
|
+
(i32.add (local.get $off) (i32.shl (local.get $s) (i32.const 3)))
|
|
1242
|
+
(call $__ptr_offset (local.get $ins))
|
|
1243
|
+
(i32.shl (local.get $m) (i32.const 3)))
|
|
1244
|
+
(i32.store (i32.sub (local.get $off) (i32.const 8)) (local.get $newLen))
|
|
1245
|
+
(local.get $out))`
|
|
1246
|
+
|
|
1247
|
+
// Early-exit callback iterator: init value, exit test, value on match.
|
|
1248
|
+
const earlyExitMethod = ({ tag, init, test, onMatch, reverse }) => (arr, fn) => {
|
|
1103
1249
|
const recv = hoistArrayValue(arr)
|
|
1104
|
-
const r = temp(
|
|
1250
|
+
const r = temp(tag)
|
|
1105
1251
|
const exit = `$exit${ctx.func.uniq++}`
|
|
1106
1252
|
const cb = makeCallback(fn, callbackArgReps(arr))
|
|
1107
1253
|
const loop = arrayLoop(recv.value, (_ptr, _len, i, item) => [
|
|
1108
|
-
['if',
|
|
1109
|
-
['then', ['local.set', `$${r}`,
|
|
1110
|
-
])
|
|
1254
|
+
['if', test(cb, i, item),
|
|
1255
|
+
['then', ['local.set', `$${r}`, onMatch(cb, i, item)], ['br', exit]]]
|
|
1256
|
+
], undefined, undefined, reverse)
|
|
1111
1257
|
return typed(['block', ['result', 'f64'],
|
|
1112
1258
|
recv.setup,
|
|
1113
1259
|
cb.setup,
|
|
1114
|
-
['local.set', `$${r}`,
|
|
1260
|
+
['local.set', `$${r}`, init],
|
|
1115
1261
|
['block', exit, ...loop],
|
|
1116
1262
|
['local.get', `$${r}`]], 'f64')
|
|
1117
1263
|
}
|
|
1118
1264
|
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
const
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
const loop = arrayLoop(recv.value, (_ptr, _len, i, item) => [
|
|
1126
|
-
['if', ['i32.eqz', truthyIR(cb.call([item, idxArg(cb, i)]))],
|
|
1127
|
-
['then', ['local.set', `$${r}`, ['f64.const', 0]], ['br', exit]]]
|
|
1128
|
-
])
|
|
1129
|
-
return typed(['block', ['result', 'f64'],
|
|
1130
|
-
recv.setup,
|
|
1131
|
-
cb.setup,
|
|
1132
|
-
['local.set', `$${r}`, ['f64.const', 1]],
|
|
1133
|
-
['block', exit, ...loop],
|
|
1134
|
-
['local.get', `$${r}`]], 'f64')
|
|
1135
|
-
}
|
|
1265
|
+
ctx.core.emit['.some'] = earlyExitMethod({
|
|
1266
|
+
tag: 'sr',
|
|
1267
|
+
init: ['f64.const', 0],
|
|
1268
|
+
test: (cb, i, item) => truthyIR(cb.call([item, idxArg(cb, i)])),
|
|
1269
|
+
onMatch: () => ['f64.const', 1],
|
|
1270
|
+
})
|
|
1136
1271
|
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
const
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
]
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1272
|
+
ctx.core.emit['.every'] = earlyExitMethod({
|
|
1273
|
+
tag: 'ev',
|
|
1274
|
+
init: ['f64.const', 1],
|
|
1275
|
+
test: (cb, i, item) => ['i32.eqz', truthyIR(cb.call([item, idxArg(cb, i)]))],
|
|
1276
|
+
onMatch: () => ['f64.const', 0],
|
|
1277
|
+
})
|
|
1278
|
+
|
|
1279
|
+
ctx.core.emit['.findIndex'] = earlyExitMethod({
|
|
1280
|
+
tag: 'fi',
|
|
1281
|
+
init: ['f64.const', -1],
|
|
1282
|
+
test: (cb, i, item) => truthyIR(cb.call([item, idxArg(cb, i)])),
|
|
1283
|
+
onMatch: (_cb, i) => ['f64.convert_i32_s', ['local.get', `$${i}`]],
|
|
1284
|
+
})
|
|
1285
|
+
|
|
1286
|
+
ctx.core.emit['.find'] = earlyExitMethod({
|
|
1287
|
+
tag: 'ff',
|
|
1288
|
+
init: ['f64.reinterpret_i64', ['i64.const', NULL_NAN]],
|
|
1289
|
+
test: (cb, i, item) => truthyIR(cb.call([item, idxArg(cb, i)])),
|
|
1290
|
+
onMatch: (_cb, _i, item) => item,
|
|
1291
|
+
})
|
|
1292
|
+
|
|
1293
|
+
ctx.core.emit['.findLastIndex'] = earlyExitMethod({
|
|
1294
|
+
tag: 'fli',
|
|
1295
|
+
init: ['f64.const', -1],
|
|
1296
|
+
test: (cb, i, item) => truthyIR(cb.call([item, idxArg(cb, i)])),
|
|
1297
|
+
onMatch: (_cb, i) => ['f64.convert_i32_s', ['local.get', `$${i}`]],
|
|
1298
|
+
reverse: true,
|
|
1299
|
+
})
|
|
1300
|
+
|
|
1301
|
+
ctx.core.emit['.findLast'] = earlyExitMethod({
|
|
1302
|
+
tag: 'fl',
|
|
1303
|
+
init: ['f64.reinterpret_i64', ['i64.const', NULL_NAN]],
|
|
1304
|
+
test: (cb, i, item) => truthyIR(cb.call([item, idxArg(cb, i)])),
|
|
1305
|
+
onMatch: (_cb, _i, item) => item,
|
|
1306
|
+
reverse: true,
|
|
1307
|
+
})
|
|
1154
1308
|
|
|
1155
1309
|
// === Array methods ===
|
|
1156
1310
|
|
|
@@ -1170,13 +1324,13 @@ export default (ctx) => {
|
|
|
1170
1324
|
}
|
|
1171
1325
|
function isPureCallback(fn) {
|
|
1172
1326
|
if (!Array.isArray(fn) || fn[0] !== '=>') return false
|
|
1173
|
-
const body = fn[2]
|
|
1174
1327
|
const params = new Set()
|
|
1175
|
-
const
|
|
1176
|
-
|
|
1177
|
-
|
|
1328
|
+
for (const r of extractParams(fn[1])) {
|
|
1329
|
+
const p = classifyParam(r)
|
|
1330
|
+
if (p.name) params.add(p.name)
|
|
1331
|
+
}
|
|
1178
1332
|
const locals = new Set(params)
|
|
1179
|
-
collectLocals(
|
|
1333
|
+
collectLocals(fn[2], locals)
|
|
1180
1334
|
let pure = true
|
|
1181
1335
|
;(function walk(node) {
|
|
1182
1336
|
if (!pure || !Array.isArray(node)) return
|
|
@@ -1184,13 +1338,13 @@ export default (ctx) => {
|
|
|
1184
1338
|
if (op === '=>') return
|
|
1185
1339
|
if (op === '()' || op === '?.()' || op === 'new') { pure = false; return }
|
|
1186
1340
|
if (op === '++' || op === '--') { pure = false; return }
|
|
1187
|
-
if (op
|
|
1341
|
+
if (ASSIGN_OPS.has(op)) {
|
|
1188
1342
|
const t = args[0]
|
|
1189
1343
|
if (typeof t === 'string') { if (!locals.has(t)) { pure = false; return } }
|
|
1190
1344
|
else { pure = false; return }
|
|
1191
1345
|
}
|
|
1192
1346
|
for (const a of args) walk(a)
|
|
1193
|
-
})(
|
|
1347
|
+
})(fn[2])
|
|
1194
1348
|
return pure
|
|
1195
1349
|
}
|
|
1196
1350
|
|
|
@@ -1315,30 +1469,56 @@ export default (ctx) => {
|
|
|
1315
1469
|
const acc = temp('ra'), mapped = temp('mv')
|
|
1316
1470
|
const upReps = callbackArgReps(up.source)
|
|
1317
1471
|
const mapCb = makeCallback(up.fn, upReps), redCb = makeCallback(fn)
|
|
1472
|
+
const mget = typed(['local.get', `$${mapped}`], 'f64')
|
|
1473
|
+
// map preserves indices → the reduce callback's index is the loop counter.
|
|
1474
|
+
const fold = i => ['local.set', `$${acc}`, asF64(redCb.call([typed(['local.get', `$${acc}`], 'f64'), mget, idxArg(redCb, i, 2)]))]
|
|
1318
1475
|
const loop = arrayLoop(recv.value, (_p, _l, i, item) => [
|
|
1319
1476
|
['local.set', `$${mapped}`, asF64(mapCb.call([item, idxArg(mapCb, i)]))],
|
|
1320
|
-
|
|
1477
|
+
// No-init: seed accumulator with the first mapped element (see base path).
|
|
1478
|
+
init !== undefined ? fold(i)
|
|
1479
|
+
: ['if', ['i32.eqz', ['local.get', `$${i}`]],
|
|
1480
|
+
['then', ['local.set', `$${acc}`, mget]],
|
|
1481
|
+
['else', fold(i)]]
|
|
1321
1482
|
])
|
|
1322
1483
|
return typed(['block', ['result', 'f64'],
|
|
1323
1484
|
recv.setup, mapCb.setup, redCb.setup,
|
|
1324
|
-
['local.set', `$${acc}`, init ? asF64(emit(init)) : ['f64.const', 0]],
|
|
1485
|
+
['local.set', `$${acc}`, init !== undefined ? asF64(emit(init)) : ['f64.const', 0]],
|
|
1325
1486
|
...loop, ['local.get', `$${acc}`]], 'f64')
|
|
1326
1487
|
}
|
|
1327
1488
|
// .filter(f).reduce(g, init) → single loop: test f, accumulate with g if passes
|
|
1328
1489
|
if (up && up.method === 'filter') {
|
|
1329
1490
|
const recv = hoistArrayValue(up.source)
|
|
1330
1491
|
const acc = temp('ra')
|
|
1492
|
+
// No-init: seed is the first *passing* element, whose index isn't known
|
|
1493
|
+
// statically (filter), so track a seeded flag rather than i==0.
|
|
1494
|
+
const seeded = init !== undefined ? null : tempI32('rs')
|
|
1331
1495
|
const upReps = callbackArgReps(up.source)
|
|
1332
1496
|
const filterCb = makeCallback(up.fn, upReps)
|
|
1333
1497
|
// reduce cb signature: (acc, item, idx). Item rep mirrors upstream's item rep.
|
|
1334
1498
|
const redCb = makeCallback(fn, [null, upReps[0], { val: VAL.NUMBER }])
|
|
1499
|
+
// filter renumbers: the reduce index counts *passing* elements, not the
|
|
1500
|
+
// source position, so track a dedicated filtered-position counter (only when
|
|
1501
|
+
// the callback actually reads its index — else idxArg drops the arg anyway).
|
|
1502
|
+
const usesIdx = redCb.usedParams ? !!redCb.usedParams[2] : true
|
|
1503
|
+
const fpos = usesIdx ? tempI32('rp') : null
|
|
1504
|
+
const fold = item => ['local.set', `$${acc}`, asF64(redCb.call([typed(['local.get', `$${acc}`], 'f64'), item, fpos ? idxArg(redCb, fpos, 2) : null]))]
|
|
1505
|
+
const bump = fpos ? [['local.set', `$${fpos}`, ['i32.add', ['local.get', `$${fpos}`], ['i32.const', 1]]]] : []
|
|
1506
|
+
const accumulate = item => ['block',
|
|
1507
|
+
seeded
|
|
1508
|
+
? ['if', ['local.get', `$${seeded}`],
|
|
1509
|
+
['then', fold(item)],
|
|
1510
|
+
['else', ['block', ['local.set', `$${acc}`, asF64(item)], ['local.set', `$${seeded}`, ['i32.const', 1]]]]]
|
|
1511
|
+
: fold(item),
|
|
1512
|
+
...bump]
|
|
1335
1513
|
const loop = arrayLoop(recv.value, (_p, _l, i, item) => [
|
|
1336
1514
|
['if', truthyIR(filterCb.call([item, idxArg(filterCb, i)])),
|
|
1337
|
-
['then',
|
|
1515
|
+
['then', accumulate(item)]]
|
|
1338
1516
|
])
|
|
1339
1517
|
return typed(['block', ['result', 'f64'],
|
|
1340
1518
|
recv.setup, filterCb.setup, redCb.setup,
|
|
1341
|
-
['local.set', `$${
|
|
1519
|
+
...(fpos ? [['local.set', `$${fpos}`, ['i32.const', 0]]] : []),
|
|
1520
|
+
...(seeded ? [['local.set', `$${seeded}`, ['i32.const', 0]]] : []),
|
|
1521
|
+
['local.set', `$${acc}`, init !== undefined ? asF64(emit(init)) : ['f64.const', 0]],
|
|
1342
1522
|
...loop, ['local.get', `$${acc}`]], 'f64')
|
|
1343
1523
|
}
|
|
1344
1524
|
const recv = hoistArrayValue(arr)
|
|
@@ -1346,13 +1526,47 @@ export default (ctx) => {
|
|
|
1346
1526
|
// reduce cb signature: (acc, item, idx). Item rep mirrors recv's elem val type.
|
|
1347
1527
|
const reps = callbackArgReps(arr)
|
|
1348
1528
|
const cb = makeCallback(fn, [null, reps[0], { val: VAL.NUMBER }])
|
|
1349
|
-
|
|
1350
|
-
|
|
1529
|
+
// No initial value: JS seeds the accumulator with element 0 and folds from
|
|
1530
|
+
// index 1 — NOT a 0 seed folded over every element. A 0 seed is invisible
|
|
1531
|
+
// for `+` (additive identity) but wrong for `*` (→0) and corrupts non-numeric
|
|
1532
|
+
// folds (string reduce emits a bare `0` in the joined result). Seed at i==0.
|
|
1533
|
+
const fold = (item, i) => ['local.set', `$${acc}`, asF64(cb.call([typed(['local.get', `$${acc}`], 'f64'), item, idxArg(cb, i, 2)]))]
|
|
1534
|
+
const loop = arrayLoop(recv.value, (_ptr, _len, i, item) => [
|
|
1535
|
+
init !== undefined ? fold(item, i)
|
|
1536
|
+
: ['if', ['i32.eqz', ['local.get', `$${i}`]],
|
|
1537
|
+
['then', ['local.set', `$${acc}`, asF64(item)]],
|
|
1538
|
+
['else', fold(item, i)]]
|
|
1351
1539
|
])
|
|
1352
1540
|
return typed(['block', ['result', 'f64'],
|
|
1353
1541
|
recv.setup,
|
|
1354
1542
|
cb.setup,
|
|
1355
|
-
['local.set', `$${acc}`, init ? asF64(emit(init)) : ['f64.const', 0]],
|
|
1543
|
+
['local.set', `$${acc}`, init !== undefined ? asF64(emit(init)) : ['f64.const', 0]],
|
|
1544
|
+
...loop,
|
|
1545
|
+
['local.get', `$${acc}`]], 'f64')
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
// .reduceRight(fn, init) — same accumulator fold as .reduce but the last
|
|
1549
|
+
// arrayLoop arg (reverse) walks elements len-1→0. No map/filter fusion: the
|
|
1550
|
+
// reverse-fused shapes don't occur in practice and the base case is the only
|
|
1551
|
+
// form jz emits. (Previously absent despite being autoload-declared + test262-
|
|
1552
|
+
// tracked — a phantom builtin that silently returned undefined.)
|
|
1553
|
+
ctx.core.emit['.reduceRight'] = (arr, fn, init) => {
|
|
1554
|
+
const recv = hoistArrayValue(arr)
|
|
1555
|
+
const acc = temp('ra')
|
|
1556
|
+
const reps = callbackArgReps(arr)
|
|
1557
|
+
const cb = makeCallback(fn, [null, reps[0], { val: VAL.NUMBER }])
|
|
1558
|
+
// No-init: reverse walk seeds with the last element (i == len-1), folds down.
|
|
1559
|
+
const fold = (item, i) => ['local.set', `$${acc}`, asF64(cb.call([typed(['local.get', `$${acc}`], 'f64'), item, idxArg(cb, i, 2)]))]
|
|
1560
|
+
const loop = arrayLoop(recv.value, (_ptr, len, i, item) => [
|
|
1561
|
+
init !== undefined ? fold(item, i)
|
|
1562
|
+
: ['if', ['i32.eq', ['local.get', `$${i}`], ['i32.sub', ['local.get', `$${len}`], ['i32.const', 1]]],
|
|
1563
|
+
['then', ['local.set', `$${acc}`, asF64(item)]],
|
|
1564
|
+
['else', fold(item, i)]]
|
|
1565
|
+
], null, null, true)
|
|
1566
|
+
return typed(['block', ['result', 'f64'],
|
|
1567
|
+
recv.setup,
|
|
1568
|
+
cb.setup,
|
|
1569
|
+
['local.set', `$${acc}`, init !== undefined ? asF64(emit(init)) : ['f64.const', 0]],
|
|
1356
1570
|
...loop,
|
|
1357
1571
|
['local.get', `$${acc}`]], 'f64')
|
|
1358
1572
|
}
|
|
@@ -1391,23 +1605,6 @@ export default (ctx) => {
|
|
|
1391
1605
|
return typed(['block', ['result', 'f64'], recv.setup, cb.setup, ...loop, ['f64.const', 0]], 'f64')
|
|
1392
1606
|
}
|
|
1393
1607
|
|
|
1394
|
-
ctx.core.emit['.find'] = (arr, fn) => {
|
|
1395
|
-
const recv = hoistArrayValue(arr)
|
|
1396
|
-
const result = temp('ff')
|
|
1397
|
-
const exit = `$exit${ctx.func.uniq++}`
|
|
1398
|
-
const cb = makeCallback(fn, callbackArgReps(arr))
|
|
1399
|
-
const loop = arrayLoop(recv.value, (_ptr, _len, i, item) => [
|
|
1400
|
-
['if', truthyIR(cb.call([item, idxArg(cb, i)])),
|
|
1401
|
-
['then', ['local.set', `$${result}`, item], ['br', exit]]]
|
|
1402
|
-
])
|
|
1403
|
-
return typed(['block', ['result', 'f64'],
|
|
1404
|
-
recv.setup,
|
|
1405
|
-
cb.setup,
|
|
1406
|
-
['local.set', `$${result}`, ['f64.reinterpret_i64', ['i64.const', NULL_NAN]]],
|
|
1407
|
-
['block', exit, ...loop],
|
|
1408
|
-
['local.get', `$${result}`]], 'f64')
|
|
1409
|
-
}
|
|
1410
|
-
|
|
1411
1608
|
// .reverse() → in-place swap arr[i] ↔ arr[len-1-i], returns the array.
|
|
1412
1609
|
ctx.core.emit['.reverse'] = (arr) => {
|
|
1413
1610
|
const recv = hoistArrayValue(arr)
|
|
@@ -1697,6 +1894,10 @@ export default (ctx) => {
|
|
|
1697
1894
|
body.push(out.ptr)
|
|
1698
1895
|
return typed(['block', ['result', 'f64'], ...body], 'f64')
|
|
1699
1896
|
}
|
|
1897
|
+
// Unqualified alias so an untyped-receiver `.concat` gets emit's runtime
|
|
1898
|
+
// string-vs-array ptr-type branch (string → `.string:concat`, array → this),
|
|
1899
|
+
// mirroring `.at`. A known-ARRAY receiver still dispatches here directly.
|
|
1900
|
+
ctx.core.emit['.concat'] = ctx.core.emit['.array:concat']
|
|
1700
1901
|
|
|
1701
1902
|
// .flat() → flatten one level of nested arrays
|
|
1702
1903
|
ctx.core.stdlib['__arr_flat'] = `(func $__arr_flat (param $src i64) (result f64)
|