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/narrow.js
CHANGED
|
@@ -8,16 +8,17 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { ctx, warn, err } from '../ctx.js'
|
|
11
|
-
import { isBlockBody, alwaysReturns, hasBareReturn, returnExprs } from '../ast.js'
|
|
11
|
+
import { isBlockBody, alwaysReturns, hasBareReturn, returnExprs, callArgs, ASSIGN_OPS, extractParams, classifyParam } from '../ast.js'
|
|
12
12
|
import { isLiteralStr, I32_MIN, I32_MAX } from '../ir.js'
|
|
13
13
|
import {
|
|
14
|
-
analyzeBody, findMutations, invalidateLocalsCache,
|
|
14
|
+
analyzeBody, findMutations, invalidateLocalsCache, mayBeNullish,
|
|
15
15
|
} from './analyze.js'
|
|
16
16
|
import { staticObjectProps } from '../static.js'
|
|
17
17
|
import { scanBoundedLoops, exprType, typedElemCtor } from '../type.js'
|
|
18
18
|
import { typedElemAux, ctorFromElemAux } from '../../layout.js'
|
|
19
19
|
import { observeProgramSlots } from './program-facts.js'
|
|
20
20
|
import { valTypeOf } from '../kind.js'
|
|
21
|
+
import { typedCtorElemValType } from '../kind-traits.js'
|
|
21
22
|
import { VAL, updateRep } from '../reps.js'
|
|
22
23
|
import {
|
|
23
24
|
paramFactsOf, ensureParamRep, mergeParamFact,
|
|
@@ -93,13 +94,23 @@ function applyI32ParamSpecialization(paramReps, valueUsed, { skipTyped = false }
|
|
|
93
94
|
const reps = paramReps.get(func.name)
|
|
94
95
|
if (!reps) continue
|
|
95
96
|
const restIdx = func.rest ? func.sig.params.length - 1 : -1
|
|
97
|
+
// A narrowed param type is a CALLER-side contract; the body keeps it only if it
|
|
98
|
+
// never writes the param (a body write emits through the generic f64 assign path
|
|
99
|
+
// — `a += 1` after an i32 narrow is a validation-failing local.set type clash).
|
|
100
|
+
// Same exclusion validateIntConstParams applies, for the same reason.
|
|
101
|
+
let mutated = null
|
|
96
102
|
for (const [k, r] of reps) {
|
|
97
103
|
if (k === restIdx || k >= func.sig.params.length) continue
|
|
98
104
|
const p = func.sig.params[k]
|
|
99
105
|
if (func.defaults?.[p.name] != null) continue
|
|
106
|
+
if (r.wasm !== 'v128' && (r.wasm !== 'i32' || p.type === 'i32')) continue
|
|
107
|
+
if (mutated === null) {
|
|
108
|
+
mutated = new Set()
|
|
109
|
+
if (func.body) findMutations(func.body, new Set(func.sig.params.map(p => p.name)), mutated)
|
|
110
|
+
}
|
|
111
|
+
if (mutated.has(p.name)) continue
|
|
100
112
|
// SIMD: a param passed a v128 (lane vector) at every call site is a v128 param.
|
|
101
113
|
if (r.wasm === 'v128') { p.type = 'v128'; continue }
|
|
102
|
-
if (r.wasm !== 'i32' || p.type === 'i32') continue
|
|
103
114
|
if (skipTyped && r.val === VAL.TYPED) continue
|
|
104
115
|
p.type = 'i32'
|
|
105
116
|
}
|
|
@@ -134,7 +145,14 @@ function applyPointerParamAbi(paramReps, valueUsed, hardParamVal) {
|
|
|
134
145
|
const reps = paramReps.get(func.name)
|
|
135
146
|
if (!reps) continue
|
|
136
147
|
const restIdx = func.rest ? func.sig.params.length - 1 : -1
|
|
137
|
-
|
|
148
|
+
// A pointer-narrowed param is a CALLER-side contract (unboxed i32 offset,
|
|
149
|
+
// reads rebox via ptrKind/ptrAux). The body keeps it only if it never
|
|
150
|
+
// WRITES the param: a reassignment (`v = v['@@iterator']()`) stores a
|
|
151
|
+
// boxed f64 into the i32 local — mixed views, wasm validation failure
|
|
152
|
+
// (the recorded reassigned-param kind bug). Same rule the wasm-type
|
|
153
|
+
// narrowing applies, for the same reason.
|
|
154
|
+
let mutated = null
|
|
155
|
+
for (const [k, r] of reps) {
|
|
138
156
|
// Re-fold call sites HARD (the shared val lattice is soft, so r.val may be a
|
|
139
157
|
// partial consensus from typed sites alone) — only specialize when every site
|
|
140
158
|
// proves the same pointer kind.
|
|
@@ -145,6 +163,31 @@ function applyPointerParamAbi(paramReps, valueUsed, hardParamVal) {
|
|
|
145
163
|
const p = func.sig.params[k]
|
|
146
164
|
if (p.type === 'i32') continue
|
|
147
165
|
if (func.defaults?.[p.name] != null) continue
|
|
166
|
+
if (mutated === null) {
|
|
167
|
+
mutated = new Set()
|
|
168
|
+
if (func.body) findMutations(func.body, new Set(func.sig.params.map(q => q.name)), mutated)
|
|
169
|
+
}
|
|
170
|
+
if (mutated.has(p.name)) continue
|
|
171
|
+
// OBJECT is the one PTR_ABI_KINDS member whose unboxed i32 offset is
|
|
172
|
+
// ambiguous without a schema id: SET/MAP/BUFFER have a fixed runtime layout
|
|
173
|
+
// (aux always 0, per narrowPointerResults), but an OBJECT's payload slots are
|
|
174
|
+
// laid out per-schema, and the offset alone can't tell a reader which schema
|
|
175
|
+
// to rebox against. r.schemaId is the SAME hard (never-reset) call-site fact
|
|
176
|
+
// narrowPointerResults' return-value arm trusts (param-reps.js: "schemaId ...
|
|
177
|
+
// stay HARD"); demanding it here too, and skipping the narrow when it's absent
|
|
178
|
+
// or conflicting, closes the gap this function used to leave: it set
|
|
179
|
+
// p.ptrKind = VAL.OBJECT with no p.ptrAux, so every later reboxer of this
|
|
180
|
+
// param — asF64's boxPtrIR defaults an omitted aux to 0 — stamped the offset
|
|
181
|
+
// with WHATEVER schema happens to be id 0 program-wide (a live, unrelated
|
|
182
|
+
// object's field layout, not this parameter's own). A fresh instance built
|
|
183
|
+
// from that mistagged parameter (watr's own `normalize(opts)`, opts narrowed
|
|
184
|
+
// this way with no callers.length>1 disagreement to save it) then reads as
|
|
185
|
+
// if it belonged to schema 0 — the wild "phantom keys" class of miscompile.
|
|
186
|
+
if (hv === VAL.OBJECT) {
|
|
187
|
+
const aux = r.schemaId
|
|
188
|
+
if (aux == null) continue
|
|
189
|
+
p.ptrAux = aux
|
|
190
|
+
}
|
|
148
191
|
p.type = 'i32'
|
|
149
192
|
p.ptrKind = hv
|
|
150
193
|
}
|
|
@@ -202,6 +245,7 @@ function applyTypedPointerParamAbi(paramReps, valueUsed) {
|
|
|
202
245
|
const reps = paramReps.get(func.name)
|
|
203
246
|
if (!reps) continue
|
|
204
247
|
const restIdx = func.rest ? func.sig.params.length - 1 : -1
|
|
248
|
+
let mutated = null // body-write guard — same contract as applyPointerParamAbi
|
|
205
249
|
for (const [k, r] of reps) {
|
|
206
250
|
const ctor = r.typedCtor
|
|
207
251
|
if (ctor == null) continue
|
|
@@ -210,6 +254,11 @@ function applyTypedPointerParamAbi(paramReps, valueUsed) {
|
|
|
210
254
|
const p = func.sig.params[k]
|
|
211
255
|
if (p.type === 'i32') continue
|
|
212
256
|
if (func.defaults?.[p.name] != null) continue
|
|
257
|
+
if (mutated === null) {
|
|
258
|
+
mutated = new Set()
|
|
259
|
+
if (func.body) findMutations(func.body, new Set(func.sig.params.map(q => q.name)), mutated)
|
|
260
|
+
}
|
|
261
|
+
if (mutated.has(p.name)) continue
|
|
213
262
|
const aux = typedElemAux(ctor)
|
|
214
263
|
if (aux == null) continue
|
|
215
264
|
p.type = 'i32'
|
|
@@ -502,14 +551,51 @@ function typedAuxOfReturn(expr, localElemMap) {
|
|
|
502
551
|
* Fixpoint: a chain `outer → inner → {a,b}` needs inner to narrow first so
|
|
503
552
|
* outer's call to inner contributes a known schema-id.
|
|
504
553
|
*/
|
|
554
|
+
/** True iff return-expr `e` is provably just `paramName` unchanged: either the bare
|
|
555
|
+
* name itself, or a recursive call to `func.name` that forwards `paramName` at its
|
|
556
|
+
* OWN parameter index (`return f(x, out, y)` inside `f`) — by induction on recursion
|
|
557
|
+
* depth, that call's result is whatever `f` would return given that same value, which
|
|
558
|
+
* bottoms out at the direct-return arms below. Strict AST-identity match only (no
|
|
559
|
+
* attempt to prove two *different* expressions are equal at runtime). */
|
|
560
|
+
function passesParamThrough(e, paramName, paramIdx, funcName) {
|
|
561
|
+
if (e === paramName) return true
|
|
562
|
+
if (!Array.isArray(e) || e[0] !== '()' || e[1] !== funcName) return false
|
|
563
|
+
return callArgs(e)[paramIdx] === paramName
|
|
564
|
+
}
|
|
565
|
+
|
|
505
566
|
/** A function whose every return is the same parameter that was pointer-ABI
|
|
506
|
-
* narrowed to an unboxed i32 (p.ptrKind set)
|
|
567
|
+
* narrowed to an unboxed i32 (p.ptrKind set) — directly, or via a same-function
|
|
568
|
+
* recursive call that forwards it unchanged (see passesParamThrough). Without the
|
|
569
|
+
* recursive case, a function like flow-types.js's extractRefinements — whose `!`
|
|
570
|
+
* branch delegates via `return extractRefinements(cond[1], out, !sense)` instead of
|
|
571
|
+
* a bare `return out` — fails the naive all-return-exprs-are-the-bare-name check on
|
|
572
|
+
* that ONE path, so the whole function loses ptrKind tracking (see
|
|
573
|
+
* narrowPointerResults below): the caller then numeric-converts the returned offset
|
|
574
|
+
* bits into a bogus float instead of reboxing them into a NaN-boxed pointer — a
|
|
575
|
+
* silent value corruption, not a compile error, that only a receiver expecting the
|
|
576
|
+
* real pointer (e.g. Map.prototype.size's raw __len dispatch) turns into a wild
|
|
577
|
+
* offset read.
|
|
578
|
+
*
|
|
579
|
+
* Every path must also yield a real value, not fall through / bare-`return` into
|
|
580
|
+
* `undefined` (an f64 atom) — a match here forces this function's signature to
|
|
581
|
+
* unconditional i32, so a value-less path would be a genuine wasm type error
|
|
582
|
+
* (validated on encode, not just a mistracked type). Same guarantee
|
|
583
|
+
* narrowPointerResults' func.valResult-driven arm takes via alwaysReturns, and
|
|
584
|
+
* narrowValResults skips via hasBareReturn, for the identical reason: a recursive
|
|
585
|
+
* walker whose tail happens to read `return helper(...)` (a value only its OWN
|
|
586
|
+
* recursive call consumes) commonly has OTHER arms that are bare `return;`
|
|
587
|
+
* early-exits — real shape, not hypothetical (plan/literals.js's
|
|
588
|
+
* _disqualifyPromotion: single value-bearing return via self-recursion, but
|
|
589
|
+
* multiple bare `return`s alongside it that returnExprs below never sees).
|
|
590
|
+
*
|
|
591
|
+
* Returns that param, else null. */
|
|
507
592
|
function passthroughPtrParam(func) {
|
|
508
|
-
const
|
|
593
|
+
const body = func.body
|
|
594
|
+
if (isBlockBody(body) && (!alwaysReturns(body) || hasBareReturn(body))) return null
|
|
595
|
+
const exprs = returnExprs(body)
|
|
509
596
|
if (!exprs.length) return null
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
return func.sig.params.find(p => p.name === name && p.ptrKind) || null
|
|
597
|
+
return func.sig.params.find((p, idx) =>
|
|
598
|
+
p.ptrKind && exprs.every(e => passesParamThrough(e, p.name, idx, func.name))) || null
|
|
513
599
|
}
|
|
514
600
|
|
|
515
601
|
function narrowPointerResults(funcs, paramReps) {
|
|
@@ -528,9 +614,12 @@ function narrowPointerResults(funcs, paramReps) {
|
|
|
528
614
|
if (func.sig.ptrKind == null) {
|
|
529
615
|
const pp = passthroughPtrParam(func)
|
|
530
616
|
if (pp) {
|
|
617
|
+
// OBJECT reboxes by schema-id; TYPED by the param's ELEM-TYPE bits
|
|
618
|
+
// (pp.ptrAux) — without them the caller reboxes with aux 0 (Int8
|
|
619
|
+
// dispatch) and every read of the returned array mis-strides to 0.
|
|
531
620
|
const aux = pp.ptrKind === VAL.OBJECT
|
|
532
621
|
? paramFactsOf(paramReps, func, 'schemaId')?.get(pp.name) ?? null
|
|
533
|
-
: null
|
|
622
|
+
: pp.ptrAux ?? null
|
|
534
623
|
// OBJECT needs a known schema-id to rebox; a polymorphic pass-through
|
|
535
624
|
// (conflicting schemas → null) keeps its current handling.
|
|
536
625
|
if (pp.ptrKind !== VAL.OBJECT || aux != null) {
|
|
@@ -769,7 +858,17 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
769
858
|
const inferValAtSite = (arg, state) => {
|
|
770
859
|
const v = inferValType(arg, state.callerValTypes)
|
|
771
860
|
if (v != null) return v
|
|
772
|
-
if (typeof arg !== 'string')
|
|
861
|
+
if (typeof arg !== 'string') {
|
|
862
|
+
// Typed-array element read `recv[i]` where the receiver is a TYPED param/local of the CALLER:
|
|
863
|
+
// valTypeOf can't see this (it queries ctx.func, not the caller), so `f(src[i])` with a
|
|
864
|
+
// Float64Array PARAM `src` never propagated Number to f's param. Mirror VT['[]'] exactly,
|
|
865
|
+
// but resolve the receiver through the caller's own context — sound: only fires when the
|
|
866
|
+
// receiver is provably VAL.TYPED, and the ctor decides Number vs BigInt (BigInt64Array).
|
|
867
|
+
if (Array.isArray(arg) && arg[0] === '[]' && arg.length === 3 && typeof arg[1] === 'string' &&
|
|
868
|
+
(state.callerValTypes?.get(arg[1]) || ctx.scope.globalValTypes?.get(arg[1])) === VAL.TYPED)
|
|
869
|
+
return typedCtorElemValType(state.callerTypedElems?.get(arg[1])) || VAL.NUMBER
|
|
870
|
+
return null
|
|
871
|
+
}
|
|
773
872
|
const fromParam = state.callerParamFacts('val')?.get(arg)
|
|
774
873
|
if (fromParam != null) return fromParam
|
|
775
874
|
const def = state.callerFunc?.defaults?.[arg]
|
|
@@ -863,8 +962,22 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
863
962
|
}
|
|
864
963
|
const prev = ctx.func.localTypedElemsOverlay
|
|
865
964
|
ctx.func.localTypedElemsOverlay = state._teOverlay
|
|
866
|
-
|
|
965
|
+
let wt
|
|
966
|
+
try { wt = exprType(arg, state.callerLocals) }
|
|
867
967
|
finally { ctx.func.localTypedElemsOverlay = prev }
|
|
968
|
+
// An i32-typed BARE NAME that carries a POINTER kind in the caller (a local
|
|
969
|
+
// or param already narrowed to an unboxed i32 offset) is NOT integer
|
|
970
|
+
// evidence: narrowing the callee's param to plain i32 on it makes every
|
|
971
|
+
// callee read widen the raw offset NUMERICALLY (f64.convert_i32_s) — the
|
|
972
|
+
// pointer arrives as a small number and every prop probe silently misses
|
|
973
|
+
// (this ate `Promise.any(obj)`'s GetIterator through __p_any → __p_list).
|
|
974
|
+
// Report the boxed f64 lane instead; only applyPointerParamAbi — which
|
|
975
|
+
// stamps ptrKind/ptrAux so reads REBOX — may unbox pointer params.
|
|
976
|
+
if (wt === 'i32' && typeof arg === 'string') {
|
|
977
|
+
const vk = state.callerValTypes?.get?.(arg)
|
|
978
|
+
if (vk != null && vk !== VAL.NUMBER && vk !== VAL.BOOL) return 'f64'
|
|
979
|
+
}
|
|
980
|
+
return wt
|
|
868
981
|
}
|
|
869
982
|
const runFixpoint = () => runCallsiteLattice([
|
|
870
983
|
// val runs SOFT (monotone): a TYPED param's val only becomes inferable after the
|
|
@@ -899,8 +1012,8 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
899
1012
|
// Fix: iterate a *soft* merge — propagate known ctors, treat "can't tell yet"
|
|
900
1013
|
// as skip (no poison) — to a fixpoint, then one *hard* validating sweep that
|
|
901
1014
|
// poisons params whose call sites still can't be proven (genuinely-untyped args).
|
|
902
|
-
const runArrElemFixpoint = (field, inferFn, elemsCtxMap) => {
|
|
903
|
-
const infer = (arg, _k, state) => inferFn(arg, elemsCtxMap.get(state.callerFunc), state.callerParamFacts(field))
|
|
1015
|
+
const runArrElemFixpoint = (field, inferFn, elemsCtxMap, sidsCtxMap) => {
|
|
1016
|
+
const infer = (arg, _k, state) => inferFn(arg, elemsCtxMap.get(state.callerFunc), state.callerParamFacts(field), sidsCtxMap?.get(state.callerFunc))
|
|
904
1017
|
let changed
|
|
905
1018
|
const bump = (r, v) => { if (v == null || r[field] === null) return; const b = r[field]; mergeParamFact(r, field, v); if (r[field] !== b) changed = true }
|
|
906
1019
|
const soft = {
|
|
@@ -922,6 +1035,23 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
922
1035
|
// narrowing stays below — it benefits from i32 params being narrowed first.
|
|
923
1036
|
const funcsWithNarrowableResult = narrowableFuncs(valueUsed)
|
|
924
1037
|
narrowValResults(funcsWithNarrowableResult)
|
|
1038
|
+
// Own-default typed annotation: `(arr = new Int32Array(0)) => …` self-declares
|
|
1039
|
+
// the param's element ctor — the ONLY evidence a host-called export can carry
|
|
1040
|
+
// (no call sites to lattice over; Workers v1 SPMD kernels are exactly this
|
|
1041
|
+
// shape). A WEAK seed: set only at BOTTOM, so call-site facts merge/poison
|
|
1042
|
+
// over it as usual. Runtime safety for Atomics receivers is the tag+elem
|
|
1043
|
+
// guard in __atomics_addr (module/atomics.js) — a wrong host arg throws.
|
|
1044
|
+
for (const func of ctx.func.list) {
|
|
1045
|
+
if (!func.sig?.params || !func.defaults) continue
|
|
1046
|
+
for (let k = 0; k < func.sig.params.length; k++) {
|
|
1047
|
+
const d = func.defaults[func.sig.params[k].name]
|
|
1048
|
+
if (Array.isArray(d) && d[0] === '()' && typeof d[1] === 'string' &&
|
|
1049
|
+
d[1].startsWith('new.') && d[1].endsWith('Array')) {
|
|
1050
|
+
const r = ensureParamRep(paramReps, func.name, k)
|
|
1051
|
+
if (r.typedCtor === undefined) r.typedCtor = d[1]
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
925
1055
|
runFixpoint()
|
|
926
1056
|
runFixpoint()
|
|
927
1057
|
|
|
@@ -1005,11 +1135,65 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
1005
1135
|
// (before E3 ran) are stale (mkInput's ptrKind was unset then).
|
|
1006
1136
|
phase.invalidateBodyFacts()
|
|
1007
1137
|
const callerTypedCtx = phase.callerTyped()
|
|
1138
|
+
// Per-caller receiver schemas for field-provenance args (`transform(plan.tw…)`):
|
|
1139
|
+
// a small decl scan per body — collision-free and live-rep-independent (the
|
|
1140
|
+
// rep/schema.vars chains aren't trustworthy mid-lattice). inferSchemaId covers
|
|
1141
|
+
// literals and calls (valResult/ptrAux — E-complete by this phase); chained
|
|
1142
|
+
// decls resolve through the accumulating map. Module-const sids (`const P =
|
|
1143
|
+
// mk(n)` at top level) seed every caller's map; a local decl SHADOWS the seed
|
|
1144
|
+
// (unresolvable → masks it), and any reassignment drops the name (the second
|
|
1145
|
+
// write could carry a different schema).
|
|
1146
|
+
const moduleSids = new Map()
|
|
1147
|
+
{
|
|
1148
|
+
const visitDecl = (node) => {
|
|
1149
|
+
if (!Array.isArray(node)) return
|
|
1150
|
+
if (node[0] === 'let' || node[0] === 'const') {
|
|
1151
|
+
for (const d of node.slice(1)) {
|
|
1152
|
+
if (!Array.isArray(d) || d[0] !== '=' || typeof d[1] !== 'string') continue
|
|
1153
|
+
if (!ctx.scope.consts?.has(d[1])) continue
|
|
1154
|
+
const sid = inferSchemaId(d[2], moduleSids)
|
|
1155
|
+
if (sid != null && !moduleSids.has(d[1])) moduleSids.set(d[1], sid)
|
|
1156
|
+
}
|
|
1157
|
+
return
|
|
1158
|
+
}
|
|
1159
|
+
if (node[0] === ';' || node[0] === 'export') for (let i = 1; i < node.length; i++) visitDecl(node[i])
|
|
1160
|
+
}
|
|
1161
|
+
if (Array.isArray(ast)) {
|
|
1162
|
+
if (ast[0] === ';') for (let i = 1; i < ast.length; i++) visitDecl(ast[i])
|
|
1163
|
+
else visitDecl(ast)
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
const callerSidsCtx = new Map()
|
|
1167
|
+
for (const func of ctx.func.list) {
|
|
1168
|
+
if (!func.body || func.raw) continue
|
|
1169
|
+
const sids = new Map(moduleSids), poisoned = new Set()
|
|
1170
|
+
const scan = (n) => {
|
|
1171
|
+
if (!Array.isArray(n)) return
|
|
1172
|
+
if (n[0] === '=>') return
|
|
1173
|
+
if (n[0] === 'let' || n[0] === 'const') {
|
|
1174
|
+
for (const d of n.slice(1)) {
|
|
1175
|
+
if (Array.isArray(d) && d[0] === '=' && typeof d[1] === 'string') {
|
|
1176
|
+
const sid = inferSchemaId(d[2], sids)
|
|
1177
|
+
sids.delete(d[1]) // a local decl shadows any module seed
|
|
1178
|
+
if (sid != null && !poisoned.has(d[1])) sids.set(d[1], sid)
|
|
1179
|
+
scan(d[2])
|
|
1180
|
+
} else scan(d)
|
|
1181
|
+
}
|
|
1182
|
+
return
|
|
1183
|
+
}
|
|
1184
|
+
if ((ASSIGN_OPS.has(n[0]) || n[0] === '++' || n[0] === '--') && typeof n[1] === 'string') { poisoned.add(n[1]); sids.delete(n[1]) }
|
|
1185
|
+
for (let i = 1; i < n.length; i++) scan(n[i])
|
|
1186
|
+
}
|
|
1187
|
+
// params shadow module seeds too — an arg-bound `P` is not the module const
|
|
1188
|
+
for (const p of func.sig?.params || []) sids.delete(p.name)
|
|
1189
|
+
scan(func.body)
|
|
1190
|
+
callerSidsCtx.set(func, sids)
|
|
1191
|
+
}
|
|
1008
1192
|
// Two-pass fixpoint: lets a caller's params, once typed, propagate further to
|
|
1009
1193
|
// its own callees (e.g. if `outer(buf)` calls `inner(buf)` and we learn `buf`
|
|
1010
1194
|
// for outer, the second pass picks it up for inner). Reuses runArrElemFixpoint
|
|
1011
1195
|
// (same shape — field/inferFn/elemsCtxMap parameterization).
|
|
1012
|
-
const runTypedFixpoint = () => runArrElemFixpoint('typedCtor', inferTypedCtor, callerTypedCtx)
|
|
1196
|
+
const runTypedFixpoint = () => runArrElemFixpoint('typedCtor', inferTypedCtor, callerTypedCtx, callerSidsCtx)
|
|
1013
1197
|
runTypedFixpoint()
|
|
1014
1198
|
runTypedFixpoint()
|
|
1015
1199
|
|
|
@@ -1042,6 +1226,15 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
1042
1226
|
// (callback bench: mix is FNV — params and result all i32-shaped, but inferred
|
|
1043
1227
|
// only after E phase narrowed mix's result).
|
|
1044
1228
|
phase.refreshLocals()
|
|
1229
|
+
// I1: Re-run POINTER results now that Phase G has tagged typed params — a
|
|
1230
|
+
// pass-through like `norm = (w) => { …w[i]…; return w }` gains w.ptrKind only
|
|
1231
|
+
// in G, AFTER the first narrowPointerResults sweep, so its sig.ptrKind stayed
|
|
1232
|
+
// null and the I2 numeric-results pass below then STOLE the return as plain
|
|
1233
|
+
// i32 — the caller f64.convert_i32_s'd the returned offset into a bogus float
|
|
1234
|
+
// (the cross-module memo miscompile: `g._w = norm(w)` stored a number, the
|
|
1235
|
+
// slot read dispatched on it as a pointer → undefined). The rerun stamps
|
|
1236
|
+
// sig.ptrKind first; I2's f64-results guard then skips these functions.
|
|
1237
|
+
narrowPointerResults(funcsWithNarrowableResult, paramReps)
|
|
1045
1238
|
// I2: Re-narrow i32 RESULTS now that Phase G (applyTypedPointerParamAbi) has tagged
|
|
1046
1239
|
// typed-array params ptrKind=TYPED. Phase E ran before G, so a function returning a
|
|
1047
1240
|
// typed-array element — dict's `lookup = (keys, vals, k) => { … return vals[h] }` with
|
|
@@ -1064,6 +1257,63 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
1064
1257
|
// r.val is sound for emit + the late/post-return consumers (applyI32ParamSpecial-
|
|
1065
1258
|
// ization's skipTyped guard, specializeBimorphicTyped) — which read it directly.
|
|
1066
1259
|
runCallsiteLattice([mergeRule('val', (arg, _k, state) => inferValAtSite(arg, state))])
|
|
1260
|
+
// BIGINT params: re-derive nullability the val claim just erased. VT['?:']
|
|
1261
|
+
// carries BIGINT through a nullish-literal arm (the kind is untaggable at
|
|
1262
|
+
// runtime — kind.js), so a site arg like `c ? BigInt(x) : null` PROVES
|
|
1263
|
+
// BIGINT while still passing null on the other path; the settle above
|
|
1264
|
+
// stamps bare val=BIGINT and strictSentinel would then FOLD the callee's
|
|
1265
|
+
// `x == null` guard (watr's `_i64Arith(r)`: fold-miss null crosses for
|
|
1266
|
+
// real — the guard must return null, not format atom bits as hex). Any
|
|
1267
|
+
// site arg not structurally non-nullish marks the param nullable, which
|
|
1268
|
+
// only suppresses that fold (emitFunc's caller-side nullability block).
|
|
1269
|
+
// Scoped to BIGINT vals: tagged kinds keep their runtime forks and their
|
|
1270
|
+
// folds. Name args resolve through the CALLER body's own writes — at plan
|
|
1271
|
+
// time no caller ctx.func is installed, so rep lookups would misread;
|
|
1272
|
+
// an unwritten name (param/global/closure) fails closed.
|
|
1273
|
+
const bodyNameNullable = (callerFunc) => {
|
|
1274
|
+
const seen = new Set()
|
|
1275
|
+
const nameNullable = (name) => {
|
|
1276
|
+
if (seen.has(name)) return false // cyclic alias: no new evidence
|
|
1277
|
+
seen.add(name)
|
|
1278
|
+
let found = false, nullish = false
|
|
1279
|
+
const walk = (node) => {
|
|
1280
|
+
if (nullish || !Array.isArray(node)) return
|
|
1281
|
+
const op = node[0]
|
|
1282
|
+
if ((op === 'let' || op === 'const') && node.length >= 2) {
|
|
1283
|
+
for (let i = 1; i < node.length; i++) {
|
|
1284
|
+
const d = node[i]
|
|
1285
|
+
if (Array.isArray(d) && d[0] === '=' && d[1] === name) {
|
|
1286
|
+
found = true
|
|
1287
|
+
if (mayBeNullish(d[2], nameNullable)) nullish = true
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
} else if (op === '=' && node[1] === name) {
|
|
1291
|
+
found = true
|
|
1292
|
+
if (mayBeNullish(node[2], nameNullable)) nullish = true
|
|
1293
|
+
} else if (typeof op === 'string' && op.length > 1 && op.endsWith('=') &&
|
|
1294
|
+
ASSIGN_OPS.has(op) && node[1] === name) {
|
|
1295
|
+
found = true
|
|
1296
|
+
nullish = true // ??=/||= etc. — fail closed
|
|
1297
|
+
}
|
|
1298
|
+
for (let i = 1; i < node.length; i++) walk(node[i])
|
|
1299
|
+
}
|
|
1300
|
+
walk(callerFunc?.body)
|
|
1301
|
+
return found ? nullish : true // unwritten name — fail closed
|
|
1302
|
+
}
|
|
1303
|
+
return nameNullable
|
|
1304
|
+
}
|
|
1305
|
+
for (const [fname, reps] of paramReps) {
|
|
1306
|
+
for (const [k, r] of reps) {
|
|
1307
|
+
if (r.val !== VAL.BIGINT || r.nullable) continue
|
|
1308
|
+
for (const cs of callSites) {
|
|
1309
|
+
if (cs.callee !== fname || k >= cs.argList.length) continue
|
|
1310
|
+
if (mayBeNullish(cs.argList[k], bodyNameNullable(cs.callerFunc))) {
|
|
1311
|
+
r.nullable = true
|
|
1312
|
+
break
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1067
1317
|
// Don't steal typed-array params from specializeBimorphicTyped: F phase parks
|
|
1068
1318
|
// bimorphic typed params at type='f64' with sticky-null typedCtor (two distinct
|
|
1069
1319
|
// ctors at call sites). Their callers post-F pass them as i32 (pointer ABI),
|
|
@@ -1076,6 +1326,22 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
1076
1326
|
// slot from f64 (nanbox SSO carrier) to externref so the JS host passes the
|
|
1077
1327
|
// native string directly. Zero copy, zero transcoding. See applyJsstringBoundaryCarrier.
|
|
1078
1328
|
if (jsstringEnabled()) applyJsstringBoundaryCarrier(paramReps, valueUsed)
|
|
1329
|
+
|
|
1330
|
+
// Stamp the settled per-param val kind onto sig.params (mirror of emitFunc's
|
|
1331
|
+
// updateRep(pname, { val: r.val }) merge — same source, same condition). The
|
|
1332
|
+
// call-site emitter needs it to pick the arg carrier: a BOOL arg into an
|
|
1333
|
+
// UNTYPED f64 param boxes to its TRUE/FALSE atom (boolean identity crosses
|
|
1334
|
+
// the boundary), while a val-known param keeps the raw 0/1 ABI its body
|
|
1335
|
+
// assumes. Read by coerceArg (emit.js).
|
|
1336
|
+
for (const func of ctx.func.list) {
|
|
1337
|
+
if (!func.sig || func.raw) continue
|
|
1338
|
+
const reps = paramReps.get(func.name)
|
|
1339
|
+
if (!reps) continue
|
|
1340
|
+
for (const [k, r] of reps) {
|
|
1341
|
+
const p = func.sig.params[k]
|
|
1342
|
+
if (p && r.val != null && p.val == null) p.val = r.val
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1079
1345
|
}
|
|
1080
1346
|
|
|
1081
1347
|
/** Gate the jsstring carrier on the host. ON by default for the JS host: a
|
|
@@ -1529,6 +1795,268 @@ export function specializeBimorphicTyped(programFacts) {
|
|
|
1529
1795
|
}
|
|
1530
1796
|
}
|
|
1531
1797
|
|
|
1798
|
+
/**
|
|
1799
|
+
* Speculative typed-param specialization — the GUARDED sibling of
|
|
1800
|
+
* specializeBimorphicTyped, for params whose args can never be statically
|
|
1801
|
+
* PROVEN typed: plan objects through Map caches, nullable module-let memos,
|
|
1802
|
+
* returned-object fields — the fftplan/provenance shape-class, where the
|
|
1803
|
+
* receiver's schema is unknowable but every value that ever reaches the call
|
|
1804
|
+
* is, in practice, the same typed array.
|
|
1805
|
+
*
|
|
1806
|
+
* When every static call site's arg at a position carries the SAME ctor as
|
|
1807
|
+
* evidence — a proven inferTypedCtor, or the program-wide write-gated slot
|
|
1808
|
+
* census for a bare field read (ctx.schema.slotTypedCtorByProp, the
|
|
1809
|
+
* guardedSlotOf contract) — clone the callee with those params typed
|
|
1810
|
+
* (identical machinery to the bimorphic clones) and record it in
|
|
1811
|
+
* ctx.types.specFns. Emit rewrites every static direct call into
|
|
1812
|
+
*
|
|
1813
|
+
* tags-all-match? call $f$spec(raw offsets…) : call $f(boxes…)
|
|
1814
|
+
*
|
|
1815
|
+
* (emitSpeculativeCall) — one masked NaN-box compare per speculated arg per
|
|
1816
|
+
* CALL, amortized over the callee's loops. A nullish / other-kind / view
|
|
1817
|
+
* value simply falls to the original call unchanged, so speculation can only
|
|
1818
|
+
* ever be as fast, never wrong; the original function stays for indirect and
|
|
1819
|
+
* boundary callers. Gate on a loop in the body: the win is per-ELEMENT
|
|
1820
|
+
* access, so guarding loop-free leaf calls only spends the compare.
|
|
1821
|
+
*
|
|
1822
|
+
* Evidence is a recursive WEAK lattice (soundness never depends on it — the
|
|
1823
|
+
* runtime guard does; evidence only decides where speculating is worth it):
|
|
1824
|
+
* - proven inferTypedCtor at the site (strong)
|
|
1825
|
+
* - `x.prop` → program-wide write-gated slot census (slotTypedCtorByProp)
|
|
1826
|
+
* - a name → its single `=` binding's init, chased recursively
|
|
1827
|
+
* - a name that is an ENCLOSING ARROW's param → meet over the arrow's own
|
|
1828
|
+
* call sites at that position (the `edge(wre, wim)` harness shape: the
|
|
1829
|
+
* kernel call sits inside a local arrow whose args carry the evidence)
|
|
1830
|
+
* - `f(...)` → f's return census: every return a `new K`, a local bound to
|
|
1831
|
+
* one, a censused field read, or another censused call; nullish returns
|
|
1832
|
+
* are SKIPPED (they fail the guard at runtime, by design — this is what
|
|
1833
|
+
* lets Map-cache/memo getters like getPlan(n) census through)
|
|
1834
|
+
*/
|
|
1835
|
+
export function speculateTypedParams(programFacts, ast) {
|
|
1836
|
+
const { callSites, paramReps } = programFacts
|
|
1837
|
+
if (!ctx.schema.slotTypedCtorByProp) return
|
|
1838
|
+
|
|
1839
|
+
const sitesByCallee = new Map()
|
|
1840
|
+
for (const cs of callSites) {
|
|
1841
|
+
const list = sitesByCallee.get(cs.callee)
|
|
1842
|
+
if (list) list.push(cs); else sitesByCallee.set(cs.callee, [cs])
|
|
1843
|
+
}
|
|
1844
|
+
const callerTypedCtx = buildCallerTypedCtx()
|
|
1845
|
+
const callerTypedParamsCtx = new Map()
|
|
1846
|
+
for (const func of ctx.func.list) {
|
|
1847
|
+
const m = paramFactsOf(paramReps, func, 'typedCtor') || null
|
|
1848
|
+
let acc = m
|
|
1849
|
+
if (func.sig?.params) for (const p of func.sig.params) {
|
|
1850
|
+
if (p.ptrKind === VAL.TYPED && p.ptrAux != null) {
|
|
1851
|
+
acc ||= new Map()
|
|
1852
|
+
if (!acc.has(p.name)) acc.set(p.name, ctorFromElemAux(p.ptrAux))
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
if (acc) callerTypedParamsCtx.set(func, acc)
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
const hasLoop = (n) => Array.isArray(n)
|
|
1859
|
+
&& (n[0] === 'for' || n[0] === 'while' || n[0] === 'do' || n.some((c, i) => i > 0 && hasLoop(c)))
|
|
1860
|
+
|
|
1861
|
+
// ---- weak evidence engine (see doc above) ----
|
|
1862
|
+
const DBG2 = typeof process !== 'undefined' && !!process.env?.JZ_DBG_SPEC
|
|
1863
|
+
const isNullish = (r) => r == null || r === 'null' || r === 'undefined'
|
|
1864
|
+
|| (Array.isArray(r) && (r[0] === 'null' || r[0] === 'undefined'))
|
|
1865
|
+
const retMemo = new Map()
|
|
1866
|
+
const MAX_DEPTH = 6
|
|
1867
|
+
const bodyOf = (callerFunc) => callerFunc ? callerFunc.body : ast
|
|
1868
|
+
|
|
1869
|
+
// Return census of a named function: the single ctor every non-nullish
|
|
1870
|
+
// return resolves to, or null.
|
|
1871
|
+
function retCensus(fname, depth) {
|
|
1872
|
+
if (retMemo.has(fname)) return retMemo.get(fname)
|
|
1873
|
+
retMemo.set(fname, null) // cycle guard
|
|
1874
|
+
const func = ctx.func.map.get(fname)
|
|
1875
|
+
if (!func?.body || func.raw || depth > MAX_DEPTH) return null
|
|
1876
|
+
const te = analyzeBody(func.body).typedElems
|
|
1877
|
+
let ctor = null
|
|
1878
|
+
for (const r of returnExprs(func.body)) {
|
|
1879
|
+
if (isNullish(r)) continue
|
|
1880
|
+
const c = typedElemCtor(r)
|
|
1881
|
+
|| (typeof r === 'string' ? te?.get(r) : null)
|
|
1882
|
+
|| (Array.isArray(r) && r[0] === '.' && typeof r[2] === 'string' ? ctx.schema.slotTypedCtorByProp(r[2]) : null)
|
|
1883
|
+
|| (Array.isArray(r) && r[0] === '()' && typeof r[1] === 'string' ? retCensus(r[1], depth + 1) : null)
|
|
1884
|
+
if (!c || (ctor && c !== ctor)) return null
|
|
1885
|
+
ctor = c
|
|
1886
|
+
}
|
|
1887
|
+
retMemo.set(fname, ctor)
|
|
1888
|
+
return ctor
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
// Chain of arrow nodes enclosing `target` inside `root` (outermost first).
|
|
1892
|
+
function arrowPathTo(root, target) {
|
|
1893
|
+
const path = []
|
|
1894
|
+
const walk = (n) => {
|
|
1895
|
+
if (!Array.isArray(n)) return false
|
|
1896
|
+
if (n === target) return true
|
|
1897
|
+
const isArrow = n[0] === '=>'
|
|
1898
|
+
if (isArrow) path.push(n)
|
|
1899
|
+
for (let i = 1; i < n.length; i++) if (walk(n[i])) return true
|
|
1900
|
+
if (isArrow) path.pop()
|
|
1901
|
+
return false
|
|
1902
|
+
}
|
|
1903
|
+
return walk(root) ? path : null
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
function evidenceOfArg(arg, callerFunc, siteNode, depth, seen) {
|
|
1907
|
+
const r = evidenceOfArgInner(arg, callerFunc, siteNode, depth, seen)
|
|
1908
|
+
if (DBG2) console.error('[evid=]', JSON.stringify(arg)?.slice(0, 50), '→', r)
|
|
1909
|
+
return r
|
|
1910
|
+
}
|
|
1911
|
+
function evidenceOfArgInner(arg, callerFunc, siteNode, depth, seen) {
|
|
1912
|
+
if (arg == null || depth > MAX_DEPTH) return null
|
|
1913
|
+
const proven = inferTypedCtor(arg, callerTypedCtx.get(callerFunc), callerTypedParamsCtx.get(callerFunc))
|
|
1914
|
+
if (proven) return proven
|
|
1915
|
+
if (Array.isArray(arg)) {
|
|
1916
|
+
if (arg[0] === '.' && typeof arg[2] === 'string') return ctx.schema.slotTypedCtorByProp(arg[2])
|
|
1917
|
+
if (arg[0] === '()' && typeof arg[1] === 'string' && ctx.func.map.has(arg[1])) return retCensus(arg[1], depth)
|
|
1918
|
+
return typedElemCtor(arg)
|
|
1919
|
+
}
|
|
1920
|
+
if (typeof arg !== 'string') return null
|
|
1921
|
+
// Cycle guard is PATH-local: the key backtracks on exit (a name legitimately
|
|
1922
|
+
// recurs across sibling meet branches — e.g. duplicated call sites after
|
|
1923
|
+
// lambda inlining — and must resolve fresh in each).
|
|
1924
|
+
const key = (callerFunc?.name || '') + '|' + arg
|
|
1925
|
+
if (seen.has(key)) return null
|
|
1926
|
+
seen.add(key)
|
|
1927
|
+
try {
|
|
1928
|
+
return resolveName()
|
|
1929
|
+
} finally { seen.delete(key) }
|
|
1930
|
+
|
|
1931
|
+
function resolveName() {
|
|
1932
|
+
const body = bodyOf(callerFunc)
|
|
1933
|
+
if (!body) return null
|
|
1934
|
+
// Enclosing-arrow param? Meet the arrow's own call sites at that position.
|
|
1935
|
+
const arrows = siteNode ? arrowPathTo(body, siteNode) : null
|
|
1936
|
+
if (arrows) for (let a = arrows.length - 1; a >= 0; a--) {
|
|
1937
|
+
const names = extractParams(arrows[a][1]).map(p => classifyParam(p)).filter(c => c.kind === 'plain').map(c => c.name)
|
|
1938
|
+
const j = names.indexOf(arg)
|
|
1939
|
+
if (j < 0) continue
|
|
1940
|
+
// The binding name of this arrow (`const edge = (…) => …`), then its calls.
|
|
1941
|
+
let bindName = null
|
|
1942
|
+
const findBind = (n) => {
|
|
1943
|
+
if (!Array.isArray(n)) return
|
|
1944
|
+
if (n[0] === '=' && typeof n[1] === 'string' && n[2] === arrows[a]) bindName = n[1]
|
|
1945
|
+
for (let i = 1; i < n.length && !bindName; i++) findBind(n[i])
|
|
1946
|
+
}
|
|
1947
|
+
findBind(body)
|
|
1948
|
+
if (!bindName) return null
|
|
1949
|
+
const calls = []
|
|
1950
|
+
const findCalls = (n) => {
|
|
1951
|
+
if (!Array.isArray(n)) return
|
|
1952
|
+
if (n[0] === '()' && n[1] === bindName) calls.push(n)
|
|
1953
|
+
for (let i = 1; i < n.length; i++) findCalls(n[i])
|
|
1954
|
+
}
|
|
1955
|
+
findCalls(body)
|
|
1956
|
+
if (!calls.length) return null
|
|
1957
|
+
let ctor = null
|
|
1958
|
+
for (const cn of calls) {
|
|
1959
|
+
const list = cn[2] == null ? [] : (Array.isArray(cn[2]) && cn[2][0] === ',') ? cn[2].slice(1) : [cn[2]]
|
|
1960
|
+
const c = evidenceOfArg(list[j], callerFunc, cn, depth + 1, seen)
|
|
1961
|
+
if (!c || (ctor && c !== ctor)) return null
|
|
1962
|
+
ctor = c
|
|
1963
|
+
}
|
|
1964
|
+
return ctor
|
|
1965
|
+
}
|
|
1966
|
+
// Single-`=` local binding: chase its init. More than one write → too
|
|
1967
|
+
// murky to be worth the guard.
|
|
1968
|
+
let decl = null, writes = 0
|
|
1969
|
+
const findDecl = (n) => {
|
|
1970
|
+
if (!Array.isArray(n)) return
|
|
1971
|
+
if (typeof n[1] === 'string' && n[1] === arg) {
|
|
1972
|
+
if (n[0] === '=') { decl = n; writes++ }
|
|
1973
|
+
else if (ASSIGN_OPS.has(n[0]) || n[0] === '++' || n[0] === '--') writes++
|
|
1974
|
+
}
|
|
1975
|
+
for (let i = 1; i < n.length; i++) findDecl(n[i])
|
|
1976
|
+
}
|
|
1977
|
+
findDecl(body)
|
|
1978
|
+
if (!decl || writes !== 1) return null
|
|
1979
|
+
return evidenceOfArg(decl[2], callerFunc, decl, depth + 1, seen)
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
const DBG = typeof process !== 'undefined' && !!process.env?.JZ_DBG_SPEC
|
|
1984
|
+
const originals = ctx.func.list.slice()
|
|
1985
|
+
for (const func of originals) {
|
|
1986
|
+
if (func.raw || func.rest || !func.body) continue
|
|
1987
|
+
if (func.sig?.results?.length !== 1) continue
|
|
1988
|
+
const sites = sitesByCallee.get(func.name)
|
|
1989
|
+
if (!sites?.length) { if (DBG) console.error('[spec]', func.name, 'no sites'); continue }
|
|
1990
|
+
if (!hasLoop(func.body)) continue
|
|
1991
|
+
|
|
1992
|
+
// Candidate positions: still a dyn f64 box after F-phase/bimorphic (no
|
|
1993
|
+
// pointer ABI, not numeric-narrowed), no default (undefined must reach it).
|
|
1994
|
+
const reps = paramReps.get(func.name)
|
|
1995
|
+
const candidates = []
|
|
1996
|
+
for (let k = 0; k < func.sig.params.length; k++) {
|
|
1997
|
+
const p = func.sig.params[k]
|
|
1998
|
+
if (p.type !== 'f64' || p.ptrKind != null) continue
|
|
1999
|
+
if (func.defaults?.[p.name] != null) continue
|
|
2000
|
+
const r = reps?.get(k)
|
|
2001
|
+
if (r && (r.val != null && r.val !== VAL.TYPED)) continue
|
|
2002
|
+
candidates.push(k)
|
|
2003
|
+
}
|
|
2004
|
+
if (!candidates.length) continue
|
|
2005
|
+
|
|
2006
|
+
// Evidence meet across sites per position. A site WITHOUT evidence is
|
|
2007
|
+
// NEUTRAL — its calls just miss the guard at runtime and take the
|
|
2008
|
+
// original (a plain-array or debug caller must not veto the hot sites'
|
|
2009
|
+
// win). Only CONFLICTING evidence (two different ctors) kills the
|
|
2010
|
+
// position — a guard can only test one kind.
|
|
2011
|
+
const specs = []
|
|
2012
|
+
for (const k of candidates) {
|
|
2013
|
+
let ctor = null, dead = false
|
|
2014
|
+
for (const site of sites) {
|
|
2015
|
+
const arg = site.argList[k]
|
|
2016
|
+
if (arg == null) continue
|
|
2017
|
+
const proven = inferTypedCtor(arg, callerTypedCtx.get(site.callerFunc), callerTypedParamsCtx.get(site.callerFunc))
|
|
2018
|
+
const c = proven ?? evidenceOfArg(arg, site.callerFunc, site.node, 0, new Set())
|
|
2019
|
+
if (DBG) console.error('[spec]', func.name, 'k=' + k, JSON.stringify(arg)?.slice(0, 60), 'proven=' + proven, 'c=' + c)
|
|
2020
|
+
if (c == null) continue
|
|
2021
|
+
if (ctor && c !== ctor) { dead = true; break }
|
|
2022
|
+
ctor = c
|
|
2023
|
+
}
|
|
2024
|
+
if (dead || !ctor) continue
|
|
2025
|
+
const aux = typedElemAux(ctor)
|
|
2026
|
+
if (aux == null) continue
|
|
2027
|
+
specs.push({ k, ctor, aux })
|
|
2028
|
+
}
|
|
2029
|
+
if (!specs.length) continue
|
|
2030
|
+
|
|
2031
|
+
let cloneName = `${func.name}$spec`
|
|
2032
|
+
while (ctx.func.names.has(cloneName)) cloneName += '$'
|
|
2033
|
+
const specAt = new Map(specs.map(s => [s.k, s]))
|
|
2034
|
+
const cloneSig = {
|
|
2035
|
+
params: func.sig.params.map((p, idx) => {
|
|
2036
|
+
const s = specAt.get(idx)
|
|
2037
|
+
return s ? { name: p.name, type: 'i32', ptrKind: VAL.TYPED, ptrAux: s.aux } : { ...p }
|
|
2038
|
+
}),
|
|
2039
|
+
results: [...func.sig.results],
|
|
2040
|
+
}
|
|
2041
|
+
const clone = { ...func, name: cloneName, sig: cloneSig, exported: false }
|
|
2042
|
+
ctx.func.list.push(clone)
|
|
2043
|
+
ctx.func.map.set(cloneName, clone)
|
|
2044
|
+
ctx.func.names.add(cloneName)
|
|
2045
|
+
|
|
2046
|
+
const cloneReps = new Map()
|
|
2047
|
+
if (reps) for (const [k, r] of reps) cloneReps.set(k, { ...r })
|
|
2048
|
+
for (const s of specs) {
|
|
2049
|
+
const r = cloneReps.get(s.k) || {}
|
|
2050
|
+
r.typedCtor = s.ctor
|
|
2051
|
+
r.val = VAL.TYPED
|
|
2052
|
+
cloneReps.set(s.k, r)
|
|
2053
|
+
}
|
|
2054
|
+
paramReps.set(cloneName, cloneReps)
|
|
2055
|
+
|
|
2056
|
+
;(ctx.types.specFns ||= new Map()).set(func.name, { clone: cloneName, guards: specs.map(s => ({ k: s.k, aux: s.aux })) })
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
|
|
1532
2060
|
/**
|
|
1533
2061
|
* Phase: refine ctx.types.anyDynKey using post-narrowSignatures type info.
|
|
1534
2062
|
*/
|