jz 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +315 -318
- package/bench/README.md +369 -0
- package/bench/bench.svg +102 -0
- package/cli.js +104 -30
- package/dist/interop.js +1 -0
- package/dist/jz.js +6024 -0
- package/index.d.ts +126 -0
- package/index.js +362 -84
- package/interop.js +159 -186
- package/jz.svg +5 -0
- package/jzify/arguments.js +97 -0
- package/jzify/bundler.js +382 -0
- package/jzify/classes.js +328 -0
- package/jzify/hoist-vars.js +177 -0
- package/jzify/index.js +51 -0
- package/jzify/names.js +37 -0
- package/jzify/switch.js +106 -0
- package/jzify/transform.js +349 -0
- package/layout.js +184 -0
- package/module/array.js +377 -176
- package/module/collection.js +639 -145
- package/module/console.js +55 -43
- package/module/core.js +264 -153
- package/module/date.js +15 -3
- package/module/function.js +73 -6
- package/module/index.js +2 -1
- package/module/json.js +226 -61
- package/module/math.js +551 -187
- package/module/number.js +327 -60
- package/module/object.js +474 -184
- package/module/regex.js +255 -25
- package/module/schema.js +24 -6
- package/module/simd.js +117 -0
- package/module/string.js +621 -226
- package/module/symbol.js +1 -1
- package/module/timer.js +9 -14
- package/module/typedarray.js +459 -73
- package/package.json +58 -14
- package/src/abi/index.js +39 -23
- package/src/abi/string.js +38 -41
- package/src/ast.js +460 -0
- package/src/autoload.js +29 -24
- package/src/bridge.js +111 -0
- package/src/compile/analyze-scans.js +824 -0
- package/src/compile/analyze.js +1600 -0
- package/src/compile/emit-assign.js +411 -0
- package/src/compile/emit.js +3512 -0
- package/src/compile/flow-types.js +103 -0
- package/src/{compile.js → compile/index.js} +778 -128
- package/src/{infer.js → compile/infer.js} +62 -98
- package/src/compile/loop-divmod.js +155 -0
- package/src/{narrow.js → compile/narrow.js} +409 -106
- package/src/compile/peel-stencil.js +261 -0
- package/src/compile/plan/advise.js +370 -0
- package/src/compile/plan/common.js +150 -0
- package/src/compile/plan/index.js +122 -0
- package/src/compile/plan/inline.js +682 -0
- package/src/compile/plan/literals.js +1199 -0
- package/src/compile/plan/loops.js +472 -0
- package/src/compile/plan/scope.js +649 -0
- package/src/compile/program-facts.js +423 -0
- package/src/ctx.js +220 -64
- package/src/ir.js +589 -172
- package/src/kind-traits.js +132 -0
- package/src/kind.js +524 -0
- package/src/op-policy.js +57 -0
- package/src/{optimize.js → optimize/index.js} +1432 -473
- package/src/optimize/vectorize.js +4660 -0
- package/src/param-reps.js +65 -0
- package/src/parse.js +44 -0
- package/src/{prepare.js → prepare/index.js} +649 -205
- package/src/prepare/lift-iife.js +149 -0
- package/src/reps.js +116 -0
- package/src/resolve.js +12 -3
- package/src/static.js +208 -0
- package/src/type.js +651 -0
- package/src/{assemble.js → wat/assemble.js} +275 -55
- package/src/wat/optimize.js +3938 -0
- package/transform.js +21 -0
- package/wasi.js +47 -5
- package/src/analyze.js +0 -3818
- package/src/emit.js +0 -3040
- package/src/jzify.js +0 -1580
- package/src/plan.js +0 -2132
- package/src/vectorize.js +0 -1088
- /package/src/{codegen.js → wat/codegen.js} +0 -0
|
@@ -7,26 +7,29 @@
|
|
|
7
7
|
* function `sig` records change.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { ctx } from '
|
|
11
|
-
import {
|
|
10
|
+
import { ctx, warn, err } 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
|
-
inferSchemaId, inferValType, inferTypedCtor,
|
|
27
|
+
inferSchemaId, inferValType, inferTypedCtor, inferParams,
|
|
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
|
}
|
|
@@ -148,12 +156,32 @@ function refreshCallerValTypes(callerCtx) {
|
|
|
148
156
|
}
|
|
149
157
|
}
|
|
150
158
|
|
|
159
|
+
// Per-caller typed-elem context: the caller's body-local typed arrays, layered
|
|
160
|
+
// over the module's typed-array globals so a call like `f(globalArr)` resolves
|
|
161
|
+
// `globalArr`'s ctor (inferTypedCtor reads only this map for a bare-name arg).
|
|
162
|
+
// A global is visible UNLESS the caller shadows the name with a param or local
|
|
163
|
+
// of its own — only then could the name denote a non-typed value. Globals are
|
|
164
|
+
// sound to consult: globalTypedElem holds a name only when EVERY assignment to
|
|
165
|
+
// it is the same single typed-array ctor (scope.js invalidates on any conflict),
|
|
166
|
+
// so it can't denote a different kind at the call site.
|
|
167
|
+
function callerTypedElemsFor(func, globalTE) {
|
|
168
|
+
const local = analyzeBody(func.body).typedElems
|
|
169
|
+
if (!globalTE.size) return local
|
|
170
|
+
const shadowed = new Set(analyzeBody(func.body).locals.keys())
|
|
171
|
+
for (const p of func.sig?.params || []) shadowed.add(p.name)
|
|
172
|
+
const merged = new Map()
|
|
173
|
+
for (const [k, v] of globalTE) if (!shadowed.has(k)) merged.set(k, v)
|
|
174
|
+
for (const [k, v] of local) merged.set(k, v) // local typed binding shadows the global
|
|
175
|
+
return merged
|
|
176
|
+
}
|
|
177
|
+
|
|
151
178
|
function buildCallerTypedCtx() {
|
|
152
179
|
const callerTypedCtx = new Map()
|
|
153
|
-
|
|
180
|
+
const globalTE = ctx.scope.globalTypedElem || new Map()
|
|
181
|
+
callerTypedCtx.set(null, globalTE)
|
|
154
182
|
for (const func of ctx.func.list) {
|
|
155
183
|
if (!func.body || func.raw) continue
|
|
156
|
-
callerTypedCtx.set(func,
|
|
184
|
+
callerTypedCtx.set(func, callerTypedElemsFor(func, globalTE))
|
|
157
185
|
}
|
|
158
186
|
return callerTypedCtx
|
|
159
187
|
}
|
|
@@ -256,7 +284,7 @@ function narrowI32Results(funcs) {
|
|
|
256
284
|
while (changed) {
|
|
257
285
|
changed = false
|
|
258
286
|
for (const func of funcs) {
|
|
259
|
-
if (func.sig.results[0] === 'i32') continue
|
|
287
|
+
if (func.sig.results[0] === 'i32' || func.sig.results[0] === 'v128') continue
|
|
260
288
|
const body = func.body
|
|
261
289
|
if (isBlockBody(body) && hasBareReturn(body)) continue
|
|
262
290
|
const exprs = returnExprs(body)
|
|
@@ -265,12 +293,16 @@ function narrowI32Results(funcs) {
|
|
|
265
293
|
ctx.func.current = func.sig
|
|
266
294
|
const locals = isBlockBody(body) ? analyzeBody(body).locals : new Map()
|
|
267
295
|
for (const p of func.sig.params) if (!locals.has(p.name)) locals.set(p.name, p.type)
|
|
268
|
-
const
|
|
296
|
+
const allV128 = exprs.every(e => exprType(e, locals) === 'v128')
|
|
297
|
+
const allI32 = !allV128 && exprs.every(e => exprType(e, locals) === 'i32')
|
|
269
298
|
const anyUnsigned = exprs.some(isUnsignedTail)
|
|
270
299
|
const allUnsigned = exprs.every(isUnsignedTail)
|
|
271
300
|
ctx.func.current = savedCurrent
|
|
272
|
-
//
|
|
273
|
-
if (
|
|
301
|
+
// SIMD: every tail returns a lane vector → v128 result.
|
|
302
|
+
if (allV128) {
|
|
303
|
+
func.sig.results = ['v128']
|
|
304
|
+
changed = true
|
|
305
|
+
} else if (allI32 && (!anyUnsigned || allUnsigned)) { // sign-consistent i32 tails
|
|
274
306
|
func.sig.results = ['i32']
|
|
275
307
|
if (allUnsigned) func.sig.unsignedResult = true
|
|
276
308
|
changed = true
|
|
@@ -401,11 +433,47 @@ function typedAuxOfReturn(expr, localElemMap) {
|
|
|
401
433
|
* Fixpoint: a chain `outer → inner → {a,b}` needs inner to narrow first so
|
|
402
434
|
* outer's call to inner contributes a known schema-id.
|
|
403
435
|
*/
|
|
436
|
+
/** A function whose every return is the same parameter that was pointer-ABI
|
|
437
|
+
* narrowed to an unboxed i32 (p.ptrKind set). Returns that param, else null. */
|
|
438
|
+
function passthroughPtrParam(func) {
|
|
439
|
+
const exprs = returnExprs(func.body)
|
|
440
|
+
if (!exprs.length) return null
|
|
441
|
+
const name = exprs[0]
|
|
442
|
+
if (typeof name !== 'string' || !exprs.every(e => e === name)) return null
|
|
443
|
+
return func.sig.params.find(p => p.name === name && p.ptrKind) || null
|
|
444
|
+
}
|
|
445
|
+
|
|
404
446
|
function narrowPointerResults(funcs, paramReps) {
|
|
405
447
|
let changed = true
|
|
406
448
|
while (changed) {
|
|
407
449
|
changed = false
|
|
408
450
|
for (const func of funcs) {
|
|
451
|
+
// Pointer pass-through: every return is the same parameter that
|
|
452
|
+
// applyPointerParamAbi narrowed to an unboxed i32 pointer. The result IS that
|
|
453
|
+
// pointer, so its sig must carry the param's ptrKind (+ schemaId for OBJECT).
|
|
454
|
+
// Without this the result is a bare i32 the caller numeric-converts
|
|
455
|
+
// (`f64.convert_i32_s`) instead of reboxing — dropping the schema-id so a
|
|
456
|
+
// later `.prop` read mis-resolves to `undefined`. narrowValResults can't see
|
|
457
|
+
// this (it reads body-locals, not param facts) and narrowI32Results steals it
|
|
458
|
+
// as a numeric i32, so resolve it here from the settled param lattice.
|
|
459
|
+
if (func.sig.ptrKind == null) {
|
|
460
|
+
const pp = passthroughPtrParam(func)
|
|
461
|
+
if (pp) {
|
|
462
|
+
const aux = pp.ptrKind === VAL.OBJECT
|
|
463
|
+
? paramFactsOf(paramReps, func, 'schemaId')?.get(pp.name) ?? null
|
|
464
|
+
: null
|
|
465
|
+
// OBJECT needs a known schema-id to rebox; a polymorphic pass-through
|
|
466
|
+
// (conflicting schemas → null) keeps its current handling.
|
|
467
|
+
if (pp.ptrKind !== VAL.OBJECT || aux != null) {
|
|
468
|
+
func.sig.results = ['i32']
|
|
469
|
+
func.sig.ptrKind = pp.ptrKind
|
|
470
|
+
func.valResult = pp.ptrKind
|
|
471
|
+
if (aux != null) func.sig.ptrAux = aux
|
|
472
|
+
changed = true
|
|
473
|
+
continue
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
409
477
|
if (!func.valResult) continue
|
|
410
478
|
if (func.sig.results[0] !== 'f64') continue
|
|
411
479
|
const isBlock = isBlockBody(func.body)
|
|
@@ -467,7 +535,7 @@ function createPhaseState() {
|
|
|
467
535
|
|
|
468
536
|
invalidateBodyFacts() {
|
|
469
537
|
for (const func of ctx.func.list) {
|
|
470
|
-
if (func.body && !func.raw)
|
|
538
|
+
if (func.body && !func.raw) invalidateLocalsCache(func.body)
|
|
471
539
|
}
|
|
472
540
|
clearDerived()
|
|
473
541
|
},
|
|
@@ -484,6 +552,67 @@ function createPhaseState() {
|
|
|
484
552
|
}
|
|
485
553
|
}
|
|
486
554
|
|
|
555
|
+
const _FIELD_TO_SLICE = {
|
|
556
|
+
arrayElemSchema: 'arrElemSchemas',
|
|
557
|
+
arrayElemValType: 'arrElemValTypes',
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/** Propagate Array<T> element facts from return paths into caller paramReps (phase G). */
|
|
561
|
+
function narrowReturnArrayElems(field, paramReps, valueUsed) {
|
|
562
|
+
const sliceKey = _FIELD_TO_SLICE[field]
|
|
563
|
+
const targets = ctx.func.list.filter(f =>
|
|
564
|
+
!f.raw && !f.exported && !valueUsed.has(f.name) &&
|
|
565
|
+
f.valResult === VAL.ARRAY && f[field] == null
|
|
566
|
+
)
|
|
567
|
+
let changed = true
|
|
568
|
+
while (changed) {
|
|
569
|
+
changed = false
|
|
570
|
+
for (const f of targets) invalidateLocalsCache(f.body)
|
|
571
|
+
for (const func of targets) {
|
|
572
|
+
if (func[field] != null) continue
|
|
573
|
+
const isBlock = isBlockBody(func.body)
|
|
574
|
+
if (isBlock && !alwaysReturns(func.body)) continue
|
|
575
|
+
const exprs = returnExprs(func.body)
|
|
576
|
+
if (!exprs.length) continue
|
|
577
|
+
const savedLocals = ctx.func.locals
|
|
578
|
+
const facts = analyzeBody(func.body)
|
|
579
|
+
ctx.func.locals = new Map(facts.locals)
|
|
580
|
+
for (const p of func.sig.params) if (!ctx.func.locals.has(p.name)) ctx.func.locals.set(p.name, p.type)
|
|
581
|
+
const localElems = facts[sliceKey]
|
|
582
|
+
ctx.func.locals = savedLocals
|
|
583
|
+
const paramElemMap = paramFactsOf(paramReps, func, field) || new Map()
|
|
584
|
+
const resolveExpr = (expr) => {
|
|
585
|
+
if (typeof expr === 'string') {
|
|
586
|
+
if (localElems.has(expr)) {
|
|
587
|
+
const v = localElems.get(expr)
|
|
588
|
+
if (v != null) return v
|
|
589
|
+
}
|
|
590
|
+
if (paramElemMap.has(expr)) return paramElemMap.get(expr)
|
|
591
|
+
return null
|
|
592
|
+
}
|
|
593
|
+
if (Array.isArray(expr) && expr[0] === '()' && typeof expr[1] === 'string') {
|
|
594
|
+
const f = ctx.func.map?.get(expr[1])
|
|
595
|
+
if (f?.[field] != null) return f[field]
|
|
596
|
+
}
|
|
597
|
+
if (Array.isArray(expr) && expr[0] === '?:') {
|
|
598
|
+
const a = resolveExpr(expr[2]), b = resolveExpr(expr[3])
|
|
599
|
+
return a != null && a === b ? a : null
|
|
600
|
+
}
|
|
601
|
+
if (Array.isArray(expr) && (expr[0] === '&&' || expr[0] === '||')) {
|
|
602
|
+
const a = resolveExpr(expr[1]), b = resolveExpr(expr[2])
|
|
603
|
+
return a != null && a === b ? a : null
|
|
604
|
+
}
|
|
605
|
+
return null
|
|
606
|
+
}
|
|
607
|
+
const v0 = resolveExpr(exprs[0])
|
|
608
|
+
if (v0 == null) continue
|
|
609
|
+
if (!exprs.every(e => resolveExpr(e) === v0)) continue
|
|
610
|
+
func[field] = v0
|
|
611
|
+
changed = true
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
487
616
|
export default function narrowSignatures(programFacts, ast) {
|
|
488
617
|
const { callSites, valueUsed, paramReps, hasSchemaLiterals } = programFacts
|
|
489
618
|
|
|
@@ -518,29 +647,35 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
518
647
|
return (raw != null && Number.isInteger(raw) && raw >= I32_MIN && raw <= I32_MAX) ? raw : null
|
|
519
648
|
}
|
|
520
649
|
|
|
650
|
+
// Per-call-site inference context for a narrowable callee. null for call sites
|
|
651
|
+
// whose callee is exported / value-used / unknown, or whose caller has no ctx.
|
|
652
|
+
const siteState = (cs) => {
|
|
653
|
+
const { callee, argList, callerFunc } = cs
|
|
654
|
+
const func = ctx.func.map.get(callee)
|
|
655
|
+
if (!func || func.exported || valueUsed.has(callee)) return null
|
|
656
|
+
const ctxEntry = callerCtx.get(callerFunc)
|
|
657
|
+
if (!ctxEntry) return null
|
|
658
|
+
const restIdx = func.rest ? func.sig.params.length - 1 : -1
|
|
659
|
+
const paramFacts = new Map()
|
|
660
|
+
return {
|
|
661
|
+
callee, callerFunc, argList, func, restIdx,
|
|
662
|
+
callerLocals: ctxEntry.callerLocals,
|
|
663
|
+
callerValTypes: ctxEntry.callerValTypes,
|
|
664
|
+
callerParamFacts(key) {
|
|
665
|
+
if (!paramFacts.has(key)) paramFacts.set(key, paramFactsOf(paramReps, callerFunc, key))
|
|
666
|
+
return paramFacts.get(key)
|
|
667
|
+
},
|
|
668
|
+
}
|
|
669
|
+
}
|
|
521
670
|
const runCallsiteLattice = (rules) => {
|
|
522
671
|
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
|
-
}
|
|
672
|
+
const state = siteState(callSites[s])
|
|
673
|
+
if (!state) continue
|
|
674
|
+
const { func, argList } = state
|
|
539
675
|
for (let k = 0; k < func.sig.params.length; k++) {
|
|
540
|
-
const r = ensureParamRep(paramReps, callee, k)
|
|
676
|
+
const r = ensureParamRep(paramReps, state.callee, k)
|
|
541
677
|
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)
|
|
678
|
+
for (const rule of rules) rule.apply(r, argList[k], k, state)
|
|
544
679
|
}
|
|
545
680
|
}
|
|
546
681
|
}
|
|
@@ -568,7 +703,33 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
568
703
|
const pname = state.func.sig.params[k]?.name
|
|
569
704
|
return pname != null ? state.func.defaults?.[pname] : null
|
|
570
705
|
}
|
|
571
|
-
|
|
706
|
+
// Hard consensus val for (funcName, param k): the kind every live call site
|
|
707
|
+
// agrees on, or null if any site is untyped / missing / disagrees. The shared
|
|
708
|
+
// `val` lattice runs SOFT (a value can come from typed sites alone, untyped
|
|
709
|
+
// sites skipped); a consumer that *mutates the signature* off val must instead
|
|
710
|
+
// ask this — it re-folds the sites HARD so it never specializes a param that
|
|
711
|
+
// some call site can't prove. (applyPointerParamAbi is that consumer.)
|
|
712
|
+
const hardParamVal = (funcName, k) => {
|
|
713
|
+
let consensus
|
|
714
|
+
for (let s = 0; s < callSites.length; s++) {
|
|
715
|
+
if (callSites[s].callee !== funcName) continue
|
|
716
|
+
const state = siteState(callSites[s])
|
|
717
|
+
if (!state) continue
|
|
718
|
+
if (k >= state.argList.length) return null // missing → undefined at runtime
|
|
719
|
+
const v = inferValAtSite(state.argList[k], state)
|
|
720
|
+
if (v == null) return null // an untyped site ⇒ not specializable
|
|
721
|
+
if (consensus === undefined) consensus = v
|
|
722
|
+
else if (consensus !== v) return null // disagreement ⇒ TOP
|
|
723
|
+
}
|
|
724
|
+
return consensus ?? null
|
|
725
|
+
}
|
|
726
|
+
// `soft` makes apply treat a null inference as BOTTOM (skip — "this site can't
|
|
727
|
+
// tell yet") instead of TOP (poison): the monotone meet. A soft field never
|
|
728
|
+
// needs clearStickyNull; its consumers either re-validate hard (hardParamVal)
|
|
729
|
+
// or read it after a final hard settling sweep. `missing` poisons regardless —
|
|
730
|
+
// an omitted arg with no default is undefined at runtime, a real reason not to
|
|
731
|
+
// specialize, and must stay sticky.
|
|
732
|
+
const mergeRule = (field, infer, soft = false) => ({
|
|
572
733
|
missing(r, k, state) {
|
|
573
734
|
if (r[field] === null) return
|
|
574
735
|
const def = defaultArg(state, k)
|
|
@@ -576,11 +737,19 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
576
737
|
else r[field] = null
|
|
577
738
|
},
|
|
578
739
|
apply(r, arg, k, state) {
|
|
579
|
-
if (r[field]
|
|
740
|
+
if (r[field] === null) return
|
|
741
|
+
const v = infer(arg, k, state)
|
|
742
|
+
if (v == null) { if (!soft) r[field] = null; return }
|
|
743
|
+
mergeParamFact(r, field, v)
|
|
580
744
|
},
|
|
581
745
|
})
|
|
582
746
|
const runFixpoint = () => runCallsiteLattice([
|
|
583
|
-
|
|
747
|
+
// val runs SOFT (monotone): a TYPED param's val only becomes inferable after the
|
|
748
|
+
// typedCtor fixpoint + pointer-ABI enrichment, so an early hard merge would
|
|
749
|
+
// sticky-poison it (the old clearStickyNull undid that). Soft leaves it BOTTOM;
|
|
750
|
+
// the post-enrichment rerun fills it in. applyPointerParamAbi re-validates via
|
|
751
|
+
// hardParamVal; a final hard sweep settles val for emit + late consumers.
|
|
752
|
+
mergeRule('val', (arg, _k, state) => inferValAtSite(arg, state), true),
|
|
584
753
|
{
|
|
585
754
|
missing: poison('wasm'),
|
|
586
755
|
apply(r, arg, _k, state) {
|
|
@@ -620,6 +789,16 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
620
789
|
}
|
|
621
790
|
const runArrFixpoint = () => runArrElemFixpoint('arrayElemSchema', inferArrElemSchema, phase.callerElems('arrElemSchemas'))
|
|
622
791
|
const runArrValTypeFixpoint = () => runArrElemFixpoint('arrayElemValType', inferArrElemValType, phase.callerElems('arrElemValTypes'))
|
|
792
|
+
|
|
793
|
+
// E2 (VAL-kind result inference) FIRST: it's body-driven and call-chain self-
|
|
794
|
+
// fixpointing — independent of the param lattice and the narrowing acts (it reads
|
|
795
|
+
// analyzeBody valTypes + callees' valResult, never paramReps or sig.params). Running
|
|
796
|
+
// it up front means a call arg like `initRows()` resolves to its VAL.ARRAY result on
|
|
797
|
+
// the param fixpoint's FIRST pass, so val/schemaId never get the can't-tell-yet
|
|
798
|
+
// poison that clearStickyNull used to un-stick (root B). Numeric (i32) result
|
|
799
|
+
// narrowing stays below — it benefits from i32 params being narrowed first.
|
|
800
|
+
const funcsWithNarrowableResult = narrowableFuncs(valueUsed)
|
|
801
|
+
narrowValResults(funcsWithNarrowableResult)
|
|
623
802
|
runFixpoint()
|
|
624
803
|
runFixpoint()
|
|
625
804
|
|
|
@@ -645,14 +824,12 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
645
824
|
// (aux carries element-type, handled separately by applyTypedPointerParamAbi).
|
|
646
825
|
// - exclude params with defaults (nullish sentinel needs the f64 NaN space).
|
|
647
826
|
// - exclude rest position (array pack/unpack stays f64).
|
|
648
|
-
applyPointerParamAbi(paramReps, valueUsed)
|
|
827
|
+
applyPointerParamAbi(paramReps, valueUsed, hardParamVal)
|
|
649
828
|
|
|
650
|
-
// E
|
|
651
|
-
//
|
|
652
|
-
//
|
|
653
|
-
const funcsWithNarrowableResult = narrowableFuncs(valueUsed)
|
|
829
|
+
// E: numeric (i32) result narrowing — kept here, after applyI32ParamSpecialization,
|
|
830
|
+
// so a body returning `param + 1` sees param already narrowed to i32. (E2 / VAL
|
|
831
|
+
// result inference ran up front — see above.) funcsWithNarrowableResult hoisted there.
|
|
654
832
|
narrowI32Results(funcsWithNarrowableResult)
|
|
655
|
-
narrowValResults(funcsWithNarrowableResult)
|
|
656
833
|
|
|
657
834
|
// Now that E2 set `valResult` on funcs, narrow per-func `arrayElemSchema` for
|
|
658
835
|
// VAL.ARRAY-returning funcs (via push observations + call chains). Then re-run the
|
|
@@ -673,11 +850,9 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
673
850
|
// observation upgrade `undefined` → NUMBER without poisoning earlier
|
|
674
851
|
// monomorphic observations.
|
|
675
852
|
if (hasSchemaLiterals) observeProgramSlots(ast)
|
|
676
|
-
//
|
|
677
|
-
//
|
|
678
|
-
//
|
|
679
|
-
clearStickyNull(paramReps, 'val')
|
|
680
|
-
clearStickyNull(paramReps, 'schemaId')
|
|
853
|
+
// Re-run with refreshed callerValTypes + the new program-slot observations. (No
|
|
854
|
+
// clearStickyNull needed: valResult was known before the first pass — see E2 hoist
|
|
855
|
+
// above — so val/schemaId never got the can't-tell-yet poison this used to undo.)
|
|
681
856
|
runFixpoint()
|
|
682
857
|
// Now that .val is refreshed, dedicated arr-elem-schema fixpoint.
|
|
683
858
|
runArrFixpoint()
|
|
@@ -720,7 +895,7 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
720
895
|
// i32 offset (with ptrAux carrying the elem-type bits). Eliminates the
|
|
721
896
|
// per-read `i32.wrap_i64 (i64.reinterpret_f64 (local.get $arr))` unbox dance
|
|
722
897
|
// that today dominates hot loops dominated by typed-array indexing.
|
|
723
|
-
// Call sites coerce via
|
|
898
|
+
// Call sites coerce via coerceArg → ptrOffsetIR(arg, VAL.TYPED).
|
|
724
899
|
// Safety: same exclusions as the OBJECT/SET/MAP/BUFFER narrowing above —
|
|
725
900
|
// exported, value-used, raw, defaults, rest position.
|
|
726
901
|
applyTypedPointerParamAbi(paramReps, valueUsed)
|
|
@@ -728,11 +903,11 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
728
903
|
// H: Post-F/G re-fixpoint — propagates VAL kinds through bimorphic call sites
|
|
729
904
|
// where ptrKind narrowed but ptrAux disagreed (e.g. `sum(f64arr)` and `sum(i32arr)`
|
|
730
905
|
// → both VAL.TYPED, different ctors). Without this, callerValTypes carries no entry
|
|
731
|
-
// for caller's params, so inferValType
|
|
732
|
-
// sticky
|
|
733
|
-
//
|
|
906
|
+
// for caller's params, so inferValType returned null and (under the old hard merge)
|
|
907
|
+
// sticky-poisoned the param's val. The soft val merge leaves it BOTTOM instead, so
|
|
908
|
+
// this rerun — now that enrichment has put VAL.TYPED into callerValTypes — simply
|
|
909
|
+
// fills it in (array.js then skips __is_str_key + __str_idx dispatch on `arr[i]`).
|
|
734
910
|
enrichCallerValTypesFromPointerParams(callerCtx)
|
|
735
|
-
clearStickyNull(paramReps, 'val')
|
|
736
911
|
runFixpoint()
|
|
737
912
|
|
|
738
913
|
// I: Post-E re-narrow of numeric (i32) params. The first numeric narrowing pass
|
|
@@ -750,6 +925,12 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
750
925
|
// so the refreshed exprType view propagates.
|
|
751
926
|
resetParamWasmFacts(paramReps)
|
|
752
927
|
runFixpoint()
|
|
928
|
+
// Settle val HARD now that every producer (results, typedCtor, enrichment) has run
|
|
929
|
+
// and the soft lattice has converged: re-fold each param's sites and poison any
|
|
930
|
+
// whose val isn't unanimous (a site left BOTTOM = genuinely untyped). After this,
|
|
931
|
+
// r.val is sound for emit + the late/post-return consumers (applyI32ParamSpecial-
|
|
932
|
+
// ization's skipTyped guard, specializeBimorphicTyped) — which read it directly.
|
|
933
|
+
runCallsiteLattice([mergeRule('val', (arg, _k, state) => inferValAtSite(arg, state))])
|
|
753
934
|
// Don't steal typed-array params from specializeBimorphicTyped: F phase parks
|
|
754
935
|
// bimorphic typed params at type='f64' with sticky-null typedCtor (two distinct
|
|
755
936
|
// ctors at call sites). Their callers post-F pass them as i32 (pointer ABI),
|
|
@@ -764,9 +945,12 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
764
945
|
if (jsstringEnabled()) applyJsstringBoundaryCarrier(paramReps, valueUsed)
|
|
765
946
|
}
|
|
766
947
|
|
|
767
|
-
/** Gate the jsstring
|
|
768
|
-
*
|
|
769
|
-
* `
|
|
948
|
+
/** Gate the jsstring carrier on the host. ON by default for the JS host: a
|
|
949
|
+
* js-host build is already JS-locked (it imports `env.*`), so the externref +
|
|
950
|
+
* `wasm:js-string` carrier's JS dependency is free there, and the zero-copy
|
|
951
|
+
* string-read path is a clear win. OFF under WASI: the carrier needs a JS host,
|
|
952
|
+
* and wasi builds must stay portable (wasmtime/Go/Rust). Opt out on JS with
|
|
953
|
+
* `optimize: { jsstring: false }` (e.g. side-by-side benchmarks). */
|
|
770
954
|
function jsstringEnabled() {
|
|
771
955
|
if (ctx.transform.host === 'wasi') return false
|
|
772
956
|
if (ctx.transform.optimize?.jsstring === false) return false
|
|
@@ -782,13 +966,15 @@ export function applyJsstringBoundaryCarrierStandalone(programFacts) {
|
|
|
782
966
|
}
|
|
783
967
|
|
|
784
968
|
/**
|
|
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
|
-
*
|
|
969
|
+
* Body-local boolean/bigint-result inference. `narrowValResults` is the general
|
|
970
|
+
* (any VAL.*) pass, but it lives inside whole-program narrowing, which is skipped
|
|
971
|
+
* for trivial leaf modules (no call sites). Boolean and bigint are the two kinds
|
|
972
|
+
* whose internal carrier differs from the host-boundary carrier — bool rides a 0/1
|
|
973
|
+
* number internally but crosses as the TRUE_NAN/FALSE_NAN atom; bigint rides an
|
|
974
|
+
* i64-reinterpreted f64 internally but must cross as a real Number — so an exported
|
|
975
|
+
* `(a) => a > 2` or `() => 100n` still needs its boundary thunk even on the skip path.
|
|
976
|
+
* This pass only ever *sets* valResult to VAL.BOOL / VAL.BIGINT, so it is safe to run
|
|
977
|
+
* unconditionally — pointer/array/number results are untouched.
|
|
792
978
|
*/
|
|
793
979
|
export function narrowBoolResults() {
|
|
794
980
|
for (const func of ctx.func.list) {
|
|
@@ -803,6 +989,7 @@ export function narrowBoolResults() {
|
|
|
803
989
|
? (localValTypes?.get(e) || ctx.scope.globalValTypes?.get(e) || null)
|
|
804
990
|
: valTypeOf(e)
|
|
805
991
|
if (exprs.every(e => vt(e) === VAL.BOOL)) func.valResult = VAL.BOOL
|
|
992
|
+
else if (exprs.every(e => vt(e) === VAL.BIGINT)) func.valResult = VAL.BIGINT
|
|
806
993
|
}
|
|
807
994
|
}
|
|
808
995
|
|
|
@@ -828,35 +1015,37 @@ const JSS_OK_PROPS = new Set(['length', 'charCodeAt'])
|
|
|
828
1015
|
* `.charCodeAt` whose callee node lives in `safeCC` (provably bounded).
|
|
829
1016
|
* Reassignment / `++` / `--` / closure capture all reject conservatively.
|
|
830
1017
|
*
|
|
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.
|
|
1018
|
+
* Returns `{ ok, stringDiscriminating, reason? }` — `stringDiscriminating` is
|
|
1019
|
+
* true iff we saw at least one string-only use (`.charCodeAt`); `reason` names
|
|
1020
|
+
* the first blocking use when `ok` is false.
|
|
835
1021
|
*/
|
|
836
1022
|
function paramAllUsesJsstringMappable(body, name, safeCC) {
|
|
837
|
-
if (body == null) return { ok: false, stringDiscriminating: false }
|
|
1023
|
+
if (body == null) return { ok: false, stringDiscriminating: false, reason: null }
|
|
838
1024
|
let ok = true
|
|
839
1025
|
let stringDiscriminating = false
|
|
1026
|
+
let reason = null
|
|
1027
|
+
const fail = (msg) => { ok = false; reason ||= msg }
|
|
1028
|
+
const refsParam = (node) => {
|
|
1029
|
+
if (node === name) return true
|
|
1030
|
+
if (!Array.isArray(node)) return false
|
|
1031
|
+
for (let i = 1; i < node.length; i++) if (refsParam(node[i])) return true
|
|
1032
|
+
return false
|
|
1033
|
+
}
|
|
840
1034
|
const walk = (node) => {
|
|
841
1035
|
if (!ok) return
|
|
842
1036
|
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
|
|
1037
|
+
if (node === name) fail('bare use of the string param disables the zero-copy externref boundary carrier')
|
|
847
1038
|
return
|
|
848
1039
|
}
|
|
849
1040
|
if (!Array.isArray(node)) return
|
|
850
1041
|
const op = node[0]
|
|
851
1042
|
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
1043
|
const params = node[1]
|
|
855
1044
|
const shadowed = Array.isArray(params)
|
|
856
1045
|
? params.some(p => (typeof p === 'string' && p === name) ||
|
|
857
1046
|
(Array.isArray(p) && p[1] === name))
|
|
858
1047
|
: params === name
|
|
859
|
-
if (!shadowed)
|
|
1048
|
+
if (!shadowed) fail('closure capture of the string param disables the zero-copy externref boundary carrier')
|
|
860
1049
|
return
|
|
861
1050
|
}
|
|
862
1051
|
if ((op === '=' || op === '+=' || op === '-=' || op === '*=' || op === '/=' ||
|
|
@@ -864,26 +1053,23 @@ function paramAllUsesJsstringMappable(body, name, safeCC) {
|
|
|
864
1053
|
op === '>>=' || op === '<<=' || op === '>>>=' ||
|
|
865
1054
|
op === '||=' || op === '&&=' || op === '??=' ||
|
|
866
1055
|
op === '++' || op === '--') && node[1] === name) {
|
|
867
|
-
|
|
1056
|
+
fail('reassigning the string param disables the zero-copy externref boundary carrier')
|
|
1057
|
+
return
|
|
1058
|
+
}
|
|
1059
|
+
if (op === '+' && node.slice(1).some(arg => refsParam(arg))) {
|
|
1060
|
+
fail('string concatenation on the param disables the zero-copy externref boundary carrier')
|
|
868
1061
|
return
|
|
869
1062
|
}
|
|
870
1063
|
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
1064
|
if (node[2] === 'length') return
|
|
876
1065
|
if (safeCC.has(node)) { stringDiscriminating = true; return }
|
|
877
|
-
|
|
1066
|
+
fail(`\`.${node[2]}\` on the string param disables the zero-copy externref boundary carrier`)
|
|
878
1067
|
return
|
|
879
1068
|
}
|
|
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
1069
|
for (let i = 1; i < node.length; i++) walk(node[i])
|
|
884
1070
|
}
|
|
885
1071
|
walk(body)
|
|
886
|
-
return { ok, stringDiscriminating }
|
|
1072
|
+
return { ok, stringDiscriminating, reason }
|
|
887
1073
|
}
|
|
888
1074
|
|
|
889
1075
|
function applyJsstringBoundaryCarrier(paramReps, valueUsed) {
|
|
@@ -919,13 +1105,129 @@ function applyJsstringBoundaryCarrier(paramReps, valueUsed) {
|
|
|
919
1105
|
if (!stringDiscriminating && r?.val !== VAL.STRING && !hasStringDefault) continue
|
|
920
1106
|
p.type = 'externref'
|
|
921
1107
|
p.jsstring = true
|
|
922
|
-
|
|
923
|
-
// (the JS-side interop wrapper applies the default on `undefined`).
|
|
1108
|
+
updateRep(p.name, { carrier: 'jsstring', val: VAL.STRING })
|
|
924
1109
|
if (hasStringDefault) p.jsstringDefault = defVal[1]
|
|
925
1110
|
}
|
|
926
1111
|
}
|
|
927
1112
|
}
|
|
928
1113
|
|
|
1114
|
+
/** Soft warnings when a string param could use the externref carrier but doesn't. */
|
|
1115
|
+
export function adviseJsstringCarrier(paramReps, valueUsed) {
|
|
1116
|
+
if (!ctx.warnings || !jsstringEnabled()) return
|
|
1117
|
+
|
|
1118
|
+
for (const func of ctx.func.list) {
|
|
1119
|
+
if (func.raw || !func.exported || !func.body || func.rest) continue
|
|
1120
|
+
if (valueUsed?.has(func.name)) continue
|
|
1121
|
+
|
|
1122
|
+
const safeCC = new Set()
|
|
1123
|
+
scanBoundedLoops(func.body, safeCC)
|
|
1124
|
+
const reps = paramReps?.get(func.name)
|
|
1125
|
+
|
|
1126
|
+
for (let k = 0; k < func.sig.params.length; k++) {
|
|
1127
|
+
const p = func.sig.params[k]
|
|
1128
|
+
if (p.jsstring) continue
|
|
1129
|
+
if (p.type !== 'f64' || p.ptrKind != null) continue
|
|
1130
|
+
|
|
1131
|
+
const defVal = func.defaults?.[p.name]
|
|
1132
|
+
if (defVal != null && !isLiteralStr(defVal)) continue
|
|
1133
|
+
|
|
1134
|
+
const r = reps?.get(k)
|
|
1135
|
+
if (r && r.val != null && r.val !== VAL.STRING) continue
|
|
1136
|
+
|
|
1137
|
+
const hasStringDefault = defVal != null && isLiteralStr(defVal)
|
|
1138
|
+
const { ok: usesOk, stringDiscriminating, reason } =
|
|
1139
|
+
paramAllUsesJsstringMappable(func.body, p.name, safeCC)
|
|
1140
|
+
const isCandidate = stringDiscriminating || r?.val === VAL.STRING || hasStringDefault
|
|
1141
|
+
if (!isCandidate || usesOk) continue
|
|
1142
|
+
|
|
1143
|
+
warn('jsstring-declined',
|
|
1144
|
+
`export '${func.name}' param '${p.name}': ${reason || 'string param uses disable the zero-copy externref boundary carrier'}`,
|
|
1145
|
+
{ fn: func.name }, func.body.loc)
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
// Two value-kinds CONFLICT when passing one where the other is expected would
|
|
1151
|
+
// rely on a JS boundary coercion jz does not implement (so the result diverges).
|
|
1152
|
+
// NUMBER↔STRING is the canonical pair: `"5" * 2` is 10 in JS but NaN in jz, and a
|
|
1153
|
+
// STRING passed to a numeric param reads its NaN-boxed bits as an f64. ARRAY/OBJECT
|
|
1154
|
+
// vs NUMBER/STRING likewise. We treat the four primitive-ish kinds as mutually
|
|
1155
|
+
// exclusive; BOOL is omitted (it nanboxes to 0/1 and numeric code tolerates it).
|
|
1156
|
+
const STRICT_CONFLICT = {
|
|
1157
|
+
[VAL.NUMBER]: new Set([VAL.STRING, VAL.ARRAY, VAL.OBJECT]),
|
|
1158
|
+
[VAL.STRING]: new Set([VAL.NUMBER, VAL.ARRAY, VAL.OBJECT]),
|
|
1159
|
+
[VAL.ARRAY]: new Set([VAL.NUMBER, VAL.STRING]),
|
|
1160
|
+
[VAL.OBJECT]: new Set([VAL.NUMBER, VAL.STRING]),
|
|
1161
|
+
}
|
|
1162
|
+
const kindName = (v) => ({ [VAL.NUMBER]: 'number', [VAL.STRING]: 'string', [VAL.ARRAY]: 'array', [VAL.OBJECT]: 'object' }[v] || 'value')
|
|
1163
|
+
|
|
1164
|
+
/**
|
|
1165
|
+
* Strict-mode boundary type check (standalone; runs in both the full-narrow and
|
|
1166
|
+
* skip-narrow plan paths).
|
|
1167
|
+
*
|
|
1168
|
+
* jz infers a param's type from how it's used (`x * 2` → number, `s.charCodeAt`
|
|
1169
|
+
* → string) or from an explicit default (`x = 0` → number). In permissive mode a
|
|
1170
|
+
* caller may pass any type and jz silently computes a divergent result (`"5"*2`
|
|
1171
|
+
* is 10 in JS, NaN here). Strict mode is the canonical subset where that misuse
|
|
1172
|
+
* is a compile error instead — consistent with strict already rejecting `==`,
|
|
1173
|
+
* dynamic dispatch, and `void`.
|
|
1174
|
+
*
|
|
1175
|
+
* Fires ONLY when BOTH sides are statically certain and conflict:
|
|
1176
|
+
* - the callee param has a known kind (its default-value type, or a settled
|
|
1177
|
+
* `val` rep from body-usage / call-site inference), AND
|
|
1178
|
+
* - the argument expression has a known, conflicting kind (a literal or a
|
|
1179
|
+
* locally-typed binding).
|
|
1180
|
+
* An untyped param or untyped arg is never flagged — no false positives on
|
|
1181
|
+
* genuinely polymorphic code.
|
|
1182
|
+
*/
|
|
1183
|
+
export function strictBoundaryTypeCheck(programFacts) {
|
|
1184
|
+
if (!ctx.transform.strict) return
|
|
1185
|
+
const { callSites, paramReps } = programFacts
|
|
1186
|
+
|
|
1187
|
+
// Per-callee body-evidence cache: methodEvidence (.charCodeAt→STRING, .push→
|
|
1188
|
+
// ARRAY) keyed by param name. Computed lazily, once per callee.
|
|
1189
|
+
const bodyEvidence = new Map()
|
|
1190
|
+
const evidenceOf = (func) => {
|
|
1191
|
+
if (!bodyEvidence.has(func.name)) {
|
|
1192
|
+
const names = func.sig.params.map(p => p.name).filter(Boolean)
|
|
1193
|
+
bodyEvidence.set(func.name, func.body ? inferParams(func.body, names) : new Map())
|
|
1194
|
+
}
|
|
1195
|
+
return bodyEvidence.get(func.name)
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// Expected kind of callee param k, from any statically-certain source:
|
|
1199
|
+
// 1. explicit default value type — the source's own declaration (x = 0 → number)
|
|
1200
|
+
// 2. settled call-site/usage rep `val` (paramReps; present after full narrow)
|
|
1201
|
+
// 3. type-exclusive body evidence (methodEvidence; works in skip-narrow path too)
|
|
1202
|
+
// First certain source wins; null when the param is genuinely polymorphic.
|
|
1203
|
+
const paramKind = (func, k) => {
|
|
1204
|
+
const p = func.sig.params[k]
|
|
1205
|
+
if (!p) return null
|
|
1206
|
+
const def = func.defaults?.[p.name]
|
|
1207
|
+
if (def != null) { const dv = valTypeOf(def); if (dv) return dv }
|
|
1208
|
+
const repVal = paramReps?.get(func.name)?.get(k)?.val
|
|
1209
|
+
if (repVal != null) return repVal
|
|
1210
|
+
return evidenceOf(func).get(p.name)?.val ?? null
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
for (const cs of callSites) {
|
|
1214
|
+
const func = ctx.func.map.get(cs.callee)
|
|
1215
|
+
if (!func || func.raw || !func.sig) continue
|
|
1216
|
+
if (func.rest) continue // rest packs args into an array
|
|
1217
|
+
for (let k = 0; k < cs.argList.length && k < func.sig.params.length; k++) {
|
|
1218
|
+
const want = paramKind(func, k)
|
|
1219
|
+
if (want == null) continue
|
|
1220
|
+
const conflicts = STRICT_CONFLICT[want]
|
|
1221
|
+
if (!conflicts) continue
|
|
1222
|
+
const got = valTypeOf(cs.argList[k])
|
|
1223
|
+
if (got != null && conflicts.has(got)) {
|
|
1224
|
+
const pname = func.sig.params[k].name
|
|
1225
|
+
err(`strict mode: ${kindName(want)} parameter '${pname}' of '${cs.callee}' received a ${kindName(got)} argument — jz does not coerce ${kindName(got)}→${kindName(want)} at the call boundary (the result would diverge from JS). Pass a ${kindName(want)}, or { strict: false }.`)
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
|
|
929
1231
|
/**
|
|
930
1232
|
* Phase: bimorphic typed-array param specialization.
|
|
931
1233
|
*
|
|
@@ -960,13 +1262,9 @@ export function specializeBimorphicTyped(programFacts) {
|
|
|
960
1262
|
if (list) list.push(cs); else sitesByCallee.set(cs.callee, [cs])
|
|
961
1263
|
}
|
|
962
1264
|
|
|
963
|
-
// Per-caller typedElem map
|
|
964
|
-
|
|
965
|
-
callerTypedCtx
|
|
966
|
-
for (const func of ctx.func.list) {
|
|
967
|
-
if (!func.body || func.raw) continue
|
|
968
|
-
callerTypedCtx.set(func, analyzeBody(func.body).typedElems)
|
|
969
|
-
}
|
|
1265
|
+
// Per-caller typedElem map: body-local `new TypedArray(N)` bindings layered
|
|
1266
|
+
// over the module's typed globals (shared with buildCallerTypedCtx).
|
|
1267
|
+
const callerTypedCtx = buildCallerTypedCtx()
|
|
970
1268
|
// Per-caller typed-param map: caller's own params that F/G already narrowed
|
|
971
1269
|
// (so transitive `sum(arr)` inside a func that took `arr` from above resolves).
|
|
972
1270
|
const callerTypedParamsCtx = new Map()
|
|
@@ -1142,7 +1440,12 @@ export function refineDynKeys(programFacts) {
|
|
|
1142
1440
|
if (!NON_DYN_VTS.has(vt)) real = true
|
|
1143
1441
|
}
|
|
1144
1442
|
} else if (op === 'for-in') real = true
|
|
1145
|
-
|
|
1443
|
+
// Recurse into nested arrows too. Closures stay inline (defFunc skips
|
|
1444
|
+
// depth>0), so a dynamic-key access captured in one — e.g. `handlers[op]`
|
|
1445
|
+
// in a dispatch closure — is reachable only through its parent's body.
|
|
1446
|
+
// Matches collectProgramFacts, which also crosses arrows when setting
|
|
1447
|
+
// anyDyn; not crossing here let refineDynKeys reset a flag the initial scan
|
|
1448
|
+
// correctly raised. Monotone-safe: extra visits only ever raise `real`.
|
|
1146
1449
|
for (let i = 1; i < node.length; i++) visit(typeMap, node[i])
|
|
1147
1450
|
}
|
|
1148
1451
|
|