jz 0.8.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -9
- package/bench/README.md +121 -50
- package/bench/bench.svg +27 -27
- package/cli.js +3 -1
- package/dist/interop.js +1 -1
- package/dist/jz.js +7541 -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/src/compile/index.js
CHANGED
|
@@ -27,9 +27,11 @@
|
|
|
27
27
|
|
|
28
28
|
import parseWat from 'watr/parse'
|
|
29
29
|
import { ctx, err, inc, resolveIncludes, PTR, LAYOUT, declGlobal } from '../ctx.js'
|
|
30
|
+
import { i64Hex } from '../../layout.js'
|
|
30
31
|
import { T, isBlockBody, isReassigned, refsName, REFS_IN_EXPR, returnExprs } from '../ast.js'
|
|
31
32
|
import { valTypeOf } from '../kind.js'
|
|
32
33
|
import { intLiteralValue } from '../static.js'
|
|
34
|
+
import { intCertainMap, typedStaticLen } from '../type.js'
|
|
33
35
|
import {
|
|
34
36
|
analyzeBody, unboxablePtrs, cseSafeLoadBases, boxedCaptures,
|
|
35
37
|
analyzeStructInline, invalidateLocalsCache,
|
|
@@ -43,6 +45,9 @@ import { narrowBoundedSquare } from './loop-square.js'
|
|
|
43
45
|
import { unrollRecurrence } from './loop-recurrence.js'
|
|
44
46
|
import { peelClampedStencil } from './peel-stencil.js'
|
|
45
47
|
import { cseLoads } from './cse-load.js'
|
|
48
|
+
import {
|
|
49
|
+
scanDynClosureTableCandidates, recordParamClosureDefault, recordDirectReturnClosure, resolveDynFnTables,
|
|
50
|
+
} from './dyn-closure-tables.js'
|
|
46
51
|
|
|
47
52
|
// Monotonic across all functions so a CSE temp never collides (even after later inlining).
|
|
48
53
|
let __cseCtr = 0
|
|
@@ -50,7 +55,7 @@ const freshCseName = () => `${T}cse${__cseCtr++}`
|
|
|
50
55
|
import { emit, emitter, emitVoid, emitBlockBody } from './emit.js'
|
|
51
56
|
import { emitCharDecompPrologue, JSS_IMPORT_SIGS } from '../abi/string.js'
|
|
52
57
|
import {
|
|
53
|
-
typed, asF64, asI32, asPtrOffset, asParamType, toI32, asI64, fromI64,
|
|
58
|
+
typed, asF64, asI32, asPtrOffset, asParamType, toI32, asI64, fromI64, ptrTypeEq,
|
|
54
59
|
NULL_NAN, UNDEF_NAN, NULL_WAT, UNDEF_WAT, NULL_IR, UNDEF_IR, nullExpr, undefExpr,
|
|
55
60
|
MAX_CLOSURE_ARITY,
|
|
56
61
|
mkPtrIR,
|
|
@@ -67,7 +72,7 @@ import plan from './plan/index.js'
|
|
|
67
72
|
import { foldStaticConstAggregates } from './plan/literals.js'
|
|
68
73
|
import {
|
|
69
74
|
buildStartFn, dedupClosureBodies, finalizeClosureTable,
|
|
70
|
-
pullStdlib, syncImports, optimizeModule, stripStaticDataPrefix, hoistConstGlobalInits,
|
|
75
|
+
pullStdlib, syncImports, optimizeModule, stripStaticDataPrefix, hoistConstGlobalInits, stripDeadLazyTables,
|
|
71
76
|
} from '../wat/assemble.js'
|
|
72
77
|
import { instrumentHelperCallsites } from '../helper-counters.js'
|
|
73
78
|
|
|
@@ -393,6 +398,8 @@ function enterFunc(sig, body, { uniq = 0, directClosures = null } = {}) {
|
|
|
393
398
|
ctx.func.directClosures = directClosures
|
|
394
399
|
ctx.func.localProps = null
|
|
395
400
|
ctx.func.charDecomp = null
|
|
401
|
+
ctx.func.charDecompGlobals = false // only emitFunc's named path drains — it re-arms
|
|
402
|
+
ctx.func.probeHoist = null
|
|
396
403
|
if (ctx.transform.optimize) {
|
|
397
404
|
scanAndTagNonEscapingClosures(body)
|
|
398
405
|
}
|
|
@@ -443,21 +450,68 @@ function analyzeFuncForEmit(func, programFacts) {
|
|
|
443
450
|
ctx.func.boxed = new Map()
|
|
444
451
|
ctx.func.localReps = null
|
|
445
452
|
ctx.types.typedElem = ctx.scope.globalTypedElem ? new Map(ctx.scope.globalTypedElem) : null
|
|
453
|
+
// typedLen mirrors typedElem's per-function lifecycle EXACTLY — a stale entry from a
|
|
454
|
+
// sibling function's same-named local would prove a wrong bound (names are per-function).
|
|
455
|
+
ctx.types.typedLen = ctx.scope.globalTypedLen ? new Map(ctx.scope.globalTypedLen) : null
|
|
446
456
|
|
|
447
457
|
const _reps = paramReps.get(name)
|
|
448
458
|
if (_reps) {
|
|
449
459
|
for (const [k, r] of _reps) {
|
|
450
460
|
if (k >= sig.params.length) continue
|
|
451
461
|
const pname = sig.params[k].name
|
|
452
|
-
|
|
462
|
+
// r.val/r.typedCtor describe the CALLER's argument — the param's value AT
|
|
463
|
+
// ENTRY, before this function's own body runs. A param the body reassigns
|
|
464
|
+
// (`opts = normalize(opts)`) no longer necessarily holds that entry-time
|
|
465
|
+
// kind past the write, so seeding it here is only sound when the body
|
|
466
|
+
// never writes the name. Without this guard the stale entry-time kind
|
|
467
|
+
// stands unchallenged: analyzeBody's OWN valType tracker (below, `bodyFacts.
|
|
468
|
+
// valTypes`) starts with no memory of this pre-seeded value (it's a fresh
|
|
469
|
+
// Map, not the shared ctx.func.localReps store), so when the reassignment's
|
|
470
|
+
// RHS type can't be resolved (e.g. a call to a function whose own valResult
|
|
471
|
+
// never converges), makeValTracker's poison path requires a PRIOR value
|
|
472
|
+
// in ITS OWN map to fire — there isn't one — so it neither confirms nor
|
|
473
|
+
// invalidates the seed, and the merge loop below never touches the name at
|
|
474
|
+
// all. A hardcoded-wrong kind then rides every read of that binding for the
|
|
475
|
+
// rest of the function (watr's own `optimize()`: opts's param-fact kind is
|
|
476
|
+
// VAL.OBJECT from callers that pass object literals; `opts = normalize(opts)`
|
|
477
|
+
// reassigns it to normalize's actual return — a HASH in the schema-less-
|
|
478
|
+
// spread shape — but the stale OBJECT kind survives, so `emitTypeTag` bakes
|
|
479
|
+
// a hardcoded `(i32.const PTR.OBJECT)` tag into `opts.inlineOnce`'s dyn-get
|
|
480
|
+
// dispatch instead of reading the receiver's true runtime tag, and the probe
|
|
481
|
+
// walks a schema this HASH was never shaped as — a silent miss, not a trap).
|
|
482
|
+
const reassigned = isReassigned(body, pname)
|
|
483
|
+
if (r.typedCtor && !reassigned) {
|
|
453
484
|
if (!ctx.types.typedElem) ctx.types.typedElem = new Map()
|
|
454
485
|
if (!ctx.types.typedElem.has(pname)) ctx.types.typedElem.set(pname, r.typedCtor)
|
|
455
486
|
updateRep(pname, { val: VAL.TYPED })
|
|
456
487
|
}
|
|
457
|
-
if (r.val && !ctx.func.localReps?.get(pname)?.val) updateRep(pname, { val: r.val })
|
|
488
|
+
if (r.val && !reassigned && !ctx.func.localReps?.get(pname)?.val) updateRep(pname, { val: r.val })
|
|
458
489
|
if (r.arrayElemSchema != null) updateRep(pname, { arrayElemSchema: r.arrayElemSchema })
|
|
459
490
|
if (r.arrayElemValType != null) updateRep(pname, { arrayElemValType: r.arrayElemValType })
|
|
460
491
|
if (r.intConst != null) updateRep(pname, { intConst: r.intConst })
|
|
492
|
+
// Cross-function never-relocation proof (analyzeParamNeverGrown) — the
|
|
493
|
+
// raw-base array read (module/array.js arrBase) keys off this rep.
|
|
494
|
+
if (r.neverGrown) updateRep(pname, { neverGrown: true })
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
// Caller-side nullability: a NO-DEFAULT param observes the UNDEF pad whenever a
|
|
498
|
+
// site omits its position (narrow's missing rule poisons r.val) or when callers
|
|
499
|
+
// are unknown (exported / value-used — no fact at all). A later body write
|
|
500
|
+
// (`nbar = 4` inside `if (nbar == null)`) sets val=NUMBER, which used to
|
|
501
|
+
// constant-fold the very null-check guarding it — under-arity callers then read
|
|
502
|
+
// the raw UNDEF box as NaN (window-function's taylor manual-default idiom).
|
|
503
|
+
// `nullable` only suppresses the nullish-compare FOLD; arithmetic typing keeps.
|
|
504
|
+
// `r.nullable` alongside a SETTLED val is narrow.js's BIGINT re-derivation:
|
|
505
|
+
// a `c ? BigInt(x) : null` site proves the kind yet still passes null — the
|
|
506
|
+
// callee's `x == null` sentinel must stay a live bit-compare.
|
|
507
|
+
{
|
|
508
|
+
const restIdx = func.rest ? sig.params.length - 1 : -1
|
|
509
|
+
for (let k = 0; k < sig.params.length; k++) {
|
|
510
|
+
if (k === restIdx) continue // rest arrays are never undefined
|
|
511
|
+
const pname = sig.params[k].name
|
|
512
|
+
if (func.defaults?.[pname] != null) continue // default fires on the UNDEF pad
|
|
513
|
+
const r = _reps?.get(k)
|
|
514
|
+
if (!r || r.val == null || r.nullable) updateRep(pname, { nullable: true })
|
|
461
515
|
}
|
|
462
516
|
}
|
|
463
517
|
// Trust numeric export params. An exported f64 param used only in numeric
|
|
@@ -502,7 +556,32 @@ function analyzeFuncForEmit(func, programFacts) {
|
|
|
502
556
|
const bodyFacts = block ? analyzeBody(body) : null
|
|
503
557
|
ctx.func.locals = bodyFacts ? bodyFacts.locals : new Map()
|
|
504
558
|
if (bodyFacts?.valTypes) {
|
|
505
|
-
|
|
559
|
+
// A PARAMETER name has no `let`/`const` declaration node inside body for
|
|
560
|
+
// analyzeBody's own tracker to seed a baseline "unknown" observation from
|
|
561
|
+
// (makeValTracker's poison logic needs a PRIOR value in ITS OWN map to
|
|
562
|
+
// detect a conflict — see that function's doc). So when a parameter is
|
|
563
|
+
// reassigned only CONDITIONALLY (`if (typeof opts === 'string' && …) opts
|
|
564
|
+
// = { profile: … }` — watr's own normalize()), the tracker's first (and
|
|
565
|
+
// only) observation is that ONE branch's type, with no competing
|
|
566
|
+
// observation for the other, equally-reachable path where the param keeps
|
|
567
|
+
// its original, caller-supplied value — a path this walk never visits
|
|
568
|
+
// because there's no assignment node ON it to visit. The merge below would
|
|
569
|
+
// then adopt the conditional branch's type as if it held on EVERY path.
|
|
570
|
+
// Trust it only when the param's own call-site-proven entry type (_reps,
|
|
571
|
+
// the fixpoint-settled cross-call-site fact — unlike this per-body walk,
|
|
572
|
+
// it already answers "what can this param be at entry, always") agrees:
|
|
573
|
+
// if it does, both the reassigned and the original-value paths carry the
|
|
574
|
+
// same kind, so unconditional-adoption is sound; if it's absent or
|
|
575
|
+
// different, the conditional branch's type does NOT generalize and must
|
|
576
|
+
// not override the (correctly) unresolved entry-time kind.
|
|
577
|
+
const paramIdx = block ? new Map(sig.params.map((p, i) => [p.name, i])) : null
|
|
578
|
+
for (const [name, vt] of bodyFacts.valTypes) {
|
|
579
|
+
if (paramIdx?.has(name)) {
|
|
580
|
+
const entryVal = _reps?.get(paramIdx.get(name))?.val
|
|
581
|
+
if (entryVal !== vt) continue
|
|
582
|
+
}
|
|
583
|
+
updateRep(name, { val: vt })
|
|
584
|
+
}
|
|
506
585
|
}
|
|
507
586
|
// Never-relocated array bindings — the `[]` reader skips the forwarding follow.
|
|
508
587
|
if (bodyFacts?.neverGrown) for (const name of bodyFacts.neverGrown) updateRep(name, { neverGrown: true })
|
|
@@ -530,6 +609,17 @@ function analyzeFuncForEmit(func, programFacts) {
|
|
|
530
609
|
.filter(p => !ctx.func.localReps?.get(p.name)?.val)
|
|
531
610
|
.map(p => p.name)
|
|
532
611
|
inferLocals(body, candidates)
|
|
612
|
+
// analyzeBody's locals slice (line above bodyFacts) ran BEFORE inferLocals
|
|
613
|
+
// bound elem-alias schema ids (`const p = ps[i]` → p.schemaId via
|
|
614
|
+
// analyzeValTypes). With strict-int32 slots in the program, re-derive the
|
|
615
|
+
// widths so exprType's slotI32CertainAt consult resolves through p — then
|
|
616
|
+
// `const x = hitX ? p.x : nx` declares i32 and the raw i32 slot load lands
|
|
617
|
+
// without an f64 round-trip. Gated: programs without strict slots skip the
|
|
618
|
+
// extra walk.
|
|
619
|
+
if (block && ctx.schema.slotI32Certain?.size) {
|
|
620
|
+
invalidateLocalsCache(body)
|
|
621
|
+
ctx.func.locals = analyzeBody(body).locals
|
|
622
|
+
}
|
|
533
623
|
if (block) {
|
|
534
624
|
boxedCaptures(body)
|
|
535
625
|
// Lower provably-monomorphic pointer locals to i32 offset storage.
|
|
@@ -573,15 +663,30 @@ function analyzeFuncForEmit(func, programFacts) {
|
|
|
573
663
|
: new Set()
|
|
574
664
|
|
|
575
665
|
// Closure-capture narrowing: a boxed var whose every defining RHS — owner
|
|
576
|
-
// body AND nested arrows
|
|
577
|
-
//
|
|
578
|
-
//
|
|
579
|
-
//
|
|
580
|
-
//
|
|
666
|
+
// body AND nested arrows — is integer-valued keeps its CELL in i32, so
|
|
667
|
+
// readVar/writeVar skip the f64↔i32 round-trip per access. Params are
|
|
668
|
+
// excluded: their cell is seeded from the raw f64 param value, which would
|
|
669
|
+
// desync an i32-read cell. Same asm.js-style range contract as plain
|
|
670
|
+
// intCertain locals.
|
|
671
|
+
//
|
|
672
|
+
// `ctx.func.localReps.get(name).intCertain` (forward-propagated in analyze.js
|
|
673
|
+
// via the plain, single-arg `intCertainMap(body)`) only sees defs in THIS
|
|
674
|
+
// scope's own top level — correct for an ordinary local (it can't be
|
|
675
|
+
// assigned from inside a nested arrow without becoming a capture) but blind
|
|
676
|
+
// to the writes that make a name "boxed" in the first place: `let env = 0;
|
|
677
|
+
// let set = () => { env = 1.5 }` has no top-level def contradicting `env`'s
|
|
678
|
+
// integer init, so it read back intCertain=true and the cell stayed i32,
|
|
679
|
+
// silently truncating every closure-body float write. Recompute instead with
|
|
680
|
+
// `capturedNames` — collectIntDefs' arrow-descending mode — scoped to just
|
|
681
|
+
// the boxed names, so their nested-arrow write sites join the SAME fixpoint.
|
|
581
682
|
const cellTypes = new Set()
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
683
|
+
const boxedNames = new Set(ctx.func.boxed.keys())
|
|
684
|
+
if (boxedNames.size) {
|
|
685
|
+
const capturedIntCertain = intCertainMap(body, boxedNames)
|
|
686
|
+
for (const name of boxedNames) {
|
|
687
|
+
if (sig.params.some(p => p.name === name)) continue
|
|
688
|
+
if (capturedIntCertain.get(name) === true) cellTypes.add(name)
|
|
689
|
+
}
|
|
585
690
|
}
|
|
586
691
|
|
|
587
692
|
// Snapshot each param's JS-boundary carrier while reps are live — synthesizeBoundaryWrappers
|
|
@@ -620,6 +725,7 @@ function analyzeFuncForEmit(func, programFacts) {
|
|
|
620
725
|
cseLoadBases,
|
|
621
726
|
distinctParams: func.distinctParams || null,
|
|
622
727
|
typedElem: ctx.types.typedElem ? new Map(ctx.types.typedElem) : null,
|
|
728
|
+
typedLen: ctx.types.typedLen ? new Map(ctx.types.typedLen) : null,
|
|
623
729
|
localReps: cloneRepMap(ctx.func.localReps),
|
|
624
730
|
}
|
|
625
731
|
}
|
|
@@ -1002,6 +1108,9 @@ function emitFunc(func, funcFacts, programFacts) {
|
|
|
1002
1108
|
const _reps = paramReps.get(name)
|
|
1003
1109
|
|
|
1004
1110
|
enterFunc(sig, body)
|
|
1111
|
+
// Only this path drains charDecomp prologues (collectParamInits below) —
|
|
1112
|
+
// the shape-1b global-receiver decomposition may mint only here.
|
|
1113
|
+
ctx.func.charDecompGlobals = true
|
|
1005
1114
|
const block = funcFacts.block
|
|
1006
1115
|
ctx.func.locals = new Map(funcFacts.locals)
|
|
1007
1116
|
ctx.func.boxed = new Map(funcFacts.boxed)
|
|
@@ -1010,6 +1119,8 @@ function emitFunc(func, funcFacts, programFacts) {
|
|
|
1010
1119
|
ctx.func.sliceViews = new Set(funcFacts.sliceViews)
|
|
1011
1120
|
ctx.func.localReps = cloneRepMap(funcFacts.localReps)
|
|
1012
1121
|
ctx.types.typedElem = funcFacts.typedElem ? new Map(funcFacts.typedElem) : null
|
|
1122
|
+
ctx.types.typedLen = funcFacts.typedLen ? new Map(funcFacts.typedLen)
|
|
1123
|
+
: ctx.scope.globalTypedLen ? new Map(ctx.scope.globalTypedLen) : null
|
|
1013
1124
|
|
|
1014
1125
|
// D: Apply call-site param facts (only if body analysis didn't already set them).
|
|
1015
1126
|
// Schema bindings additionally write into ctx.schema.vars so prop-access dispatch
|
|
@@ -1019,13 +1130,20 @@ function emitFunc(func, funcFacts, programFacts) {
|
|
|
1019
1130
|
for (const [k, r] of _reps) {
|
|
1020
1131
|
if (k >= sig.params.length) continue
|
|
1021
1132
|
const pname = sig.params[k].name
|
|
1022
|
-
|
|
1023
|
-
|
|
1133
|
+
// Same entry-vs-body-reassignment hazard analyzeFuncForEmit guards against
|
|
1134
|
+
// (see its comment): r.val/r.typedCtor/r.schemaId describe the CALLER's
|
|
1135
|
+
// argument, sound only while the body never writes the name. This step
|
|
1136
|
+
// duplicates that seeding (funcFacts.localReps already carries whatever
|
|
1137
|
+
// analyzeFuncForEmit settled, guarded — but re-applying the UNGUARDED
|
|
1138
|
+
// call-site fact here would undo it) so it needs the identical guard.
|
|
1139
|
+
const reassigned = isReassigned(body, pname)
|
|
1140
|
+
if (r.val && !reassigned && !ctx.func.localReps?.get(pname)?.val) updateRep(pname, { val: r.val })
|
|
1141
|
+
if (r.typedCtor && !reassigned) {
|
|
1024
1142
|
if (!ctx.types.typedElem) ctx.types.typedElem = new Map()
|
|
1025
1143
|
if (!ctx.types.typedElem.has(pname)) ctx.types.typedElem.set(pname, r.typedCtor)
|
|
1026
1144
|
if (!ctx.func.localReps?.get(pname)?.val) updateRep(pname, { val: VAL.TYPED })
|
|
1027
1145
|
}
|
|
1028
|
-
if (r.schemaId != null && !exported && !ctx.schema.vars.has(pname)) {
|
|
1146
|
+
if (r.schemaId != null && !reassigned && !exported && !ctx.schema.vars.has(pname)) {
|
|
1029
1147
|
ctx.schema.vars.set(pname, r.schemaId)
|
|
1030
1148
|
updateRep(pname, { schemaId: r.schemaId })
|
|
1031
1149
|
}
|
|
@@ -1070,9 +1188,16 @@ function emitFunc(func, funcFacts, programFacts) {
|
|
|
1070
1188
|
// externref so no `ref.is_null` branch is needed.
|
|
1071
1189
|
if (p?.jsstring && p.jsstringDefault != null) continue
|
|
1072
1190
|
const t = p?.type || 'f64'
|
|
1191
|
+
// emit(defVal) ONCE, before branching on t — same self-host miscompile class as
|
|
1192
|
+
// emit.js's 'return' handler. See .work/todo.md (groundtruth archive).
|
|
1193
|
+
const emittedDefVal = emit(defVal)
|
|
1194
|
+
// dyn-closure-tables.js: a default value that's provably a closure literal
|
|
1195
|
+
// (e.g. subscript's `dispatch(ops, tail, fn = (a, …) => {…})`) is the fact
|
|
1196
|
+
// proveClosureFactory needs to see through `dispatch`'s forwarded return.
|
|
1197
|
+
recordParamClosureDefault(name, pname, emittedDefVal)
|
|
1073
1198
|
defaultInits.set(pname,
|
|
1074
1199
|
['if', isUndef(typed(['local.get', `$${pname}`], 'f64')),
|
|
1075
|
-
['then', ['local.set', `$${pname}`, t === 'f64' ? asF64(
|
|
1200
|
+
['then', ['local.set', `$${pname}`, t === 'f64' ? asF64(emittedDefVal) : asI32(emittedDefVal)]]])
|
|
1076
1201
|
}
|
|
1077
1202
|
|
|
1078
1203
|
// Box params that are mutably captured: allocate cell, copy param value
|
|
@@ -1108,6 +1233,28 @@ function emitFunc(func, funcFacts, programFacts) {
|
|
|
1108
1233
|
// in-bounds char wrongly decodes as the OOB NaN.
|
|
1109
1234
|
const collectParamInits = () => {
|
|
1110
1235
|
const inits = []
|
|
1236
|
+
// Global-receiver decompositions (shape 1b) come first: a param default
|
|
1237
|
+
// like `(c = cur.charCodeAt(0))` must see the global's prologue locals
|
|
1238
|
+
// populated. Globals are readable at entry, so nothing precedes them.
|
|
1239
|
+
if (ctx.func.charDecomp) for (const dec of ctx.func.charDecomp.values())
|
|
1240
|
+
if (dec.global) inits.push(...emitCharDecompPrologue(dec))
|
|
1241
|
+
// Hoisted method-override probes (emit.js tryGenericEmitter): the probe's
|
|
1242
|
+
// answer is loop-invariant for a stable-global receiver — resolve it once.
|
|
1243
|
+
// Mirrors sidecarOverride's arm: primitives (real numbers, strings) can
|
|
1244
|
+
// never carry an own override, so only NaN-boxed non-STRING receivers probe.
|
|
1245
|
+
if (ctx.func.probeHoist) for (const ph of ctx.func.probeHoist.values()) {
|
|
1246
|
+
const g = () => ['i64.reinterpret_f64', ph.recvIR()]
|
|
1247
|
+
inits.push(
|
|
1248
|
+
['local.set', `$${ph.ovr}`, ['if', ['result', 'f64'],
|
|
1249
|
+
['i32.and',
|
|
1250
|
+
['f64.ne', ph.recvIR(), ph.recvIR()],
|
|
1251
|
+
['i64.ne',
|
|
1252
|
+
['i64.and', g(), ['i64.const', i64Hex(BigInt(LAYOUT.TAG_MASK) << BigInt(LAYOUT.TAG_SHIFT))]],
|
|
1253
|
+
['i64.const', i64Hex(BigInt(PTR.STRING) << BigInt(LAYOUT.TAG_SHIFT))]]],
|
|
1254
|
+
['then', ['f64.reinterpret_i64', ['call', '$__dyn_get_expr', g(), ph.keyIR()]]],
|
|
1255
|
+
['else', undefExpr()]]],
|
|
1256
|
+
['local.set', `$${ph.is}`, ptrTypeEq(['local.get', `$${ph.ovr}`], PTR.CLOSURE)])
|
|
1257
|
+
}
|
|
1111
1258
|
for (const p of sig.params) {
|
|
1112
1259
|
const di = defaultInits.get(p.name)
|
|
1113
1260
|
if (di) inits.push(di)
|
|
@@ -1141,6 +1288,10 @@ function emitFunc(func, funcFacts, programFacts) {
|
|
|
1141
1288
|
fn.push(...paramInits, ...boxedParamInits, ...preboxedLocalInits, ...values)
|
|
1142
1289
|
} else {
|
|
1143
1290
|
const ir = emit(body)
|
|
1291
|
+
// dyn-closure-tables.js: an expression-bodied function whose return value
|
|
1292
|
+
// is unconditionally a closure literal (e.g. `mk = (n) => (x) => x + n`) —
|
|
1293
|
+
// a direct-return closure factory, no defaulted-param indirection needed.
|
|
1294
|
+
recordDirectReturnClosure(name, ir)
|
|
1144
1295
|
const paramInits = collectParamInits()
|
|
1145
1296
|
for (const [l, t] of ctx.func.locals) fn.push(['local', dollar(l), t])
|
|
1146
1297
|
const finalIR = sig.ptrKind != null ? asPtrOffset(ir, sig.ptrKind) : asParamType(ir, sig.results[0])
|
|
@@ -1265,14 +1416,18 @@ function synthesizeBoundaryWrappers() {
|
|
|
1265
1416
|
const ptrType = valKindToPtr(sig.ptrKind)
|
|
1266
1417
|
body = toI64(mkPtrIR(ptrType, sig.ptrAux ?? 0, callIR))
|
|
1267
1418
|
} else if (resultBool) {
|
|
1268
|
-
// The
|
|
1269
|
-
//
|
|
1270
|
-
//
|
|
1271
|
-
//
|
|
1272
|
-
//
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1419
|
+
// The i32 carrier is a clean 0/1 — truthyIR's identity path boxes it
|
|
1420
|
+
// straight into the TRUE_NAN/FALSE_NAN atom. The f64 carrier is NOT
|
|
1421
|
+
// provably raw: a BOOL-valued result may already be the atom box
|
|
1422
|
+
// (JSON.parse("false") returns FALSE_NAN — a bare f64.ne(v,0) reads any
|
|
1423
|
+
// atom as truthy). __is_truthy normalizes both representations; this is
|
|
1424
|
+
// the cold host boundary, the call costs nothing that matters.
|
|
1425
|
+
let carrier
|
|
1426
|
+
if (sig.results[0] === 'i32') carrier = typed(callIR, 'i32')
|
|
1427
|
+
else {
|
|
1428
|
+
inc('__is_truthy')
|
|
1429
|
+
carrier = typed(['call', '$__is_truthy', toI64(callIR)], 'i32')
|
|
1430
|
+
}
|
|
1276
1431
|
body = toI64(boolBoxIR(carrier))
|
|
1277
1432
|
} else if (resultBigint || resultDynamic) {
|
|
1278
1433
|
// BigInt rides the i64-reinterpret-f64 carrier internally; a dynamic result is already an
|
|
@@ -1328,6 +1483,12 @@ function emitClosureBody(cb) {
|
|
|
1328
1483
|
} else {
|
|
1329
1484
|
ctx.types.typedElem = prevTypedElems
|
|
1330
1485
|
}
|
|
1486
|
+
// Static typed lengths ride the same channel (module globals + per-body): the
|
|
1487
|
+
// typedIdxProven bounds proof reads the merged view.
|
|
1488
|
+
const globalTL = ctx.scope.globalTypedLen
|
|
1489
|
+
ctx.types.typedLen = cb.typedLens
|
|
1490
|
+
? (globalTL ? new Map([...globalTL, ...cb.typedLens]) : new Map(cb.typedLens))
|
|
1491
|
+
: (globalTL ? new Map(globalTL) : null)
|
|
1331
1492
|
// In closure bodies, boxed captures use the original name as both var and cell local
|
|
1332
1493
|
ctx.func.boxed = cb.boxed ? new Map([...cb.boxed].map(v => [v, v])) : new Map()
|
|
1333
1494
|
// i32-narrowed cells: the owner decided the cell width (funcFacts.cellTypes);
|
|
@@ -1679,11 +1840,30 @@ export default function compile(ast, profiler) {
|
|
|
1679
1840
|
// Cache integer values for cross-call const-arg propagation: `f(N)` where
|
|
1680
1841
|
// `const N = 8` should observe the param as intConst=8.
|
|
1681
1842
|
if (isInt) (ctx.scope.constInts ||= new Map()).set(name, v)
|
|
1843
|
+
// Cache EVERY folded value (fractional too) so readVar substitutes the
|
|
1844
|
+
// literal at each read site — compile-time paths (emitPow's constant
|
|
1845
|
+
// non-integer exponent → exp(c·log x), narrowing, the vectorizer) see
|
|
1846
|
+
// through the global where V8 would only fold it at runtime. colorpq's
|
|
1847
|
+
// PQ exponents (nv = 2610/16384, p = 1.7·2523/32) rode global.get into
|
|
1848
|
+
// the generic runtime-exponent $math.pow because of exactly this gap.
|
|
1849
|
+
;(ctx.scope.constNums ||= new Map()).set(name, v)
|
|
1682
1850
|
}
|
|
1683
1851
|
}
|
|
1684
1852
|
}
|
|
1685
1853
|
}
|
|
1686
1854
|
|
|
1855
|
+
// Typed-ctor sizes parked at prepare (`new T(CIN*H*W)` — names only now folded):
|
|
1856
|
+
// re-run the static-len derivation with constInts populated. Feeds the interval
|
|
1857
|
+
// proof's receiver lengths (typedIdxProven class 5).
|
|
1858
|
+
if (ctx.scope.pendingTypedLens) {
|
|
1859
|
+
for (const [name, rhs] of ctx.scope.pendingTypedLens) {
|
|
1860
|
+
const len = typedStaticLen(rhs)
|
|
1861
|
+
if (len != null && ctx.scope.globalTypedElem?.has(name))
|
|
1862
|
+
(ctx.scope.globalTypedLen ||= new Map()).set(name, len)
|
|
1863
|
+
}
|
|
1864
|
+
ctx.scope.pendingTypedLens = null
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1687
1867
|
// Whole-program constant fold of module-scope aggregate literals — `var x=[1,2,3];
|
|
1688
1868
|
// y=x[0]` → `y=1`, dropping the array (no data segment, no __arr_idx_known) when
|
|
1689
1869
|
// every reference is a static read. The scalar analog of the constInts fold above.
|
|
@@ -1691,6 +1871,13 @@ export default function compile(ast, profiler) {
|
|
|
1691
1871
|
|
|
1692
1872
|
const programFacts = timePhase(profiler, 'plan', () => plan(ast, profiler))
|
|
1693
1873
|
|
|
1874
|
+
// Same-body indirect devirt (dyn-closure-tables.js): which module globals are
|
|
1875
|
+
// structurally safe candidate closure tables (never alias/escape) — the
|
|
1876
|
+
// write-family + call-site facts gathered during emission below only fire
|
|
1877
|
+
// for names in this set. Post-plan so the scan sees the AST shapes that will
|
|
1878
|
+
// actually emit.
|
|
1879
|
+
if (ctx.transform.optimize) ctx.scope.dynFnTableCandidates = scanDynClosureTableCandidates(ast)
|
|
1880
|
+
|
|
1694
1881
|
// Inspect sink: editor hosts opt in via { inspect: true } to read inferred shapes.
|
|
1695
1882
|
// Initialized here (post-plan) so paramReps and schema.list are stable, populated
|
|
1696
1883
|
// per-function below as funcFacts settle. Bytes themselves are unchanged.
|
|
@@ -1806,6 +1993,12 @@ export default function compile(ast, profiler) {
|
|
|
1806
1993
|
|
|
1807
1994
|
timePhase(profiler, 'buildStart', () => buildStartFn(ast, sec, closureFuncs, compilePendingClosures))
|
|
1808
1995
|
|
|
1996
|
+
// dyn-closure-tables.js: every function AND module init has now emitted, so
|
|
1997
|
+
// callSites/paramClosureDefaults/directReturnClosures are complete — resolve
|
|
1998
|
+
// each candidate table's write family and (if monomorphic) hand it to
|
|
1999
|
+
// devirtConstFnArrayCalls via ctx.scope.constFnArrays, same as a const array.
|
|
2000
|
+
if (ctx.transform.optimize) timePhase(profiler, 'resolveDynFnTables', () => resolveDynFnTables(programFacts))
|
|
2001
|
+
|
|
1809
2002
|
// Host globals (globalThis/process/WebAssembly/…) referenced as values are
|
|
1810
2003
|
// recorded in ctx.core.hostGlobals during emit; register them as env imports
|
|
1811
2004
|
// now (assembly owns ctx.module.imports). Drained after buildStartFn so a
|
|
@@ -1854,10 +2047,10 @@ export default function compile(ast, profiler) {
|
|
|
1854
2047
|
[`${ty}.const`, g.init]]
|
|
1855
2048
|
}))
|
|
1856
2049
|
|
|
1857
|
-
// Drop the
|
|
1858
|
-
// run after sec.globals/funcs are final (exact
|
|
1859
|
-
// below serializes ctx.runtime.data.
|
|
1860
|
-
|
|
2050
|
+
// Drop the lazy conversion tables (EL decimal→f64, Ryū float→decimal) whose owner
|
|
2051
|
+
// functions no live code calls — must run after sec.globals/funcs are final (exact
|
|
2052
|
+
// reachability) and before the data segment below serializes ctx.runtime.data.
|
|
2053
|
+
stripDeadLazyTables(sec)
|
|
1861
2054
|
|
|
1862
2055
|
// Data segments (after emit — string literals append to ctx.runtime.data / strPool during emit)
|
|
1863
2056
|
// Active segment at address 0 — skipped for shared memory (would collide across modules)
|
|
@@ -1872,6 +2065,28 @@ export default function compile(ast, profiler) {
|
|
|
1872
2065
|
}
|
|
1873
2066
|
if (ctx.runtime.data && !ctx.memory.shared)
|
|
1874
2067
|
sec.data.push(['data', ['i32.const', 0], '"' + escBytes(ctx.runtime.data) + '"'])
|
|
2068
|
+
// Shared memory: no active segment at 0 (instances would collide) — ship the
|
|
2069
|
+
// static region (static strings + lazy conversion tables) as a PASSIVE segment,
|
|
2070
|
+
// memory.init it into __alloc'd space at start, and rebase its consumers:
|
|
2071
|
+
// $__staticBase for __static_str, plus each surviving table global (their
|
|
2072
|
+
// declared inits hold offsets WITHIN the region — see injectTable/strip).
|
|
2073
|
+
else if (ctx.runtime.data && ctx.memory.shared && ctx.scope.globals.has('__staticBase')) {
|
|
2074
|
+
const len = ctx.runtime.data.length
|
|
2075
|
+
sec.data.push(['data', '$__staticData', '"' + escBytes(ctx.runtime.data) + '"'])
|
|
2076
|
+
const inits = [
|
|
2077
|
+
['global.set', '$__staticBase', ['call', '$__alloc', ['i32.const', len]]],
|
|
2078
|
+
['memory.init', '$__staticData', ['global.get', '$__staticBase'], ['i32.const', 0], ['i32.const', len]],
|
|
2079
|
+
['data.drop', '$__staticData'],
|
|
2080
|
+
...(ctx.runtime.lazySpans || []).map(t => ['global.set', `$${t.global}`,
|
|
2081
|
+
['i32.add', ['global.get', '$__staticBase'], ['i32.const', ctx.scope.globals.get(t.global)?.init || 0]]]),
|
|
2082
|
+
]
|
|
2083
|
+
let startFn = sec.start.find(n => Array.isArray(n) && n[0] === 'func' && n[1] === '$__start')
|
|
2084
|
+
if (!startFn) sec.start.push(startFn = ['func', '$__start'], ['start', '$__start'])
|
|
2085
|
+
// insert after the local decls, before any init code (module init may stringify)
|
|
2086
|
+
let at = 2
|
|
2087
|
+
while (at < startFn.length && Array.isArray(startFn[at]) && startFn[at][0] === 'local') at++
|
|
2088
|
+
startFn.splice(at, 0, ...inits)
|
|
2089
|
+
}
|
|
1875
2090
|
// Passive segment for shared-memory string literals (copied via memory.init at runtime)
|
|
1876
2091
|
if (ctx.runtime.strPool)
|
|
1877
2092
|
sec.data.push(['data', '$__strPool', '"' + escBytes(ctx.runtime.strPool) + '"'])
|
|
@@ -1990,6 +2205,33 @@ export default function compile(ast, profiler) {
|
|
|
1990
2205
|
|
|
1991
2206
|
pruneUnusedThrowRuntime(sec)
|
|
1992
2207
|
|
|
2208
|
+
// WASI reactor convention: the p1 ABI forbids WASI calls inside the wasm start
|
|
2209
|
+
// section — a JS shim literally cannot serve them (fd_write during `new
|
|
2210
|
+
// WebAssembly.Instance` has no memory to read; a top-level console.log/Date.now
|
|
2211
|
+
// crashed with "Cannot read properties of null"). Ship module init as the
|
|
2212
|
+
// standard `_initialize` export instead: hosts (jz/wasi + interop shims, node:wasi
|
|
2213
|
+
// initialize(), wasmtime) call it once memory is wired. Command entries
|
|
2214
|
+
// (`run`/`_start`) self-init through the same once-guard, so a runtime that
|
|
2215
|
+
// invokes a command without calling `_initialize` still runs init exactly once.
|
|
2216
|
+
if (ctx.transform.host === 'wasi') {
|
|
2217
|
+
const sFn = sec.start.find(n => Array.isArray(n) && n[0] === 'func')
|
|
2218
|
+
const sDirIdx = sec.start.findIndex(n => Array.isArray(n) && n[0] === 'start')
|
|
2219
|
+
if (sFn) {
|
|
2220
|
+
sec.globals.push(['global', '$__init_done', ['mut', 'i32'], ['i32.const', 0]])
|
|
2221
|
+
let at = 2
|
|
2222
|
+
while (at < sFn.length && Array.isArray(sFn[at]) && sFn[at][0] === 'local') at++
|
|
2223
|
+
sFn.splice(at, 0,
|
|
2224
|
+
['if', ['global.get', '$__init_done'], ['then', ['return']]],
|
|
2225
|
+
['global.set', '$__init_done', ['i32.const', 1]])
|
|
2226
|
+
sFn.splice(2, 0, ['export', '"_initialize"'])
|
|
2227
|
+
if (sDirIdx !== -1) sec.start.splice(sDirIdx, 1)
|
|
2228
|
+
for (const name of wasiCommandExports) {
|
|
2229
|
+
const w = sec.funcs.find(f => Array.isArray(f) && f[1] === `$${name}$wasi`)
|
|
2230
|
+
if (w) w.splice(w.findIndex(n => Array.isArray(n) && n[0] === 'drop'), 0, ['call', '$__start'])
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
|
|
1993
2235
|
// Reorder non-import funcs by call count: hot callees get low LEB128 indices.
|
|
1994
2236
|
// `call $f` encodes funcidx as ULEB128 (1 B for idx < 128, 2 B for idx < 16384).
|
|
1995
2237
|
// On watr self-host this saves ~6 KB (hot specialized helpers migrate to idx < 128).
|
package/src/compile/infer.js
CHANGED
|
@@ -47,7 +47,8 @@ import { ctx } from '../ctx.js'
|
|
|
47
47
|
import { collectParamNames, ASSIGN_OPS } from '../ast.js'
|
|
48
48
|
import { analyzeValTypes, analyzeIntCertain } from './analyze.js'
|
|
49
49
|
import { staticObjectProps, staticArrayElems } from '../static.js'
|
|
50
|
-
import {
|
|
50
|
+
import { isNullishLit } from '../ir.js'
|
|
51
|
+
import { typedElemCtor, typedStaticLen } from '../type.js'
|
|
51
52
|
import { ctorFromElemAux } from '../../layout.js'
|
|
52
53
|
import { shapeOfObjectLiteralAst, valTypeOf } from '../kind.js'
|
|
53
54
|
import { includeForStringValue } from '../autoload.js'
|
|
@@ -341,13 +342,32 @@ export const inferLocals = (body, candidates) => {
|
|
|
341
342
|
// confirmed prepare's depth-0 catch is a strict superset.
|
|
342
343
|
export function recordGlobalRep(name, expr) {
|
|
343
344
|
if (typeof name !== 'string') return
|
|
345
|
+
// A nullish decl init makes the binding NULLABLE program-wide — any later
|
|
346
|
+
// val-kind claim (typed promote, pointer-ABI assignment) must keep an
|
|
347
|
+
// `x === null` compare honest. This is THE plan-cache memo idiom
|
|
348
|
+
// (`let lastPlan = null` + `if (lastPlan === null) lastPlan = make()`):
|
|
349
|
+
// without the flag, strictSentinel folds the guard to a constant and the
|
|
350
|
+
// memo never fills (silently wrong values, not a trap).
|
|
351
|
+
if (isNullishLit(expr) || expr === 'null' || expr === 'undefined')
|
|
352
|
+
updateGlobalRep(name, { nullable: true })
|
|
344
353
|
const vt = valTypeOf(expr)
|
|
345
354
|
if (vt) {
|
|
346
355
|
;(ctx.scope.globalValTypes ||= new Map()).set(name, vt)
|
|
347
356
|
if (vt === VAL.REGEX && ctx.runtime.regex) ctx.runtime.regex.vars.set(name, expr)
|
|
348
357
|
}
|
|
349
358
|
const ctor = typedElemCtor(expr)
|
|
350
|
-
if (ctor)
|
|
359
|
+
if (ctor) {
|
|
360
|
+
;(ctx.scope.globalTypedElem ||= new Map()).set(name, ctor)
|
|
361
|
+
const len = typedStaticLen(expr)
|
|
362
|
+
if (len != null) (ctx.scope.globalTypedLen ||= new Map()).set(name, len)
|
|
363
|
+
else {
|
|
364
|
+
ctx.scope.globalTypedLen?.delete(name)
|
|
365
|
+
// `new T(CIN*H*W)` — the size is a const expression whose names fold AFTER
|
|
366
|
+
// prepare (the compile-time constInts fixpoint). Park the rhs; the fold's tail
|
|
367
|
+
// re-runs typedStaticLen over these (see "Pre-fold const globals").
|
|
368
|
+
;(ctx.scope.pendingTypedLens ||= new Map()).set(name, expr)
|
|
369
|
+
}
|
|
370
|
+
}
|
|
351
371
|
// Module-level const array literal with a uniform element val-type (e.g. a numeric
|
|
352
372
|
// table `const FREQS = [261.63, …]`): record it so `FREQS[i]` reads in any using
|
|
353
373
|
// function are typed (NUMBER) rather than untyped. Without this an untyped element
|
|
@@ -503,7 +523,7 @@ export function inferArrElemValType(expr, callerArrElemVals, callerArrValParams)
|
|
|
503
523
|
/** Infer typed-array ctor (`new.Float64Array` etc.) of an arg expression at a call site.
|
|
504
524
|
* Sources: caller's body-local typedElems, caller's typed params, literal `new TypedArray(...)`,
|
|
505
525
|
* calls to typed-narrowed user funcs. Returns null when the ctor can't be determined. */
|
|
506
|
-
export function inferTypedCtor(expr, callerTypedElems, callerTypedParams) {
|
|
526
|
+
export function inferTypedCtor(expr, callerTypedElems, callerTypedParams, callerSids) {
|
|
507
527
|
if (typeof expr === 'string') {
|
|
508
528
|
if (callerTypedElems?.has(expr)) return callerTypedElems.get(expr)
|
|
509
529
|
if (callerTypedParams?.has(expr)) return callerTypedParams.get(expr)
|
|
@@ -515,5 +535,16 @@ export function inferTypedCtor(expr, callerTypedElems, callerTypedParams) {
|
|
|
515
535
|
const f = ctx.func.map?.get(expr[1])
|
|
516
536
|
if (f?.sig?.ptrKind === VAL.TYPED && f.sig.ptrAux != null) return ctorFromElemAux(f.sig.ptrAux)
|
|
517
537
|
}
|
|
538
|
+
// Field provenance: `plan.twRe` where the receiver's schema slot holds one
|
|
539
|
+
// typed-array kind program-wide (observeProgramSlots) and the prop is never
|
|
540
|
+
// written — the write gate lives inside slotTypedCtorBySid. The receiver's
|
|
541
|
+
// schema comes from the caller's per-body localSids when provided (narrow's
|
|
542
|
+
// arg lattice — live reps aren't trustworthy there), else the idOf chain.
|
|
543
|
+
if (Array.isArray(expr) && (expr[0] === '.' || expr[0] === '?.') &&
|
|
544
|
+
typeof expr[1] === 'string' && typeof expr[2] === 'string' && ctx.schema?.slotTypedCtorAt) {
|
|
545
|
+
const sid = callerSids?.get(expr[1])
|
|
546
|
+
if (sid != null) return ctx.schema.slotTypedCtorBySid(sid, expr[2])
|
|
547
|
+
return ctx.schema.slotTypedCtorAt(expr[1], expr[2])
|
|
548
|
+
}
|
|
518
549
|
return null
|
|
519
550
|
}
|