jz 0.7.0 → 0.8.1
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 +37 -33
- package/bench/README.md +176 -73
- package/bench/bench.svg +58 -71
- package/cli.js +12 -5
- package/dist/interop.js +1 -1
- package/dist/jz.js +1366 -1101
- package/index.js +40 -9
- package/interop.js +193 -138
- package/layout.js +29 -18
- package/module/array.js +49 -73
- package/module/collection.js +83 -25
- package/module/console.js +1 -1
- package/module/core.js +161 -15
- package/module/json.js +3 -3
- package/module/math.js +167 -117
- package/module/number.js +247 -13
- package/module/object.js +11 -5
- package/module/regex.js +8 -7
- package/module/string.js +295 -171
- package/module/typedarray.js +169 -105
- package/package.json +7 -3
- package/src/abi/string.js +40 -35
- package/src/ast.js +19 -2
- package/src/compile/analyze.js +64 -2
- package/src/compile/cse-load.js +200 -0
- package/src/compile/emit-assign.js +73 -14
- package/src/compile/emit.js +324 -34
- package/src/compile/index.js +204 -61
- package/src/compile/infer.js +8 -1
- package/src/compile/loop-divmod.js +12 -58
- package/src/compile/loop-model.js +91 -0
- package/src/compile/loop-recurrence.js +167 -0
- package/src/compile/loop-square.js +102 -0
- package/src/compile/narrow.js +180 -34
- package/src/compile/peel-stencil.js +18 -64
- package/src/compile/plan/common.js +29 -0
- package/src/compile/plan/index.js +4 -1
- package/src/compile/plan/inline.js +176 -21
- package/src/compile/plan/literals.js +93 -19
- package/src/ctx.js +51 -12
- package/src/helper-counters.js +137 -0
- package/src/ir.js +102 -13
- package/src/kind-traits.js +7 -3
- package/src/kind.js +14 -1
- package/src/op-policy.js +5 -2
- package/src/ops.js +119 -0
- package/src/optimize/index.js +1125 -136
- package/src/optimize/recurse.js +182 -0
- package/src/optimize/vectorize.js +1302 -144
- package/src/prepare/index.js +29 -12
- package/src/reps.js +4 -1
- package/src/type.js +53 -45
- package/src/wat/assemble.js +92 -9
- package/src/widen.js +21 -0
- package/src/wat/optimize.js +0 -3938
package/src/compile/index.js
CHANGED
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
|
|
28
28
|
import parseWat from 'watr/parse'
|
|
29
29
|
import { ctx, err, inc, resolveIncludes, PTR, LAYOUT, declGlobal } from '../ctx.js'
|
|
30
|
-
import { T, isBlockBody, isReassigned, refsName, REFS_IN_EXPR } from '../ast.js'
|
|
30
|
+
import { T, isBlockBody, isReassigned, refsName, REFS_IN_EXPR, returnExprs } from '../ast.js'
|
|
31
|
+
import { valTypeOf } from '../kind.js'
|
|
31
32
|
import { intLiteralValue } from '../static.js'
|
|
32
33
|
import {
|
|
33
34
|
analyzeBody, unboxablePtrs, cseSafeLoadBases, boxedCaptures,
|
|
@@ -38,7 +39,14 @@ import { VAL, updateRep, REP_FIELDS } from '../reps.js'
|
|
|
38
39
|
import { inferLocals } from './infer.js'
|
|
39
40
|
import { optimizeFunc, treeshake } from '../optimize/index.js'
|
|
40
41
|
import { strengthReduceLoopDivMod } from './loop-divmod.js'
|
|
42
|
+
import { narrowBoundedSquare } from './loop-square.js'
|
|
43
|
+
import { unrollRecurrence } from './loop-recurrence.js'
|
|
41
44
|
import { peelClampedStencil } from './peel-stencil.js'
|
|
45
|
+
import { cseLoads } from './cse-load.js'
|
|
46
|
+
|
|
47
|
+
// Monotonic across all functions so a CSE temp never collides (even after later inlining).
|
|
48
|
+
let __cseCtr = 0
|
|
49
|
+
const freshCseName = () => `${T}cse${__cseCtr++}`
|
|
42
50
|
import { emit, emitter, emitVoid, emitBlockBody } from './emit.js'
|
|
43
51
|
import { emitCharDecompPrologue, JSS_IMPORT_SIGS } from '../abi/string.js'
|
|
44
52
|
import {
|
|
@@ -59,8 +67,9 @@ import plan from './plan/index.js'
|
|
|
59
67
|
import { foldStaticConstAggregates } from './plan/literals.js'
|
|
60
68
|
import {
|
|
61
69
|
buildStartFn, dedupClosureBodies, finalizeClosureTable,
|
|
62
|
-
pullStdlib, syncImports, optimizeModule, stripStaticDataPrefix, hoistConstGlobalInits,
|
|
70
|
+
pullStdlib, syncImports, optimizeModule, stripStaticDataPrefix, hoistConstGlobalInits, stripDeadElTable,
|
|
63
71
|
} from '../wat/assemble.js'
|
|
72
|
+
import { instrumentHelperCallsites } from '../helper-counters.js'
|
|
64
73
|
|
|
65
74
|
// =============================================================================
|
|
66
75
|
// Single-source export semantics
|
|
@@ -133,15 +142,21 @@ const timePhase = (profiler, name, fn) => profiler?.time ? profiler.time(name, f
|
|
|
133
142
|
* fractional Number gets the same truncation it would get from `arr[n]`).
|
|
134
143
|
*/
|
|
135
144
|
const isBoundaryWrapped = (func) => {
|
|
136
|
-
if (!isExported(func) || func.raw
|
|
145
|
+
if (!isExported(func) || func.raw) return false
|
|
146
|
+
// Multi-value return: every lane is an f64 NaN-box carrier (the `return [a,b,…]` emit forces
|
|
147
|
+
// asF64 per lane; result narrowing only touches single-result funcs), so any lane may hold a
|
|
148
|
+
// box whose NaN payload JSC/V8 erases at the boundary — wrap to i64-carry every lane.
|
|
149
|
+
if (func.sig.results.length !== 1) return true
|
|
137
150
|
if (func.sig.results[0] !== 'f64' || func.sig.ptrKind != null) return true
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
if (func.
|
|
144
|
-
|
|
151
|
+
// Any result that isn't a proven plain number can be a NaN-box — a heap pointer,
|
|
152
|
+
// a null/undef/bool atom, a bigint carrier, or a dynamic value — so it crosses as
|
|
153
|
+
// i64 and JSC (Safari) can't canonicalize the payload away. A proven-number result
|
|
154
|
+
// stays f64: free, and a number is never a NaN-box. `_resultNumeric` is set in
|
|
155
|
+
// analyzeFuncForEmit (covers value-bound arrows narrowValResults skips).
|
|
156
|
+
if (!func._resultNumeric) return true
|
|
157
|
+
// Number result, but a param may still carry a box — a pointer-ABI param, or a
|
|
158
|
+
// dynamic f64 param flagged `boundaryI64` during analyze — so wrap for i64 params.
|
|
159
|
+
return func.sig.params.some(p => p.type !== 'f64' || p.ptrKind != null || p.boundaryI64)
|
|
145
160
|
}
|
|
146
161
|
|
|
147
162
|
// Static-string intern index (the `internStrings` pass). Open-addressing table
|
|
@@ -212,20 +227,39 @@ const ensureThrowRuntime = (sec) => {
|
|
|
212
227
|
sec.tags.push(['export', '"__jz_last_err_bits"', ['global', '$__jz_last_err_bits']])
|
|
213
228
|
}
|
|
214
229
|
|
|
215
|
-
// Drop the $__jz_err tag + __jz_last_err_bits globals when
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
// throw
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
//
|
|
230
|
+
// Drop the $__jz_err tag + __jz_last_err_bits globals when no throw can be CAUGHT.
|
|
231
|
+
// ensureThrowRuntime runs before optimizeModule so dead-throw analysis sees the
|
|
232
|
+
// tag/global as live; once opt has finished, an unused tag still forces consumers
|
|
233
|
+
// (wasmtime, wasm2c, wabt) to enable the exceptions proposal just to PARSE the module.
|
|
234
|
+
//
|
|
235
|
+
// When `!userThrows`, every `throw` is compiler-internal (bounds / coercion / type
|
|
236
|
+
// errors) and — with no user try/catch — uncatchable: nothing inspects the thrown
|
|
237
|
+
// value, so it is semantically a trap. The exceptions proposal is needed only to
|
|
238
|
+
// DECLARE the tag a `throw` references; lowering each surviving uncatchable throw to
|
|
239
|
+
// `unreachable` keeps the module in the wasm MVP, so every runtime can parse it
|
|
240
|
+
// (V8 alone enables exceptions by default, which masked this). A pure-recursion or
|
|
241
|
+
// typed-array kernel (nqueens, anything pulling __to_num) thus stops emitting a Tag
|
|
242
|
+
// section it can never use. User-written throw/try/catch/finally is an ABI contract
|
|
243
|
+
// (JS-side may inspect __jz_last_err_bits), so `userThrows` keeps the runtime intact.
|
|
224
244
|
const pruneUnusedThrowRuntime = (sec) => {
|
|
225
245
|
if (!ctx.runtime.throws || ctx.runtime.userThrows) return
|
|
226
|
-
|
|
246
|
+
// A catch handler (try_table) appears only under userThrows; defensively bail if one
|
|
247
|
+
// is present so a caught throw is never silently turned into a trap.
|
|
248
|
+
const hasCatch = (n) => Array.isArray(n) &&
|
|
249
|
+
(n[0] === 'try_table' || n[0] === 'catch' || n[0] === 'catch_all' || n.some(hasCatch))
|
|
227
250
|
for (const arr of [sec.funcs, sec.stdlib, sec.start])
|
|
228
|
-
for (const f of arr) if (
|
|
251
|
+
for (const f of arr) if (hasCatch(f)) return
|
|
252
|
+
// Rewrite every surviving `(throw $__jz_err …)` to `(unreachable)` (same polymorphic
|
|
253
|
+
// stack type — a drop-in in any position). The thrown operand is side-effect-free
|
|
254
|
+
// (a local read / const), so dropping it loses nothing.
|
|
255
|
+
const lowerThrows = (n) => {
|
|
256
|
+
if (!Array.isArray(n)) return n
|
|
257
|
+
if (n[0] === 'throw') return ['unreachable']
|
|
258
|
+
for (let i = 1; i < n.length; i++) n[i] = lowerThrows(n[i])
|
|
259
|
+
return n
|
|
260
|
+
}
|
|
261
|
+
for (const arr of [sec.funcs, sec.stdlib, sec.start])
|
|
262
|
+
for (let i = 0; i < arr.length; i++) arr[i] = lowerThrows(arr[i])
|
|
229
263
|
sec.tags = sec.tags.filter(t => !(Array.isArray(t) &&
|
|
230
264
|
((t[0] === 'tag' && t[1] === '$__jz_err') ||
|
|
231
265
|
(t[0] === 'export' && t[1] === '"__jz_last_err_bits"'))))
|
|
@@ -351,6 +385,7 @@ function enterFunc(sig, body, { uniq = 0, directClosures = null } = {}) {
|
|
|
351
385
|
ctx.func.stack = []
|
|
352
386
|
ctx.func.zeroInitSeen = new Set() // names whose `let x=0` zero-init was elided once; a 2nd is a real re-init (unrolled bodies)
|
|
353
387
|
ctx.func.maybeNullish = new Set() // bindings assigned a nullish literal → coerce in arithmetic (null-flow)
|
|
388
|
+
ctx.func.refinements = new Map() // flow-sensitive type facts (typeof/instanceof guards) — per-function; clear so none leak across bodies
|
|
354
389
|
ctx.func.pendingLabel = null // label awaiting its loop, for `continue <label>`
|
|
355
390
|
ctx.func.uniq = uniq
|
|
356
391
|
ctx.func.current = sig
|
|
@@ -388,6 +423,15 @@ function analyzeFuncForEmit(func, programFacts) {
|
|
|
388
423
|
// counters are typed/narrowed like any i32 local. Off at L0 / `loopIVDivMod:false`.
|
|
389
424
|
const _o = ctx.transform.optimize
|
|
390
425
|
if (_o && _o.loopIVDivMod !== false && isBlockBody(func.body)) func.body = strengthReduceLoopDivMod(func.body)
|
|
426
|
+
// Bounded-square narrowing: `i*i` under an `i*i < CONST` (CONST ≤ 2³⁰) guard → Math.imul,
|
|
427
|
+
// so the sieve's product/counter chain carries i32 instead of f64. Before analyze so the
|
|
428
|
+
// Math.imul typed/narrows like any i32. Off at L0 / `loopSquare:false`.
|
|
429
|
+
if (_o && _o.loopSquare !== false && isBlockBody(func.body)) func.body = narrowBoundedSquare(func.body)
|
|
430
|
+
// Array-recurrence unroll: a unit-stride DP/scan that reads arr[j-1] and writes arr[j] carries
|
|
431
|
+
// its value through memory (store→load) and re-pays loop overhead per cell — both of which V8
|
|
432
|
+
// hides but Cranelift/baseline don't. Scalar-replace the recurrence + unroll ×2 (clang's fix).
|
|
433
|
+
// Off at L0 / `unrollRecurrence:false`.
|
|
434
|
+
if (_o && _o.unrollRecurrence !== false && isBlockBody(func.body)) func.body = unrollRecurrence(func.body)
|
|
391
435
|
// Edge-clamp peeling: split a clamped stencil loop into clamp-free interior + edges
|
|
392
436
|
// (the interior then lifts to SIMD). Before analyze so the new loops are analyzed.
|
|
393
437
|
if (_o && _o.clampPeel !== false && isBlockBody(func.body)) func.body = peelClampedStencil(func.body)
|
|
@@ -425,7 +469,10 @@ function analyzeFuncForEmit(func, programFacts) {
|
|
|
425
469
|
// ToNumber string-parse dep tree (`__to_str`→`__itoa`/`__toExp`/`__mkstr`/…)
|
|
426
470
|
// treeshake away — a ~4× module shrink that, decisively, lets V8 tier the hot
|
|
427
471
|
// fill loop up properly (the bloated module JITs the *identical* loop ~2× slower).
|
|
428
|
-
|
|
472
|
+
// Block AND expression bodies: value-bound arrows (`export let f = (a,b) => a*b`) are
|
|
473
|
+
// skipped by narrowValResults, so without trusting their params here they'd fall to the
|
|
474
|
+
// i64 boundary carrier. The closure path runs the same proof at line ~1300.
|
|
475
|
+
if (func.exported) {
|
|
429
476
|
for (const p of sig.params) {
|
|
430
477
|
if (p.type === 'f64' && p.ptrKind == null && !p.jsstring
|
|
431
478
|
&& !func.defaults?.[p.name] && !ctx.func.boxed?.has(p.name)
|
|
@@ -438,6 +485,12 @@ function analyzeFuncForEmit(func, programFacts) {
|
|
|
438
485
|
updateRep(p.name, { val: VAL.NUMBER })
|
|
439
486
|
}
|
|
440
487
|
}
|
|
488
|
+
// Sound load-CSE: cache a repeated pure typed-array load `arr[idx]` when every intervening
|
|
489
|
+
// store writes a provably-different element (idx2 ≠ idx). Recovers the fft butterfly's redundant
|
|
490
|
+
// `re[a]` load. Before analyze so the introduced temp is typed/narrowed like any local.
|
|
491
|
+
if (_o && _o.loadCSE !== false && block && ctx.types.typedElem?.size)
|
|
492
|
+
cseLoads(body, n => ctx.types.typedElem.has(n), freshCseName)
|
|
493
|
+
|
|
441
494
|
if (block) {
|
|
442
495
|
seedLocalIntConsts(body)
|
|
443
496
|
}
|
|
@@ -531,6 +584,32 @@ function analyzeFuncForEmit(func, programFacts) {
|
|
|
531
584
|
if (ctx.func.localReps?.get(name)?.intCertain === true) cellTypes.add(name)
|
|
532
585
|
}
|
|
533
586
|
|
|
587
|
+
// Snapshot each param's JS-boundary carrier while reps are live — synthesizeBoundaryWrappers
|
|
588
|
+
// runs after they're torn down. A dynamic f64 param crosses as i64 (the carrier JSC can't
|
|
589
|
+
// canonicalize) iff it can hold a NaN-box, i.e. it isn't proven numeric. Numeric (NUMBER /
|
|
590
|
+
// BOOL → 0/1) params keep f64; pointer-ABI (ptrKind, type i32) and jsstring params are
|
|
591
|
+
// classified directly in the wrapper, so leave their flag false here.
|
|
592
|
+
if (isExported(func)) for (const p of sig.params) {
|
|
593
|
+
if (p.jsstring || p.ptrKind != null || p.type !== 'f64') { p.boundaryI64 = false; continue }
|
|
594
|
+
const rv = ctx.func.localReps?.get(p.name)?.val
|
|
595
|
+
p.boundaryI64 = rv !== VAL.NUMBER && rv !== VAL.BOOL
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Result-numeric proof for the boundary carrier. Block bodies get func.valResult from
|
|
599
|
+
// narrowValResults; value-bound arrows (`export let f = (a,b) => a*b`) don't, so prove via
|
|
600
|
+
// the return expression(s) with params now trusted numeric. A proven-number f64 result
|
|
601
|
+
// never carries a NaN-box → crosses as plain f64; anything else rides i64 (Safari-safe).
|
|
602
|
+
if (isExported(func))
|
|
603
|
+
func._resultNumeric = func.valResult === VAL.NUMBER ||
|
|
604
|
+
(func.valResult == null && sig.results[0] === 'f64' &&
|
|
605
|
+
(() => {
|
|
606
|
+
const rex = returnExprs(body)
|
|
607
|
+
// Void body (falls off → undefined, which callers ignore) keeps the f64 carrier:
|
|
608
|
+
// undefined isn't a reference, so no i64 is needed and wrapping every void export
|
|
609
|
+
// is pure overhead. A non-empty set must be all-NUMBER to stay f64.
|
|
610
|
+
return rex.length === 0 || rex.every(e => valTypeOf(e) === VAL.NUMBER)
|
|
611
|
+
})())
|
|
612
|
+
|
|
534
613
|
return {
|
|
535
614
|
block,
|
|
536
615
|
locals: new Map(ctx.func.locals),
|
|
@@ -539,6 +618,7 @@ function analyzeFuncForEmit(func, programFacts) {
|
|
|
539
618
|
flatObjects: new Map(ctx.func.flatObjects),
|
|
540
619
|
sliceViews: new Set(ctx.func.sliceViews),
|
|
541
620
|
cseLoadBases,
|
|
621
|
+
distinctParams: func.distinctParams || null,
|
|
542
622
|
typedElem: ctx.types.typedElem ? new Map(ctx.types.typedElem) : null,
|
|
543
623
|
localReps: cloneRepMap(ctx.func.localReps),
|
|
544
624
|
}
|
|
@@ -958,6 +1038,11 @@ function emitFunc(func, funcFacts, programFacts) {
|
|
|
958
1038
|
// it the pass is a no-op. `$`-prefixed to match WAT local names directly.
|
|
959
1039
|
if (funcFacts.cseLoadBases?.size)
|
|
960
1040
|
fn.cseLoadBases = new Set([...funcFacts.cseLoadBases].map(n => `$${n}`))
|
|
1041
|
+
// Param-distinctness fact (alias analysis): typed-array params proven mutually-distinct buffers
|
|
1042
|
+
// at every call site. `$`-prefixed to match WAT param names; read by hoistInvariantLoop to hoist
|
|
1043
|
+
// a load from one such param across a store to another (they can't alias).
|
|
1044
|
+
if (funcFacts.distinctParams?.size)
|
|
1045
|
+
fn.distinctParams = new Set([...funcFacts.distinctParams].map(n => `$${n}`))
|
|
961
1046
|
// Inline `(export ...)` attribute only for the syntactic inline-export
|
|
962
1047
|
// form (`export function foo`, snapshot in `func.exported` at defFunc
|
|
963
1048
|
// time). Re-exports (`function foo; export { foo }`) and aliases (`export
|
|
@@ -1098,17 +1183,26 @@ function synthesizeBoundaryWrappers() {
|
|
|
1098
1183
|
for (const func of ctx.func.list) {
|
|
1099
1184
|
if (!isBoundaryWrapped(func)) continue
|
|
1100
1185
|
const { name, sig } = func
|
|
1101
|
-
//
|
|
1102
|
-
//
|
|
1103
|
-
//
|
|
1104
|
-
// JS↔wasm
|
|
1105
|
-
//
|
|
1106
|
-
//
|
|
1107
|
-
//
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
const
|
|
1111
|
-
|
|
1186
|
+
// i64 boundary carrier (Safari-safe). A genuine number is never a NaN-box, so it crosses
|
|
1187
|
+
// as plain f64 (zero cost). Everything that can be a NaN-box — heap pointer, null/undef/
|
|
1188
|
+
// bool atom, bigint carrier, or a dynamic value — crosses as i64: JSC (Safari) canonicalizes
|
|
1189
|
+
// f64 NaN payloads at the JS↔wasm boundary, erasing the box. The wasm signature is
|
|
1190
|
+
// self-describing; interop.js wrap() reinterprets BigInt↔f64 by bits, driven by the
|
|
1191
|
+
// `jz:i64exp` section emitted below. Non-JS hosts (WASI) read the same signature — i64 is
|
|
1192
|
+
// just int64 there, no BigInt.
|
|
1193
|
+
const resultPtr = sig.ptrKind != null
|
|
1194
|
+
const resultBool = func.valResult === VAL.BOOL && !resultPtr
|
|
1195
|
+
const resultBigint = func.valResult === VAL.BIGINT && !resultPtr
|
|
1196
|
+
// Dynamic f64 result: not pointer/bool/bigint and not a proven number → may be a NaN-box
|
|
1197
|
+
// at runtime, so i64. (An i32-carrier result is numeric → stays f64 via convert below.)
|
|
1198
|
+
const resultDynamic = !resultPtr && !resultBool && !resultBigint
|
|
1199
|
+
&& sig.results[0] === 'f64' && !func._resultNumeric
|
|
1200
|
+
const resultI64 = resultPtr || resultBool || resultBigint || resultDynamic
|
|
1201
|
+
// jz:i64exp `r` marks results interop must reinterpret then `mem.read`. A bigint result is
|
|
1202
|
+
// i64 too, but the BigInt *is* the value (no reinterpret) — so it stays unmarked.
|
|
1203
|
+
const resultReinterpret = resultPtr || resultBool || resultDynamic
|
|
1204
|
+
// i64 carrier per param: pointer-ABI (offset) or a dynamic f64 param (boundaryI64).
|
|
1205
|
+
const paramIsI64 = (p) => !p.jsstring && (p.ptrKind != null || p.boundaryI64)
|
|
1112
1206
|
// Inline `(export ...)` attribute only when the func decl carried the
|
|
1113
1207
|
// inline-export keyword (`export function foo`). For re-exports
|
|
1114
1208
|
// (`function foo; export { foo as bar }`) the `name` is the *internal*
|
|
@@ -1118,26 +1212,58 @@ function synthesizeBoundaryWrappers() {
|
|
|
1118
1212
|
const wrapNode = func.exported
|
|
1119
1213
|
? ['func', `$${name}$exp`, ['export', `"${name}"`]]
|
|
1120
1214
|
: ['func', `$${name}$exp`]
|
|
1121
|
-
// jsstring params flow as externref end-to-end;
|
|
1122
|
-
|
|
1123
|
-
|
|
1215
|
+
// jsstring params flow as externref end-to-end; boxed params ride i64; numbers f64.
|
|
1216
|
+
const i64Params = []
|
|
1217
|
+
sig.params.forEach((p, i) => {
|
|
1218
|
+
wrapNode.push(['param', `$${p.name}`, p.jsstring ? 'externref' : paramIsI64(p) ? 'i64' : 'f64'])
|
|
1219
|
+
if (paramIsI64(p)) i64Params.push(i)
|
|
1124
1220
|
})
|
|
1125
|
-
|
|
1221
|
+
// Track externref param positions so interop.js can pass JS values raw (skipping
|
|
1222
|
+
// `mem.wrapVal`) at those slots — today only `jsstring` params; future externref carriers
|
|
1223
|
+
// wire here too. `extParams` is per-slot: false | { def: '...' } for a JS-side default.
|
|
1224
|
+
const extParams = sig.params.map(p => !p.jsstring ? false : p.jsstringDefault != null ? { def: p.jsstringDefault } : true)
|
|
1225
|
+
if (extParams.some(Boolean)) func._exportExtParams = extParams
|
|
1226
|
+
// Inner→wrapper argument list, shared by both single- and multi-value result shapes.
|
|
1126
1227
|
const args = sig.params.map((p) => {
|
|
1127
1228
|
const get = ['local.get', `$${p.name}`]
|
|
1128
|
-
|
|
1129
|
-
if (p.
|
|
1130
|
-
//
|
|
1131
|
-
if (p.ptrKind != null) return ['i32.wrap_i64', ['i64.reinterpret_f64', get]]
|
|
1229
|
+
if (p.jsstring) return get // externref flows through unchanged
|
|
1230
|
+
if (p.ptrKind != null) return ['i32.wrap_i64', get] // ptr param: inner takes the i32 offset
|
|
1231
|
+
if (p.boundaryI64) return ['f64.reinterpret_i64', get] // dynamic boxed param → f64 NaN-box carrier
|
|
1132
1232
|
if (p.type === 'f64') return get
|
|
1133
|
-
//
|
|
1134
|
-
return ['i32.trunc_sat_f64_s', get]
|
|
1233
|
+
return ['i32.trunc_sat_f64_s', get] // numeric narrowing f64 → i32
|
|
1135
1234
|
})
|
|
1136
1235
|
const callIR = ['call', `$${name}`, ...args]
|
|
1236
|
+
// Multi-value return: each lane is an f64 NaN-box carrier (every `return [a,b,…]` lane is
|
|
1237
|
+
// asF64; narrowing only touches single-result funcs). A boxed lane's NaN payload is erased
|
|
1238
|
+
// at the JS boundary, so cross EVERY lane as i64 — capture the inner call's N lanes into f64
|
|
1239
|
+
// locals (last result on top of the stack ⇒ pop in reverse) and re-push each reinterpreted.
|
|
1240
|
+
// interop reads the lane tuple via mem.read / decode (both map over an array result).
|
|
1241
|
+
if (sig.results.length > 1) {
|
|
1242
|
+
sig.results.forEach(() => wrapNode.push(['result', 'i64']))
|
|
1243
|
+
// Lane temporaries — guaranteed distinct from the wrapper's params (jz doesn't reserve
|
|
1244
|
+
// `__`, so a user param could be `__mlane0`): bump the prefix until no lane name collides.
|
|
1245
|
+
const pnames = new Set(sig.params.map((p) => p.name))
|
|
1246
|
+
let pfx = '__mlane'
|
|
1247
|
+
while (sig.results.some((_, i) => pnames.has(`${pfx}${i}`))) pfx = `_${pfx}`
|
|
1248
|
+
const lanes = sig.results.map((_, i) => `$${pfx}${i}`)
|
|
1249
|
+
lanes.forEach((n) => wrapNode.push(['local', n, 'f64']))
|
|
1250
|
+
const stmts = [callIR]
|
|
1251
|
+
for (let i = lanes.length - 1; i >= 0; i--) stmts.push(['local.set', lanes[i]])
|
|
1252
|
+
for (const n of lanes) stmts.push(['i64.reinterpret_f64', ['local.get', n]])
|
|
1253
|
+
wrapNode.push(...stmts)
|
|
1254
|
+
// `m` (lane count) marks a multi-value result so interop / the test adapter decode each
|
|
1255
|
+
// lane (vs `r`'s single reinterpret). Always recorded — even with no i64 params — so the
|
|
1256
|
+
// numeric-only `(a,b)=>[a+1,b+2]` tuple still gets its lanes turned back into numbers.
|
|
1257
|
+
func._exportI64 = { p: i64Params, m: sig.results.length }
|
|
1258
|
+
wrappers.push(wrapNode)
|
|
1259
|
+
continue
|
|
1260
|
+
}
|
|
1261
|
+
wrapNode.push(['result', resultI64 ? 'i64' : 'f64'])
|
|
1262
|
+
const toI64 = (n) => ['i64.reinterpret_f64', n]
|
|
1137
1263
|
let body
|
|
1138
|
-
if (
|
|
1264
|
+
if (resultPtr) {
|
|
1139
1265
|
const ptrType = valKindToPtr(sig.ptrKind)
|
|
1140
|
-
body = mkPtrIR(ptrType, sig.ptrAux ?? 0, callIR)
|
|
1266
|
+
body = toI64(mkPtrIR(ptrType, sig.ptrAux ?? 0, callIR))
|
|
1141
1267
|
} else if (resultBool) {
|
|
1142
1268
|
// The inner func returns a clean 0/1 boolean carrier — never NaN. The i32
|
|
1143
1269
|
// carrier already takes truthyIR's identity path; the f64 carrier would
|
|
@@ -1147,28 +1273,22 @@ function synthesizeBoundaryWrappers() {
|
|
|
1147
1273
|
const carrier = sig.results[0] === 'i32'
|
|
1148
1274
|
? typed(callIR, 'i32')
|
|
1149
1275
|
: typed(['f64.ne', callIR, ['f64.const', 0]], 'i32')
|
|
1150
|
-
body = boolBoxIR(carrier)
|
|
1151
|
-
} else if (resultBigint) {
|
|
1152
|
-
// BigInt rides the i64-reinterpret-f64 carrier internally;
|
|
1153
|
-
//
|
|
1154
|
-
// callers use `$name` (the f64 carrier) untouched; only
|
|
1155
|
-
body =
|
|
1276
|
+
body = toI64(boolBoxIR(carrier))
|
|
1277
|
+
} else if (resultBigint || resultDynamic) {
|
|
1278
|
+
// BigInt rides the i64-reinterpret-f64 carrier internally; a dynamic result is already an
|
|
1279
|
+
// f64 NaN-box carrier. Either way expose the raw i64 at the JS boundary for a lossless
|
|
1280
|
+
// value. Internal callers use `$name` (the f64 carrier) untouched; only `$exp` is i64.
|
|
1281
|
+
body = toI64(callIR)
|
|
1156
1282
|
} else if (sig.results[0] === 'i32') {
|
|
1157
1283
|
body = [sig.unsignedResult ? 'f64.convert_i32_u' : 'f64.convert_i32_s', callIR]
|
|
1158
1284
|
} else {
|
|
1159
1285
|
body = callIR
|
|
1160
1286
|
}
|
|
1161
1287
|
wrapNode.push(body)
|
|
1162
|
-
//
|
|
1163
|
-
//
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
// for jsstring params with a JS-side default substitution.
|
|
1167
|
-
const extParams = sig.params.map(p => {
|
|
1168
|
-
if (!p.jsstring) return false
|
|
1169
|
-
return p.jsstringDefault != null ? { def: p.jsstringDefault } : true
|
|
1170
|
-
})
|
|
1171
|
-
if (extParams.some(Boolean)) func._exportExtParams = extParams
|
|
1288
|
+
// Record the i64 carrier map for interop.js (jz:i64exp). A pure-numeric export
|
|
1289
|
+
// (no i64 params, f64 result) records nothing — zero footprint off the box path.
|
|
1290
|
+
if (i64Params.length || resultReinterpret)
|
|
1291
|
+
func._exportI64 = { p: i64Params, r: resultReinterpret ? 1 : 0 }
|
|
1172
1292
|
wrappers.push(wrapNode)
|
|
1173
1293
|
}
|
|
1174
1294
|
return wrappers
|
|
@@ -1709,6 +1829,7 @@ export default function compile(ast, profiler) {
|
|
|
1709
1829
|
ensureThrowRuntime(sec)
|
|
1710
1830
|
|
|
1711
1831
|
timePhase(profiler, 'optimizeModule', () => optimizeModule(sec, profiler))
|
|
1832
|
+
if (ctx.transform.helperCallsites) instrumentHelperCallsites([...sec.funcs, ...sec.stdlib, ...sec.start])
|
|
1712
1833
|
|
|
1713
1834
|
// Fold constant `__start` global inits into immutable inline decls (drops the
|
|
1714
1835
|
// store, and `__start` with it when that empties it). Runs HERE — after
|
|
@@ -1733,6 +1854,11 @@ export default function compile(ast, profiler) {
|
|
|
1733
1854
|
[`${ty}.const`, g.init]]
|
|
1734
1855
|
}))
|
|
1735
1856
|
|
|
1857
|
+
// Drop the Eisel-Lemire decimal table if no live code parses decimals at runtime — must
|
|
1858
|
+
// run after sec.globals/funcs are final (exact reachability) and before the data segment
|
|
1859
|
+
// below serializes ctx.runtime.data. See stripDeadElTable.
|
|
1860
|
+
stripDeadElTable(sec)
|
|
1861
|
+
|
|
1736
1862
|
// Data segments (after emit — string literals append to ctx.runtime.data / strPool during emit)
|
|
1737
1863
|
// Active segment at address 0 — skipped for shared memory (would collide across modules)
|
|
1738
1864
|
const escBytes = (s) => {
|
|
@@ -1817,6 +1943,23 @@ export default function compile(ast, profiler) {
|
|
|
1817
1943
|
if (extExports.length)
|
|
1818
1944
|
sec.customs.push(['@custom', '"jz:extparam"', `"${JSON.stringify(extExports).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`])
|
|
1819
1945
|
|
|
1946
|
+
// jz:i64exp — per-export i64 carrier map (NaN-canonicalization dodging). Each entry
|
|
1947
|
+
// `{name, p:[i64 param indices], r:1? | m:N?}`: `p` lists params interop must pass as BigInt
|
|
1948
|
+
// (f64ToI64); `r` marks a single result to reinterpret (i64ToF64) before mem.read; `m` marks
|
|
1949
|
+
// an N-lane multi-value result whose lanes interop/the adapter decode element-wise. Pure-
|
|
1950
|
+
// numeric single-result exports emit no entry. A bigint result is i64 but unmarked (the BigInt
|
|
1951
|
+
// is the value). Written under every JS-visible alias, like jz:extparam. Each shape is built as
|
|
1952
|
+
// a direct literal (no spread) — the self-host kernel's fixed schemas don't enumerate post-hoc keys.
|
|
1953
|
+
const i64Exports = []
|
|
1954
|
+
for (const f of ctx.func.list) {
|
|
1955
|
+
if (!isExported(f) || !isBoundaryWrapped(f) || !f._exportI64) continue
|
|
1956
|
+
const { p, r, m } = f._exportI64
|
|
1957
|
+
for (const exportName of exportNamesOf(f.name))
|
|
1958
|
+
i64Exports.push(m ? { name: exportName, p, m } : r ? { name: exportName, p, r } : { name: exportName, p })
|
|
1959
|
+
}
|
|
1960
|
+
if (i64Exports.length)
|
|
1961
|
+
sec.customs.push(['@custom', '"jz:i64exp"', `"${JSON.stringify(i64Exports).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`])
|
|
1962
|
+
|
|
1820
1963
|
// Named export aliases: export { name } or export { source as alias }
|
|
1821
1964
|
for (const [name, val] of Object.entries(ctx.func.exports)) {
|
|
1822
1965
|
if (wasiCommandExports.has(name)) continue
|
package/src/compile/infer.js
CHANGED
|
@@ -130,11 +130,18 @@ export const inferParams = (body, candidates) => {
|
|
|
130
130
|
// regardless of prior evidence — a later method call can't re-induce a shape
|
|
131
131
|
// already contradicted by an earlier scalar assignment.
|
|
132
132
|
|
|
133
|
+
// Methods that exist ONLY on String.prototype — seeing one on a bare binding proves
|
|
134
|
+
// it is a string. `indexOf`/`includes`/`lastIndexOf`/`concat`/`slice`/`at` are NOT here:
|
|
135
|
+
// Array.prototype has them too, so the receiver is genuinely ambiguous (and the argument
|
|
136
|
+
// can't disambiguate — String coerces it, Arrays hold strings). Those keep the runtime
|
|
137
|
+
// __ptr_type fork, which is correct for both; forcing `lastIndexOf` to string here used to
|
|
138
|
+
// miscompile `arr.lastIndexOf(x)` to -1. A string-using param still narrows via any of the
|
|
139
|
+
// real discriminators below (charCodeAt, a string assignment, a string-passing call site).
|
|
133
140
|
const STRING_ONLY_METHODS = new Set([
|
|
134
141
|
'charCodeAt', 'charAt', 'codePointAt', 'startsWith', 'endsWith',
|
|
135
142
|
'toUpperCase', 'toLowerCase', 'toLocaleLowerCase', 'normalize', 'localeCompare',
|
|
136
143
|
'padStart', 'padEnd', 'repeat', 'trimStart', 'trimEnd', 'trim',
|
|
137
|
-
'matchAll', 'match', 'replace', 'replaceAll', 'split',
|
|
144
|
+
'matchAll', 'match', 'replace', 'replaceAll', 'split',
|
|
138
145
|
])
|
|
139
146
|
const ARRAY_ONLY_POISON = new Set([
|
|
140
147
|
'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'fill', 'reverse',
|
|
@@ -20,64 +20,33 @@
|
|
|
20
20
|
// hole = undefined), so literal tests use `== null`; created literals are bare numbers.
|
|
21
21
|
|
|
22
22
|
import { findMutations } from './analyze-scans.js'
|
|
23
|
+
import { litN, unitIncVar, normalizeLoop, closureMutatedVars, rewriteBlocks, freshLoopId } from './loop-model.js'
|
|
23
24
|
|
|
24
|
-
const litN = (n, k) => Array.isArray(n) && n.length === 2 && n[0] == null && n[1] === k
|
|
25
25
|
const isMod = (n, i, w) => Array.isArray(n) && n[0] === '%' && n[1] === i && n[2] === w
|
|
26
26
|
const isFloorDiv = (n, i, w) =>
|
|
27
27
|
Array.isArray(n) && n[0] === '|' && litN(n[2], 0) &&
|
|
28
28
|
Array.isArray(n[1]) && n[1][0] === '/' && n[1][1] === i && n[1][2] === w
|
|
29
29
|
|
|
30
|
-
// IV a statement increments by exactly +1, or null. Covers `i++` (post-inc desugars
|
|
31
|
-
// to `(++i) - 1`), `++i`, `i += 1`, `i = i + 1`.
|
|
32
|
-
function incVarOf(stmt) {
|
|
33
|
-
if (!Array.isArray(stmt)) return null
|
|
34
|
-
let inc = stmt
|
|
35
|
-
if (stmt[0] === '-' && litN(stmt[2], 1) && Array.isArray(stmt[1]) && stmt[1][0] === '++') inc = stmt[1]
|
|
36
|
-
if (inc[0] === '++' && typeof inc[1] === 'string') return inc[1]
|
|
37
|
-
if (stmt[0] === '+=' && typeof stmt[1] === 'string' && litN(stmt[2], 1)) return stmt[1]
|
|
38
|
-
if (stmt[0] === '=' && typeof stmt[1] === 'string' && Array.isArray(stmt[2]) && stmt[2][0] === '+') {
|
|
39
|
-
const [, a, b] = stmt[2]
|
|
40
|
-
if (a === stmt[1] && litN(b, 1)) return stmt[1]
|
|
41
|
-
if (b === stmt[1] && litN(a, 1)) return stmt[1]
|
|
42
|
-
}
|
|
43
|
-
return null
|
|
44
|
-
}
|
|
45
|
-
|
|
46
30
|
const usesPattern = (n, i, w, pred) => Array.isArray(n) && (pred(n, i, w) || n.some(c => usesPattern(c, i, w, pred)))
|
|
47
31
|
const replace = (n, i, w, cx, cy) =>
|
|
48
32
|
!Array.isArray(n) ? n : isMod(n, i, w) ? cx : isFloorDiv(n, i, w) ? cy : n.map(c => replace(c, i, w, cx, cy))
|
|
49
33
|
// a `continue` that targets THIS loop (not one nested inside) — would skip the increment
|
|
50
34
|
const hasOuterContinue = (n) => Array.isArray(n) &&
|
|
51
35
|
(n[0] === 'continue' || (n[0] !== 'while' && n[0] !== 'for' && n[0] !== 'do' && n[0] !== '=>' && n.some(hasOuterContinue)))
|
|
52
|
-
// Vars assigned anywhere inside a closure in the function. Such a var can be
|
|
53
|
-
// mutated by a call in the loop body (the closure may be defined outside the loop,
|
|
54
|
-
// so a body-local findMutations misses it), so it is not safe as the IV or divisor.
|
|
55
|
-
const ASSIGN_OPS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '**=', '&=', '|=', '^=', '<<=', '>>=', '>>>=', '&&=', '||=', '??='])
|
|
56
|
-
const collectAssigns = (n, out) => {
|
|
57
|
-
if (!Array.isArray(n)) return
|
|
58
|
-
if (typeof n[1] === 'string' && (ASSIGN_OPS.has(n[0]) || n[0] === '++' || n[0] === '--')) out.add(n[1])
|
|
59
|
-
n.forEach(c => collectAssigns(c, out))
|
|
60
|
-
}
|
|
61
|
-
const closureMutated = (n, out) => {
|
|
62
|
-
if (!Array.isArray(n)) return out
|
|
63
|
-
if (n[0] === '=>') collectAssigns(n, out)
|
|
64
|
-
n.forEach(c => closureMutated(c, out))
|
|
65
|
-
return out
|
|
66
|
-
}
|
|
67
36
|
|
|
68
|
-
let _uniq = 0
|
|
69
|
-
let _cm = new Set() // closure-mutated vars for the function currently being transformed
|
|
70
37
|
|
|
71
|
-
// Try to strength-reduce one `while` statement. Returns [seed, loop] or null.
|
|
72
|
-
function
|
|
73
|
-
|
|
74
|
-
const
|
|
38
|
+
// Try to strength-reduce one `while` statement. Returns [seed, loop] or null. `cm` is
|
|
39
|
+
// the function's closure-mutated-vars set (an IV/divisor in it is unsafe).
|
|
40
|
+
function tryReduce(stmt, cm) {
|
|
41
|
+
const L = normalizeLoop(stmt)
|
|
42
|
+
if (!L || L.kind !== 'while') return null
|
|
43
|
+
const cond = L.cond, lbody = L.body
|
|
75
44
|
if (!Array.isArray(lbody) || lbody[0] !== ';') return null
|
|
76
45
|
|
|
77
46
|
// exactly one IV incremented by +1
|
|
78
47
|
let iv = null, ivIdx = -1
|
|
79
48
|
for (let k = 1; k < lbody.length; k++) {
|
|
80
|
-
const v =
|
|
49
|
+
const v = unitIncVar(lbody[k])
|
|
81
50
|
if (v) { if (iv) return null; iv = v; ivIdx = k }
|
|
82
51
|
}
|
|
83
52
|
if (!iv) return null
|
|
@@ -105,9 +74,9 @@ function tryReduce(stmt) {
|
|
|
105
74
|
findMutations([';', ...lbody.slice(1).filter((_, k) => k !== ivIdx - 1)], new Set([iv]), ivMut)
|
|
106
75
|
if (ivMut.has(iv)) return null
|
|
107
76
|
if (hasOuterContinue(lbody)) return null
|
|
108
|
-
if (
|
|
77
|
+
if (cm.has(iv) || cm.has(w)) return null // IV/divisor mutable via a closure call
|
|
109
78
|
|
|
110
|
-
const id =
|
|
79
|
+
const id = freshLoopId()
|
|
111
80
|
const cx = `__lsrx${id}`, cy = `__lsry${id}`
|
|
112
81
|
// seed (inside the w>0 branch): cx = (i%w)|0, cy = (i/w)|0 — one-time, i32 via |0
|
|
113
82
|
const seedDecls = [['=', cx, ['|', ['%', iv, w], 0]]]
|
|
@@ -134,22 +103,7 @@ function tryReduce(stmt) {
|
|
|
134
103
|
return [['if', ['&&', ['>', w, 0], ['>=', iv, 0]], ['{}', [';', seed, fast]], stmt]]
|
|
135
104
|
}
|
|
136
105
|
|
|
137
|
-
// Walk the body; transform `while` loops inside every block (post-order so a nested
|
|
138
|
-
// loop is reduced before its enclosing one is examined).
|
|
139
|
-
function walk(node) {
|
|
140
|
-
if (!Array.isArray(node)) return node
|
|
141
|
-
const n = node.map(walk)
|
|
142
|
-
if (n[0] !== ';') return n
|
|
143
|
-
const out = [';']
|
|
144
|
-
for (let k = 1; k < n.length; k++) {
|
|
145
|
-
const r = tryReduce(n[k])
|
|
146
|
-
if (r) out.push(...r)
|
|
147
|
-
else out.push(n[k])
|
|
148
|
-
}
|
|
149
|
-
return out
|
|
150
|
-
}
|
|
151
|
-
|
|
152
106
|
export function strengthReduceLoopDivMod(body) {
|
|
153
|
-
|
|
154
|
-
return
|
|
107
|
+
const cm = closureMutatedVars(body)
|
|
108
|
+
return rewriteBlocks(body, stmt => tryReduce(stmt, cm))
|
|
155
109
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Shared AST-level loop primitives for the per-function loop transforms
|
|
2
|
+
// (loop-divmod, loop-square, peel-stencil). Operates on the post-prepare AST.
|
|
3
|
+
//
|
|
4
|
+
// Each of those passes recognizes one narrow loop idiom and rewrites it, but they had
|
|
5
|
+
// re-derived the same building blocks verbatim: the `[, v]` number-literal-hole
|
|
6
|
+
// recognizer, the "+1 induction variable" matcher, the closure-mutated-variable
|
|
7
|
+
// analysis (a var assigned inside any `=>` may be mutated by a call in the loop, so it
|
|
8
|
+
// is unsafe as an IV/bound/divisor), the while/for structural normalizer, and the
|
|
9
|
+
// post-order block walk that applies a per-statement rewrite. One home for all of them
|
|
10
|
+
// — adding the next loop transform is then one recognizer over these, not a fourth copy.
|
|
11
|
+
|
|
12
|
+
import { ASSIGN_OPS } from '../ast.js'
|
|
13
|
+
import { ctx } from '../ctx.js'
|
|
14
|
+
|
|
15
|
+
// Fresh id for a loop transform's generated locals (`__lsrx<id>`, `__pks<id>`, …). Backed by
|
|
16
|
+
// a per-compile counter (reset in ctx.reset), NOT a module-global: a module-`let` counter
|
|
17
|
+
// grows unbounded across a long-lived host and makes compile(P) non-deterministic — its output
|
|
18
|
+
// names depend on how many programs were compiled before it. Distinct prefixes keep the shared
|
|
19
|
+
// id space collision-free across transforms.
|
|
20
|
+
export const freshLoopId = () => ctx.transform.loopXformId++
|
|
21
|
+
|
|
22
|
+
// Post-prepare number literals are sparse-array holes `[<hole>, v]` (length 2, the op
|
|
23
|
+
// slot `n[0]` is the elided hole == null). `litVal` returns the numeric value or null;
|
|
24
|
+
// `litN(n, k)` tests for the exact literal `k`.
|
|
25
|
+
export const litVal = (n) => Array.isArray(n) && n.length === 2 && n[0] == null && typeof n[1] === 'number' ? n[1] : null
|
|
26
|
+
export const litN = (n, k) => Array.isArray(n) && n.length === 2 && n[0] == null && n[1] === k
|
|
27
|
+
|
|
28
|
+
// The induction variable a statement increments by exactly +1, else null. Covers
|
|
29
|
+
// `i++` (post-inc desugars to `(++i) - 1`), `++i`, `i += 1`, `i = i + 1` / `i = 1 + i`.
|
|
30
|
+
export function unitIncVar(stmt) {
|
|
31
|
+
if (!Array.isArray(stmt)) return null
|
|
32
|
+
let inc = stmt
|
|
33
|
+
if (stmt[0] === '-' && litN(stmt[2], 1) && Array.isArray(stmt[1]) && stmt[1][0] === '++') inc = stmt[1]
|
|
34
|
+
if (inc[0] === '++' && typeof inc[1] === 'string') return inc[1]
|
|
35
|
+
if (stmt[0] === '+=' && typeof stmt[1] === 'string' && litN(stmt[2], 1)) return stmt[1]
|
|
36
|
+
if (stmt[0] === '=' && typeof stmt[1] === 'string' && Array.isArray(stmt[2]) && stmt[2][0] === '+') {
|
|
37
|
+
const [, a, b] = stmt[2]
|
|
38
|
+
if (a === stmt[1] && litN(b, 1)) return stmt[1]
|
|
39
|
+
if (b === stmt[1] && litN(a, 1)) return stmt[1]
|
|
40
|
+
}
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Normalize a `while` / flat `for` (`['for', init, cond, update, body]`) into a common
|
|
45
|
+
// shape, else null. `init`/`step` are null for a while; a `for` with fewer than the 5
|
|
46
|
+
// flat slots (no body) is not a loop here.
|
|
47
|
+
export function normalizeLoop(stmt) {
|
|
48
|
+
if (!Array.isArray(stmt)) return null
|
|
49
|
+
if (stmt[0] === 'while') return { kind: 'while', init: null, cond: stmt[1], step: null, body: stmt[2] }
|
|
50
|
+
if (stmt[0] === 'for' && stmt.length >= 5) return { kind: 'for', init: stmt[1], cond: stmt[2], step: stmt[3], body: stmt[4] }
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Variables assigned anywhere inside a closure (`=>`) in `body`. A call in the loop can
|
|
55
|
+
// mutate such a var even though a direct-write scan (findMutations) misses it (the
|
|
56
|
+
// closure may be defined outside the loop), so an IV / bound / divisor in this set is
|
|
57
|
+
// unsafe to strength-reduce. (ASSIGN_OPS plus the `++`/`--` updates.)
|
|
58
|
+
export function closureMutatedVars(body) {
|
|
59
|
+
const out = new Set()
|
|
60
|
+
const collect = (n) => {
|
|
61
|
+
if (!Array.isArray(n)) return
|
|
62
|
+
if (typeof n[1] === 'string' && (ASSIGN_OPS.has(n[0]) || n[0] === '++' || n[0] === '--')) out.add(n[1])
|
|
63
|
+
n.forEach(collect)
|
|
64
|
+
}
|
|
65
|
+
const walk = (n) => {
|
|
66
|
+
if (!Array.isArray(n)) return
|
|
67
|
+
if (n[0] === '=>') collect(n)
|
|
68
|
+
n.forEach(walk)
|
|
69
|
+
}
|
|
70
|
+
walk(body)
|
|
71
|
+
return out
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Post-order block rewriter: walk `body`; for every `;`-sequence, apply `tryStmt` to
|
|
75
|
+
// each statement. A truthy result is an ARRAY of replacement statements spliced in
|
|
76
|
+
// place; a falsy result keeps the statement unchanged. Children are rewritten first, so
|
|
77
|
+
// a nested loop is transformed before the block that encloses it.
|
|
78
|
+
export function rewriteBlocks(body, tryStmt) {
|
|
79
|
+
const walk = (node) => {
|
|
80
|
+
if (!Array.isArray(node)) return node
|
|
81
|
+
const n = node.map(walk)
|
|
82
|
+
if (n[0] !== ';') return n
|
|
83
|
+
const out = [';']
|
|
84
|
+
for (let k = 1; k < n.length; k++) {
|
|
85
|
+
const r = tryStmt(n[k])
|
|
86
|
+
if (r) out.push(...r); else out.push(n[k])
|
|
87
|
+
}
|
|
88
|
+
return out
|
|
89
|
+
}
|
|
90
|
+
return walk(body)
|
|
91
|
+
}
|