jz 0.3.1 → 0.5.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 +277 -258
- package/cli.js +10 -52
- package/index.js +181 -29
- package/{src/host.js → interop.js} +291 -88
- package/module/array.js +195 -76
- package/module/collection.js +249 -20
- package/module/console.js +1 -1
- package/module/core.js +267 -119
- package/module/date.js +9 -5
- package/module/function.js +11 -1
- package/module/json.js +367 -61
- package/module/math.js +568 -128
- package/module/number.js +390 -227
- package/module/object.js +352 -39
- package/module/regex.js +123 -16
- package/module/schema.js +15 -1
- package/module/string.js +555 -96
- package/module/timer.js +1 -1
- package/module/typedarray.js +674 -57
- package/package.json +12 -6
- package/src/abi/array.js +89 -0
- package/src/abi/index.js +35 -0
- package/src/abi/number.js +137 -0
- package/src/abi/object.js +57 -0
- package/src/abi/string.js +529 -0
- package/src/analyze.js +1872 -487
- package/src/assemble.js +58 -20
- package/src/autoload.js +13 -1
- package/src/codegen.js +177 -0
- package/src/compile.js +462 -186
- package/src/ctx.js +85 -18
- package/src/emit.js +668 -197
- package/src/infer.js +548 -0
- package/src/ir.js +302 -27
- package/src/jzify.js +898 -259
- package/src/narrow.js +455 -246
- package/src/optimize.js +263 -99
- package/src/plan.js +713 -35
- package/src/prepare.js +818 -293
- package/src/resolve.js +85 -0
- package/src/vectorize.js +99 -18
- package/src/ast.js +0 -160
- package/src/auto-config.js +0 -120
package/src/narrow.js
CHANGED
|
@@ -8,17 +8,22 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { ctx } from './ctx.js'
|
|
11
|
-
import { isLiteralStr } from './ir.js'
|
|
11
|
+
import { isLiteralStr, I32_MIN, I32_MAX } from './ir.js'
|
|
12
12
|
import {
|
|
13
13
|
VAL,
|
|
14
|
-
analyzeBody,
|
|
15
|
-
|
|
16
|
-
exprType, findMutations, hasBareReturn,
|
|
17
|
-
inferArgArrElemValType, inferArgSchema, inferArgType, inferArgTypedCtor,
|
|
14
|
+
analyzeBody,
|
|
15
|
+
paramFactsOf,
|
|
16
|
+
exprType, findMutations, hasBareReturn,
|
|
18
17
|
invalidateLocalsCache, invalidateValTypesCache, isBlockBody, alwaysReturns,
|
|
19
18
|
narrowReturnArrayElems, observeProgramSlots, returnExprs, staticObjectProps,
|
|
19
|
+
scanBoundedLoops,
|
|
20
20
|
typedElemAux, typedElemCtor, ctorFromElemAux, valTypeOf,
|
|
21
21
|
} from './analyze.js'
|
|
22
|
+
import {
|
|
23
|
+
clearStickyNull, ensureParamRep, mergeParamFact,
|
|
24
|
+
inferArrElemSchema, inferArrElemValType,
|
|
25
|
+
inferSchemaId, inferValType, inferTypedCtor,
|
|
26
|
+
} from './infer.js'
|
|
22
27
|
|
|
23
28
|
const PTR_ABI_KINDS = new Set([VAL.OBJECT, VAL.SET, VAL.MAP, VAL.BUFFER])
|
|
24
29
|
|
|
@@ -192,17 +197,250 @@ function enrichCallerValTypesFromPointerParams(callerCtx) {
|
|
|
192
197
|
function refreshCallerLocals(callerCtx) {
|
|
193
198
|
for (const func of ctx.func.list) {
|
|
194
199
|
if (!func.body || func.raw) continue
|
|
200
|
+
// Seed pointer-narrowed params' val-kind so analyzeBody recognises e.g.
|
|
201
|
+
// `n = arr.length` (arr a TYPED/BUFFER pointer param) as an i32 local — without
|
|
202
|
+
// this, post-G `refreshCallerLocals` still walks bodies with arr untyped, the
|
|
203
|
+
// length stays f64, and any callee taking that length never gets an i32 param
|
|
204
|
+
// (heapsort→siftDown's `end`). analyzeFuncForEmit re-seeds + re-invalidates at
|
|
205
|
+
// emit time, so this transient localReps doesn't leak past narrowing.
|
|
206
|
+
ctx.func.localReps = new Map()
|
|
207
|
+
for (const p of func.sig.params) if (p.ptrKind != null) ctx.func.localReps.set(p.name, { val: p.ptrKind })
|
|
195
208
|
invalidateLocalsCache(func.body)
|
|
196
|
-
const fresh =
|
|
209
|
+
const fresh = analyzeBody(func.body).locals
|
|
197
210
|
for (const p of func.sig.params) if (!fresh.has(p.name)) fresh.set(p.name, p.type)
|
|
198
211
|
callerCtx.get(func).callerLocals = fresh
|
|
199
212
|
}
|
|
213
|
+
ctx.func.localReps = null
|
|
200
214
|
}
|
|
201
215
|
|
|
202
216
|
function resetParamWasmFacts(paramReps) {
|
|
203
217
|
for (const m of paramReps.values()) for (const r of m.values()) r.wasm = undefined
|
|
204
218
|
}
|
|
205
219
|
|
|
220
|
+
/**
|
|
221
|
+
* Phase E: numeric result narrowing.
|
|
222
|
+
*
|
|
223
|
+
* For every narrowable func whose body returns only i32-typed expressions,
|
|
224
|
+
* narrow sig.results[0] to 'i32'. An *unsigned* tail flips sig.unsignedResult so
|
|
225
|
+
* the call-site rebox uses f64.convert_i32_u and preserves [0, 2^32) range.
|
|
226
|
+
* A tail is unsigned when it is a top-level `(x >>> 0)` OR a call to a function
|
|
227
|
+
* already narrowed `unsignedResult` — the latter propagates the flag through
|
|
228
|
+
* helper chains (`const u = x => (x|0)>>>0; const main = x => u(x)`), which a
|
|
229
|
+
* literal-`>>>`-only check would miss, reboxing main's result signed and
|
|
230
|
+
* silently turning `4294967295` into `-1`.
|
|
231
|
+
*
|
|
232
|
+
* Sign must be consistent across *all* tails: the same i32 bit pattern maps to
|
|
233
|
+
* two different JS numbers under signed vs unsigned conversion, so a function
|
|
234
|
+
* mixing signed (`x|0`) and unsigned (`x>>>0`) tails cannot be reboxed with a
|
|
235
|
+
* single boundary flag. Such functions are left at f64 — the body then converts
|
|
236
|
+
* each tail with its own sign. (Pre-fix, a top-level `>>>` next to a signed tail
|
|
237
|
+
* narrowed unsigned and corrupted the signed branch.)
|
|
238
|
+
*
|
|
239
|
+
* Fixpoint: a call to another narrowed func contributes i32; iterate until
|
|
240
|
+
* stable so chains of i32-only helpers all narrow together. exprType already
|
|
241
|
+
* consults ctx.func.map for narrowed user-function results plus the
|
|
242
|
+
* Math.imul/Math.clz32/charCodeAt stdlib subset.
|
|
243
|
+
*
|
|
244
|
+
* Safe for exports — boundary wrapper restores the f64 JS ABI. `return;`
|
|
245
|
+
* (bare) is preserved as f64; multi-value / raw / value-used are skipped by
|
|
246
|
+
* the narrowable filter.
|
|
247
|
+
*/
|
|
248
|
+
function narrowI32Results(funcs) {
|
|
249
|
+
// A return tail is unsigned-valued when it is a top-level `>>>` or a call to
|
|
250
|
+
// a function already proven unsignedResult. Other i32 tails are signed.
|
|
251
|
+
const isUnsignedTail = (e) => Array.isArray(e) && (
|
|
252
|
+
e[0] === '>>>' ||
|
|
253
|
+
(e[0] === '()' && typeof e[1] === 'string' && ctx.func.map?.get(e[1])?.sig?.unsignedResult === true)
|
|
254
|
+
)
|
|
255
|
+
let changed = true
|
|
256
|
+
while (changed) {
|
|
257
|
+
changed = false
|
|
258
|
+
for (const func of funcs) {
|
|
259
|
+
if (func.sig.results[0] === 'i32') continue
|
|
260
|
+
const body = func.body
|
|
261
|
+
if (isBlockBody(body) && hasBareReturn(body)) continue
|
|
262
|
+
const exprs = returnExprs(body)
|
|
263
|
+
if (!exprs.length) continue
|
|
264
|
+
const savedCurrent = ctx.func.current
|
|
265
|
+
ctx.func.current = func.sig
|
|
266
|
+
const locals = isBlockBody(body) ? analyzeBody(body).locals : new Map()
|
|
267
|
+
for (const p of func.sig.params) if (!locals.has(p.name)) locals.set(p.name, p.type)
|
|
268
|
+
const allI32 = exprs.every(e => exprType(e, locals) === 'i32')
|
|
269
|
+
const anyUnsigned = exprs.some(isUnsignedTail)
|
|
270
|
+
const allUnsigned = exprs.every(isUnsignedTail)
|
|
271
|
+
ctx.func.current = savedCurrent
|
|
272
|
+
// Narrow only when sign is consistent (all-signed or all-unsigned tails).
|
|
273
|
+
if (allI32 && (!anyUnsigned || allUnsigned)) {
|
|
274
|
+
func.sig.results = ['i32']
|
|
275
|
+
if (allUnsigned) func.sig.unsignedResult = true
|
|
276
|
+
changed = true
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Phase E2: VAL-kind result inference.
|
|
284
|
+
*
|
|
285
|
+
* When every return-tail resolves to the same VAL.* kind, record it on
|
|
286
|
+
* func.valResult so call-site valTypeOf inherits it (enables static dispatch
|
|
287
|
+
* on .length / [i] / .prop through the call chain). Fixpoint propagates
|
|
288
|
+
* through helper chains. Exports are safe — same boundary-wrapper guarantee
|
|
289
|
+
* as numeric narrowing.
|
|
290
|
+
*/
|
|
291
|
+
function narrowValResults(funcs) {
|
|
292
|
+
const valTypeOfWithCalls = (expr, localValTypes) => {
|
|
293
|
+
if (expr == null) return null
|
|
294
|
+
if (typeof expr === 'string') return localValTypes?.get(expr) || ctx.scope.globalValTypes?.get(expr) || null
|
|
295
|
+
if (!Array.isArray(expr)) return valTypeOf(expr)
|
|
296
|
+
const [op, ...args] = expr
|
|
297
|
+
if (op === '()' && typeof args[0] === 'string') {
|
|
298
|
+
const f = ctx.func.map.get(args[0])
|
|
299
|
+
if (f?.valResult) return f.valResult
|
|
300
|
+
}
|
|
301
|
+
if (op === '?:') {
|
|
302
|
+
const a = valTypeOfWithCalls(args[1], localValTypes), b = valTypeOfWithCalls(args[2], localValTypes)
|
|
303
|
+
return a && a === b ? a : null
|
|
304
|
+
}
|
|
305
|
+
if (op === '&&' || op === '||') {
|
|
306
|
+
const a = valTypeOfWithCalls(args[0], localValTypes), b = valTypeOfWithCalls(args[1], localValTypes)
|
|
307
|
+
return a && a === b ? a : null
|
|
308
|
+
}
|
|
309
|
+
return valTypeOf(expr)
|
|
310
|
+
}
|
|
311
|
+
let changed = true
|
|
312
|
+
while (changed) {
|
|
313
|
+
changed = false
|
|
314
|
+
for (const func of funcs) {
|
|
315
|
+
if (func.valResult) continue
|
|
316
|
+
const body = func.body
|
|
317
|
+
const isBlock = isBlockBody(body)
|
|
318
|
+
if (isBlock && hasBareReturn(body)) continue
|
|
319
|
+
const exprs = returnExprs(body)
|
|
320
|
+
if (!exprs.length) continue
|
|
321
|
+
const localValTypes = isBlock ? analyzeBody(body).valTypes : new Map()
|
|
322
|
+
const vt0 = valTypeOfWithCalls(exprs[0], localValTypes)
|
|
323
|
+
if (!vt0) continue
|
|
324
|
+
const allSame = exprs.every(e => valTypeOfWithCalls(e, localValTypes) === vt0)
|
|
325
|
+
if (allSame) { func.valResult = vt0; changed = true }
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const PTR_RESULT_KINDS_NOAUX = new Set([VAL.SET, VAL.MAP, VAL.BUFFER])
|
|
331
|
+
|
|
332
|
+
// Per-body local elemAux map: scans `let/const x = new TypedArray(...)` decls so a return
|
|
333
|
+
// like `let a = new Float64Array(...); return a` resolves to a constant aux.
|
|
334
|
+
function localElemAuxMap(body) {
|
|
335
|
+
const m = new Map()
|
|
336
|
+
const walk = (n) => {
|
|
337
|
+
if (!Array.isArray(n)) return
|
|
338
|
+
const op = n[0]
|
|
339
|
+
if (op === '=>') return
|
|
340
|
+
if ((op === 'let' || op === 'const') && n.length > 1) {
|
|
341
|
+
for (let i = 1; i < n.length; i++) {
|
|
342
|
+
const a = n[i]
|
|
343
|
+
if (Array.isArray(a) && a[0] === '=' && typeof a[1] === 'string') {
|
|
344
|
+
const aux = typedElemAux(typedElemCtor(a[2]))
|
|
345
|
+
if (aux != null) m.set(a[1], aux)
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
for (let i = 1; i < n.length; i++) walk(n[i])
|
|
350
|
+
}
|
|
351
|
+
walk(body)
|
|
352
|
+
return m
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function typedAuxOfReturn(expr, localElemMap) {
|
|
356
|
+
if (typeof expr === 'string') return localElemMap?.get(expr) ?? null
|
|
357
|
+
if (!Array.isArray(expr)) return null
|
|
358
|
+
const [op, ...args] = expr
|
|
359
|
+
if (op === '()' && typeof args[0] === 'string') {
|
|
360
|
+
if (args[0].startsWith('new.')) {
|
|
361
|
+
const ctor = typedElemCtor(expr)
|
|
362
|
+
return ctor != null ? typedElemAux(ctor) : null
|
|
363
|
+
}
|
|
364
|
+
const f = ctx.func.map.get(args[0])
|
|
365
|
+
if (f?.valResult === VAL.TYPED && f.sig.ptrAux != null) return f.sig.ptrAux
|
|
366
|
+
return null
|
|
367
|
+
}
|
|
368
|
+
if (op === '?:') {
|
|
369
|
+
const a = typedAuxOfReturn(args[1], localElemMap)
|
|
370
|
+
const b = typedAuxOfReturn(args[2], localElemMap)
|
|
371
|
+
return a != null && a === b ? a : null
|
|
372
|
+
}
|
|
373
|
+
if (op === '&&' || op === '||') {
|
|
374
|
+
const a = typedAuxOfReturn(args[0], localElemMap)
|
|
375
|
+
const b = typedAuxOfReturn(args[1], localElemMap)
|
|
376
|
+
return a != null && a === b ? a : null
|
|
377
|
+
}
|
|
378
|
+
return null
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Phase E3: pointer result narrowing.
|
|
383
|
+
*
|
|
384
|
+
* For narrowable funcs whose valResult is a non-ambiguous pointer kind with a
|
|
385
|
+
* constant aux, narrow sig.results[0] from f64 to i32 and tag sig.ptrKind/.ptrAux.
|
|
386
|
+
* Eliminates the f64.reinterpret_i64+i64.or rebox at every return and the
|
|
387
|
+
* matching unbox dance at every call site that uses the value as a pointer.
|
|
388
|
+
*
|
|
389
|
+
* Aux strategy:
|
|
390
|
+
* - SET/MAP/BUFFER: aux always 0 — no per-callsite preservation needed.
|
|
391
|
+
* - OBJECT: aux is schema-id; narrow only when all return exprs share a constant
|
|
392
|
+
* schema (literal, schemaId-bound param, module-bound var, or call to another
|
|
393
|
+
* OBJECT-narrowed func). Caller picks aux up via callIR.ptrAux → readVar →
|
|
394
|
+
* localReps.schemaId, restoring property-slot dispatch through the call boundary.
|
|
395
|
+
* - TYPED: aux is elem-type; require all return tails to agree on a single aux.
|
|
396
|
+
*
|
|
397
|
+
* Skipped: ARRAY forwards on realloc, STRING dual-encoded SSO/heap, CLOSURE
|
|
398
|
+
* (aux carries funcIdx for call_indirect). Body must guarantee-return so the
|
|
399
|
+
* fallthrough fallback can't produce a wrong-typed undef.
|
|
400
|
+
*
|
|
401
|
+
* Fixpoint: a chain `outer → inner → {a,b}` needs inner to narrow first so
|
|
402
|
+
* outer's call to inner contributes a known schema-id.
|
|
403
|
+
*/
|
|
404
|
+
function narrowPointerResults(funcs, paramReps) {
|
|
405
|
+
let changed = true
|
|
406
|
+
while (changed) {
|
|
407
|
+
changed = false
|
|
408
|
+
for (const func of funcs) {
|
|
409
|
+
if (!func.valResult) continue
|
|
410
|
+
if (func.sig.results[0] !== 'f64') continue
|
|
411
|
+
const isBlock = isBlockBody(func.body)
|
|
412
|
+
if (isBlock && !alwaysReturns(func.body)) continue
|
|
413
|
+
if (PTR_RESULT_KINDS_NOAUX.has(func.valResult)) {
|
|
414
|
+
func.sig.results = ['i32']
|
|
415
|
+
func.sig.ptrKind = func.valResult
|
|
416
|
+
changed = true
|
|
417
|
+
continue
|
|
418
|
+
}
|
|
419
|
+
const exprs = returnExprs(func.body)
|
|
420
|
+
if (!exprs.length) continue
|
|
421
|
+
if (func.valResult === VAL.OBJECT) {
|
|
422
|
+
const paramSchemasMap = paramFactsOf(paramReps, func, 'schemaId')
|
|
423
|
+
const sid0 = inferSchemaId(exprs[0], paramSchemasMap)
|
|
424
|
+
if (sid0 == null) continue
|
|
425
|
+
if (!exprs.every(e => inferSchemaId(e, paramSchemasMap) === sid0)) continue
|
|
426
|
+
func.sig.results = ['i32']
|
|
427
|
+
func.sig.ptrKind = VAL.OBJECT
|
|
428
|
+
func.sig.ptrAux = sid0
|
|
429
|
+
changed = true
|
|
430
|
+
} else if (func.valResult === VAL.TYPED) {
|
|
431
|
+
const localMap = isBlock ? localElemAuxMap(func.body) : null
|
|
432
|
+
const aux0 = typedAuxOfReturn(exprs[0], localMap)
|
|
433
|
+
if (aux0 == null) continue
|
|
434
|
+
if (!exprs.every(e => typedAuxOfReturn(e, localMap) === aux0)) continue
|
|
435
|
+
func.sig.results = ['i32']
|
|
436
|
+
func.sig.ptrKind = VAL.TYPED
|
|
437
|
+
func.sig.ptrAux = aux0
|
|
438
|
+
changed = true
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
206
444
|
function createPhaseState() {
|
|
207
445
|
const callerCtx = buildCallerCtx()
|
|
208
446
|
const elemCtx = new Map()
|
|
@@ -260,13 +498,13 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
260
498
|
// D: Call-site type propagation — infer param types from how functions are called.
|
|
261
499
|
// Drives off `callSites` collected during the ProgramFacts walk; no AST re-walking.
|
|
262
500
|
// For non-exported internal functions, if all call sites agree on a param's type,
|
|
263
|
-
// seed the param's val rep (ctx.func.
|
|
501
|
+
// seed the param's val rep (ctx.func.localReps) during per-function compilation.
|
|
264
502
|
// Also infer i32/f64 WASM type — when all call sites pass i32 for a param, specialize
|
|
265
503
|
// sig.params[k].type to i32 (no default, no rest, not exported, not value-used).
|
|
266
504
|
// Also propagate schema ID — when all call sites pass objects with the same schema,
|
|
267
505
|
// bind the callee's param to that schema so `p.x` becomes a direct slot load.
|
|
268
|
-
// Inference helpers (
|
|
269
|
-
// live in
|
|
506
|
+
// Inference helpers (inferValType/inferSchemaId/inferArr*/inferTypedCtor)
|
|
507
|
+
// live in infer.js — pure AST→fact resolvers shared across fixpoint phases.
|
|
270
508
|
// Per-caller analysis is stable across fixpoint iterations — precompute once.
|
|
271
509
|
// callerCtx[null] (top-level) uses module globals for both locals and valTypes.
|
|
272
510
|
const phase = createPhaseState()
|
|
@@ -277,7 +515,7 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
277
515
|
else if (Array.isArray(arg) && arg[0] == null && typeof arg[1] === 'number') raw = arg[1]
|
|
278
516
|
else if (Array.isArray(arg) && arg[0] === 'u-' && typeof arg[1] === 'number') raw = -arg[1]
|
|
279
517
|
else if (typeof arg === 'string' && ctx.scope.constInts?.has(arg)) raw = ctx.scope.constInts.get(arg)
|
|
280
|
-
return (raw != null && Number.isInteger(raw) && raw >=
|
|
518
|
+
return (raw != null && Number.isInteger(raw) && raw >= I32_MIN && raw <= I32_MAX) ? raw : null
|
|
281
519
|
}
|
|
282
520
|
|
|
283
521
|
const runCallsiteLattice = (rules) => {
|
|
@@ -294,7 +532,7 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
294
532
|
callerLocals: ctxEntry.callerLocals,
|
|
295
533
|
callerValTypes: ctxEntry.callerValTypes,
|
|
296
534
|
callerParamFacts(key) {
|
|
297
|
-
if (!paramFacts.has(key)) paramFacts.set(key,
|
|
535
|
+
if (!paramFacts.has(key)) paramFacts.set(key, paramFactsOf(paramReps, callerFunc, key))
|
|
298
536
|
return paramFacts.get(key)
|
|
299
537
|
},
|
|
300
538
|
}
|
|
@@ -308,13 +546,13 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
308
546
|
}
|
|
309
547
|
|
|
310
548
|
const poison = field => r => { r[field] = null }
|
|
311
|
-
// Default-aware val inference. Adds two fallbacks beyond
|
|
549
|
+
// Default-aware val inference. Adds two fallbacks beyond inferValType's
|
|
312
550
|
// body-local `callerValTypes` lookup so a hot recursive helper like
|
|
313
551
|
// `uleb(n, buffer = []) { ... return uleb(n, buffer) }` resolves the
|
|
314
552
|
// recursive `buffer` arg to VAL.ARRAY (via callerParamFacts on iter 2,
|
|
315
553
|
// or via the caller's own default expression on iter 1).
|
|
316
554
|
const inferValAtSite = (arg, state) => {
|
|
317
|
-
const v =
|
|
555
|
+
const v = inferValType(arg, state.callerValTypes)
|
|
318
556
|
if (v != null) return v
|
|
319
557
|
if (typeof arg !== 'string') return null
|
|
320
558
|
const fromParam = state.callerParamFacts('val')?.get(arg)
|
|
@@ -352,7 +590,7 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
352
590
|
else if (r.wasm !== wt) r.wasm = null
|
|
353
591
|
},
|
|
354
592
|
},
|
|
355
|
-
mergeRule('schemaId', (arg, _k, state) =>
|
|
593
|
+
mergeRule('schemaId', (arg, _k, state) => inferSchemaId(arg, state.callerParamFacts('schemaId'))),
|
|
356
594
|
{
|
|
357
595
|
missing: poison('intConst'),
|
|
358
596
|
apply(r, arg, k, state) {
|
|
@@ -361,13 +599,27 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
361
599
|
},
|
|
362
600
|
},
|
|
363
601
|
])
|
|
602
|
+
// Transitive ctor/schema propagation down call chains. A naive single-pass
|
|
603
|
+
// mergeRule poisons a callee's param on the *first* sweep if the caller's own
|
|
604
|
+
// param (the very thing that supplies the ctor) hasn't been typed yet — and the
|
|
605
|
+
// poison is sticky, so later sweeps can't recover. Two-pass was the old patch;
|
|
606
|
+
// it still loses any chain deeper than `main→f→g→h` (e.g. heapsort's siftDown).
|
|
607
|
+
// Fix: iterate a *soft* merge — propagate known ctors, treat "can't tell yet"
|
|
608
|
+
// as skip (no poison) — to a fixpoint, then one *hard* validating sweep that
|
|
609
|
+
// poisons params whose call sites still can't be proven (genuinely-untyped args).
|
|
364
610
|
const runArrElemFixpoint = (field, inferFn, elemsCtxMap) => {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
)])
|
|
611
|
+
const infer = (arg, _k, state) => inferFn(arg, elemsCtxMap.get(state.callerFunc), state.callerParamFacts(field))
|
|
612
|
+
let changed
|
|
613
|
+
const bump = (r, v) => { if (v == null || r[field] === null) return; const b = r[field]; mergeParamFact(r, field, v); if (r[field] !== b) changed = true }
|
|
614
|
+
const soft = {
|
|
615
|
+
missing(r, k, state) { const def = defaultArg(state, k); if (def != null) bump(r, infer(def, k, state)) },
|
|
616
|
+
apply(r, arg, k, state) { bump(r, infer(arg, k, state)) },
|
|
617
|
+
}
|
|
618
|
+
do { changed = false; runCallsiteLattice([soft]) } while (changed)
|
|
619
|
+
runCallsiteLattice([mergeRule(field, infer)])
|
|
368
620
|
}
|
|
369
|
-
const runArrFixpoint = () => runArrElemFixpoint('arrayElemSchema',
|
|
370
|
-
const runArrValTypeFixpoint = () => runArrElemFixpoint('arrayElemValType',
|
|
621
|
+
const runArrFixpoint = () => runArrElemFixpoint('arrayElemSchema', inferArrElemSchema, phase.callerElems('arrElemSchemas'))
|
|
622
|
+
const runArrValTypeFixpoint = () => runArrElemFixpoint('arrayElemValType', inferArrElemValType, phase.callerElems('arrElemValTypes'))
|
|
371
623
|
runFixpoint()
|
|
372
624
|
runFixpoint()
|
|
373
625
|
|
|
@@ -395,93 +647,12 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
395
647
|
// - exclude rest position (array pack/unpack stays f64).
|
|
396
648
|
applyPointerParamAbi(paramReps, valueUsed)
|
|
397
649
|
|
|
398
|
-
// E:
|
|
399
|
-
//
|
|
400
|
-
//
|
|
401
|
-
// Safety: skip exported (JS boundary preserves number semantics), value-used (closure
|
|
402
|
-
// trampolines assume f64 result), raw WAT, multi-value. `undefined` return = skip.
|
|
403
|
-
// exprType already consults ctx.func.map for narrowed user-function results
|
|
404
|
-
// (analyze.js exprType `()` branch), plus the Math.imul/Math.clz32/charCodeAt
|
|
405
|
-
// stdlib subset and primitive-op rules. Earlier we had a local shim here that
|
|
406
|
-
// shadowed exprType's stdlib rules with `return 'f64'` for any non-user call;
|
|
407
|
-
// unifying through exprType lets a single rule (math.imul → i32) flow through
|
|
408
|
-
// to mix-style helpers (`(h, x) => Math.imul(h ^ (x|0), C)`) and unblocks the
|
|
409
|
-
// E-phase result narrowing on every call site that consumes them.
|
|
410
|
-
const exprTypeWithCalls = exprType
|
|
411
|
-
// Body-driven: safe for exports — the result type is determined by what the body
|
|
412
|
-
// computes, not by what JS callers might pass. JS-visible f64 ABI is restored at
|
|
413
|
-
// the boundary via a synthesized wrapper (see synthesizeBoundaryWrappers below).
|
|
414
|
-
// Shared pool for E (numeric), E2 (valType) and E3 (ptr) narrowing — same predicate.
|
|
650
|
+
// E + E2: body-driven result-type inference. Pool of narrowable funcs is shared
|
|
651
|
+
// across the i32/VAL/pointer passes — same predicate (non-raw, non-value-used,
|
|
652
|
+
// single-result).
|
|
415
653
|
const funcsWithNarrowableResult = narrowableFuncs(valueUsed)
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
changed = false
|
|
419
|
-
for (const func of funcsWithNarrowableResult) {
|
|
420
|
-
if (func.sig.results[0] === 'i32') continue
|
|
421
|
-
const body = func.body
|
|
422
|
-
// Bare `return;` produces undef (f64) — narrowing to i32 would lose that.
|
|
423
|
-
if (isBlockBody(body) && hasBareReturn(body)) continue
|
|
424
|
-
const exprs = returnExprs(body)
|
|
425
|
-
if (!exprs.length) continue
|
|
426
|
-
// Skip narrowing when any return-tail is `>>>` (unsigned uint32). Narrowing to i32
|
|
427
|
-
// loses the unsigned interpretation: the wrapper rebox via `f64.convert_i32_s` would
|
|
428
|
-
// sign-flip values with bit 31 set, breaking the canonical `(x >>> 0)` uint32 idiom.
|
|
429
|
-
// A future pass could track sig.unsignedResult and emit `f64.convert_i32_u` instead.
|
|
430
|
-
if (exprs.some(e => Array.isArray(e) && e[0] === '>>>')) continue
|
|
431
|
-
const savedCurrent = ctx.func.current
|
|
432
|
-
ctx.func.current = func.sig
|
|
433
|
-
const locals = isBlockBody(body) ? analyzeLocals(body) : new Map()
|
|
434
|
-
for (const p of func.sig.params) if (!locals.has(p.name)) locals.set(p.name, p.type)
|
|
435
|
-
const allI32 = exprs.every(e => exprTypeWithCalls(e, locals) === 'i32')
|
|
436
|
-
ctx.func.current = savedCurrent
|
|
437
|
-
if (allI32) { func.sig.results = ['i32']; changed = true }
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
// E2: VAL-type result inference — if a function always returns the same VAL kind,
|
|
442
|
-
// record it so callers inherit that type (enables static dispatch on .length, .[],
|
|
443
|
-
// .prop through a call chain). Fixpoint propagates through helper chains.
|
|
444
|
-
// Safety: skip exported (host sees raw f64), value-used (indirect call signature).
|
|
445
|
-
// Shim so calls to already-typed funcs contribute their result type.
|
|
446
|
-
const valTypeOfWithCalls = (expr, localValTypes) => {
|
|
447
|
-
if (expr == null) return null
|
|
448
|
-
if (typeof expr === 'string') return localValTypes?.get(expr) || ctx.scope.globalValTypes?.get(expr) || null
|
|
449
|
-
if (!Array.isArray(expr)) return valTypeOf(expr)
|
|
450
|
-
const [op, ...args] = expr
|
|
451
|
-
if (op === '()' && typeof args[0] === 'string') {
|
|
452
|
-
const f = ctx.func.map.get(args[0])
|
|
453
|
-
if (f?.valResult) return f.valResult
|
|
454
|
-
}
|
|
455
|
-
if (op === '?:') {
|
|
456
|
-
const a = valTypeOfWithCalls(args[1], localValTypes), b = valTypeOfWithCalls(args[2], localValTypes)
|
|
457
|
-
return a && a === b ? a : null
|
|
458
|
-
}
|
|
459
|
-
if (op === '&&' || op === '||') {
|
|
460
|
-
const a = valTypeOfWithCalls(args[0], localValTypes), b = valTypeOfWithCalls(args[1], localValTypes)
|
|
461
|
-
return a && a === b ? a : null
|
|
462
|
-
}
|
|
463
|
-
return valTypeOf(expr)
|
|
464
|
-
}
|
|
465
|
-
// Body-driven valResult inference: same safety analysis as numeric narrowing
|
|
466
|
-
// above — exports OK because boundary wrapper restores f64 ABI for JS callers.
|
|
467
|
-
changed = true
|
|
468
|
-
while (changed) {
|
|
469
|
-
changed = false
|
|
470
|
-
for (const func of funcsWithNarrowableResult) {
|
|
471
|
-
if (func.valResult) continue
|
|
472
|
-
const body = func.body
|
|
473
|
-
const isBlock = isBlockBody(body)
|
|
474
|
-
if (isBlock && hasBareReturn(body)) continue
|
|
475
|
-
const exprs = returnExprs(body)
|
|
476
|
-
if (!exprs.length) continue
|
|
477
|
-
const localValTypes = isBlock ? analyzeBody(body).valTypes : new Map()
|
|
478
|
-
// Params of this function contribute no known VAL type yet (paramReps may help later).
|
|
479
|
-
const vt0 = valTypeOfWithCalls(exprs[0], localValTypes)
|
|
480
|
-
if (!vt0) continue
|
|
481
|
-
const allSame = exprs.every(e => valTypeOfWithCalls(e, localValTypes) === vt0)
|
|
482
|
-
if (allSame) { func.valResult = vt0; changed = true }
|
|
483
|
-
}
|
|
484
|
-
}
|
|
654
|
+
narrowI32Results(funcsWithNarrowableResult)
|
|
655
|
+
narrowValResults(funcsWithNarrowableResult)
|
|
485
656
|
|
|
486
657
|
// Now that E2 set `valResult` on funcs, narrow per-func `arrayElemSchema` for
|
|
487
658
|
// VAL.ARRAY-returning funcs (via push observations + call chains). Then re-run the
|
|
@@ -515,141 +686,9 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
515
686
|
// through helper chains: `init()→main→runKernel`.
|
|
516
687
|
runArrValTypeFixpoint()
|
|
517
688
|
runArrValTypeFixpoint()
|
|
518
|
-
// E3:
|
|
519
|
-
//
|
|
520
|
-
|
|
521
|
-
// i32.wrap_i64+i64.reinterpret_f64 unbox at every callsite that uses the value as a
|
|
522
|
-
// pointer (load .[], .length, .prop slot dispatch).
|
|
523
|
-
// - SET/MAP/BUFFER: aux always 0 — no per-callsite aux preservation needed.
|
|
524
|
-
// - OBJECT: aux is schema-id; narrow only when all return exprs share a constant
|
|
525
|
-
// schema (literal `{a,b,c}`, schemaId-bound param, module-bound var, or call to
|
|
526
|
-
// another OBJECT-narrowed func). Caller picks aux up via callIR.ptrAux → readVar →
|
|
527
|
-
// repByLocal.schemaId, restoring property-slot dispatch through the call boundary.
|
|
528
|
-
// Safety: ARRAY forwards on realloc (no narrowing). STRING dual-encoded SSO/heap.
|
|
529
|
-
// CLOSURE/TYPED also carry meaningful aux — TYPED narrowing is a follow-up. Body must
|
|
530
|
-
// be a guaranteed-return form — fallthrough fallback i32.const 0 would be a valid
|
|
531
|
-
// offset 0 of the narrowed kind, not undefined.
|
|
532
|
-
const PTR_RESULT_KINDS_NOAUX = new Set([VAL.SET, VAL.MAP, VAL.BUFFER])
|
|
533
|
-
// Schema-id inference for a return expression. Returns id (number), or null if unknown
|
|
534
|
-
// / not constant. Mirrors inferArgSchema but extends with calls to already-narrowed
|
|
535
|
-
// OBJECT-result funcs (fixpoint propagation through helper chains).
|
|
536
|
-
const schemaIdOfReturn = (expr, paramSchemasMap) => {
|
|
537
|
-
if (typeof expr === 'string') {
|
|
538
|
-
if (paramSchemasMap?.has(expr)) return paramSchemasMap.get(expr)
|
|
539
|
-
if (ctx.schema.vars.has(expr)) return ctx.schema.vars.get(expr)
|
|
540
|
-
return null
|
|
541
|
-
}
|
|
542
|
-
if (!Array.isArray(expr)) return null
|
|
543
|
-
const [op, ...args] = expr
|
|
544
|
-
if (op === '{}') {
|
|
545
|
-
// Object literal: bail to null on block body, dynamic key, or spread.
|
|
546
|
-
const parsed = staticObjectProps(args)
|
|
547
|
-
return parsed ? ctx.schema.register(parsed.names) : null
|
|
548
|
-
}
|
|
549
|
-
if (op === '()' && typeof args[0] === 'string') {
|
|
550
|
-
const f = ctx.func.map.get(args[0])
|
|
551
|
-
if (f?.valResult === VAL.OBJECT && f.sig.ptrAux != null) return f.sig.ptrAux
|
|
552
|
-
return null
|
|
553
|
-
}
|
|
554
|
-
if (op === '?:') {
|
|
555
|
-
const a = schemaIdOfReturn(args[1], paramSchemasMap)
|
|
556
|
-
const b = schemaIdOfReturn(args[2], paramSchemasMap)
|
|
557
|
-
return a != null && a === b ? a : null
|
|
558
|
-
}
|
|
559
|
-
if (op === '&&' || op === '||') {
|
|
560
|
-
const a = schemaIdOfReturn(args[0], paramSchemasMap)
|
|
561
|
-
const b = schemaIdOfReturn(args[1], paramSchemasMap)
|
|
562
|
-
return a != null && a === b ? a : null
|
|
563
|
-
}
|
|
564
|
-
return null
|
|
565
|
-
}
|
|
566
|
-
// Per-body local elemAux map: scans `let/const x = new TypedArray(...)` decls so
|
|
567
|
-
// a return like `let a = new Float64Array(...); return a` resolves to a constant
|
|
568
|
-
// aux. Result calls + ?: are handled inline in typedAuxOfReturn.
|
|
569
|
-
const localElemAuxMap = (body) => {
|
|
570
|
-
const m = new Map()
|
|
571
|
-
const walk = (n) => {
|
|
572
|
-
if (!Array.isArray(n)) return
|
|
573
|
-
const op = n[0]
|
|
574
|
-
if (op === '=>') return
|
|
575
|
-
if ((op === 'let' || op === 'const') && n.length > 1) {
|
|
576
|
-
for (let i = 1; i < n.length; i++) {
|
|
577
|
-
const a = n[i]
|
|
578
|
-
if (Array.isArray(a) && a[0] === '=' && typeof a[1] === 'string') {
|
|
579
|
-
const aux = typedElemAux(typedElemCtor(a[2]))
|
|
580
|
-
if (aux != null) m.set(a[1], aux)
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
for (let i = 1; i < n.length; i++) walk(n[i])
|
|
585
|
-
}
|
|
586
|
-
walk(body)
|
|
587
|
-
return m
|
|
588
|
-
}
|
|
589
|
-
const typedAuxOfReturn = (expr, localElemMap) => {
|
|
590
|
-
if (typeof expr === 'string') return localElemMap?.get(expr) ?? null
|
|
591
|
-
if (!Array.isArray(expr)) return null
|
|
592
|
-
const [op, ...args] = expr
|
|
593
|
-
if (op === '()' && typeof args[0] === 'string') {
|
|
594
|
-
if (args[0].startsWith('new.')) {
|
|
595
|
-
const ctor = typedElemCtor(expr)
|
|
596
|
-
return ctor != null ? typedElemAux(ctor) : null
|
|
597
|
-
}
|
|
598
|
-
const f = ctx.func.map.get(args[0])
|
|
599
|
-
if (f?.valResult === VAL.TYPED && f.sig.ptrAux != null) return f.sig.ptrAux
|
|
600
|
-
return null
|
|
601
|
-
}
|
|
602
|
-
if (op === '?:') {
|
|
603
|
-
const a = typedAuxOfReturn(args[1], localElemMap)
|
|
604
|
-
const b = typedAuxOfReturn(args[2], localElemMap)
|
|
605
|
-
return a != null && a === b ? a : null
|
|
606
|
-
}
|
|
607
|
-
if (op === '&&' || op === '||') {
|
|
608
|
-
const a = typedAuxOfReturn(args[0], localElemMap)
|
|
609
|
-
const b = typedAuxOfReturn(args[1], localElemMap)
|
|
610
|
-
return a != null && a === b ? a : null
|
|
611
|
-
}
|
|
612
|
-
return null
|
|
613
|
-
}
|
|
614
|
-
// Fixpoint: a chain `outer → inner → {a,b}` needs inner to narrow first so outer's
|
|
615
|
-
// call to inner contributes a known schema-id.
|
|
616
|
-
let narrowChanged = true
|
|
617
|
-
while (narrowChanged) {
|
|
618
|
-
narrowChanged = false
|
|
619
|
-
for (const func of funcsWithNarrowableResult) {
|
|
620
|
-
if (!func.valResult) continue
|
|
621
|
-
if (func.sig.results[0] !== 'f64') continue
|
|
622
|
-
const isBlock = isBlockBody(func.body)
|
|
623
|
-
if (isBlock && !alwaysReturns(func.body)) continue
|
|
624
|
-
if (PTR_RESULT_KINDS_NOAUX.has(func.valResult)) {
|
|
625
|
-
func.sig.results = ['i32']
|
|
626
|
-
func.sig.ptrKind = func.valResult
|
|
627
|
-
narrowChanged = true
|
|
628
|
-
continue
|
|
629
|
-
}
|
|
630
|
-
const exprs = returnExprs(func.body)
|
|
631
|
-
if (!exprs.length) continue
|
|
632
|
-
if (func.valResult === VAL.OBJECT) {
|
|
633
|
-
const paramSchemasMap = callerParamFactMap(paramReps, func, 'schemaId')
|
|
634
|
-
const sid0 = schemaIdOfReturn(exprs[0], paramSchemasMap)
|
|
635
|
-
if (sid0 == null) continue
|
|
636
|
-
if (!exprs.every(e => schemaIdOfReturn(e, paramSchemasMap) === sid0)) continue
|
|
637
|
-
func.sig.results = ['i32']
|
|
638
|
-
func.sig.ptrKind = VAL.OBJECT
|
|
639
|
-
func.sig.ptrAux = sid0
|
|
640
|
-
narrowChanged = true
|
|
641
|
-
} else if (func.valResult === VAL.TYPED) {
|
|
642
|
-
const localMap = isBlock ? localElemAuxMap(func.body) : null
|
|
643
|
-
const aux0 = typedAuxOfReturn(exprs[0], localMap)
|
|
644
|
-
if (aux0 == null) continue
|
|
645
|
-
if (!exprs.every(e => typedAuxOfReturn(e, localMap) === aux0)) continue
|
|
646
|
-
func.sig.results = ['i32']
|
|
647
|
-
func.sig.ptrKind = VAL.TYPED
|
|
648
|
-
func.sig.ptrAux = aux0
|
|
649
|
-
narrowChanged = true
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
}
|
|
689
|
+
// E3: pointer-kind result narrowing — once valResult is set, lift the wasm
|
|
690
|
+
// return type to i32 + ptrKind/ptrAux when aux is statically resolvable.
|
|
691
|
+
narrowPointerResults(funcsWithNarrowableResult, paramReps)
|
|
653
692
|
|
|
654
693
|
// F: Cross-call typed-array element ctor propagation. Runs AFTER E3 so that
|
|
655
694
|
// calls to user functions returning a TYPED-narrowed pointer (with constant
|
|
@@ -658,8 +697,10 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
658
697
|
// for their own params and `arr[i]` reads emit a direct `f64.load` instead of
|
|
659
698
|
// the runtime `__is_str_key + __typed_idx` dispatch — closes the largest
|
|
660
699
|
// chunk of the JS→wasm gap on f64-heavy hot loops.
|
|
661
|
-
// (
|
|
662
|
-
//
|
|
700
|
+
// (Helper `inferTypedCtor` lives in src/infer.js — the call-site mirror
|
|
701
|
+
// of body-walk evidence — and is reused by the bimorphic-typed
|
|
702
|
+
// specialization pass below; `ctorFromElemAux` stays in analyze.js next
|
|
703
|
+
// to its encode/decode partner.)
|
|
663
704
|
// Per-caller typed-elem map, recomputed now that E3 has tagged helper sigs.
|
|
664
705
|
// Cache invalidation: analyzeBody.typedElems reads `ctx.func.map.get(...).sig.ptrKind`
|
|
665
706
|
// for `let x = mkInput(...)` decls; entries cached during the initial walk
|
|
@@ -670,7 +711,7 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
670
711
|
// its own callees (e.g. if `outer(buf)` calls `inner(buf)` and we learn `buf`
|
|
671
712
|
// for outer, the second pass picks it up for inner). Reuses runArrElemFixpoint
|
|
672
713
|
// (same shape — field/inferFn/elemsCtxMap parameterization).
|
|
673
|
-
const runTypedFixpoint = () => runArrElemFixpoint('typedCtor',
|
|
714
|
+
const runTypedFixpoint = () => runArrElemFixpoint('typedCtor', inferTypedCtor, callerTypedCtx)
|
|
674
715
|
runTypedFixpoint()
|
|
675
716
|
runTypedFixpoint()
|
|
676
717
|
|
|
@@ -687,7 +728,7 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
687
728
|
// H: Post-F/G re-fixpoint — propagates VAL kinds through bimorphic call sites
|
|
688
729
|
// where ptrKind narrowed but ptrAux disagreed (e.g. `sum(f64arr)` and `sum(i32arr)`
|
|
689
730
|
// → both VAL.TYPED, different ctors). Without this, callerValTypes carries no entry
|
|
690
|
-
// for caller's params, so
|
|
731
|
+
// for caller's params, so inferValType returns null and paramReps[callee][k].val is
|
|
691
732
|
// sticky null. With ptrKind enriching callerValTypes, sum's arr gets val=TYPED in
|
|
692
733
|
// its rep, letting array.js skip __is_str_key + __str_idx dispatch on `arr[i]`.
|
|
693
734
|
enrichCallerValTypesFromPointerParams(callerCtx)
|
|
@@ -715,6 +756,174 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
715
756
|
// so r.wasm flips to 'i32' here — but narrowing now breaks the clone path
|
|
716
757
|
// that still needs to mint per-ctor sigs with ptrKind=TYPED, ptrAux=ctor-aux.
|
|
717
758
|
applyI32ParamSpecialization(paramReps, valueUsed, { skipTyped: true })
|
|
759
|
+
|
|
760
|
+
// J: jsstring boundary opt-in — for exported funcs with a string param whose
|
|
761
|
+
// every use is mappable to a wasm:js-string builtin, flip the param's wasm
|
|
762
|
+
// slot from f64 (nanbox SSO carrier) to externref so the JS host passes the
|
|
763
|
+
// native string directly. Zero copy, zero transcoding. See applyJsstringBoundaryCarrier.
|
|
764
|
+
if (jsstringEnabled()) applyJsstringBoundaryCarrier(paramReps, valueUsed)
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/** Gate the jsstring opt-in: enabled by default for JS hosts; disabled under
|
|
768
|
+
* WASI (env imports unavailable) or when explicitly opted out via the
|
|
769
|
+
* `optimize: { jsstring: false }` knob (used by side-by-side benchmarks). */
|
|
770
|
+
function jsstringEnabled() {
|
|
771
|
+
if (ctx.transform.host === 'wasi') return false
|
|
772
|
+
if (ctx.transform.optimize?.jsstring === false) return false
|
|
773
|
+
return true
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/** Phase J standalone: runs even when `canSkipWholeProgramNarrowing` short-circuits
|
|
777
|
+
* the main narrow pass. The check is body-local and export-boundary-only, so call-
|
|
778
|
+
* site lattice isn't needed; just guard on host and run the use-scan. */
|
|
779
|
+
export function applyJsstringBoundaryCarrierStandalone(programFacts) {
|
|
780
|
+
if (!jsstringEnabled()) return
|
|
781
|
+
applyJsstringBoundaryCarrier(new Map(), programFacts.valueUsed)
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Body-local boolean-result inference. `narrowValResults` is the general (any
|
|
786
|
+
* VAL.*) pass, but it lives inside whole-program narrowing, which is skipped for
|
|
787
|
+
* trivial leaf modules (no call sites). A boolean result is the one kind whose
|
|
788
|
+
* internal carrier (the 0/1 number) differs from its host-boundary carrier (the
|
|
789
|
+
* TRUE_NAN/FALSE_NAN atom), so an exported `(a) => a > 2` still needs its boundary
|
|
790
|
+
* box even on the skip path. This pass only ever *sets* valResult = VAL.BOOL, so
|
|
791
|
+
* it is safe to run unconditionally — pointer/array/number results are untouched.
|
|
792
|
+
*/
|
|
793
|
+
export function narrowBoolResults() {
|
|
794
|
+
for (const func of ctx.func.list) {
|
|
795
|
+
if (func.raw || func.valResult || !func.body || func.sig.results.length !== 1) continue
|
|
796
|
+
const body = func.body
|
|
797
|
+
const isBlock = isBlockBody(body)
|
|
798
|
+
if (isBlock && hasBareReturn(body)) continue
|
|
799
|
+
const exprs = returnExprs(body)
|
|
800
|
+
if (!exprs.length) continue
|
|
801
|
+
const localValTypes = isBlock ? analyzeBody(body).valTypes : null
|
|
802
|
+
const vt = e => typeof e === 'string'
|
|
803
|
+
? (localValTypes?.get(e) || ctx.scope.globalValTypes?.get(e) || null)
|
|
804
|
+
: valTypeOf(e)
|
|
805
|
+
if (exprs.every(e => vt(e) === VAL.BOOL)) func.valResult = VAL.BOOL
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// ── jsstring boundary carrier ───────────────────────────────────────────────
|
|
810
|
+
//
|
|
811
|
+
// Mappable use of an exported string param:
|
|
812
|
+
// - `s.length` → wasm:js-string.length
|
|
813
|
+
// - `s.charCodeAt(idx)` → wasm:js-string.charCodeAt — but ONLY when the
|
|
814
|
+
// index is provably in-bounds (scanBoundedLoops).
|
|
815
|
+
// The builtin traps on OOB; JS semantics return
|
|
816
|
+
// NaN. The only way to preserve JS semantics with
|
|
817
|
+
// zero overhead is to refuse non-bounded use.
|
|
818
|
+
// Anything else (concat, indexing `s[i]`, regex, hash key, passing to a non-
|
|
819
|
+
// externref param, reassignment, closure capture, `==` with anything, …) is a
|
|
820
|
+
// fallback trigger and disqualifies the param.
|
|
821
|
+
|
|
822
|
+
const JSS_OK_PROPS = new Set(['length', 'charCodeAt'])
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Decide whether `name` (an exported func's STRING-shaped param) can flow
|
|
826
|
+
* through the boundary as `externref`. Walk the body once: every leaf
|
|
827
|
+
* occurrence of `name` must be the receiver of `.length` (always safe) or
|
|
828
|
+
* `.charCodeAt` whose callee node lives in `safeCC` (provably bounded).
|
|
829
|
+
* Reassignment / `++` / `--` / closure capture all reject conservatively.
|
|
830
|
+
*
|
|
831
|
+
* Returns `{ ok, stringDiscriminating }` — `stringDiscriminating` is true iff
|
|
832
|
+
* we saw at least one string-only use (`.charCodeAt`); the caller uses this
|
|
833
|
+
* to gate on whether the body actually proves the param is a string, since
|
|
834
|
+
* `.length` alone is polymorphic across string/array/typed-array.
|
|
835
|
+
*/
|
|
836
|
+
function paramAllUsesJsstringMappable(body, name, safeCC) {
|
|
837
|
+
if (body == null) return { ok: false, stringDiscriminating: false }
|
|
838
|
+
let ok = true
|
|
839
|
+
let stringDiscriminating = false
|
|
840
|
+
const walk = (node) => {
|
|
841
|
+
if (!ok) return
|
|
842
|
+
if (typeof node === 'string') {
|
|
843
|
+
// A bare reference to the param outside an eligible `[., name, prop]`
|
|
844
|
+
// callee slot is a fallback trigger — the param escaped to a non-
|
|
845
|
+
// mappable use.
|
|
846
|
+
if (node === name) ok = false
|
|
847
|
+
return
|
|
848
|
+
}
|
|
849
|
+
if (!Array.isArray(node)) return
|
|
850
|
+
const op = node[0]
|
|
851
|
+
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
|
+
const params = node[1]
|
|
855
|
+
const shadowed = Array.isArray(params)
|
|
856
|
+
? params.some(p => (typeof p === 'string' && p === name) ||
|
|
857
|
+
(Array.isArray(p) && p[1] === name))
|
|
858
|
+
: params === name
|
|
859
|
+
if (!shadowed) ok = false
|
|
860
|
+
return
|
|
861
|
+
}
|
|
862
|
+
if ((op === '=' || op === '+=' || op === '-=' || op === '*=' || op === '/=' ||
|
|
863
|
+
op === '%=' || op === '&=' || op === '|=' || op === '^=' ||
|
|
864
|
+
op === '>>=' || op === '<<=' || op === '>>>=' ||
|
|
865
|
+
op === '||=' || op === '&&=' || op === '??=' ||
|
|
866
|
+
op === '++' || op === '--') && node[1] === name) {
|
|
867
|
+
ok = false
|
|
868
|
+
return
|
|
869
|
+
}
|
|
870
|
+
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
|
+
if (node[2] === 'length') return
|
|
876
|
+
if (safeCC.has(node)) { stringDiscriminating = true; return }
|
|
877
|
+
ok = false
|
|
878
|
+
return
|
|
879
|
+
}
|
|
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
|
+
for (let i = 1; i < node.length; i++) walk(node[i])
|
|
884
|
+
}
|
|
885
|
+
walk(body)
|
|
886
|
+
return { ok, stringDiscriminating }
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function applyJsstringBoundaryCarrier(paramReps, valueUsed) {
|
|
890
|
+
for (const func of ctx.func.list) {
|
|
891
|
+
if (func.raw || !func.exported) continue
|
|
892
|
+
if (!func.body) continue
|
|
893
|
+
if (func.rest) continue // rest position stays packed-array
|
|
894
|
+
if (valueUsed.has(func.name)) continue // value-used → callers may pass non-string
|
|
895
|
+
// Pre-compute the in-bounds .charCodeAt callee nodes once per body.
|
|
896
|
+
const safeCC = new Set()
|
|
897
|
+
scanBoundedLoops(func.body, safeCC)
|
|
898
|
+
const reps = paramReps.get(func.name)
|
|
899
|
+
for (let k = 0; k < func.sig.params.length; k++) {
|
|
900
|
+
const p = func.sig.params[k]
|
|
901
|
+
if (p.type !== 'f64' || p.ptrKind != null) continue
|
|
902
|
+
// String-literal defaults (`s = ''`, `s = 'default'`) are both string-
|
|
903
|
+
// discrimination proof AND substituted JS-side by the interop wrapper —
|
|
904
|
+
// see `jz:extparam` def field. Non-string defaults still disqualify:
|
|
905
|
+
// the wasm side has no way to materialise an arbitrary externref default
|
|
906
|
+
// at boundary-check time without a host import.
|
|
907
|
+
const defVal = func.defaults?.[p.name]
|
|
908
|
+
if (defVal != null && !isLiteralStr(defVal)) continue
|
|
909
|
+
const { ok: usesOk, stringDiscriminating } = paramAllUsesJsstringMappable(func.body, p.name, safeCC)
|
|
910
|
+
if (!usesOk) continue
|
|
911
|
+
const r = reps?.get(k)
|
|
912
|
+
// Skip if any rep says non-STRING (`r.val` set to ARRAY/TYPED at any
|
|
913
|
+
// call site rules out jsstring).
|
|
914
|
+
if (r && r.val != null && r.val !== VAL.STRING) continue
|
|
915
|
+
// Discrimination signal: either a string-discriminating body use
|
|
916
|
+
// (`.charCodeAt`), a call-site proof (`r.val === STRING`), or an
|
|
917
|
+
// explicit string-literal default (the source intent declaration).
|
|
918
|
+
const hasStringDefault = defVal != null && isLiteralStr(defVal)
|
|
919
|
+
if (!stringDiscriminating && r?.val !== VAL.STRING && !hasStringDefault) continue
|
|
920
|
+
p.type = 'externref'
|
|
921
|
+
p.jsstring = true
|
|
922
|
+
// Record the literal default so compile.js can omit wasm-side substitution
|
|
923
|
+
// (the JS-side interop wrapper applies the default on `undefined`).
|
|
924
|
+
if (hasStringDefault) p.jsstringDefault = defVal[1]
|
|
925
|
+
}
|
|
926
|
+
}
|
|
718
927
|
}
|
|
719
928
|
|
|
720
929
|
/**
|
|
@@ -762,7 +971,7 @@ export function specializeBimorphicTyped(programFacts) {
|
|
|
762
971
|
// (so transitive `sum(arr)` inside a func that took `arr` from above resolves).
|
|
763
972
|
const callerTypedParamsCtx = new Map()
|
|
764
973
|
for (const func of ctx.func.list) {
|
|
765
|
-
const m =
|
|
974
|
+
const m = paramFactsOf(paramReps, func, 'typedCtor') || null
|
|
766
975
|
let acc = m
|
|
767
976
|
if (func.sig?.params) for (const p of func.sig.params) {
|
|
768
977
|
if (p.ptrKind === VAL.TYPED && p.ptrAux != null) {
|
|
@@ -807,7 +1016,7 @@ export function specializeBimorphicTyped(programFacts) {
|
|
|
807
1016
|
const combo = []
|
|
808
1017
|
for (const k of bimorphic) {
|
|
809
1018
|
if (k >= site.argList.length) { abort = true; break }
|
|
810
|
-
const c =
|
|
1019
|
+
const c = inferTypedCtor(site.argList[k], callerTypedElems, callerTypedParams)
|
|
811
1020
|
if (c == null || typedElemAux(c) == null) { abort = true; break }
|
|
812
1021
|
combo.push(c)
|
|
813
1022
|
}
|