jz 0.6.0 → 0.8.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 +99 -72
- package/bench/README.md +253 -100
- package/bench/bench.svg +58 -81
- package/cli.js +85 -12
- package/dist/interop.js +1 -0
- package/dist/jz.js +6196 -0
- package/index.d.ts +126 -0
- package/index.js +222 -35
- package/interop.js +240 -141
- package/layout.js +34 -18
- package/module/array.js +99 -111
- package/module/collection.js +124 -30
- package/module/console.js +1 -1
- package/module/core.js +163 -19
- package/module/json.js +3 -3
- package/module/math.js +162 -3
- package/module/number.js +268 -13
- package/module/object.js +37 -5
- package/module/regex.js +8 -7
- package/module/simd.js +37 -5
- package/module/string.js +203 -168
- package/module/typedarray.js +565 -112
- package/package.json +26 -7
- package/src/abi/string.js +29 -29
- package/src/ast.js +19 -2
- package/src/autoload.js +3 -0
- package/src/compile/analyze-scans.js +174 -11
- package/src/compile/analyze.js +101 -4
- package/src/compile/cse-load.js +200 -0
- package/src/compile/emit-assign.js +82 -20
- package/src/compile/emit.js +592 -51
- package/src/compile/index.js +504 -89
- package/src/compile/infer.js +36 -1
- package/src/compile/loop-divmod.js +109 -0
- package/src/compile/loop-model.js +91 -0
- package/src/compile/loop-square.js +102 -0
- package/src/compile/narrow.js +275 -41
- package/src/compile/peel-stencil.js +215 -0
- package/src/compile/plan/advise.js +55 -1
- package/src/compile/plan/common.js +29 -0
- package/src/compile/plan/index.js +8 -1
- package/src/compile/plan/inline.js +180 -22
- package/src/compile/plan/literals.js +313 -24
- package/src/compile/plan/scope.js +115 -39
- package/src/compile/program-facts.js +21 -2
- package/src/ctx.js +96 -19
- package/src/helper-counters.js +137 -0
- package/src/ir.js +157 -20
- package/src/kind-traits.js +34 -3
- package/src/kind.js +79 -4
- package/src/op-policy.js +5 -2
- package/src/ops.js +119 -0
- package/src/optimize/index.js +1274 -151
- package/src/optimize/recurse.js +182 -0
- package/src/optimize/vectorize.js +4187 -253
- package/src/prepare/index.js +75 -14
- package/src/prepare/lift-iife.js +149 -0
- package/src/reps.js +6 -2
- package/src/static.js +9 -0
- package/src/type.js +63 -51
- package/src/wat/assemble.js +286 -21
- package/src/widen.js +21 -0
- package/src/wat/optimize.js +0 -3760
package/src/compile/narrow.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* function `sig` records change.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { ctx, warn } from '../ctx.js'
|
|
10
|
+
import { ctx, warn, err } from '../ctx.js'
|
|
11
11
|
import { isBlockBody, alwaysReturns, hasBareReturn, returnExprs } from '../ast.js'
|
|
12
12
|
import { isLiteralStr, I32_MIN, I32_MAX } from '../ir.js'
|
|
13
13
|
import {
|
|
@@ -24,10 +24,14 @@ import {
|
|
|
24
24
|
} from '../param-reps.js'
|
|
25
25
|
import {
|
|
26
26
|
inferArrElemSchema, inferArrElemValType,
|
|
27
|
-
inferSchemaId, inferValType, inferTypedCtor,
|
|
27
|
+
inferSchemaId, inferValType, inferTypedCtor, inferParams,
|
|
28
28
|
} from './infer.js'
|
|
29
29
|
|
|
30
30
|
const PTR_ABI_KINDS = new Set([VAL.OBJECT, VAL.SET, VAL.MAP, VAL.BUFFER])
|
|
31
|
+
// Integer-preserving ops: an expr over integers stays integer (ToInt32-consistent) through these.
|
|
32
|
+
// Excludes /, %, ** (fractional). Used to recognize a recursive arg whose i32-ness follows from
|
|
33
|
+
// its inputs' i32-ness (`f(n - 1)`), so it carries no independent type evidence.
|
|
34
|
+
const RECUR_INT_OPS = new Set(['+', '-', '*', 'u-', 'u+', '&', '|', '^', '<<', '>>', '>>>', '~'])
|
|
31
35
|
|
|
32
36
|
|
|
33
37
|
function filterLiveCallSites(callSites, valueUsed) {
|
|
@@ -156,12 +160,33 @@ function refreshCallerValTypes(callerCtx) {
|
|
|
156
160
|
}
|
|
157
161
|
}
|
|
158
162
|
|
|
163
|
+
// Per-caller typed-elem context: the caller's body-local typed arrays, layered
|
|
164
|
+
// over the module's typed-array globals so a call like `f(globalArr)` resolves
|
|
165
|
+
// `globalArr`'s ctor (inferTypedCtor reads only this map for a bare-name arg).
|
|
166
|
+
// A global is visible UNLESS the caller shadows the name with a param or local
|
|
167
|
+
// of its own — only then could the name denote a non-typed value. Globals are
|
|
168
|
+
// sound to consult: globalTypedElem holds a name only when EVERY assignment to
|
|
169
|
+
// it is the same single typed-array ctor (scope.js invalidates on any conflict),
|
|
170
|
+
// so it can't denote a different kind at the call site.
|
|
171
|
+
function callerTypedElemsFor(func, globalTE) {
|
|
172
|
+
const facts = analyzeBody(func.body)
|
|
173
|
+
const local = facts.typedElems
|
|
174
|
+
if (!globalTE.size) return local
|
|
175
|
+
const shadowed = new Set(facts.locals.keys())
|
|
176
|
+
for (const p of func.sig?.params || []) shadowed.add(p.name)
|
|
177
|
+
const merged = new Map()
|
|
178
|
+
for (const [k, v] of globalTE) if (!shadowed.has(k)) merged.set(k, v)
|
|
179
|
+
for (const [k, v] of local) merged.set(k, v) // local typed binding shadows the global
|
|
180
|
+
return merged
|
|
181
|
+
}
|
|
182
|
+
|
|
159
183
|
function buildCallerTypedCtx() {
|
|
160
184
|
const callerTypedCtx = new Map()
|
|
161
|
-
|
|
185
|
+
const globalTE = ctx.scope.globalTypedElem || new Map()
|
|
186
|
+
callerTypedCtx.set(null, globalTE)
|
|
162
187
|
for (const func of ctx.func.list) {
|
|
163
188
|
if (!func.body || func.raw) continue
|
|
164
|
-
callerTypedCtx.set(func,
|
|
189
|
+
callerTypedCtx.set(func, callerTypedElemsFor(func, globalTE))
|
|
165
190
|
}
|
|
166
191
|
return callerTypedCtx
|
|
167
192
|
}
|
|
@@ -203,6 +228,7 @@ function enrichCallerValTypesFromPointerParams(callerCtx) {
|
|
|
203
228
|
}
|
|
204
229
|
|
|
205
230
|
function refreshCallerLocals(callerCtx) {
|
|
231
|
+
const prevTE = ctx.types.typedElem
|
|
206
232
|
for (const func of ctx.func.list) {
|
|
207
233
|
if (!func.body || func.raw) continue
|
|
208
234
|
// Seed pointer-narrowed params' val-kind so analyzeBody recognises e.g.
|
|
@@ -212,13 +238,24 @@ function refreshCallerLocals(callerCtx) {
|
|
|
212
238
|
// (heapsort→siftDown's `end`). analyzeFuncForEmit re-seeds + re-invalidates at
|
|
213
239
|
// emit time, so this transient localReps doesn't leak past narrowing.
|
|
214
240
|
ctx.func.localReps = new Map()
|
|
215
|
-
|
|
241
|
+
// Seed the typedElem overlay with this func's TYPED-pointer params (element ctor from
|
|
242
|
+
// ptrAux), exactly as analyzeFuncForEmit does at emit time. Without it, a local bound to
|
|
243
|
+
// an integer typed-array PARAM element — `aa = perm[perm[X]+Y]` (noise), perm an Int32
|
|
244
|
+
// pointer param — types f64 here, so a callee fed it (`grad(aa,…)`, used only as `aa&3`)
|
|
245
|
+
// never narrows its param to i32. Mirrors emit so narrow-time callerLocals agree with it.
|
|
246
|
+
const te = ctx.scope.globalTypedElem ? new Map(ctx.scope.globalTypedElem) : new Map()
|
|
247
|
+
for (const p of func.sig.params) {
|
|
248
|
+
if (p.ptrKind != null) ctx.func.localReps.set(p.name, { val: p.ptrKind })
|
|
249
|
+
if (p.ptrKind === VAL.TYPED && p.ptrAux != null) { const c = ctorFromElemAux(p.ptrAux); if (c != null) te.set(p.name, c) }
|
|
250
|
+
}
|
|
251
|
+
ctx.types.typedElem = te
|
|
216
252
|
invalidateLocalsCache(func.body)
|
|
217
253
|
const fresh = analyzeBody(func.body).locals
|
|
218
254
|
for (const p of func.sig.params) if (!fresh.has(p.name)) fresh.set(p.name, p.type)
|
|
219
255
|
callerCtx.get(func).callerLocals = fresh
|
|
220
256
|
}
|
|
221
257
|
ctx.func.localReps = null
|
|
258
|
+
ctx.types.typedElem = prevTE
|
|
222
259
|
}
|
|
223
260
|
|
|
224
261
|
function resetParamWasmFacts(paramReps) {
|
|
@@ -260,6 +297,35 @@ function narrowI32Results(funcs) {
|
|
|
260
297
|
e[0] === '>>>' ||
|
|
261
298
|
(e[0] === '()' && typeof e[1] === 'string' && ctx.func.map?.get(e[1])?.sig?.unsignedResult === true)
|
|
262
299
|
)
|
|
300
|
+
const callsSelf = (n, name) => Array.isArray(n) && ((n[0] === '()' && n[1] === name) || n.some(c => callsSelf(c, name)))
|
|
301
|
+
// Classify a func's return tails as all-v128 / all-i32 (+ sign) under the CURRENT sig.results.
|
|
302
|
+
const evalTails = (func, body, exprs) => {
|
|
303
|
+
const savedCurrent = ctx.func.current
|
|
304
|
+
ctx.func.current = func.sig
|
|
305
|
+
const locals = isBlockBody(body) ? analyzeBody(body).locals : new Map()
|
|
306
|
+
for (const p of func.sig.params) if (!locals.has(p.name)) locals.set(p.name, p.type)
|
|
307
|
+
// Seed the typedElem overlay with this func's TYPED-pointer params so a return tail
|
|
308
|
+
// reading a typed-array element — `return vals[h]`, vals an Int32Array param (dict's
|
|
309
|
+
// `lookup`) — types as i32, not NaN-boxed f64. Without it the call site keeps the full
|
|
310
|
+
// __typed_idx/ToNumber unbox dispatch (491520× per dict kernel run). Mirrors
|
|
311
|
+
// refreshCallerLocals + analyzeFuncForEmit. Only meaningful once Phase G has tagged params
|
|
312
|
+
// ptrKind=TYPED (the I2 re-run below); harmless before (no typed params → overlay untouched).
|
|
313
|
+
const savedTE = ctx.types.typedElem
|
|
314
|
+
let te = null
|
|
315
|
+
for (const p of func.sig.params) {
|
|
316
|
+
if (p.ptrKind === VAL.TYPED && p.ptrAux != null) {
|
|
317
|
+
const c = ctorFromElemAux(p.ptrAux)
|
|
318
|
+
if (c != null) { if (!te) te = savedTE ? new Map(savedTE) : new Map(); te.set(p.name, c) }
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (te) ctx.types.typedElem = te
|
|
322
|
+
const allV128 = exprs.every(e => exprType(e, locals) === 'v128')
|
|
323
|
+
const allI32 = !allV128 && exprs.every(e => exprType(e, locals) === 'i32')
|
|
324
|
+
if (te) ctx.types.typedElem = savedTE
|
|
325
|
+
const r = { allV128, allI32, anyUnsigned: exprs.some(isUnsignedTail), allUnsigned: exprs.every(isUnsignedTail) }
|
|
326
|
+
ctx.func.current = savedCurrent
|
|
327
|
+
return r
|
|
328
|
+
}
|
|
263
329
|
let changed = true
|
|
264
330
|
while (changed) {
|
|
265
331
|
changed = false
|
|
@@ -269,22 +335,40 @@ function narrowI32Results(funcs) {
|
|
|
269
335
|
if (isBlockBody(body) && hasBareReturn(body)) continue
|
|
270
336
|
const exprs = returnExprs(body)
|
|
271
337
|
if (!exprs.length) continue
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
338
|
+
let r = evalTails(func, body, exprs)
|
|
339
|
+
// Recursive result cycle: a self-call in a return tail — or feeding a returned local
|
|
340
|
+
// (nqueens' `cnt = cnt + solve(…); return cnt`) — reads solve's own not-yet-narrowed
|
|
341
|
+
// f64 result, so `cnt` widens to f64 and the i32 narrowing never fires. Break the cycle
|
|
342
|
+
// optimistically: tentatively assume the i32 result, re-analyze, and keep it ONLY if every
|
|
343
|
+
// tail is then i32 (else revert). Sound — committed only when self-consistent.
|
|
344
|
+
if (!r.allI32 && !r.allV128 && callsSelf(body, func.name)) {
|
|
345
|
+
const saved = func.sig.results
|
|
346
|
+
func.sig.results = ['i32']
|
|
347
|
+
invalidateLocalsCache(body)
|
|
348
|
+
const opt = evalTails(func, body, exprs)
|
|
349
|
+
if (opt.allI32 && (!opt.anyUnsigned || opt.allUnsigned)) {
|
|
350
|
+
if (opt.allUnsigned) func.sig.unsignedResult = true
|
|
351
|
+
changed = true
|
|
352
|
+
continue
|
|
353
|
+
}
|
|
354
|
+
func.sig.results = saved
|
|
355
|
+
invalidateLocalsCache(body)
|
|
356
|
+
r = evalTails(func, body, exprs)
|
|
357
|
+
}
|
|
281
358
|
// SIMD: every tail returns a lane vector → v128 result.
|
|
282
|
-
if (allV128) {
|
|
359
|
+
if (r.allV128) {
|
|
283
360
|
func.sig.results = ['v128']
|
|
284
361
|
changed = true
|
|
285
|
-
} else if (allI32 && (!anyUnsigned || allUnsigned)) { // sign-consistent i32 tails
|
|
362
|
+
} else if (r.allI32 && (!r.anyUnsigned || r.allUnsigned)) { // sign-consistent i32 tails
|
|
286
363
|
func.sig.results = ['i32']
|
|
287
|
-
if (allUnsigned) func.sig.unsignedResult = true
|
|
364
|
+
if (r.allUnsigned) func.sig.unsignedResult = true
|
|
365
|
+
// A committed i32 result is a genuine NUMBER, so stamp valResult for the call-site
|
|
366
|
+
// VAL dispatch — E2 (narrowValResults) ran ABOVE the param lattice and so couldn't
|
|
367
|
+
// type a `return typedArrayParam[idx]` tail (hashjoin's `probe` → `vals[h]`), leaving
|
|
368
|
+
// valResult unset → the hot `sum + probe()` stayed the polymorphic string-or-number
|
|
369
|
+
// `+`. Only-if-unset: an UNBOXED-pointer i32 result already carries its ARRAY/OBJECT/
|
|
370
|
+
// TYPED valResult (the unboxing ABI needs it), so this never overwrites a pointer kind.
|
|
371
|
+
if (func.valResult == null) func.valResult = VAL.NUMBER
|
|
288
372
|
changed = true
|
|
289
373
|
}
|
|
290
374
|
}
|
|
@@ -652,10 +736,20 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
652
736
|
const state = siteState(callSites[s])
|
|
653
737
|
if (!state) continue
|
|
654
738
|
const { func, argList } = state
|
|
739
|
+
const recursive = state.callee === state.callerFunc?.name
|
|
655
740
|
for (let k = 0; k < func.sig.params.length; k++) {
|
|
656
741
|
const r = ensureParamRep(paramReps, state.callee, k)
|
|
657
742
|
if (k >= argList.length) { for (const rule of rules) rule.missing(r, k, state); continue }
|
|
658
|
-
|
|
743
|
+
const arg = argList[k]
|
|
744
|
+
// Recursive identity arg — `f(…, p, …)` calling itself with its own param p threaded
|
|
745
|
+
// through at the same position — is a fixpoint identity: it carries whatever type p
|
|
746
|
+
// settles to, so it constrains nothing. Skip it, else exprType(p) reads p's not-yet-
|
|
747
|
+
// narrowed f64 and the meet poisons the type the non-recursive call sites would prove
|
|
748
|
+
// (nqueens' `solve(all, …)` — `all` stuck f64 while cols/d1/d2, passed as i32 bitwise
|
|
749
|
+
// exprs, narrowed fine).
|
|
750
|
+
const pname = func.sig.params[k].name
|
|
751
|
+
if (recursive && (arg === pname || (Array.isArray(arg) && arg[0] === 'local.get' && arg[1] === pname))) continue
|
|
752
|
+
for (const rule of rules) rule.apply(r, arg, k, state)
|
|
659
753
|
}
|
|
660
754
|
}
|
|
661
755
|
}
|
|
@@ -723,6 +817,46 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
723
817
|
mergeParamFact(r, field, v)
|
|
724
818
|
},
|
|
725
819
|
})
|
|
820
|
+
// WASM type of a call arg. exprType resolves most shapes, but an INTEGER typed-array
|
|
821
|
+
// element read `intArr[idx]` (and arithmetic over it, `intArr[idx]+1`) types f64 here:
|
|
822
|
+
// exprType's `[]` rule reads the typedElem OVERLAY, which doesn't see a typedCtor-narrowed
|
|
823
|
+
// PARAM array at fixpoint time — yet the element is a 32-bit machine integer. Install the
|
|
824
|
+
// caller's resolved param-typedCtors (+ module globals) as that overlay for the duration
|
|
825
|
+
// of the type query, so a param fed only such integer elements (dict's key `k` ← src[i],
|
|
826
|
+
// threaded through Math.imul / === keys[h] / keys[h]=k) narrows to i32 instead of paying
|
|
827
|
+
// convert + f64-compare + trunc round-trips through its probe loop.
|
|
828
|
+
// A value built ONLY from the callee's own params + already-i32 locals + integer constants via
|
|
829
|
+
// integer-preserving ops. Its i32-ness follows from its inputs' — for a recursive self-call it
|
|
830
|
+
// carries no INDEPENDENT evidence about whether the params are i32. Used for the optimism below.
|
|
831
|
+
const isRecurIntExpr = (n, pnames, callerLocals) => {
|
|
832
|
+
if (typeof n === 'string') return pnames.has(n) || callerLocals?.get?.(n) === 'i32'
|
|
833
|
+
if (typeof n === 'number') return Number.isInteger(n)
|
|
834
|
+
if (!Array.isArray(n)) return false
|
|
835
|
+
if (n[0] == null) return typeof n[1] === 'number' && Number.isInteger(n[1]) // boxed int literal
|
|
836
|
+
if (n[0] === 'local.get') return pnames.has(n[1]) || callerLocals?.get?.(n[1]) === 'i32'
|
|
837
|
+
if (RECUR_INT_OPS.has(n[0])) return n.slice(1).every(c => isRecurIntExpr(c, pnames, callerLocals))
|
|
838
|
+
return false
|
|
839
|
+
}
|
|
840
|
+
const argWasmType = (arg, state) => {
|
|
841
|
+
// Recursive self-call: an arg built only from the callee's own params + already-i32 locals +
|
|
842
|
+
// int constants (`f(n - 1)`, `f(n - 1 - i)`) is i32 IFF those params are i32 — a fixpoint
|
|
843
|
+
// identity carrying no INDEPENDENT type evidence. Optimistically type it i32 so the NON-
|
|
844
|
+
// recursive call sites decide: all i32 ⇒ the param narrows; any f64 ⇒ the meet still poisons
|
|
845
|
+
// it. Lets a plain decreasing recursion narrow with no `|0` source crutch. (The bare-identity
|
|
846
|
+
// arg `f(n)` is already skipped wholesale in runCallsiteLattice.)
|
|
847
|
+
if (state.callee === state.callerFunc?.name &&
|
|
848
|
+
isRecurIntExpr(arg, new Set(state.func.sig.params.map(p => p.name)), state.callerLocals)) return 'i32'
|
|
849
|
+
if (!state._teOverlay) {
|
|
850
|
+
const m = new Map(ctx.scope.globalTypedElem || [])
|
|
851
|
+
const pf = state.callerParamFacts('typedCtor')
|
|
852
|
+
if (pf) for (const [name, ctor] of pf) if (ctor != null) m.set(name, ctor)
|
|
853
|
+
state._teOverlay = m
|
|
854
|
+
}
|
|
855
|
+
const prev = ctx.func.localTypedElemsOverlay
|
|
856
|
+
ctx.func.localTypedElemsOverlay = state._teOverlay
|
|
857
|
+
try { return exprType(arg, state.callerLocals) }
|
|
858
|
+
finally { ctx.func.localTypedElemsOverlay = prev }
|
|
859
|
+
}
|
|
726
860
|
const runFixpoint = () => runCallsiteLattice([
|
|
727
861
|
// val runs SOFT (monotone): a TYPED param's val only becomes inferable after the
|
|
728
862
|
// typedCtor fixpoint + pointer-ABI enrichment, so an early hard merge would
|
|
@@ -734,7 +868,7 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
734
868
|
missing: poison('wasm'),
|
|
735
869
|
apply(r, arg, _k, state) {
|
|
736
870
|
if (r.wasm === null) return
|
|
737
|
-
const wt =
|
|
871
|
+
const wt = argWasmType(arg, state)
|
|
738
872
|
if (r.wasm === undefined) r.wasm = wt
|
|
739
873
|
else if (r.wasm !== wt) r.wasm = null
|
|
740
874
|
},
|
|
@@ -899,6 +1033,16 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
899
1033
|
// (callback bench: mix is FNV — params and result all i32-shaped, but inferred
|
|
900
1034
|
// only after E phase narrowed mix's result).
|
|
901
1035
|
phase.refreshLocals()
|
|
1036
|
+
// I2: Re-narrow i32 RESULTS now that Phase G (applyTypedPointerParamAbi) has tagged
|
|
1037
|
+
// typed-array params ptrKind=TYPED. Phase E ran before G, so a function returning a
|
|
1038
|
+
// typed-array element — dict's `lookup = (keys, vals, k) => { … return vals[h] }` with
|
|
1039
|
+
// vals an Int32Array param — had its return tail type as NaN-boxed f64 (vals not yet a
|
|
1040
|
+
// typed pointer), leaving sig.results f64 and the call site running the full
|
|
1041
|
+
// __typed_idx/ToNumber unbox on every probe step (491520× per dict kernel run). Now that
|
|
1042
|
+
// evalTails seeds the typed-param overlay and params carry ptrAux, the fixpoint catches
|
|
1043
|
+
// `vals[h]` as i32, narrows the result, and the dispatch vanishes; the runFixpoint below
|
|
1044
|
+
// then propagates the i32 result into `let v = lookup(...)` at the call sites.
|
|
1045
|
+
narrowI32Results(funcsWithNarrowableResult)
|
|
902
1046
|
// Reset wasm field unconditionally — first pass populated it from stale callerLocals
|
|
903
1047
|
// (where `let h = mix(...)` widened h to f64 because mix's result wasn't narrowed
|
|
904
1048
|
// yet). clearStickyNull only resets null; here we need to reset f64-observed too
|
|
@@ -1127,6 +1271,87 @@ export function adviseJsstringCarrier(paramReps, valueUsed) {
|
|
|
1127
1271
|
}
|
|
1128
1272
|
}
|
|
1129
1273
|
|
|
1274
|
+
// Two value-kinds CONFLICT when passing one where the other is expected would
|
|
1275
|
+
// rely on a JS boundary coercion jz does not implement (so the result diverges).
|
|
1276
|
+
// NUMBER↔STRING is the canonical pair: `"5" * 2` is 10 in JS but NaN in jz, and a
|
|
1277
|
+
// STRING passed to a numeric param reads its NaN-boxed bits as an f64. ARRAY/OBJECT
|
|
1278
|
+
// vs NUMBER/STRING likewise. We treat the four primitive-ish kinds as mutually
|
|
1279
|
+
// exclusive; BOOL is omitted (it nanboxes to 0/1 and numeric code tolerates it).
|
|
1280
|
+
const STRICT_CONFLICT = {
|
|
1281
|
+
[VAL.NUMBER]: new Set([VAL.STRING, VAL.ARRAY, VAL.OBJECT]),
|
|
1282
|
+
[VAL.STRING]: new Set([VAL.NUMBER, VAL.ARRAY, VAL.OBJECT]),
|
|
1283
|
+
[VAL.ARRAY]: new Set([VAL.NUMBER, VAL.STRING]),
|
|
1284
|
+
[VAL.OBJECT]: new Set([VAL.NUMBER, VAL.STRING]),
|
|
1285
|
+
}
|
|
1286
|
+
const kindName = (v) => ({ [VAL.NUMBER]: 'number', [VAL.STRING]: 'string', [VAL.ARRAY]: 'array', [VAL.OBJECT]: 'object' }[v] || 'value')
|
|
1287
|
+
|
|
1288
|
+
/**
|
|
1289
|
+
* Strict-mode boundary type check (standalone; runs in both the full-narrow and
|
|
1290
|
+
* skip-narrow plan paths).
|
|
1291
|
+
*
|
|
1292
|
+
* jz infers a param's type from how it's used (`x * 2` → number, `s.charCodeAt`
|
|
1293
|
+
* → string) or from an explicit default (`x = 0` → number). In permissive mode a
|
|
1294
|
+
* caller may pass any type and jz silently computes a divergent result (`"5"*2`
|
|
1295
|
+
* is 10 in JS, NaN here). Strict mode is the canonical subset where that misuse
|
|
1296
|
+
* is a compile error instead — consistent with strict already rejecting `==`,
|
|
1297
|
+
* dynamic dispatch, and `void`.
|
|
1298
|
+
*
|
|
1299
|
+
* Fires ONLY when BOTH sides are statically certain and conflict:
|
|
1300
|
+
* - the callee param has a known kind (its default-value type, or a settled
|
|
1301
|
+
* `val` rep from body-usage / call-site inference), AND
|
|
1302
|
+
* - the argument expression has a known, conflicting kind (a literal or a
|
|
1303
|
+
* locally-typed binding).
|
|
1304
|
+
* An untyped param or untyped arg is never flagged — no false positives on
|
|
1305
|
+
* genuinely polymorphic code.
|
|
1306
|
+
*/
|
|
1307
|
+
export function strictBoundaryTypeCheck(programFacts) {
|
|
1308
|
+
if (!ctx.transform.strict) return
|
|
1309
|
+
const { callSites, paramReps } = programFacts
|
|
1310
|
+
|
|
1311
|
+
// Per-callee body-evidence cache: methodEvidence (.charCodeAt→STRING, .push→
|
|
1312
|
+
// ARRAY) keyed by param name. Computed lazily, once per callee.
|
|
1313
|
+
const bodyEvidence = new Map()
|
|
1314
|
+
const evidenceOf = (func) => {
|
|
1315
|
+
if (!bodyEvidence.has(func.name)) {
|
|
1316
|
+
const names = func.sig.params.map(p => p.name).filter(Boolean)
|
|
1317
|
+
bodyEvidence.set(func.name, func.body ? inferParams(func.body, names) : new Map())
|
|
1318
|
+
}
|
|
1319
|
+
return bodyEvidence.get(func.name)
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
// Expected kind of callee param k, from any statically-certain source:
|
|
1323
|
+
// 1. explicit default value type — the source's own declaration (x = 0 → number)
|
|
1324
|
+
// 2. settled call-site/usage rep `val` (paramReps; present after full narrow)
|
|
1325
|
+
// 3. type-exclusive body evidence (methodEvidence; works in skip-narrow path too)
|
|
1326
|
+
// First certain source wins; null when the param is genuinely polymorphic.
|
|
1327
|
+
const paramKind = (func, k) => {
|
|
1328
|
+
const p = func.sig.params[k]
|
|
1329
|
+
if (!p) return null
|
|
1330
|
+
const def = func.defaults?.[p.name]
|
|
1331
|
+
if (def != null) { const dv = valTypeOf(def); if (dv) return dv }
|
|
1332
|
+
const repVal = paramReps?.get(func.name)?.get(k)?.val
|
|
1333
|
+
if (repVal != null) return repVal
|
|
1334
|
+
return evidenceOf(func).get(p.name)?.val ?? null
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
for (const cs of callSites) {
|
|
1338
|
+
const func = ctx.func.map.get(cs.callee)
|
|
1339
|
+
if (!func || func.raw || !func.sig) continue
|
|
1340
|
+
if (func.rest) continue // rest packs args into an array
|
|
1341
|
+
for (let k = 0; k < cs.argList.length && k < func.sig.params.length; k++) {
|
|
1342
|
+
const want = paramKind(func, k)
|
|
1343
|
+
if (want == null) continue
|
|
1344
|
+
const conflicts = STRICT_CONFLICT[want]
|
|
1345
|
+
if (!conflicts) continue
|
|
1346
|
+
const got = valTypeOf(cs.argList[k])
|
|
1347
|
+
if (got != null && conflicts.has(got)) {
|
|
1348
|
+
const pname = func.sig.params[k].name
|
|
1349
|
+
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 }.`)
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1130
1355
|
/**
|
|
1131
1356
|
* Phase: bimorphic typed-array param specialization.
|
|
1132
1357
|
*
|
|
@@ -1161,13 +1386,9 @@ export function specializeBimorphicTyped(programFacts) {
|
|
|
1161
1386
|
if (list) list.push(cs); else sitesByCallee.set(cs.callee, [cs])
|
|
1162
1387
|
}
|
|
1163
1388
|
|
|
1164
|
-
// Per-caller typedElem map
|
|
1165
|
-
|
|
1166
|
-
callerTypedCtx
|
|
1167
|
-
for (const func of ctx.func.list) {
|
|
1168
|
-
if (!func.body || func.raw) continue
|
|
1169
|
-
callerTypedCtx.set(func, analyzeBody(func.body).typedElems)
|
|
1170
|
-
}
|
|
1389
|
+
// Per-caller typedElem map: body-local `new TypedArray(N)` bindings layered
|
|
1390
|
+
// over the module's typed globals (shared with buildCallerTypedCtx).
|
|
1391
|
+
const callerTypedCtx = buildCallerTypedCtx()
|
|
1171
1392
|
// Per-caller typed-param map: caller's own params that F/G already narrowed
|
|
1172
1393
|
// (so transitive `sum(arr)` inside a func that took `arr` from above resolves).
|
|
1173
1394
|
const callerTypedParamsCtx = new Map()
|
|
@@ -1237,25 +1458,37 @@ export function specializeBimorphicTyped(programFacts) {
|
|
|
1237
1458
|
|
|
1238
1459
|
// Build one clone per distinct combo.
|
|
1239
1460
|
const cloneByKey = new Map()
|
|
1240
|
-
for (const [
|
|
1241
|
-
|
|
1461
|
+
for (const [dkey, cmb] of distinct) {
|
|
1462
|
+
// NB: this loop variable must NOT reuse the name `combo` (declared twice above, at the
|
|
1463
|
+
// site loop and the distinct-building loop). The self-host miscompiles a for-of loop
|
|
1464
|
+
// variable whose name collides with an earlier block-scoped declaration — it aliases the
|
|
1465
|
+
// prior binding instead of rebinding per iteration, so `combo` would stay stuck at the
|
|
1466
|
+
// last site's ctor and every clone would get the same (wrong) element type → silent
|
|
1467
|
+
// garbage. A unique name gets a clean per-iteration binding.
|
|
1468
|
+
const suffix = cmb.map(c => c.replace(/^new\./, '').replace(/\./g, '_')).join('$')
|
|
1242
1469
|
let cloneName = `${func.name}$${suffix}`
|
|
1243
1470
|
let n = 0
|
|
1244
1471
|
while (ctx.func.names.has(cloneName)) cloneName = `${func.name}$${suffix}$${++n}`
|
|
1245
1472
|
|
|
1473
|
+
// Build cloneSig with clean, fully-formed object literals — never by spreading a
|
|
1474
|
+
// live object and then overriding/extending its keys. A MULTI-prop spread of a
|
|
1475
|
+
// member-access source (`{ ...func.sig, params, results }`) takes the static
|
|
1476
|
+
// allKnown OBJECT-merge path, which trusts func.sig's COMPILE-TIME schema; sig
|
|
1477
|
+
// objects are polymorphic (some carry result/ptrKind/unsignedResult), so that
|
|
1478
|
+
// schema can be a subset of the runtime shape and the slot-copy then faults a
|
|
1479
|
+
// later `sig.params` read out of bounds in the self-host. (The single-unknown
|
|
1480
|
+
// `{ ...x }` clone is fixed at the root — __obj_clone — but the allKnown merge
|
|
1481
|
+
// path is a separate hazard.) Constructing each param with its pointer ABI baked
|
|
1482
|
+
// in sidesteps it; output is unchanged on the host.
|
|
1246
1483
|
const cloneSig = {
|
|
1247
|
-
|
|
1248
|
-
|
|
1484
|
+
params: func.sig.params.map((p, idx) => {
|
|
1485
|
+
const bi = bimorphic.indexOf(idx)
|
|
1486
|
+
return bi < 0
|
|
1487
|
+
? { ...p }
|
|
1488
|
+
: { name: p.name, type: 'i32', ptrKind: VAL.TYPED, ptrAux: typedElemAux(cmb[bi]) }
|
|
1489
|
+
}),
|
|
1249
1490
|
results: [...func.sig.results],
|
|
1250
1491
|
}
|
|
1251
|
-
for (let i = 0; i < bimorphic.length; i++) {
|
|
1252
|
-
const k = bimorphic[i]
|
|
1253
|
-
const aux = typedElemAux(combo[i])
|
|
1254
|
-
const p = cloneSig.params[k]
|
|
1255
|
-
p.type = 'i32'
|
|
1256
|
-
p.ptrKind = VAL.TYPED
|
|
1257
|
-
p.ptrAux = aux
|
|
1258
|
-
}
|
|
1259
1492
|
const clone = { ...func, name: cloneName, sig: cloneSig }
|
|
1260
1493
|
ctx.func.list.push(clone)
|
|
1261
1494
|
ctx.func.map.set(cloneName, clone)
|
|
@@ -1263,19 +1496,20 @@ export function specializeBimorphicTyped(programFacts) {
|
|
|
1263
1496
|
|
|
1264
1497
|
// Mirror per-param reps under the clone's name with mono ctors at bimorphic
|
|
1265
1498
|
// positions. emitFunc's preseed reads typedCtor → seeds typedElem map →
|
|
1266
|
-
// `arr[i]` lowers to direct typed load.
|
|
1499
|
+
// `arr[i]` lowers to direct typed load. Each `{ ...r }` is a true clone, so
|
|
1500
|
+
// pinning typedCtor on it leaves the source rep untouched (__obj_clone).
|
|
1267
1501
|
const cloneReps = new Map()
|
|
1268
1502
|
for (const [k, r] of reps) cloneReps.set(k, { ...r })
|
|
1269
1503
|
for (let i = 0; i < bimorphic.length; i++) {
|
|
1270
1504
|
const k = bimorphic[i]
|
|
1271
1505
|
const r = cloneReps.get(k) || {}
|
|
1272
|
-
r.typedCtor =
|
|
1506
|
+
r.typedCtor = cmb[i]
|
|
1273
1507
|
r.val = VAL.TYPED
|
|
1274
1508
|
cloneReps.set(k, r)
|
|
1275
1509
|
}
|
|
1276
1510
|
paramReps.set(cloneName, cloneReps)
|
|
1277
1511
|
|
|
1278
|
-
cloneByKey.set(
|
|
1512
|
+
cloneByKey.set(dkey, clone)
|
|
1279
1513
|
}
|
|
1280
1514
|
|
|
1281
1515
|
// Rewrite each site's call AST to point at the matching clone.
|