jz 0.5.0 → 0.6.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 +288 -314
- package/bench/README.md +319 -0
- package/bench/bench.svg +112 -0
- package/cli.js +32 -24
- package/index.js +177 -55
- package/interop.js +88 -159
- package/jz.svg +5 -0
- package/jzify/arguments.js +97 -0
- package/jzify/bundler.js +382 -0
- package/jzify/classes.js +328 -0
- package/jzify/hoist-vars.js +177 -0
- package/jzify/index.js +51 -0
- package/jzify/names.js +37 -0
- package/jzify/switch.js +106 -0
- package/jzify/transform.js +349 -0
- package/layout.js +179 -0
- package/module/array.js +322 -153
- package/module/collection.js +603 -145
- package/module/console.js +55 -43
- package/module/core.js +281 -142
- package/module/date.js +15 -3
- package/module/function.js +73 -6
- package/module/index.js +2 -1
- package/module/json.js +226 -61
- package/module/math.js +461 -185
- package/module/number.js +306 -60
- package/module/object.js +448 -184
- package/module/regex.js +255 -25
- package/module/schema.js +24 -6
- package/module/simd.js +85 -0
- package/module/string.js +591 -220
- package/module/symbol.js +1 -1
- package/module/timer.js +9 -14
- package/module/typedarray.js +45 -48
- package/package.json +41 -12
- package/src/abi/index.js +39 -23
- package/src/abi/string.js +38 -41
- package/src/ast.js +460 -0
- package/src/autoload.js +26 -24
- package/src/bridge.js +111 -0
- package/src/compile/analyze-scans.js +661 -0
- package/src/compile/analyze.js +1565 -0
- package/src/compile/emit-assign.js +408 -0
- package/src/compile/emit.js +3201 -0
- package/src/compile/flow-types.js +103 -0
- package/src/{compile.js → compile/index.js} +497 -125
- package/src/{infer.js → compile/infer.js} +27 -98
- package/src/{narrow.js → compile/narrow.js} +302 -96
- package/src/compile/plan/advise.js +316 -0
- package/src/compile/plan/common.js +150 -0
- package/src/compile/plan/index.js +118 -0
- package/src/compile/plan/inline.js +679 -0
- package/src/compile/plan/literals.js +984 -0
- package/src/compile/plan/loops.js +472 -0
- package/src/compile/plan/scope.js +573 -0
- package/src/compile/program-facts.js +404 -0
- package/src/ctx.js +176 -58
- package/src/ir.js +540 -171
- package/src/kind-traits.js +105 -0
- package/src/kind.js +462 -0
- package/src/op-policy.js +57 -0
- package/src/{optimize.js → optimize/index.js} +1106 -446
- package/src/optimize/vectorize.js +1874 -0
- package/src/param-reps.js +65 -0
- package/src/parse.js +44 -0
- package/src/{prepare.js → prepare/index.js} +600 -205
- package/src/reps.js +115 -0
- package/src/resolve.js +12 -3
- package/src/static.js +199 -0
- package/src/type.js +647 -0
- package/src/{assemble.js → wat/assemble.js} +86 -48
- package/src/wat/optimize.js +3760 -0
- package/transform.js +21 -0
- package/wasi.js +47 -5
- package/src/analyze.js +0 -3818
- package/src/emit.js +0 -2997
- package/src/jzify.js +0 -1553
- package/src/plan.js +0 -2132
- package/src/vectorize.js +0 -1088
- /package/src/{codegen.js → wat/codegen.js} +0 -0
|
@@ -7,26 +7,29 @@
|
|
|
7
7
|
* function `sig` records change.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { ctx } from '
|
|
11
|
-
import {
|
|
10
|
+
import { ctx, warn } from '../ctx.js'
|
|
11
|
+
import { isBlockBody, alwaysReturns, hasBareReturn, returnExprs } from '../ast.js'
|
|
12
|
+
import { isLiteralStr, I32_MIN, I32_MAX } from '../ir.js'
|
|
12
13
|
import {
|
|
13
|
-
|
|
14
|
-
analyzeBody,
|
|
15
|
-
paramFactsOf,
|
|
16
|
-
exprType, findMutations, hasBareReturn,
|
|
17
|
-
invalidateLocalsCache, invalidateValTypesCache, isBlockBody, alwaysReturns,
|
|
18
|
-
narrowReturnArrayElems, observeProgramSlots, returnExprs, staticObjectProps,
|
|
19
|
-
scanBoundedLoops,
|
|
20
|
-
typedElemAux, typedElemCtor, ctorFromElemAux, valTypeOf,
|
|
14
|
+
analyzeBody, findMutations, invalidateLocalsCache,
|
|
21
15
|
} from './analyze.js'
|
|
16
|
+
import { staticObjectProps } from '../static.js'
|
|
17
|
+
import { scanBoundedLoops, exprType, typedElemCtor } from '../type.js'
|
|
18
|
+
import { typedElemAux, ctorFromElemAux } from '../../layout.js'
|
|
19
|
+
import { observeProgramSlots } from './program-facts.js'
|
|
20
|
+
import { valTypeOf } from '../kind.js'
|
|
21
|
+
import { VAL, updateRep } from '../reps.js'
|
|
22
|
+
import {
|
|
23
|
+
paramFactsOf, ensureParamRep, mergeParamFact,
|
|
24
|
+
} from '../param-reps.js'
|
|
22
25
|
import {
|
|
23
|
-
clearStickyNull, ensureParamRep, mergeParamFact,
|
|
24
26
|
inferArrElemSchema, inferArrElemValType,
|
|
25
27
|
inferSchemaId, inferValType, inferTypedCtor,
|
|
26
28
|
} from './infer.js'
|
|
27
29
|
|
|
28
30
|
const PTR_ABI_KINDS = new Set([VAL.OBJECT, VAL.SET, VAL.MAP, VAL.BUFFER])
|
|
29
31
|
|
|
32
|
+
|
|
30
33
|
function filterLiveCallSites(callSites, valueUsed) {
|
|
31
34
|
if (!callSites.length) return
|
|
32
35
|
|
|
@@ -82,11 +85,12 @@ function applyI32ParamSpecialization(paramReps, valueUsed, { skipTyped = false }
|
|
|
82
85
|
if (!reps) continue
|
|
83
86
|
const restIdx = func.rest ? func.sig.params.length - 1 : -1
|
|
84
87
|
for (const [k, r] of reps) {
|
|
85
|
-
if (
|
|
86
|
-
if (k >= func.sig.params.length) continue
|
|
88
|
+
if (k === restIdx || k >= func.sig.params.length) continue
|
|
87
89
|
const p = func.sig.params[k]
|
|
88
|
-
if (p.type === 'i32') continue
|
|
89
90
|
if (func.defaults?.[p.name] != null) continue
|
|
91
|
+
// SIMD: a param passed a v128 (lane vector) at every call site is a v128 param.
|
|
92
|
+
if (r.wasm === 'v128') { p.type = 'v128'; continue }
|
|
93
|
+
if (r.wasm !== 'i32' || p.type === 'i32') continue
|
|
90
94
|
if (skipTyped && r.val === VAL.TYPED) continue
|
|
91
95
|
p.type = 'i32'
|
|
92
96
|
}
|
|
@@ -115,21 +119,25 @@ function validateIntConstParams(paramReps, valueUsed) {
|
|
|
115
119
|
}
|
|
116
120
|
}
|
|
117
121
|
|
|
118
|
-
function applyPointerParamAbi(paramReps, valueUsed) {
|
|
122
|
+
function applyPointerParamAbi(paramReps, valueUsed, hardParamVal) {
|
|
119
123
|
for (const func of ctx.func.list) {
|
|
120
124
|
if (func.exported || func.raw || valueUsed.has(func.name)) continue
|
|
121
125
|
const reps = paramReps.get(func.name)
|
|
122
126
|
if (!reps) continue
|
|
123
127
|
const restIdx = func.rest ? func.sig.params.length - 1 : -1
|
|
124
|
-
for (const [k
|
|
125
|
-
|
|
128
|
+
for (const [k] of reps) {
|
|
129
|
+
// Re-fold call sites HARD (the shared val lattice is soft, so r.val may be a
|
|
130
|
+
// partial consensus from typed sites alone) — only specialize when every site
|
|
131
|
+
// proves the same pointer kind.
|
|
132
|
+
const hv = hardParamVal(func.name, k)
|
|
133
|
+
if (!PTR_ABI_KINDS.has(hv)) continue
|
|
126
134
|
if (k === restIdx) continue
|
|
127
135
|
if (k >= func.sig.params.length) continue
|
|
128
136
|
const p = func.sig.params[k]
|
|
129
137
|
if (p.type === 'i32') continue
|
|
130
138
|
if (func.defaults?.[p.name] != null) continue
|
|
131
139
|
p.type = 'i32'
|
|
132
|
-
p.ptrKind =
|
|
140
|
+
p.ptrKind = hv
|
|
133
141
|
}
|
|
134
142
|
}
|
|
135
143
|
}
|
|
@@ -256,7 +264,7 @@ function narrowI32Results(funcs) {
|
|
|
256
264
|
while (changed) {
|
|
257
265
|
changed = false
|
|
258
266
|
for (const func of funcs) {
|
|
259
|
-
if (func.sig.results[0] === 'i32') continue
|
|
267
|
+
if (func.sig.results[0] === 'i32' || func.sig.results[0] === 'v128') continue
|
|
260
268
|
const body = func.body
|
|
261
269
|
if (isBlockBody(body) && hasBareReturn(body)) continue
|
|
262
270
|
const exprs = returnExprs(body)
|
|
@@ -265,12 +273,16 @@ function narrowI32Results(funcs) {
|
|
|
265
273
|
ctx.func.current = func.sig
|
|
266
274
|
const locals = isBlockBody(body) ? analyzeBody(body).locals : new Map()
|
|
267
275
|
for (const p of func.sig.params) if (!locals.has(p.name)) locals.set(p.name, p.type)
|
|
268
|
-
const
|
|
276
|
+
const allV128 = exprs.every(e => exprType(e, locals) === 'v128')
|
|
277
|
+
const allI32 = !allV128 && exprs.every(e => exprType(e, locals) === 'i32')
|
|
269
278
|
const anyUnsigned = exprs.some(isUnsignedTail)
|
|
270
279
|
const allUnsigned = exprs.every(isUnsignedTail)
|
|
271
280
|
ctx.func.current = savedCurrent
|
|
272
|
-
//
|
|
273
|
-
if (
|
|
281
|
+
// SIMD: every tail returns a lane vector → v128 result.
|
|
282
|
+
if (allV128) {
|
|
283
|
+
func.sig.results = ['v128']
|
|
284
|
+
changed = true
|
|
285
|
+
} else if (allI32 && (!anyUnsigned || allUnsigned)) { // sign-consistent i32 tails
|
|
274
286
|
func.sig.results = ['i32']
|
|
275
287
|
if (allUnsigned) func.sig.unsignedResult = true
|
|
276
288
|
changed = true
|
|
@@ -401,11 +413,47 @@ function typedAuxOfReturn(expr, localElemMap) {
|
|
|
401
413
|
* Fixpoint: a chain `outer → inner → {a,b}` needs inner to narrow first so
|
|
402
414
|
* outer's call to inner contributes a known schema-id.
|
|
403
415
|
*/
|
|
416
|
+
/** A function whose every return is the same parameter that was pointer-ABI
|
|
417
|
+
* narrowed to an unboxed i32 (p.ptrKind set). Returns that param, else null. */
|
|
418
|
+
function passthroughPtrParam(func) {
|
|
419
|
+
const exprs = returnExprs(func.body)
|
|
420
|
+
if (!exprs.length) return null
|
|
421
|
+
const name = exprs[0]
|
|
422
|
+
if (typeof name !== 'string' || !exprs.every(e => e === name)) return null
|
|
423
|
+
return func.sig.params.find(p => p.name === name && p.ptrKind) || null
|
|
424
|
+
}
|
|
425
|
+
|
|
404
426
|
function narrowPointerResults(funcs, paramReps) {
|
|
405
427
|
let changed = true
|
|
406
428
|
while (changed) {
|
|
407
429
|
changed = false
|
|
408
430
|
for (const func of funcs) {
|
|
431
|
+
// Pointer pass-through: every return is the same parameter that
|
|
432
|
+
// applyPointerParamAbi narrowed to an unboxed i32 pointer. The result IS that
|
|
433
|
+
// pointer, so its sig must carry the param's ptrKind (+ schemaId for OBJECT).
|
|
434
|
+
// Without this the result is a bare i32 the caller numeric-converts
|
|
435
|
+
// (`f64.convert_i32_s`) instead of reboxing — dropping the schema-id so a
|
|
436
|
+
// later `.prop` read mis-resolves to `undefined`. narrowValResults can't see
|
|
437
|
+
// this (it reads body-locals, not param facts) and narrowI32Results steals it
|
|
438
|
+
// as a numeric i32, so resolve it here from the settled param lattice.
|
|
439
|
+
if (func.sig.ptrKind == null) {
|
|
440
|
+
const pp = passthroughPtrParam(func)
|
|
441
|
+
if (pp) {
|
|
442
|
+
const aux = pp.ptrKind === VAL.OBJECT
|
|
443
|
+
? paramFactsOf(paramReps, func, 'schemaId')?.get(pp.name) ?? null
|
|
444
|
+
: null
|
|
445
|
+
// OBJECT needs a known schema-id to rebox; a polymorphic pass-through
|
|
446
|
+
// (conflicting schemas → null) keeps its current handling.
|
|
447
|
+
if (pp.ptrKind !== VAL.OBJECT || aux != null) {
|
|
448
|
+
func.sig.results = ['i32']
|
|
449
|
+
func.sig.ptrKind = pp.ptrKind
|
|
450
|
+
func.valResult = pp.ptrKind
|
|
451
|
+
if (aux != null) func.sig.ptrAux = aux
|
|
452
|
+
changed = true
|
|
453
|
+
continue
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
409
457
|
if (!func.valResult) continue
|
|
410
458
|
if (func.sig.results[0] !== 'f64') continue
|
|
411
459
|
const isBlock = isBlockBody(func.body)
|
|
@@ -467,7 +515,7 @@ function createPhaseState() {
|
|
|
467
515
|
|
|
468
516
|
invalidateBodyFacts() {
|
|
469
517
|
for (const func of ctx.func.list) {
|
|
470
|
-
if (func.body && !func.raw)
|
|
518
|
+
if (func.body && !func.raw) invalidateLocalsCache(func.body)
|
|
471
519
|
}
|
|
472
520
|
clearDerived()
|
|
473
521
|
},
|
|
@@ -484,6 +532,67 @@ function createPhaseState() {
|
|
|
484
532
|
}
|
|
485
533
|
}
|
|
486
534
|
|
|
535
|
+
const _FIELD_TO_SLICE = {
|
|
536
|
+
arrayElemSchema: 'arrElemSchemas',
|
|
537
|
+
arrayElemValType: 'arrElemValTypes',
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/** Propagate Array<T> element facts from return paths into caller paramReps (phase G). */
|
|
541
|
+
function narrowReturnArrayElems(field, paramReps, valueUsed) {
|
|
542
|
+
const sliceKey = _FIELD_TO_SLICE[field]
|
|
543
|
+
const targets = ctx.func.list.filter(f =>
|
|
544
|
+
!f.raw && !f.exported && !valueUsed.has(f.name) &&
|
|
545
|
+
f.valResult === VAL.ARRAY && f[field] == null
|
|
546
|
+
)
|
|
547
|
+
let changed = true
|
|
548
|
+
while (changed) {
|
|
549
|
+
changed = false
|
|
550
|
+
for (const f of targets) invalidateLocalsCache(f.body)
|
|
551
|
+
for (const func of targets) {
|
|
552
|
+
if (func[field] != null) continue
|
|
553
|
+
const isBlock = isBlockBody(func.body)
|
|
554
|
+
if (isBlock && !alwaysReturns(func.body)) continue
|
|
555
|
+
const exprs = returnExprs(func.body)
|
|
556
|
+
if (!exprs.length) continue
|
|
557
|
+
const savedLocals = ctx.func.locals
|
|
558
|
+
const facts = analyzeBody(func.body)
|
|
559
|
+
ctx.func.locals = new Map(facts.locals)
|
|
560
|
+
for (const p of func.sig.params) if (!ctx.func.locals.has(p.name)) ctx.func.locals.set(p.name, p.type)
|
|
561
|
+
const localElems = facts[sliceKey]
|
|
562
|
+
ctx.func.locals = savedLocals
|
|
563
|
+
const paramElemMap = paramFactsOf(paramReps, func, field) || new Map()
|
|
564
|
+
const resolveExpr = (expr) => {
|
|
565
|
+
if (typeof expr === 'string') {
|
|
566
|
+
if (localElems.has(expr)) {
|
|
567
|
+
const v = localElems.get(expr)
|
|
568
|
+
if (v != null) return v
|
|
569
|
+
}
|
|
570
|
+
if (paramElemMap.has(expr)) return paramElemMap.get(expr)
|
|
571
|
+
return null
|
|
572
|
+
}
|
|
573
|
+
if (Array.isArray(expr) && expr[0] === '()' && typeof expr[1] === 'string') {
|
|
574
|
+
const f = ctx.func.map?.get(expr[1])
|
|
575
|
+
if (f?.[field] != null) return f[field]
|
|
576
|
+
}
|
|
577
|
+
if (Array.isArray(expr) && expr[0] === '?:') {
|
|
578
|
+
const a = resolveExpr(expr[2]), b = resolveExpr(expr[3])
|
|
579
|
+
return a != null && a === b ? a : null
|
|
580
|
+
}
|
|
581
|
+
if (Array.isArray(expr) && (expr[0] === '&&' || expr[0] === '||')) {
|
|
582
|
+
const a = resolveExpr(expr[1]), b = resolveExpr(expr[2])
|
|
583
|
+
return a != null && a === b ? a : null
|
|
584
|
+
}
|
|
585
|
+
return null
|
|
586
|
+
}
|
|
587
|
+
const v0 = resolveExpr(exprs[0])
|
|
588
|
+
if (v0 == null) continue
|
|
589
|
+
if (!exprs.every(e => resolveExpr(e) === v0)) continue
|
|
590
|
+
func[field] = v0
|
|
591
|
+
changed = true
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
487
596
|
export default function narrowSignatures(programFacts, ast) {
|
|
488
597
|
const { callSites, valueUsed, paramReps, hasSchemaLiterals } = programFacts
|
|
489
598
|
|
|
@@ -518,29 +627,35 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
518
627
|
return (raw != null && Number.isInteger(raw) && raw >= I32_MIN && raw <= I32_MAX) ? raw : null
|
|
519
628
|
}
|
|
520
629
|
|
|
630
|
+
// Per-call-site inference context for a narrowable callee. null for call sites
|
|
631
|
+
// whose callee is exported / value-used / unknown, or whose caller has no ctx.
|
|
632
|
+
const siteState = (cs) => {
|
|
633
|
+
const { callee, argList, callerFunc } = cs
|
|
634
|
+
const func = ctx.func.map.get(callee)
|
|
635
|
+
if (!func || func.exported || valueUsed.has(callee)) return null
|
|
636
|
+
const ctxEntry = callerCtx.get(callerFunc)
|
|
637
|
+
if (!ctxEntry) return null
|
|
638
|
+
const restIdx = func.rest ? func.sig.params.length - 1 : -1
|
|
639
|
+
const paramFacts = new Map()
|
|
640
|
+
return {
|
|
641
|
+
callee, callerFunc, argList, func, restIdx,
|
|
642
|
+
callerLocals: ctxEntry.callerLocals,
|
|
643
|
+
callerValTypes: ctxEntry.callerValTypes,
|
|
644
|
+
callerParamFacts(key) {
|
|
645
|
+
if (!paramFacts.has(key)) paramFacts.set(key, paramFactsOf(paramReps, callerFunc, key))
|
|
646
|
+
return paramFacts.get(key)
|
|
647
|
+
},
|
|
648
|
+
}
|
|
649
|
+
}
|
|
521
650
|
const runCallsiteLattice = (rules) => {
|
|
522
651
|
for (let s = 0; s < callSites.length; s++) {
|
|
523
|
-
const
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
const ctxEntry = callerCtx.get(callerFunc)
|
|
527
|
-
if (!ctxEntry) continue
|
|
528
|
-
const restIdx = func.rest ? func.sig.params.length - 1 : -1
|
|
529
|
-
const paramFacts = new Map()
|
|
530
|
-
const state = {
|
|
531
|
-
callee, callerFunc, argList, func, restIdx,
|
|
532
|
-
callerLocals: ctxEntry.callerLocals,
|
|
533
|
-
callerValTypes: ctxEntry.callerValTypes,
|
|
534
|
-
callerParamFacts(key) {
|
|
535
|
-
if (!paramFacts.has(key)) paramFacts.set(key, paramFactsOf(paramReps, callerFunc, key))
|
|
536
|
-
return paramFacts.get(key)
|
|
537
|
-
},
|
|
538
|
-
}
|
|
652
|
+
const state = siteState(callSites[s])
|
|
653
|
+
if (!state) continue
|
|
654
|
+
const { func, argList } = state
|
|
539
655
|
for (let k = 0; k < func.sig.params.length; k++) {
|
|
540
|
-
const r = ensureParamRep(paramReps, callee, k)
|
|
656
|
+
const r = ensureParamRep(paramReps, state.callee, k)
|
|
541
657
|
if (k >= argList.length) { for (const rule of rules) rule.missing(r, k, state); continue }
|
|
542
|
-
const
|
|
543
|
-
for (const rule of rules) rule.apply(r, arg, k, state)
|
|
658
|
+
for (const rule of rules) rule.apply(r, argList[k], k, state)
|
|
544
659
|
}
|
|
545
660
|
}
|
|
546
661
|
}
|
|
@@ -568,7 +683,33 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
568
683
|
const pname = state.func.sig.params[k]?.name
|
|
569
684
|
return pname != null ? state.func.defaults?.[pname] : null
|
|
570
685
|
}
|
|
571
|
-
|
|
686
|
+
// Hard consensus val for (funcName, param k): the kind every live call site
|
|
687
|
+
// agrees on, or null if any site is untyped / missing / disagrees. The shared
|
|
688
|
+
// `val` lattice runs SOFT (a value can come from typed sites alone, untyped
|
|
689
|
+
// sites skipped); a consumer that *mutates the signature* off val must instead
|
|
690
|
+
// ask this — it re-folds the sites HARD so it never specializes a param that
|
|
691
|
+
// some call site can't prove. (applyPointerParamAbi is that consumer.)
|
|
692
|
+
const hardParamVal = (funcName, k) => {
|
|
693
|
+
let consensus
|
|
694
|
+
for (let s = 0; s < callSites.length; s++) {
|
|
695
|
+
if (callSites[s].callee !== funcName) continue
|
|
696
|
+
const state = siteState(callSites[s])
|
|
697
|
+
if (!state) continue
|
|
698
|
+
if (k >= state.argList.length) return null // missing → undefined at runtime
|
|
699
|
+
const v = inferValAtSite(state.argList[k], state)
|
|
700
|
+
if (v == null) return null // an untyped site ⇒ not specializable
|
|
701
|
+
if (consensus === undefined) consensus = v
|
|
702
|
+
else if (consensus !== v) return null // disagreement ⇒ TOP
|
|
703
|
+
}
|
|
704
|
+
return consensus ?? null
|
|
705
|
+
}
|
|
706
|
+
// `soft` makes apply treat a null inference as BOTTOM (skip — "this site can't
|
|
707
|
+
// tell yet") instead of TOP (poison): the monotone meet. A soft field never
|
|
708
|
+
// needs clearStickyNull; its consumers either re-validate hard (hardParamVal)
|
|
709
|
+
// or read it after a final hard settling sweep. `missing` poisons regardless —
|
|
710
|
+
// an omitted arg with no default is undefined at runtime, a real reason not to
|
|
711
|
+
// specialize, and must stay sticky.
|
|
712
|
+
const mergeRule = (field, infer, soft = false) => ({
|
|
572
713
|
missing(r, k, state) {
|
|
573
714
|
if (r[field] === null) return
|
|
574
715
|
const def = defaultArg(state, k)
|
|
@@ -576,11 +717,19 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
576
717
|
else r[field] = null
|
|
577
718
|
},
|
|
578
719
|
apply(r, arg, k, state) {
|
|
579
|
-
if (r[field]
|
|
720
|
+
if (r[field] === null) return
|
|
721
|
+
const v = infer(arg, k, state)
|
|
722
|
+
if (v == null) { if (!soft) r[field] = null; return }
|
|
723
|
+
mergeParamFact(r, field, v)
|
|
580
724
|
},
|
|
581
725
|
})
|
|
582
726
|
const runFixpoint = () => runCallsiteLattice([
|
|
583
|
-
|
|
727
|
+
// val runs SOFT (monotone): a TYPED param's val only becomes inferable after the
|
|
728
|
+
// typedCtor fixpoint + pointer-ABI enrichment, so an early hard merge would
|
|
729
|
+
// sticky-poison it (the old clearStickyNull undid that). Soft leaves it BOTTOM;
|
|
730
|
+
// the post-enrichment rerun fills it in. applyPointerParamAbi re-validates via
|
|
731
|
+
// hardParamVal; a final hard sweep settles val for emit + late consumers.
|
|
732
|
+
mergeRule('val', (arg, _k, state) => inferValAtSite(arg, state), true),
|
|
584
733
|
{
|
|
585
734
|
missing: poison('wasm'),
|
|
586
735
|
apply(r, arg, _k, state) {
|
|
@@ -620,6 +769,16 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
620
769
|
}
|
|
621
770
|
const runArrFixpoint = () => runArrElemFixpoint('arrayElemSchema', inferArrElemSchema, phase.callerElems('arrElemSchemas'))
|
|
622
771
|
const runArrValTypeFixpoint = () => runArrElemFixpoint('arrayElemValType', inferArrElemValType, phase.callerElems('arrElemValTypes'))
|
|
772
|
+
|
|
773
|
+
// E2 (VAL-kind result inference) FIRST: it's body-driven and call-chain self-
|
|
774
|
+
// fixpointing — independent of the param lattice and the narrowing acts (it reads
|
|
775
|
+
// analyzeBody valTypes + callees' valResult, never paramReps or sig.params). Running
|
|
776
|
+
// it up front means a call arg like `initRows()` resolves to its VAL.ARRAY result on
|
|
777
|
+
// the param fixpoint's FIRST pass, so val/schemaId never get the can't-tell-yet
|
|
778
|
+
// poison that clearStickyNull used to un-stick (root B). Numeric (i32) result
|
|
779
|
+
// narrowing stays below — it benefits from i32 params being narrowed first.
|
|
780
|
+
const funcsWithNarrowableResult = narrowableFuncs(valueUsed)
|
|
781
|
+
narrowValResults(funcsWithNarrowableResult)
|
|
623
782
|
runFixpoint()
|
|
624
783
|
runFixpoint()
|
|
625
784
|
|
|
@@ -645,14 +804,12 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
645
804
|
// (aux carries element-type, handled separately by applyTypedPointerParamAbi).
|
|
646
805
|
// - exclude params with defaults (nullish sentinel needs the f64 NaN space).
|
|
647
806
|
// - exclude rest position (array pack/unpack stays f64).
|
|
648
|
-
applyPointerParamAbi(paramReps, valueUsed)
|
|
807
|
+
applyPointerParamAbi(paramReps, valueUsed, hardParamVal)
|
|
649
808
|
|
|
650
|
-
// E
|
|
651
|
-
//
|
|
652
|
-
//
|
|
653
|
-
const funcsWithNarrowableResult = narrowableFuncs(valueUsed)
|
|
809
|
+
// E: numeric (i32) result narrowing — kept here, after applyI32ParamSpecialization,
|
|
810
|
+
// so a body returning `param + 1` sees param already narrowed to i32. (E2 / VAL
|
|
811
|
+
// result inference ran up front — see above.) funcsWithNarrowableResult hoisted there.
|
|
654
812
|
narrowI32Results(funcsWithNarrowableResult)
|
|
655
|
-
narrowValResults(funcsWithNarrowableResult)
|
|
656
813
|
|
|
657
814
|
// Now that E2 set `valResult` on funcs, narrow per-func `arrayElemSchema` for
|
|
658
815
|
// VAL.ARRAY-returning funcs (via push observations + call chains). Then re-run the
|
|
@@ -673,11 +830,9 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
673
830
|
// observation upgrade `undefined` → NUMBER without poisoning earlier
|
|
674
831
|
// monomorphic observations.
|
|
675
832
|
if (hasSchemaLiterals) observeProgramSlots(ast)
|
|
676
|
-
//
|
|
677
|
-
//
|
|
678
|
-
//
|
|
679
|
-
clearStickyNull(paramReps, 'val')
|
|
680
|
-
clearStickyNull(paramReps, 'schemaId')
|
|
833
|
+
// Re-run with refreshed callerValTypes + the new program-slot observations. (No
|
|
834
|
+
// clearStickyNull needed: valResult was known before the first pass — see E2 hoist
|
|
835
|
+
// above — so val/schemaId never got the can't-tell-yet poison this used to undo.)
|
|
681
836
|
runFixpoint()
|
|
682
837
|
// Now that .val is refreshed, dedicated arr-elem-schema fixpoint.
|
|
683
838
|
runArrFixpoint()
|
|
@@ -720,7 +875,7 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
720
875
|
// i32 offset (with ptrAux carrying the elem-type bits). Eliminates the
|
|
721
876
|
// per-read `i32.wrap_i64 (i64.reinterpret_f64 (local.get $arr))` unbox dance
|
|
722
877
|
// that today dominates hot loops dominated by typed-array indexing.
|
|
723
|
-
// Call sites coerce via
|
|
878
|
+
// Call sites coerce via coerceArg → ptrOffsetIR(arg, VAL.TYPED).
|
|
724
879
|
// Safety: same exclusions as the OBJECT/SET/MAP/BUFFER narrowing above —
|
|
725
880
|
// exported, value-used, raw, defaults, rest position.
|
|
726
881
|
applyTypedPointerParamAbi(paramReps, valueUsed)
|
|
@@ -728,11 +883,11 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
728
883
|
// H: Post-F/G re-fixpoint — propagates VAL kinds through bimorphic call sites
|
|
729
884
|
// where ptrKind narrowed but ptrAux disagreed (e.g. `sum(f64arr)` and `sum(i32arr)`
|
|
730
885
|
// → both VAL.TYPED, different ctors). Without this, callerValTypes carries no entry
|
|
731
|
-
// for caller's params, so inferValType
|
|
732
|
-
// sticky
|
|
733
|
-
//
|
|
886
|
+
// for caller's params, so inferValType returned null and (under the old hard merge)
|
|
887
|
+
// sticky-poisoned the param's val. The soft val merge leaves it BOTTOM instead, so
|
|
888
|
+
// this rerun — now that enrichment has put VAL.TYPED into callerValTypes — simply
|
|
889
|
+
// fills it in (array.js then skips __is_str_key + __str_idx dispatch on `arr[i]`).
|
|
734
890
|
enrichCallerValTypesFromPointerParams(callerCtx)
|
|
735
|
-
clearStickyNull(paramReps, 'val')
|
|
736
891
|
runFixpoint()
|
|
737
892
|
|
|
738
893
|
// I: Post-E re-narrow of numeric (i32) params. The first numeric narrowing pass
|
|
@@ -750,6 +905,12 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
750
905
|
// so the refreshed exprType view propagates.
|
|
751
906
|
resetParamWasmFacts(paramReps)
|
|
752
907
|
runFixpoint()
|
|
908
|
+
// Settle val HARD now that every producer (results, typedCtor, enrichment) has run
|
|
909
|
+
// and the soft lattice has converged: re-fold each param's sites and poison any
|
|
910
|
+
// whose val isn't unanimous (a site left BOTTOM = genuinely untyped). After this,
|
|
911
|
+
// r.val is sound for emit + the late/post-return consumers (applyI32ParamSpecial-
|
|
912
|
+
// ization's skipTyped guard, specializeBimorphicTyped) — which read it directly.
|
|
913
|
+
runCallsiteLattice([mergeRule('val', (arg, _k, state) => inferValAtSite(arg, state))])
|
|
753
914
|
// Don't steal typed-array params from specializeBimorphicTyped: F phase parks
|
|
754
915
|
// bimorphic typed params at type='f64' with sticky-null typedCtor (two distinct
|
|
755
916
|
// ctors at call sites). Their callers post-F pass them as i32 (pointer ABI),
|
|
@@ -764,9 +925,12 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
764
925
|
if (jsstringEnabled()) applyJsstringBoundaryCarrier(paramReps, valueUsed)
|
|
765
926
|
}
|
|
766
927
|
|
|
767
|
-
/** Gate the jsstring
|
|
768
|
-
*
|
|
769
|
-
* `
|
|
928
|
+
/** Gate the jsstring carrier on the host. ON by default for the JS host: a
|
|
929
|
+
* js-host build is already JS-locked (it imports `env.*`), so the externref +
|
|
930
|
+
* `wasm:js-string` carrier's JS dependency is free there, and the zero-copy
|
|
931
|
+
* string-read path is a clear win. OFF under WASI: the carrier needs a JS host,
|
|
932
|
+
* and wasi builds must stay portable (wasmtime/Go/Rust). Opt out on JS with
|
|
933
|
+
* `optimize: { jsstring: false }` (e.g. side-by-side benchmarks). */
|
|
770
934
|
function jsstringEnabled() {
|
|
771
935
|
if (ctx.transform.host === 'wasi') return false
|
|
772
936
|
if (ctx.transform.optimize?.jsstring === false) return false
|
|
@@ -782,13 +946,15 @@ export function applyJsstringBoundaryCarrierStandalone(programFacts) {
|
|
|
782
946
|
}
|
|
783
947
|
|
|
784
948
|
/**
|
|
785
|
-
* Body-local boolean-result inference. `narrowValResults` is the general
|
|
786
|
-
* VAL.*) pass, but it lives inside whole-program narrowing, which is skipped
|
|
787
|
-
* trivial leaf modules (no call sites).
|
|
788
|
-
* internal carrier
|
|
789
|
-
*
|
|
790
|
-
*
|
|
791
|
-
*
|
|
949
|
+
* Body-local boolean/bigint-result inference. `narrowValResults` is the general
|
|
950
|
+
* (any VAL.*) pass, but it lives inside whole-program narrowing, which is skipped
|
|
951
|
+
* for trivial leaf modules (no call sites). Boolean and bigint are the two kinds
|
|
952
|
+
* whose internal carrier differs from the host-boundary carrier — bool rides a 0/1
|
|
953
|
+
* number internally but crosses as the TRUE_NAN/FALSE_NAN atom; bigint rides an
|
|
954
|
+
* i64-reinterpreted f64 internally but must cross as a real Number — so an exported
|
|
955
|
+
* `(a) => a > 2` or `() => 100n` still needs its boundary thunk even on the skip path.
|
|
956
|
+
* This pass only ever *sets* valResult to VAL.BOOL / VAL.BIGINT, so it is safe to run
|
|
957
|
+
* unconditionally — pointer/array/number results are untouched.
|
|
792
958
|
*/
|
|
793
959
|
export function narrowBoolResults() {
|
|
794
960
|
for (const func of ctx.func.list) {
|
|
@@ -803,6 +969,7 @@ export function narrowBoolResults() {
|
|
|
803
969
|
? (localValTypes?.get(e) || ctx.scope.globalValTypes?.get(e) || null)
|
|
804
970
|
: valTypeOf(e)
|
|
805
971
|
if (exprs.every(e => vt(e) === VAL.BOOL)) func.valResult = VAL.BOOL
|
|
972
|
+
else if (exprs.every(e => vt(e) === VAL.BIGINT)) func.valResult = VAL.BIGINT
|
|
806
973
|
}
|
|
807
974
|
}
|
|
808
975
|
|
|
@@ -828,35 +995,37 @@ const JSS_OK_PROPS = new Set(['length', 'charCodeAt'])
|
|
|
828
995
|
* `.charCodeAt` whose callee node lives in `safeCC` (provably bounded).
|
|
829
996
|
* Reassignment / `++` / `--` / closure capture all reject conservatively.
|
|
830
997
|
*
|
|
831
|
-
* Returns `{ ok, stringDiscriminating }` — `stringDiscriminating` is
|
|
832
|
-
* we saw at least one string-only use (`.charCodeAt`);
|
|
833
|
-
*
|
|
834
|
-
* `.length` alone is polymorphic across string/array/typed-array.
|
|
998
|
+
* Returns `{ ok, stringDiscriminating, reason? }` — `stringDiscriminating` is
|
|
999
|
+
* true iff we saw at least one string-only use (`.charCodeAt`); `reason` names
|
|
1000
|
+
* the first blocking use when `ok` is false.
|
|
835
1001
|
*/
|
|
836
1002
|
function paramAllUsesJsstringMappable(body, name, safeCC) {
|
|
837
|
-
if (body == null) return { ok: false, stringDiscriminating: false }
|
|
1003
|
+
if (body == null) return { ok: false, stringDiscriminating: false, reason: null }
|
|
838
1004
|
let ok = true
|
|
839
1005
|
let stringDiscriminating = false
|
|
1006
|
+
let reason = null
|
|
1007
|
+
const fail = (msg) => { ok = false; reason ||= msg }
|
|
1008
|
+
const refsParam = (node) => {
|
|
1009
|
+
if (node === name) return true
|
|
1010
|
+
if (!Array.isArray(node)) return false
|
|
1011
|
+
for (let i = 1; i < node.length; i++) if (refsParam(node[i])) return true
|
|
1012
|
+
return false
|
|
1013
|
+
}
|
|
840
1014
|
const walk = (node) => {
|
|
841
1015
|
if (!ok) return
|
|
842
1016
|
if (typeof node === 'string') {
|
|
843
|
-
|
|
844
|
-
// callee slot is a fallback trigger — the param escaped to a non-
|
|
845
|
-
// mappable use.
|
|
846
|
-
if (node === name) ok = false
|
|
1017
|
+
if (node === name) fail('bare use of the string param disables the zero-copy externref boundary carrier')
|
|
847
1018
|
return
|
|
848
1019
|
}
|
|
849
1020
|
if (!Array.isArray(node)) return
|
|
850
1021
|
const op = node[0]
|
|
851
1022
|
if (op === '=>') {
|
|
852
|
-
// Nested arrow may shadow `name` with its own param; if it doesn't, any
|
|
853
|
-
// capture of the outer `name` is an escape we can't track — reject.
|
|
854
1023
|
const params = node[1]
|
|
855
1024
|
const shadowed = Array.isArray(params)
|
|
856
1025
|
? params.some(p => (typeof p === 'string' && p === name) ||
|
|
857
1026
|
(Array.isArray(p) && p[1] === name))
|
|
858
1027
|
: params === name
|
|
859
|
-
if (!shadowed)
|
|
1028
|
+
if (!shadowed) fail('closure capture of the string param disables the zero-copy externref boundary carrier')
|
|
860
1029
|
return
|
|
861
1030
|
}
|
|
862
1031
|
if ((op === '=' || op === '+=' || op === '-=' || op === '*=' || op === '/=' ||
|
|
@@ -864,26 +1033,23 @@ function paramAllUsesJsstringMappable(body, name, safeCC) {
|
|
|
864
1033
|
op === '>>=' || op === '<<=' || op === '>>>=' ||
|
|
865
1034
|
op === '||=' || op === '&&=' || op === '??=' ||
|
|
866
1035
|
op === '++' || op === '--') && node[1] === name) {
|
|
867
|
-
|
|
1036
|
+
fail('reassigning the string param disables the zero-copy externref boundary carrier')
|
|
1037
|
+
return
|
|
1038
|
+
}
|
|
1039
|
+
if (op === '+' && node.slice(1).some(arg => refsParam(arg))) {
|
|
1040
|
+
fail('string concatenation on the param disables the zero-copy externref boundary carrier')
|
|
868
1041
|
return
|
|
869
1042
|
}
|
|
870
1043
|
if (op === '.' && node[1] === name && JSS_OK_PROPS.has(node[2])) {
|
|
871
|
-
// .length is always safe but polymorphic across string/array/typed.
|
|
872
|
-
// .charCodeAt is only safe when the loop-bounds analysis proves the
|
|
873
|
-
// index in-bounds (this dotted callee node is exactly the one collected
|
|
874
|
-
// by scanBoundedLoops) — but it IS string-discriminating.
|
|
875
1044
|
if (node[2] === 'length') return
|
|
876
1045
|
if (safeCC.has(node)) { stringDiscriminating = true; return }
|
|
877
|
-
|
|
1046
|
+
fail(`\`.${node[2]}\` on the string param disables the zero-copy externref boundary carrier`)
|
|
878
1047
|
return
|
|
879
1048
|
}
|
|
880
|
-
// For `['()', callee, ...args]` with callee = ['.', name, 'charCodeAt'],
|
|
881
|
-
// `name` is in the callee receiver slot (validated by the branch above).
|
|
882
|
-
// The `.length` case is just a member read — no `()` wrapping.
|
|
883
1049
|
for (let i = 1; i < node.length; i++) walk(node[i])
|
|
884
1050
|
}
|
|
885
1051
|
walk(body)
|
|
886
|
-
return { ok, stringDiscriminating }
|
|
1052
|
+
return { ok, stringDiscriminating, reason }
|
|
887
1053
|
}
|
|
888
1054
|
|
|
889
1055
|
function applyJsstringBoundaryCarrier(paramReps, valueUsed) {
|
|
@@ -919,13 +1085,48 @@ function applyJsstringBoundaryCarrier(paramReps, valueUsed) {
|
|
|
919
1085
|
if (!stringDiscriminating && r?.val !== VAL.STRING && !hasStringDefault) continue
|
|
920
1086
|
p.type = 'externref'
|
|
921
1087
|
p.jsstring = true
|
|
922
|
-
|
|
923
|
-
// (the JS-side interop wrapper applies the default on `undefined`).
|
|
1088
|
+
updateRep(p.name, { carrier: 'jsstring', val: VAL.STRING })
|
|
924
1089
|
if (hasStringDefault) p.jsstringDefault = defVal[1]
|
|
925
1090
|
}
|
|
926
1091
|
}
|
|
927
1092
|
}
|
|
928
1093
|
|
|
1094
|
+
/** Soft warnings when a string param could use the externref carrier but doesn't. */
|
|
1095
|
+
export function adviseJsstringCarrier(paramReps, valueUsed) {
|
|
1096
|
+
if (!ctx.warnings || !jsstringEnabled()) return
|
|
1097
|
+
|
|
1098
|
+
for (const func of ctx.func.list) {
|
|
1099
|
+
if (func.raw || !func.exported || !func.body || func.rest) continue
|
|
1100
|
+
if (valueUsed?.has(func.name)) continue
|
|
1101
|
+
|
|
1102
|
+
const safeCC = new Set()
|
|
1103
|
+
scanBoundedLoops(func.body, safeCC)
|
|
1104
|
+
const reps = paramReps?.get(func.name)
|
|
1105
|
+
|
|
1106
|
+
for (let k = 0; k < func.sig.params.length; k++) {
|
|
1107
|
+
const p = func.sig.params[k]
|
|
1108
|
+
if (p.jsstring) continue
|
|
1109
|
+
if (p.type !== 'f64' || p.ptrKind != null) continue
|
|
1110
|
+
|
|
1111
|
+
const defVal = func.defaults?.[p.name]
|
|
1112
|
+
if (defVal != null && !isLiteralStr(defVal)) continue
|
|
1113
|
+
|
|
1114
|
+
const r = reps?.get(k)
|
|
1115
|
+
if (r && r.val != null && r.val !== VAL.STRING) continue
|
|
1116
|
+
|
|
1117
|
+
const hasStringDefault = defVal != null && isLiteralStr(defVal)
|
|
1118
|
+
const { ok: usesOk, stringDiscriminating, reason } =
|
|
1119
|
+
paramAllUsesJsstringMappable(func.body, p.name, safeCC)
|
|
1120
|
+
const isCandidate = stringDiscriminating || r?.val === VAL.STRING || hasStringDefault
|
|
1121
|
+
if (!isCandidate || usesOk) continue
|
|
1122
|
+
|
|
1123
|
+
warn('jsstring-declined',
|
|
1124
|
+
`export '${func.name}' param '${p.name}': ${reason || 'string param uses disable the zero-copy externref boundary carrier'}`,
|
|
1125
|
+
{ fn: func.name }, func.body.loc)
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
|
|
929
1130
|
/**
|
|
930
1131
|
* Phase: bimorphic typed-array param specialization.
|
|
931
1132
|
*
|
|
@@ -1142,7 +1343,12 @@ export function refineDynKeys(programFacts) {
|
|
|
1142
1343
|
if (!NON_DYN_VTS.has(vt)) real = true
|
|
1143
1344
|
}
|
|
1144
1345
|
} else if (op === 'for-in') real = true
|
|
1145
|
-
|
|
1346
|
+
// Recurse into nested arrows too. Closures stay inline (defFunc skips
|
|
1347
|
+
// depth>0), so a dynamic-key access captured in one — e.g. `handlers[op]`
|
|
1348
|
+
// in a dispatch closure — is reachable only through its parent's body.
|
|
1349
|
+
// Matches collectProgramFacts, which also crosses arrows when setting
|
|
1350
|
+
// anyDyn; not crossing here let refineDynKeys reset a flag the initial scan
|
|
1351
|
+
// correctly raised. Monotone-safe: extra visits only ever raise `real`.
|
|
1146
1352
|
for (let i = 1; i < node.length; i++) visit(typeMap, node[i])
|
|
1147
1353
|
}
|
|
1148
1354
|
|