jz 0.4.0 → 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 +275 -255
- package/cli.js +8 -51
- package/index.js +166 -31
- package/{src/host.js → interop.js} +291 -88
- package/module/array.js +181 -71
- package/module/collection.js +222 -8
- package/module/console.js +1 -1
- package/module/core.js +251 -118
- package/module/date.js +9 -5
- package/module/function.js +11 -1
- package/module/json.js +361 -55
- package/module/math.js +551 -128
- package/module/number.js +390 -227
- package/module/object.js +252 -34
- package/module/regex.js +123 -16
- package/module/schema.js +15 -1
- package/module/string.js +540 -96
- package/module/timer.js +1 -1
- package/module/typedarray.js +674 -57
- package/package.json +10 -7
- 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 +1870 -485
- package/src/assemble.js +27 -12
- package/src/autoload.js +13 -1
- package/src/compile.js +446 -179
- package/src/ctx.js +85 -18
- package/src/emit.js +662 -196
- package/src/infer.js +548 -0
- package/src/ir.js +291 -23
- package/src/jzify.js +786 -94
- package/src/narrow.js +433 -251
- package/src/optimize.js +126 -122
- package/src/plan.js +703 -34
- package/src/prepare.js +822 -150
- 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/fuse.js +0 -159
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
|
|
|
@@ -197,21 +202,245 @@ function refreshCallerLocals(callerCtx) {
|
|
|
197
202
|
// this, post-G `refreshCallerLocals` still walks bodies with arr untyped, the
|
|
198
203
|
// length stays f64, and any callee taking that length never gets an i32 param
|
|
199
204
|
// (heapsort→siftDown's `end`). analyzeFuncForEmit re-seeds + re-invalidates at
|
|
200
|
-
// emit time, so this transient
|
|
201
|
-
ctx.func.
|
|
202
|
-
for (const p of func.sig.params) if (p.ptrKind != null) ctx.func.
|
|
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 })
|
|
203
208
|
invalidateLocalsCache(func.body)
|
|
204
|
-
const fresh =
|
|
209
|
+
const fresh = analyzeBody(func.body).locals
|
|
205
210
|
for (const p of func.sig.params) if (!fresh.has(p.name)) fresh.set(p.name, p.type)
|
|
206
211
|
callerCtx.get(func).callerLocals = fresh
|
|
207
212
|
}
|
|
208
|
-
ctx.func.
|
|
213
|
+
ctx.func.localReps = null
|
|
209
214
|
}
|
|
210
215
|
|
|
211
216
|
function resetParamWasmFacts(paramReps) {
|
|
212
217
|
for (const m of paramReps.values()) for (const r of m.values()) r.wasm = undefined
|
|
213
218
|
}
|
|
214
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
|
+
|
|
215
444
|
function createPhaseState() {
|
|
216
445
|
const callerCtx = buildCallerCtx()
|
|
217
446
|
const elemCtx = new Map()
|
|
@@ -269,13 +498,13 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
269
498
|
// D: Call-site type propagation — infer param types from how functions are called.
|
|
270
499
|
// Drives off `callSites` collected during the ProgramFacts walk; no AST re-walking.
|
|
271
500
|
// For non-exported internal functions, if all call sites agree on a param's type,
|
|
272
|
-
// seed the param's val rep (ctx.func.
|
|
501
|
+
// seed the param's val rep (ctx.func.localReps) during per-function compilation.
|
|
273
502
|
// Also infer i32/f64 WASM type — when all call sites pass i32 for a param, specialize
|
|
274
503
|
// sig.params[k].type to i32 (no default, no rest, not exported, not value-used).
|
|
275
504
|
// Also propagate schema ID — when all call sites pass objects with the same schema,
|
|
276
505
|
// bind the callee's param to that schema so `p.x` becomes a direct slot load.
|
|
277
|
-
// Inference helpers (
|
|
278
|
-
// live in
|
|
506
|
+
// Inference helpers (inferValType/inferSchemaId/inferArr*/inferTypedCtor)
|
|
507
|
+
// live in infer.js — pure AST→fact resolvers shared across fixpoint phases.
|
|
279
508
|
// Per-caller analysis is stable across fixpoint iterations — precompute once.
|
|
280
509
|
// callerCtx[null] (top-level) uses module globals for both locals and valTypes.
|
|
281
510
|
const phase = createPhaseState()
|
|
@@ -286,7 +515,7 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
286
515
|
else if (Array.isArray(arg) && arg[0] == null && typeof arg[1] === 'number') raw = arg[1]
|
|
287
516
|
else if (Array.isArray(arg) && arg[0] === 'u-' && typeof arg[1] === 'number') raw = -arg[1]
|
|
288
517
|
else if (typeof arg === 'string' && ctx.scope.constInts?.has(arg)) raw = ctx.scope.constInts.get(arg)
|
|
289
|
-
return (raw != null && Number.isInteger(raw) && raw >=
|
|
518
|
+
return (raw != null && Number.isInteger(raw) && raw >= I32_MIN && raw <= I32_MAX) ? raw : null
|
|
290
519
|
}
|
|
291
520
|
|
|
292
521
|
const runCallsiteLattice = (rules) => {
|
|
@@ -303,7 +532,7 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
303
532
|
callerLocals: ctxEntry.callerLocals,
|
|
304
533
|
callerValTypes: ctxEntry.callerValTypes,
|
|
305
534
|
callerParamFacts(key) {
|
|
306
|
-
if (!paramFacts.has(key)) paramFacts.set(key,
|
|
535
|
+
if (!paramFacts.has(key)) paramFacts.set(key, paramFactsOf(paramReps, callerFunc, key))
|
|
307
536
|
return paramFacts.get(key)
|
|
308
537
|
},
|
|
309
538
|
}
|
|
@@ -317,13 +546,13 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
317
546
|
}
|
|
318
547
|
|
|
319
548
|
const poison = field => r => { r[field] = null }
|
|
320
|
-
// Default-aware val inference. Adds two fallbacks beyond
|
|
549
|
+
// Default-aware val inference. Adds two fallbacks beyond inferValType's
|
|
321
550
|
// body-local `callerValTypes` lookup so a hot recursive helper like
|
|
322
551
|
// `uleb(n, buffer = []) { ... return uleb(n, buffer) }` resolves the
|
|
323
552
|
// recursive `buffer` arg to VAL.ARRAY (via callerParamFacts on iter 2,
|
|
324
553
|
// or via the caller's own default expression on iter 1).
|
|
325
554
|
const inferValAtSite = (arg, state) => {
|
|
326
|
-
const v =
|
|
555
|
+
const v = inferValType(arg, state.callerValTypes)
|
|
327
556
|
if (v != null) return v
|
|
328
557
|
if (typeof arg !== 'string') return null
|
|
329
558
|
const fromParam = state.callerParamFacts('val')?.get(arg)
|
|
@@ -361,7 +590,7 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
361
590
|
else if (r.wasm !== wt) r.wasm = null
|
|
362
591
|
},
|
|
363
592
|
},
|
|
364
|
-
mergeRule('schemaId', (arg, _k, state) =>
|
|
593
|
+
mergeRule('schemaId', (arg, _k, state) => inferSchemaId(arg, state.callerParamFacts('schemaId'))),
|
|
365
594
|
{
|
|
366
595
|
missing: poison('intConst'),
|
|
367
596
|
apply(r, arg, k, state) {
|
|
@@ -389,8 +618,8 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
389
618
|
do { changed = false; runCallsiteLattice([soft]) } while (changed)
|
|
390
619
|
runCallsiteLattice([mergeRule(field, infer)])
|
|
391
620
|
}
|
|
392
|
-
const runArrFixpoint = () => runArrElemFixpoint('arrayElemSchema',
|
|
393
|
-
const runArrValTypeFixpoint = () => runArrElemFixpoint('arrayElemValType',
|
|
621
|
+
const runArrFixpoint = () => runArrElemFixpoint('arrayElemSchema', inferArrElemSchema, phase.callerElems('arrElemSchemas'))
|
|
622
|
+
const runArrValTypeFixpoint = () => runArrElemFixpoint('arrayElemValType', inferArrElemValType, phase.callerElems('arrElemValTypes'))
|
|
394
623
|
runFixpoint()
|
|
395
624
|
runFixpoint()
|
|
396
625
|
|
|
@@ -418,97 +647,12 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
418
647
|
// - exclude rest position (array pack/unpack stays f64).
|
|
419
648
|
applyPointerParamAbi(paramReps, valueUsed)
|
|
420
649
|
|
|
421
|
-
// E:
|
|
422
|
-
//
|
|
423
|
-
//
|
|
424
|
-
// Safety: skip exported (JS boundary preserves number semantics), value-used (closure
|
|
425
|
-
// trampolines assume f64 result), raw WAT, multi-value. `undefined` return = skip.
|
|
426
|
-
// exprType already consults ctx.func.map for narrowed user-function results
|
|
427
|
-
// (analyze.js exprType `()` branch), plus the Math.imul/Math.clz32/charCodeAt
|
|
428
|
-
// stdlib subset and primitive-op rules. Earlier we had a local shim here that
|
|
429
|
-
// shadowed exprType's stdlib rules with `return 'f64'` for any non-user call;
|
|
430
|
-
// unifying through exprType lets a single rule (math.imul → i32) flow through
|
|
431
|
-
// to mix-style helpers (`(h, x) => Math.imul(h ^ (x|0), C)`) and unblocks the
|
|
432
|
-
// E-phase result narrowing on every call site that consumes them.
|
|
433
|
-
const exprTypeWithCalls = exprType
|
|
434
|
-
// Body-driven: safe for exports — the result type is determined by what the body
|
|
435
|
-
// computes, not by what JS callers might pass. JS-visible f64 ABI is restored at
|
|
436
|
-
// the boundary via a synthesized wrapper (see synthesizeBoundaryWrappers below).
|
|
437
|
-
// 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).
|
|
438
653
|
const funcsWithNarrowableResult = narrowableFuncs(valueUsed)
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
changed = false
|
|
442
|
-
for (const func of funcsWithNarrowableResult) {
|
|
443
|
-
if (func.sig.results[0] === 'i32') continue
|
|
444
|
-
const body = func.body
|
|
445
|
-
// Bare `return;` produces undef (f64) — narrowing to i32 would lose that.
|
|
446
|
-
if (isBlockBody(body) && hasBareReturn(body)) continue
|
|
447
|
-
const exprs = returnExprs(body)
|
|
448
|
-
if (!exprs.length) continue
|
|
449
|
-
// Narrow result to i32 when every return-tail types as i32 (incl. bitwise ops, `|0`,
|
|
450
|
-
// and `>>>`). When ANY tail is `>>>` (unsigned uint32 idiom) we still narrow, but tag
|
|
451
|
-
// `sig.unsignedResult` so the call-site rebox uses `f64.convert_i32_u` — preserving
|
|
452
|
-
// the [0, 2^32) range that `(x >>> 0)` carries through hash/CRC finalizers.
|
|
453
|
-
const anyUnsigned = exprs.some(e => Array.isArray(e) && e[0] === '>>>')
|
|
454
|
-
const savedCurrent = ctx.func.current
|
|
455
|
-
ctx.func.current = func.sig
|
|
456
|
-
const locals = isBlockBody(body) ? analyzeLocals(body) : new Map()
|
|
457
|
-
for (const p of func.sig.params) if (!locals.has(p.name)) locals.set(p.name, p.type)
|
|
458
|
-
const allI32 = exprs.every(e => exprTypeWithCalls(e, locals) === 'i32')
|
|
459
|
-
ctx.func.current = savedCurrent
|
|
460
|
-
if (allI32) {
|
|
461
|
-
func.sig.results = ['i32']
|
|
462
|
-
if (anyUnsigned) func.sig.unsignedResult = true
|
|
463
|
-
changed = true
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// E2: VAL-type result inference — if a function always returns the same VAL kind,
|
|
469
|
-
// record it so callers inherit that type (enables static dispatch on .length, .[],
|
|
470
|
-
// .prop through a call chain). Fixpoint propagates through helper chains.
|
|
471
|
-
// Safety: skip exported (host sees raw f64), value-used (indirect call signature).
|
|
472
|
-
// Shim so calls to already-typed funcs contribute their result type.
|
|
473
|
-
const valTypeOfWithCalls = (expr, localValTypes) => {
|
|
474
|
-
if (expr == null) return null
|
|
475
|
-
if (typeof expr === 'string') return localValTypes?.get(expr) || ctx.scope.globalValTypes?.get(expr) || null
|
|
476
|
-
if (!Array.isArray(expr)) return valTypeOf(expr)
|
|
477
|
-
const [op, ...args] = expr
|
|
478
|
-
if (op === '()' && typeof args[0] === 'string') {
|
|
479
|
-
const f = ctx.func.map.get(args[0])
|
|
480
|
-
if (f?.valResult) return f.valResult
|
|
481
|
-
}
|
|
482
|
-
if (op === '?:') {
|
|
483
|
-
const a = valTypeOfWithCalls(args[1], localValTypes), b = valTypeOfWithCalls(args[2], localValTypes)
|
|
484
|
-
return a && a === b ? a : null
|
|
485
|
-
}
|
|
486
|
-
if (op === '&&' || op === '||') {
|
|
487
|
-
const a = valTypeOfWithCalls(args[0], localValTypes), b = valTypeOfWithCalls(args[1], localValTypes)
|
|
488
|
-
return a && a === b ? a : null
|
|
489
|
-
}
|
|
490
|
-
return valTypeOf(expr)
|
|
491
|
-
}
|
|
492
|
-
// Body-driven valResult inference: same safety analysis as numeric narrowing
|
|
493
|
-
// above — exports OK because boundary wrapper restores f64 ABI for JS callers.
|
|
494
|
-
changed = true
|
|
495
|
-
while (changed) {
|
|
496
|
-
changed = false
|
|
497
|
-
for (const func of funcsWithNarrowableResult) {
|
|
498
|
-
if (func.valResult) continue
|
|
499
|
-
const body = func.body
|
|
500
|
-
const isBlock = isBlockBody(body)
|
|
501
|
-
if (isBlock && hasBareReturn(body)) continue
|
|
502
|
-
const exprs = returnExprs(body)
|
|
503
|
-
if (!exprs.length) continue
|
|
504
|
-
const localValTypes = isBlock ? analyzeBody(body).valTypes : new Map()
|
|
505
|
-
// Params of this function contribute no known VAL type yet (paramReps may help later).
|
|
506
|
-
const vt0 = valTypeOfWithCalls(exprs[0], localValTypes)
|
|
507
|
-
if (!vt0) continue
|
|
508
|
-
const allSame = exprs.every(e => valTypeOfWithCalls(e, localValTypes) === vt0)
|
|
509
|
-
if (allSame) { func.valResult = vt0; changed = true }
|
|
510
|
-
}
|
|
511
|
-
}
|
|
654
|
+
narrowI32Results(funcsWithNarrowableResult)
|
|
655
|
+
narrowValResults(funcsWithNarrowableResult)
|
|
512
656
|
|
|
513
657
|
// Now that E2 set `valResult` on funcs, narrow per-func `arrayElemSchema` for
|
|
514
658
|
// VAL.ARRAY-returning funcs (via push observations + call chains). Then re-run the
|
|
@@ -542,141 +686,9 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
542
686
|
// through helper chains: `init()→main→runKernel`.
|
|
543
687
|
runArrValTypeFixpoint()
|
|
544
688
|
runArrValTypeFixpoint()
|
|
545
|
-
// E3:
|
|
546
|
-
//
|
|
547
|
-
|
|
548
|
-
// i32.wrap_i64+i64.reinterpret_f64 unbox at every callsite that uses the value as a
|
|
549
|
-
// pointer (load .[], .length, .prop slot dispatch).
|
|
550
|
-
// - SET/MAP/BUFFER: aux always 0 — no per-callsite aux preservation needed.
|
|
551
|
-
// - OBJECT: aux is schema-id; narrow only when all return exprs share a constant
|
|
552
|
-
// schema (literal `{a,b,c}`, schemaId-bound param, module-bound var, or call to
|
|
553
|
-
// another OBJECT-narrowed func). Caller picks aux up via callIR.ptrAux → readVar →
|
|
554
|
-
// repByLocal.schemaId, restoring property-slot dispatch through the call boundary.
|
|
555
|
-
// Safety: ARRAY forwards on realloc (no narrowing). STRING dual-encoded SSO/heap.
|
|
556
|
-
// CLOSURE/TYPED also carry meaningful aux — TYPED narrowing is a follow-up. Body must
|
|
557
|
-
// be a guaranteed-return form — fallthrough fallback i32.const 0 would be a valid
|
|
558
|
-
// offset 0 of the narrowed kind, not undefined.
|
|
559
|
-
const PTR_RESULT_KINDS_NOAUX = new Set([VAL.SET, VAL.MAP, VAL.BUFFER])
|
|
560
|
-
// Schema-id inference for a return expression. Returns id (number), or null if unknown
|
|
561
|
-
// / not constant. Mirrors inferArgSchema but extends with calls to already-narrowed
|
|
562
|
-
// OBJECT-result funcs (fixpoint propagation through helper chains).
|
|
563
|
-
const schemaIdOfReturn = (expr, paramSchemasMap) => {
|
|
564
|
-
if (typeof expr === 'string') {
|
|
565
|
-
if (paramSchemasMap?.has(expr)) return paramSchemasMap.get(expr)
|
|
566
|
-
if (ctx.schema.vars.has(expr)) return ctx.schema.vars.get(expr)
|
|
567
|
-
return null
|
|
568
|
-
}
|
|
569
|
-
if (!Array.isArray(expr)) return null
|
|
570
|
-
const [op, ...args] = expr
|
|
571
|
-
if (op === '{}') {
|
|
572
|
-
// Object literal: bail to null on block body, dynamic key, or spread.
|
|
573
|
-
const parsed = staticObjectProps(args)
|
|
574
|
-
return parsed ? ctx.schema.register(parsed.names) : null
|
|
575
|
-
}
|
|
576
|
-
if (op === '()' && typeof args[0] === 'string') {
|
|
577
|
-
const f = ctx.func.map.get(args[0])
|
|
578
|
-
if (f?.valResult === VAL.OBJECT && f.sig.ptrAux != null) return f.sig.ptrAux
|
|
579
|
-
return null
|
|
580
|
-
}
|
|
581
|
-
if (op === '?:') {
|
|
582
|
-
const a = schemaIdOfReturn(args[1], paramSchemasMap)
|
|
583
|
-
const b = schemaIdOfReturn(args[2], paramSchemasMap)
|
|
584
|
-
return a != null && a === b ? a : null
|
|
585
|
-
}
|
|
586
|
-
if (op === '&&' || op === '||') {
|
|
587
|
-
const a = schemaIdOfReturn(args[0], paramSchemasMap)
|
|
588
|
-
const b = schemaIdOfReturn(args[1], paramSchemasMap)
|
|
589
|
-
return a != null && a === b ? a : null
|
|
590
|
-
}
|
|
591
|
-
return null
|
|
592
|
-
}
|
|
593
|
-
// Per-body local elemAux map: scans `let/const x = new TypedArray(...)` decls so
|
|
594
|
-
// a return like `let a = new Float64Array(...); return a` resolves to a constant
|
|
595
|
-
// aux. Result calls + ?: are handled inline in typedAuxOfReturn.
|
|
596
|
-
const localElemAuxMap = (body) => {
|
|
597
|
-
const m = new Map()
|
|
598
|
-
const walk = (n) => {
|
|
599
|
-
if (!Array.isArray(n)) return
|
|
600
|
-
const op = n[0]
|
|
601
|
-
if (op === '=>') return
|
|
602
|
-
if ((op === 'let' || op === 'const') && n.length > 1) {
|
|
603
|
-
for (let i = 1; i < n.length; i++) {
|
|
604
|
-
const a = n[i]
|
|
605
|
-
if (Array.isArray(a) && a[0] === '=' && typeof a[1] === 'string') {
|
|
606
|
-
const aux = typedElemAux(typedElemCtor(a[2]))
|
|
607
|
-
if (aux != null) m.set(a[1], aux)
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
for (let i = 1; i < n.length; i++) walk(n[i])
|
|
612
|
-
}
|
|
613
|
-
walk(body)
|
|
614
|
-
return m
|
|
615
|
-
}
|
|
616
|
-
const typedAuxOfReturn = (expr, localElemMap) => {
|
|
617
|
-
if (typeof expr === 'string') return localElemMap?.get(expr) ?? null
|
|
618
|
-
if (!Array.isArray(expr)) return null
|
|
619
|
-
const [op, ...args] = expr
|
|
620
|
-
if (op === '()' && typeof args[0] === 'string') {
|
|
621
|
-
if (args[0].startsWith('new.')) {
|
|
622
|
-
const ctor = typedElemCtor(expr)
|
|
623
|
-
return ctor != null ? typedElemAux(ctor) : null
|
|
624
|
-
}
|
|
625
|
-
const f = ctx.func.map.get(args[0])
|
|
626
|
-
if (f?.valResult === VAL.TYPED && f.sig.ptrAux != null) return f.sig.ptrAux
|
|
627
|
-
return null
|
|
628
|
-
}
|
|
629
|
-
if (op === '?:') {
|
|
630
|
-
const a = typedAuxOfReturn(args[1], localElemMap)
|
|
631
|
-
const b = typedAuxOfReturn(args[2], localElemMap)
|
|
632
|
-
return a != null && a === b ? a : null
|
|
633
|
-
}
|
|
634
|
-
if (op === '&&' || op === '||') {
|
|
635
|
-
const a = typedAuxOfReturn(args[0], localElemMap)
|
|
636
|
-
const b = typedAuxOfReturn(args[1], localElemMap)
|
|
637
|
-
return a != null && a === b ? a : null
|
|
638
|
-
}
|
|
639
|
-
return null
|
|
640
|
-
}
|
|
641
|
-
// Fixpoint: a chain `outer → inner → {a,b}` needs inner to narrow first so outer's
|
|
642
|
-
// call to inner contributes a known schema-id.
|
|
643
|
-
let narrowChanged = true
|
|
644
|
-
while (narrowChanged) {
|
|
645
|
-
narrowChanged = false
|
|
646
|
-
for (const func of funcsWithNarrowableResult) {
|
|
647
|
-
if (!func.valResult) continue
|
|
648
|
-
if (func.sig.results[0] !== 'f64') continue
|
|
649
|
-
const isBlock = isBlockBody(func.body)
|
|
650
|
-
if (isBlock && !alwaysReturns(func.body)) continue
|
|
651
|
-
if (PTR_RESULT_KINDS_NOAUX.has(func.valResult)) {
|
|
652
|
-
func.sig.results = ['i32']
|
|
653
|
-
func.sig.ptrKind = func.valResult
|
|
654
|
-
narrowChanged = true
|
|
655
|
-
continue
|
|
656
|
-
}
|
|
657
|
-
const exprs = returnExprs(func.body)
|
|
658
|
-
if (!exprs.length) continue
|
|
659
|
-
if (func.valResult === VAL.OBJECT) {
|
|
660
|
-
const paramSchemasMap = callerParamFactMap(paramReps, func, 'schemaId')
|
|
661
|
-
const sid0 = schemaIdOfReturn(exprs[0], paramSchemasMap)
|
|
662
|
-
if (sid0 == null) continue
|
|
663
|
-
if (!exprs.every(e => schemaIdOfReturn(e, paramSchemasMap) === sid0)) continue
|
|
664
|
-
func.sig.results = ['i32']
|
|
665
|
-
func.sig.ptrKind = VAL.OBJECT
|
|
666
|
-
func.sig.ptrAux = sid0
|
|
667
|
-
narrowChanged = true
|
|
668
|
-
} else if (func.valResult === VAL.TYPED) {
|
|
669
|
-
const localMap = isBlock ? localElemAuxMap(func.body) : null
|
|
670
|
-
const aux0 = typedAuxOfReturn(exprs[0], localMap)
|
|
671
|
-
if (aux0 == null) continue
|
|
672
|
-
if (!exprs.every(e => typedAuxOfReturn(e, localMap) === aux0)) continue
|
|
673
|
-
func.sig.results = ['i32']
|
|
674
|
-
func.sig.ptrKind = VAL.TYPED
|
|
675
|
-
func.sig.ptrAux = aux0
|
|
676
|
-
narrowChanged = true
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
}
|
|
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)
|
|
680
692
|
|
|
681
693
|
// F: Cross-call typed-array element ctor propagation. Runs AFTER E3 so that
|
|
682
694
|
// calls to user functions returning a TYPED-narrowed pointer (with constant
|
|
@@ -685,8 +697,10 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
685
697
|
// for their own params and `arr[i]` reads emit a direct `f64.load` instead of
|
|
686
698
|
// the runtime `__is_str_key + __typed_idx` dispatch — closes the largest
|
|
687
699
|
// chunk of the JS→wasm gap on f64-heavy hot loops.
|
|
688
|
-
// (
|
|
689
|
-
//
|
|
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.)
|
|
690
704
|
// Per-caller typed-elem map, recomputed now that E3 has tagged helper sigs.
|
|
691
705
|
// Cache invalidation: analyzeBody.typedElems reads `ctx.func.map.get(...).sig.ptrKind`
|
|
692
706
|
// for `let x = mkInput(...)` decls; entries cached during the initial walk
|
|
@@ -697,7 +711,7 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
697
711
|
// its own callees (e.g. if `outer(buf)` calls `inner(buf)` and we learn `buf`
|
|
698
712
|
// for outer, the second pass picks it up for inner). Reuses runArrElemFixpoint
|
|
699
713
|
// (same shape — field/inferFn/elemsCtxMap parameterization).
|
|
700
|
-
const runTypedFixpoint = () => runArrElemFixpoint('typedCtor',
|
|
714
|
+
const runTypedFixpoint = () => runArrElemFixpoint('typedCtor', inferTypedCtor, callerTypedCtx)
|
|
701
715
|
runTypedFixpoint()
|
|
702
716
|
runTypedFixpoint()
|
|
703
717
|
|
|
@@ -714,7 +728,7 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
714
728
|
// H: Post-F/G re-fixpoint — propagates VAL kinds through bimorphic call sites
|
|
715
729
|
// where ptrKind narrowed but ptrAux disagreed (e.g. `sum(f64arr)` and `sum(i32arr)`
|
|
716
730
|
// → both VAL.TYPED, different ctors). Without this, callerValTypes carries no entry
|
|
717
|
-
// for caller's params, so
|
|
731
|
+
// for caller's params, so inferValType returns null and paramReps[callee][k].val is
|
|
718
732
|
// sticky null. With ptrKind enriching callerValTypes, sum's arr gets val=TYPED in
|
|
719
733
|
// its rep, letting array.js skip __is_str_key + __str_idx dispatch on `arr[i]`.
|
|
720
734
|
enrichCallerValTypesFromPointerParams(callerCtx)
|
|
@@ -742,6 +756,174 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
742
756
|
// so r.wasm flips to 'i32' here — but narrowing now breaks the clone path
|
|
743
757
|
// that still needs to mint per-ctor sigs with ptrKind=TYPED, ptrAux=ctor-aux.
|
|
744
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
|
+
}
|
|
745
927
|
}
|
|
746
928
|
|
|
747
929
|
/**
|
|
@@ -789,7 +971,7 @@ export function specializeBimorphicTyped(programFacts) {
|
|
|
789
971
|
// (so transitive `sum(arr)` inside a func that took `arr` from above resolves).
|
|
790
972
|
const callerTypedParamsCtx = new Map()
|
|
791
973
|
for (const func of ctx.func.list) {
|
|
792
|
-
const m =
|
|
974
|
+
const m = paramFactsOf(paramReps, func, 'typedCtor') || null
|
|
793
975
|
let acc = m
|
|
794
976
|
if (func.sig?.params) for (const p of func.sig.params) {
|
|
795
977
|
if (p.ptrKind === VAL.TYPED && p.ptrAux != null) {
|
|
@@ -834,7 +1016,7 @@ export function specializeBimorphicTyped(programFacts) {
|
|
|
834
1016
|
const combo = []
|
|
835
1017
|
for (const k of bimorphic) {
|
|
836
1018
|
if (k >= site.argList.length) { abort = true; break }
|
|
837
|
-
const c =
|
|
1019
|
+
const c = inferTypedCtor(site.argList[k], callerTypedElems, callerTypedParams)
|
|
838
1020
|
if (c == null || typedElemAux(c) == null) { abort = true; break }
|
|
839
1021
|
combo.push(c)
|
|
840
1022
|
}
|