jz 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -9
- package/bench/README.md +121 -50
- package/bench/bench.svg +27 -27
- package/cli.js +3 -1
- package/dist/interop.js +1 -1
- package/dist/jz.js +7541 -6178
- package/index.js +165 -74
- package/interop.js +189 -17
- package/jzify/arguments.js +32 -1
- package/jzify/async.js +409 -0
- package/jzify/classes.js +136 -18
- package/jzify/generators.js +639 -0
- package/jzify/hoist-vars.js +73 -1
- package/jzify/index.js +123 -4
- package/jzify/names.js +1 -0
- package/jzify/transform.js +239 -1
- package/layout.js +48 -3
- package/module/array.js +318 -43
- package/module/atomics.js +144 -0
- package/module/collection.js +1429 -155
- package/module/core.js +431 -40
- package/module/date.js +142 -120
- package/module/fs.js +144 -0
- package/module/function.js +6 -3
- package/module/index.js +4 -1
- package/module/json.js +270 -49
- package/module/math.js +1032 -145
- package/module/number.js +532 -163
- package/module/object.js +353 -95
- package/module/regex.js +157 -7
- package/module/schema.js +100 -2
- package/module/string.js +428 -93
- package/module/typedarray.js +264 -72
- package/module/web.js +36 -0
- package/package.json +5 -5
- package/src/abi/string.js +82 -11
- package/src/ast.js +11 -5
- package/src/autoload.js +28 -1
- package/src/bridge.js +7 -2
- package/src/compile/analyze-scans.js +22 -7
- package/src/compile/analyze.js +136 -30
- package/src/compile/dyn-closure-tables.js +277 -0
- package/src/compile/emit-assign.js +281 -15
- package/src/compile/emit.js +1055 -70
- package/src/compile/index.js +277 -29
- package/src/compile/infer.js +42 -4
- package/src/compile/inplace-store.js +329 -0
- package/src/compile/loop-recurrence.js +167 -0
- package/src/compile/narrow.js +555 -18
- package/src/compile/peel-stencil.js +5 -0
- package/src/compile/plan/index.js +45 -3
- package/src/compile/plan/inline.js +8 -1
- package/src/compile/plan/literals.js +39 -0
- package/src/compile/plan/scope.js +430 -23
- package/src/compile/program-facts.js +732 -38
- package/src/ctx.js +64 -9
- package/src/helper-counters.js +7 -1
- package/src/ir.js +113 -5
- package/src/kind-traits.js +66 -3
- package/src/kind.js +84 -12
- package/src/op-policy.js +7 -4
- package/src/optimize/index.js +1179 -704
- package/src/optimize/recurse.js +2 -2
- package/src/optimize/vectorize.js +982 -67
- package/src/prepare/index.js +809 -67
- package/src/prepare/math-kernel.js +331 -0
- package/src/prepare/pre-eval.js +714 -0
- package/src/snapshot.js +194 -0
- package/src/type.js +1170 -56
- package/src/wat/assemble.js +403 -65
- package/src/wat/codegen.js +74 -14
- package/transform.js +113 -4
- package/wasi.js +3 -0
package/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,
|
|
@@ -40,8 +42,12 @@ import { inferLocals } from './infer.js'
|
|
|
40
42
|
import { optimizeFunc, treeshake } from '../optimize/index.js'
|
|
41
43
|
import { strengthReduceLoopDivMod } from './loop-divmod.js'
|
|
42
44
|
import { narrowBoundedSquare } from './loop-square.js'
|
|
45
|
+
import { unrollRecurrence } from './loop-recurrence.js'
|
|
43
46
|
import { peelClampedStencil } from './peel-stencil.js'
|
|
44
47
|
import { cseLoads } from './cse-load.js'
|
|
48
|
+
import {
|
|
49
|
+
scanDynClosureTableCandidates, recordParamClosureDefault, recordDirectReturnClosure, resolveDynFnTables,
|
|
50
|
+
} from './dyn-closure-tables.js'
|
|
45
51
|
|
|
46
52
|
// Monotonic across all functions so a CSE temp never collides (even after later inlining).
|
|
47
53
|
let __cseCtr = 0
|
|
@@ -49,7 +55,7 @@ const freshCseName = () => `${T}cse${__cseCtr++}`
|
|
|
49
55
|
import { emit, emitter, emitVoid, emitBlockBody } from './emit.js'
|
|
50
56
|
import { emitCharDecompPrologue, JSS_IMPORT_SIGS } from '../abi/string.js'
|
|
51
57
|
import {
|
|
52
|
-
typed, asF64, asI32, asPtrOffset, asParamType, toI32, asI64, fromI64,
|
|
58
|
+
typed, asF64, asI32, asPtrOffset, asParamType, toI32, asI64, fromI64, ptrTypeEq,
|
|
53
59
|
NULL_NAN, UNDEF_NAN, NULL_WAT, UNDEF_WAT, NULL_IR, UNDEF_IR, nullExpr, undefExpr,
|
|
54
60
|
MAX_CLOSURE_ARITY,
|
|
55
61
|
mkPtrIR,
|
|
@@ -66,7 +72,7 @@ import plan from './plan/index.js'
|
|
|
66
72
|
import { foldStaticConstAggregates } from './plan/literals.js'
|
|
67
73
|
import {
|
|
68
74
|
buildStartFn, dedupClosureBodies, finalizeClosureTable,
|
|
69
|
-
pullStdlib, syncImports, optimizeModule, stripStaticDataPrefix, hoistConstGlobalInits,
|
|
75
|
+
pullStdlib, syncImports, optimizeModule, stripStaticDataPrefix, hoistConstGlobalInits, stripDeadLazyTables,
|
|
70
76
|
} from '../wat/assemble.js'
|
|
71
77
|
import { instrumentHelperCallsites } from '../helper-counters.js'
|
|
72
78
|
|
|
@@ -392,6 +398,8 @@ function enterFunc(sig, body, { uniq = 0, directClosures = null } = {}) {
|
|
|
392
398
|
ctx.func.directClosures = directClosures
|
|
393
399
|
ctx.func.localProps = null
|
|
394
400
|
ctx.func.charDecomp = null
|
|
401
|
+
ctx.func.charDecompGlobals = false // only emitFunc's named path drains — it re-arms
|
|
402
|
+
ctx.func.probeHoist = null
|
|
395
403
|
if (ctx.transform.optimize) {
|
|
396
404
|
scanAndTagNonEscapingClosures(body)
|
|
397
405
|
}
|
|
@@ -426,6 +434,11 @@ function analyzeFuncForEmit(func, programFacts) {
|
|
|
426
434
|
// so the sieve's product/counter chain carries i32 instead of f64. Before analyze so the
|
|
427
435
|
// Math.imul typed/narrows like any i32. Off at L0 / `loopSquare:false`.
|
|
428
436
|
if (_o && _o.loopSquare !== false && isBlockBody(func.body)) func.body = narrowBoundedSquare(func.body)
|
|
437
|
+
// Array-recurrence unroll: a unit-stride DP/scan that reads arr[j-1] and writes arr[j] carries
|
|
438
|
+
// its value through memory (store→load) and re-pays loop overhead per cell — both of which V8
|
|
439
|
+
// hides but Cranelift/baseline don't. Scalar-replace the recurrence + unroll ×2 (clang's fix).
|
|
440
|
+
// Off at L0 / `unrollRecurrence:false`.
|
|
441
|
+
if (_o && _o.unrollRecurrence !== false && isBlockBody(func.body)) func.body = unrollRecurrence(func.body)
|
|
429
442
|
// Edge-clamp peeling: split a clamped stencil loop into clamp-free interior + edges
|
|
430
443
|
// (the interior then lifts to SIMD). Before analyze so the new loops are analyzed.
|
|
431
444
|
if (_o && _o.clampPeel !== false && isBlockBody(func.body)) func.body = peelClampedStencil(func.body)
|
|
@@ -437,21 +450,68 @@ function analyzeFuncForEmit(func, programFacts) {
|
|
|
437
450
|
ctx.func.boxed = new Map()
|
|
438
451
|
ctx.func.localReps = null
|
|
439
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
|
|
440
456
|
|
|
441
457
|
const _reps = paramReps.get(name)
|
|
442
458
|
if (_reps) {
|
|
443
459
|
for (const [k, r] of _reps) {
|
|
444
460
|
if (k >= sig.params.length) continue
|
|
445
461
|
const pname = sig.params[k].name
|
|
446
|
-
|
|
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) {
|
|
447
484
|
if (!ctx.types.typedElem) ctx.types.typedElem = new Map()
|
|
448
485
|
if (!ctx.types.typedElem.has(pname)) ctx.types.typedElem.set(pname, r.typedCtor)
|
|
449
486
|
updateRep(pname, { val: VAL.TYPED })
|
|
450
487
|
}
|
|
451
|
-
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 })
|
|
452
489
|
if (r.arrayElemSchema != null) updateRep(pname, { arrayElemSchema: r.arrayElemSchema })
|
|
453
490
|
if (r.arrayElemValType != null) updateRep(pname, { arrayElemValType: r.arrayElemValType })
|
|
454
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 })
|
|
455
515
|
}
|
|
456
516
|
}
|
|
457
517
|
// Trust numeric export params. An exported f64 param used only in numeric
|
|
@@ -496,7 +556,32 @@ function analyzeFuncForEmit(func, programFacts) {
|
|
|
496
556
|
const bodyFacts = block ? analyzeBody(body) : null
|
|
497
557
|
ctx.func.locals = bodyFacts ? bodyFacts.locals : new Map()
|
|
498
558
|
if (bodyFacts?.valTypes) {
|
|
499
|
-
|
|
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
|
+
}
|
|
500
585
|
}
|
|
501
586
|
// Never-relocated array bindings — the `[]` reader skips the forwarding follow.
|
|
502
587
|
if (bodyFacts?.neverGrown) for (const name of bodyFacts.neverGrown) updateRep(name, { neverGrown: true })
|
|
@@ -524,6 +609,17 @@ function analyzeFuncForEmit(func, programFacts) {
|
|
|
524
609
|
.filter(p => !ctx.func.localReps?.get(p.name)?.val)
|
|
525
610
|
.map(p => p.name)
|
|
526
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
|
+
}
|
|
527
623
|
if (block) {
|
|
528
624
|
boxedCaptures(body)
|
|
529
625
|
// Lower provably-monomorphic pointer locals to i32 offset storage.
|
|
@@ -567,15 +663,30 @@ function analyzeFuncForEmit(func, programFacts) {
|
|
|
567
663
|
: new Set()
|
|
568
664
|
|
|
569
665
|
// Closure-capture narrowing: a boxed var whose every defining RHS — owner
|
|
570
|
-
// body AND nested arrows
|
|
571
|
-
//
|
|
572
|
-
//
|
|
573
|
-
//
|
|
574
|
-
//
|
|
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.
|
|
575
682
|
const cellTypes = new Set()
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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
|
+
}
|
|
579
690
|
}
|
|
580
691
|
|
|
581
692
|
// Snapshot each param's JS-boundary carrier while reps are live — synthesizeBoundaryWrappers
|
|
@@ -614,6 +725,7 @@ function analyzeFuncForEmit(func, programFacts) {
|
|
|
614
725
|
cseLoadBases,
|
|
615
726
|
distinctParams: func.distinctParams || null,
|
|
616
727
|
typedElem: ctx.types.typedElem ? new Map(ctx.types.typedElem) : null,
|
|
728
|
+
typedLen: ctx.types.typedLen ? new Map(ctx.types.typedLen) : null,
|
|
617
729
|
localReps: cloneRepMap(ctx.func.localReps),
|
|
618
730
|
}
|
|
619
731
|
}
|
|
@@ -996,6 +1108,9 @@ function emitFunc(func, funcFacts, programFacts) {
|
|
|
996
1108
|
const _reps = paramReps.get(name)
|
|
997
1109
|
|
|
998
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
|
|
999
1114
|
const block = funcFacts.block
|
|
1000
1115
|
ctx.func.locals = new Map(funcFacts.locals)
|
|
1001
1116
|
ctx.func.boxed = new Map(funcFacts.boxed)
|
|
@@ -1004,6 +1119,8 @@ function emitFunc(func, funcFacts, programFacts) {
|
|
|
1004
1119
|
ctx.func.sliceViews = new Set(funcFacts.sliceViews)
|
|
1005
1120
|
ctx.func.localReps = cloneRepMap(funcFacts.localReps)
|
|
1006
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
|
|
1007
1124
|
|
|
1008
1125
|
// D: Apply call-site param facts (only if body analysis didn't already set them).
|
|
1009
1126
|
// Schema bindings additionally write into ctx.schema.vars so prop-access dispatch
|
|
@@ -1013,13 +1130,20 @@ function emitFunc(func, funcFacts, programFacts) {
|
|
|
1013
1130
|
for (const [k, r] of _reps) {
|
|
1014
1131
|
if (k >= sig.params.length) continue
|
|
1015
1132
|
const pname = sig.params[k].name
|
|
1016
|
-
|
|
1017
|
-
|
|
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) {
|
|
1018
1142
|
if (!ctx.types.typedElem) ctx.types.typedElem = new Map()
|
|
1019
1143
|
if (!ctx.types.typedElem.has(pname)) ctx.types.typedElem.set(pname, r.typedCtor)
|
|
1020
1144
|
if (!ctx.func.localReps?.get(pname)?.val) updateRep(pname, { val: VAL.TYPED })
|
|
1021
1145
|
}
|
|
1022
|
-
if (r.schemaId != null && !exported && !ctx.schema.vars.has(pname)) {
|
|
1146
|
+
if (r.schemaId != null && !reassigned && !exported && !ctx.schema.vars.has(pname)) {
|
|
1023
1147
|
ctx.schema.vars.set(pname, r.schemaId)
|
|
1024
1148
|
updateRep(pname, { schemaId: r.schemaId })
|
|
1025
1149
|
}
|
|
@@ -1064,9 +1188,16 @@ function emitFunc(func, funcFacts, programFacts) {
|
|
|
1064
1188
|
// externref so no `ref.is_null` branch is needed.
|
|
1065
1189
|
if (p?.jsstring && p.jsstringDefault != null) continue
|
|
1066
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)
|
|
1067
1198
|
defaultInits.set(pname,
|
|
1068
1199
|
['if', isUndef(typed(['local.get', `$${pname}`], 'f64')),
|
|
1069
|
-
['then', ['local.set', `$${pname}`, t === 'f64' ? asF64(
|
|
1200
|
+
['then', ['local.set', `$${pname}`, t === 'f64' ? asF64(emittedDefVal) : asI32(emittedDefVal)]]])
|
|
1070
1201
|
}
|
|
1071
1202
|
|
|
1072
1203
|
// Box params that are mutably captured: allocate cell, copy param value
|
|
@@ -1102,6 +1233,28 @@ function emitFunc(func, funcFacts, programFacts) {
|
|
|
1102
1233
|
// in-bounds char wrongly decodes as the OOB NaN.
|
|
1103
1234
|
const collectParamInits = () => {
|
|
1104
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
|
+
}
|
|
1105
1258
|
for (const p of sig.params) {
|
|
1106
1259
|
const di = defaultInits.get(p.name)
|
|
1107
1260
|
if (di) inits.push(di)
|
|
@@ -1135,6 +1288,10 @@ function emitFunc(func, funcFacts, programFacts) {
|
|
|
1135
1288
|
fn.push(...paramInits, ...boxedParamInits, ...preboxedLocalInits, ...values)
|
|
1136
1289
|
} else {
|
|
1137
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)
|
|
1138
1295
|
const paramInits = collectParamInits()
|
|
1139
1296
|
for (const [l, t] of ctx.func.locals) fn.push(['local', dollar(l), t])
|
|
1140
1297
|
const finalIR = sig.ptrKind != null ? asPtrOffset(ir, sig.ptrKind) : asParamType(ir, sig.results[0])
|
|
@@ -1259,14 +1416,18 @@ function synthesizeBoundaryWrappers() {
|
|
|
1259
1416
|
const ptrType = valKindToPtr(sig.ptrKind)
|
|
1260
1417
|
body = toI64(mkPtrIR(ptrType, sig.ptrAux ?? 0, callIR))
|
|
1261
1418
|
} else if (resultBool) {
|
|
1262
|
-
// The
|
|
1263
|
-
//
|
|
1264
|
-
//
|
|
1265
|
-
//
|
|
1266
|
-
//
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
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
|
+
}
|
|
1270
1431
|
body = toI64(boolBoxIR(carrier))
|
|
1271
1432
|
} else if (resultBigint || resultDynamic) {
|
|
1272
1433
|
// BigInt rides the i64-reinterpret-f64 carrier internally; a dynamic result is already an
|
|
@@ -1322,6 +1483,12 @@ function emitClosureBody(cb) {
|
|
|
1322
1483
|
} else {
|
|
1323
1484
|
ctx.types.typedElem = prevTypedElems
|
|
1324
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)
|
|
1325
1492
|
// In closure bodies, boxed captures use the original name as both var and cell local
|
|
1326
1493
|
ctx.func.boxed = cb.boxed ? new Map([...cb.boxed].map(v => [v, v])) : new Map()
|
|
1327
1494
|
// i32-narrowed cells: the owner decided the cell width (funcFacts.cellTypes);
|
|
@@ -1673,11 +1840,30 @@ export default function compile(ast, profiler) {
|
|
|
1673
1840
|
// Cache integer values for cross-call const-arg propagation: `f(N)` where
|
|
1674
1841
|
// `const N = 8` should observe the param as intConst=8.
|
|
1675
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)
|
|
1676
1850
|
}
|
|
1677
1851
|
}
|
|
1678
1852
|
}
|
|
1679
1853
|
}
|
|
1680
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
|
+
|
|
1681
1867
|
// Whole-program constant fold of module-scope aggregate literals — `var x=[1,2,3];
|
|
1682
1868
|
// y=x[0]` → `y=1`, dropping the array (no data segment, no __arr_idx_known) when
|
|
1683
1869
|
// every reference is a static read. The scalar analog of the constInts fold above.
|
|
@@ -1685,6 +1871,13 @@ export default function compile(ast, profiler) {
|
|
|
1685
1871
|
|
|
1686
1872
|
const programFacts = timePhase(profiler, 'plan', () => plan(ast, profiler))
|
|
1687
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
|
+
|
|
1688
1881
|
// Inspect sink: editor hosts opt in via { inspect: true } to read inferred shapes.
|
|
1689
1882
|
// Initialized here (post-plan) so paramReps and schema.list are stable, populated
|
|
1690
1883
|
// per-function below as funcFacts settle. Bytes themselves are unchanged.
|
|
@@ -1800,6 +1993,12 @@ export default function compile(ast, profiler) {
|
|
|
1800
1993
|
|
|
1801
1994
|
timePhase(profiler, 'buildStart', () => buildStartFn(ast, sec, closureFuncs, compilePendingClosures))
|
|
1802
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
|
+
|
|
1803
2002
|
// Host globals (globalThis/process/WebAssembly/…) referenced as values are
|
|
1804
2003
|
// recorded in ctx.core.hostGlobals during emit; register them as env imports
|
|
1805
2004
|
// now (assembly owns ctx.module.imports). Drained after buildStartFn so a
|
|
@@ -1848,10 +2047,10 @@ export default function compile(ast, profiler) {
|
|
|
1848
2047
|
[`${ty}.const`, g.init]]
|
|
1849
2048
|
}))
|
|
1850
2049
|
|
|
1851
|
-
// Drop the
|
|
1852
|
-
// run after sec.globals/funcs are final (exact
|
|
1853
|
-
// below serializes ctx.runtime.data.
|
|
1854
|
-
|
|
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)
|
|
1855
2054
|
|
|
1856
2055
|
// Data segments (after emit — string literals append to ctx.runtime.data / strPool during emit)
|
|
1857
2056
|
// Active segment at address 0 — skipped for shared memory (would collide across modules)
|
|
@@ -1866,6 +2065,28 @@ export default function compile(ast, profiler) {
|
|
|
1866
2065
|
}
|
|
1867
2066
|
if (ctx.runtime.data && !ctx.memory.shared)
|
|
1868
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
|
+
}
|
|
1869
2090
|
// Passive segment for shared-memory string literals (copied via memory.init at runtime)
|
|
1870
2091
|
if (ctx.runtime.strPool)
|
|
1871
2092
|
sec.data.push(['data', '$__strPool', '"' + escBytes(ctx.runtime.strPool) + '"'])
|
|
@@ -1984,6 +2205,33 @@ export default function compile(ast, profiler) {
|
|
|
1984
2205
|
|
|
1985
2206
|
pruneUnusedThrowRuntime(sec)
|
|
1986
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
|
+
|
|
1987
2235
|
// Reorder non-import funcs by call count: hot callees get low LEB128 indices.
|
|
1988
2236
|
// `call $f` encodes funcidx as ULEB128 (1 B for idx < 128, 2 B for idx < 16384).
|
|
1989
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'
|
|
@@ -130,11 +131,18 @@ export const inferParams = (body, candidates) => {
|
|
|
130
131
|
// regardless of prior evidence — a later method call can't re-induce a shape
|
|
131
132
|
// already contradicted by an earlier scalar assignment.
|
|
132
133
|
|
|
134
|
+
// Methods that exist ONLY on String.prototype — seeing one on a bare binding proves
|
|
135
|
+
// it is a string. `indexOf`/`includes`/`lastIndexOf`/`concat`/`slice`/`at` are NOT here:
|
|
136
|
+
// Array.prototype has them too, so the receiver is genuinely ambiguous (and the argument
|
|
137
|
+
// can't disambiguate — String coerces it, Arrays hold strings). Those keep the runtime
|
|
138
|
+
// __ptr_type fork, which is correct for both; forcing `lastIndexOf` to string here used to
|
|
139
|
+
// miscompile `arr.lastIndexOf(x)` to -1. A string-using param still narrows via any of the
|
|
140
|
+
// real discriminators below (charCodeAt, a string assignment, a string-passing call site).
|
|
133
141
|
const STRING_ONLY_METHODS = new Set([
|
|
134
142
|
'charCodeAt', 'charAt', 'codePointAt', 'startsWith', 'endsWith',
|
|
135
143
|
'toUpperCase', 'toLowerCase', 'toLocaleLowerCase', 'normalize', 'localeCompare',
|
|
136
144
|
'padStart', 'padEnd', 'repeat', 'trimStart', 'trimEnd', 'trim',
|
|
137
|
-
'matchAll', 'match', 'replace', 'replaceAll', 'split',
|
|
145
|
+
'matchAll', 'match', 'replace', 'replaceAll', 'split',
|
|
138
146
|
])
|
|
139
147
|
const ARRAY_ONLY_POISON = new Set([
|
|
140
148
|
'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'fill', 'reverse',
|
|
@@ -334,13 +342,32 @@ export const inferLocals = (body, candidates) => {
|
|
|
334
342
|
// confirmed prepare's depth-0 catch is a strict superset.
|
|
335
343
|
export function recordGlobalRep(name, expr) {
|
|
336
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 })
|
|
337
353
|
const vt = valTypeOf(expr)
|
|
338
354
|
if (vt) {
|
|
339
355
|
;(ctx.scope.globalValTypes ||= new Map()).set(name, vt)
|
|
340
356
|
if (vt === VAL.REGEX && ctx.runtime.regex) ctx.runtime.regex.vars.set(name, expr)
|
|
341
357
|
}
|
|
342
358
|
const ctor = typedElemCtor(expr)
|
|
343
|
-
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
|
+
}
|
|
344
371
|
// Module-level const array literal with a uniform element val-type (e.g. a numeric
|
|
345
372
|
// table `const FREQS = [261.63, …]`): record it so `FREQS[i]` reads in any using
|
|
346
373
|
// function are typed (NUMBER) rather than untyped. Without this an untyped element
|
|
@@ -496,7 +523,7 @@ export function inferArrElemValType(expr, callerArrElemVals, callerArrValParams)
|
|
|
496
523
|
/** Infer typed-array ctor (`new.Float64Array` etc.) of an arg expression at a call site.
|
|
497
524
|
* Sources: caller's body-local typedElems, caller's typed params, literal `new TypedArray(...)`,
|
|
498
525
|
* calls to typed-narrowed user funcs. Returns null when the ctor can't be determined. */
|
|
499
|
-
export function inferTypedCtor(expr, callerTypedElems, callerTypedParams) {
|
|
526
|
+
export function inferTypedCtor(expr, callerTypedElems, callerTypedParams, callerSids) {
|
|
500
527
|
if (typeof expr === 'string') {
|
|
501
528
|
if (callerTypedElems?.has(expr)) return callerTypedElems.get(expr)
|
|
502
529
|
if (callerTypedParams?.has(expr)) return callerTypedParams.get(expr)
|
|
@@ -508,5 +535,16 @@ export function inferTypedCtor(expr, callerTypedElems, callerTypedParams) {
|
|
|
508
535
|
const f = ctx.func.map?.get(expr[1])
|
|
509
536
|
if (f?.sig?.ptrKind === VAL.TYPED && f.sig.ptrAux != null) return ctorFromElemAux(f.sig.ptrAux)
|
|
510
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
|
+
}
|
|
511
549
|
return null
|
|
512
550
|
}
|