jz 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +99 -72
  2. package/bench/README.md +253 -100
  3. package/bench/bench.svg +58 -81
  4. package/cli.js +85 -12
  5. package/dist/interop.js +1 -0
  6. package/dist/jz.js +6196 -0
  7. package/index.d.ts +126 -0
  8. package/index.js +222 -35
  9. package/interop.js +240 -141
  10. package/layout.js +34 -18
  11. package/module/array.js +99 -111
  12. package/module/collection.js +124 -30
  13. package/module/console.js +1 -1
  14. package/module/core.js +163 -19
  15. package/module/json.js +3 -3
  16. package/module/math.js +162 -3
  17. package/module/number.js +268 -13
  18. package/module/object.js +37 -5
  19. package/module/regex.js +8 -7
  20. package/module/simd.js +37 -5
  21. package/module/string.js +203 -168
  22. package/module/typedarray.js +565 -112
  23. package/package.json +26 -7
  24. package/src/abi/string.js +29 -29
  25. package/src/ast.js +19 -2
  26. package/src/autoload.js +3 -0
  27. package/src/compile/analyze-scans.js +174 -11
  28. package/src/compile/analyze.js +101 -4
  29. package/src/compile/cse-load.js +200 -0
  30. package/src/compile/emit-assign.js +82 -20
  31. package/src/compile/emit.js +592 -51
  32. package/src/compile/index.js +504 -89
  33. package/src/compile/infer.js +36 -1
  34. package/src/compile/loop-divmod.js +109 -0
  35. package/src/compile/loop-model.js +91 -0
  36. package/src/compile/loop-square.js +102 -0
  37. package/src/compile/narrow.js +275 -41
  38. package/src/compile/peel-stencil.js +215 -0
  39. package/src/compile/plan/advise.js +55 -1
  40. package/src/compile/plan/common.js +29 -0
  41. package/src/compile/plan/index.js +8 -1
  42. package/src/compile/plan/inline.js +180 -22
  43. package/src/compile/plan/literals.js +313 -24
  44. package/src/compile/plan/scope.js +115 -39
  45. package/src/compile/program-facts.js +21 -2
  46. package/src/ctx.js +96 -19
  47. package/src/helper-counters.js +137 -0
  48. package/src/ir.js +157 -20
  49. package/src/kind-traits.js +34 -3
  50. package/src/kind.js +79 -4
  51. package/src/op-policy.js +5 -2
  52. package/src/ops.js +119 -0
  53. package/src/optimize/index.js +1274 -151
  54. package/src/optimize/recurse.js +182 -0
  55. package/src/optimize/vectorize.js +4187 -253
  56. package/src/prepare/index.js +75 -14
  57. package/src/prepare/lift-iife.js +149 -0
  58. package/src/reps.js +6 -2
  59. package/src/static.js +9 -0
  60. package/src/type.js +63 -51
  61. package/src/wat/assemble.js +286 -21
  62. package/src/widen.js +21 -0
  63. package/src/wat/optimize.js +0 -3760
@@ -39,12 +39,25 @@ import {
39
39
  // expression — null if void or no trailing return value.
40
40
  const inlinedBody = (func, args) => {
41
41
  const params = func.sig.params
42
- if (args.length !== params.length || !args.every(isSimpleArg)) return null
42
+ if (args.length !== params.length) return null
43
43
  const paramNames = new Set(params.map(p => p.name))
44
44
  if (mutatesAny(func.body, paramNames)) return null
45
45
 
46
+ // A simple arg (ident / literal / arithmetic) is cheap to substitute directly, even when its
47
+ // param is used several times. A NON-simple arg (a call, `?:`, indexed load) is bound to a fresh
48
+ // temp evaluated ONCE in call order — preserving evaluation count + left-to-right order, and
49
+ // never duplicating the expression. This lets nested calls inline: `lerp(grad(a), grad(b), u)`
50
+ // binds `t0 = grad(a); t1 = grad(b)` and substitutes the body with t0/t1; a later inliner pass
51
+ // then folds grad into those temp decls (the fixpoint in inlineHotInternalCalls).
46
52
  const subst = new Map()
47
- for (let i = 0; i < params.length; i++) subst.set(params[i].name, args[i])
53
+ const argPrefix = []
54
+ for (let i = 0; i < params.length; i++) {
55
+ const arg = args[i]
56
+ if (isSimpleArg(arg)) { subst.set(params[i].name, arg); continue }
57
+ const tmp = `${T}inarg${ctx.func.uniq++}`
58
+ argPrefix.push(['const', ['=', tmp, arg]])
59
+ subst.set(params[i].name, tmp)
60
+ }
48
61
 
49
62
  const locals = new Set()
50
63
  collectBindings(func.body, locals)
@@ -56,13 +69,13 @@ const inlinedBody = (func, args) => {
56
69
  const stmts = blockStmts(func.body)
57
70
  // Expression-bodied arrow `(c) => expr`: no statement block; the whole body
58
71
  // *is* the return value. Treat as zero-prefix + value.
59
- if (!stmts) return { prefix: [], value: cloneWithSubst(func.body, subst, rename) }
72
+ if (!stmts) return { prefix: argPrefix, value: cloneWithSubst(func.body, subst, rename) }
60
73
  const last = stmts.length ? stmts[stmts.length - 1] : null
61
74
  const isTrailingReturn = Array.isArray(last) && last[0] === 'return'
62
75
  const prefixSrc = isTrailingReturn ? stmts.slice(0, -1) : stmts
63
76
  const prefix = prefixSrc.map(stmt => cloneWithSubst(stmt, subst, rename))
64
77
  const value = isTrailingReturn && last.length > 1 ? cloneWithSubst(last[1], subst, rename) : null
65
- return { prefix, value }
78
+ return { prefix: argPrefix.length ? [...argPrefix, ...prefix] : prefix, value }
66
79
  }
67
80
 
68
81
  const stmtDeclName = (stmt) => {
@@ -109,7 +122,14 @@ const isCandidateCall = (node, candidates) =>
109
122
  // arithmetic>`, substituting the decls into the return value turns it into a
110
123
  // zero-prefix expression. Duplicated subtrees (`dx` used twice → `x1-x2`
111
124
  // twice) are pure, and the watr-layer CSE collapses the copies.
112
- const PURE_FLATTEN_OPS = new Set(['+', '-', '*', '/', '%', 'u-', 'u+', '&', '|', '^', '<<', '>>', '>>>'])
125
+ const PURE_FLATTEN_OPS = new Set([
126
+ '+', '-', '*', '/', '%', 'u-', 'u+', '&', '|', '^', '<<', '>>', '>>>',
127
+ // pure value-producing ops with no side effects — safe to duplicate (CSE collapses copies):
128
+ // comparisons, logical, bit-not, and the conditional. Lets a branchy leaf like noise's
129
+ // `grad(h,x,y) => { …; u = (h&1)===0 ? x : -x; … }` flatten to an expression so it inlines
130
+ // into its multi-call caller `perlin` — `lerp(grad(a), grad(b), u)` then collapses end to end.
131
+ '<', '<=', '>', '>=', '==', '!=', '===', '!==', '&&', '||', '!', '~', '?:',
132
+ ])
113
133
  const pureFlattenExpr = (n) => {
114
134
  if (typeof n === 'number' || typeof n === 'string') return true // literal or ident
115
135
  if (!Array.isArray(n)) return false
@@ -163,13 +183,19 @@ const inlineInExpr = (node, candidates) => {
163
183
 
164
184
  const inlineInStmt = (stmt, candidates, loopVariantNames = null) => {
165
185
  if (!Array.isArray(stmt)) return { node: stmt, changed: false }
166
- // Statement-position call: discard return value, splice prefix in place.
186
+ // Statement-position call: the result is unused, but the callee's return
187
+ // EXPRESSION may still carry side effects — an expression-bodied arrow whose body
188
+ // is itself effectful (`seek = n => idx = n` inlines to the assignment `idx = i`;
189
+ // a one-liner that calls another fn) puts the effect in `value`, not `prefix`.
190
+ // Emit it as a trailing statement so the effect runs; a pure value is dropped
191
+ // later by vacuum/DCE. (Dropping it lost the parser's seek() idx-advance → ∞ loop.)
167
192
  if (isCandidateCall(stmt, candidates)) {
168
193
  const args = callArgs(stmt)
169
194
  const shape = args && inlinedBody(candidates.get(stmt[1]), args)
170
195
  if (shape) {
171
196
  const { hoisted, rest } = partitionInvariantPrefix(shape.prefix, loopVariantNames)
172
- return { node: ['{}', [';', ...rest]], changed: true, splice: rest, hoisted }
197
+ const splice = shape.value !== null ? [...rest, shape.value] : rest
198
+ return { node: ['{}', [';', ...splice]], changed: true, splice, hoisted }
173
199
  }
174
200
  }
175
201
  // `let/const X = call(...)` with single decl: inline as prefix + decl(value).
@@ -259,9 +285,99 @@ const inlineInStmt = (stmt, candidates, loopVariantNames = null) => {
259
285
  return { node: stmt, changed: false }
260
286
  }
261
287
 
288
+ // Short-circuit operators: only the FIRST operand is unconditionally evaluated; a call in
289
+ // a later operand might not run, so it can't be hoisted.
290
+ const SHORT_CIRCUIT = new Set(['?:', '?', '&&', '||', '??'])
291
+ // Optional chaining: jz's own desugaring already tees the base to evaluate it once, and
292
+ // the key/args run conditionally — so the hoist treats the WHOLE expression as opaque (no
293
+ // operand, not even the base, is hoisted out) to avoid colliding with that desugaring.
294
+ const OPTIONAL_CHAIN = new Set(['?.', '?.[]', '?.()'])
295
+ // Mutating expression operators — evaluating one is an observable side effect.
296
+ const ASSIGN_OPS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '<<=', '>>=', '>>>=', '**=', '&&=', '||=', '??=', '++', '--'])
297
+ // Does evaluating this expression have an observable side effect (a call or assignment)?
298
+ const containsEffect = (n) => Array.isArray(n) && n[0] !== '=>' &&
299
+ (n[0] === '()' || n[0] === '?.()' || ASSIGN_OPS.has(n[0]) || n.slice(1).some(containsEffect))
300
+
301
+ // Hoist an unconditionally-evaluated NESTED call to a block-body candidate out to a
302
+ // preceding `const __h = call(...)` temp. inlineInStmt folds block-body candidates only at
303
+ // a DIRECT `const X = call` / `X = call`; a call buried in an expression (noise's
304
+ // `sum = sum + amp * perlin(x)`) is reached by neither that path nor inlineInExpr (which
305
+ // only substitutes zero-prefix expr-bodies). Hoisting normalizes it to the direct form.
306
+ // Only block-body candidates and only unconditional positions — preserving evaluation order
307
+ // + count. Statement HEADERS that are expression positions (for-init/update, while/if test)
308
+ // are left untouched: there's no sound place for a hoisted decl there, so those calls just
309
+ // stay outlined. Conservatively leaves unrecognized statement shapes alone.
310
+ const hoistNestedCalls = (body, blockNames) => {
311
+ if (!blockNames.size || !Array.isArray(body)) return { node: body, changed: false }
312
+ let changed = false
313
+ const seq = (stmts) => stmts.length === 1 ? stmts[0] : [';', ...stmts]
314
+ // Lifting a call to the pre-decl block moves its evaluation to the TOP of the statement.
315
+ // Sound only if no observable side effect is evaluated BEFORE it — else its effect jumps
316
+ // ahead of that one (`a() + helper(x)` must keep a()'s effect first). `eff.seen` threads
317
+ // left-to-right through evaluation order: a call or assignment LEFT IN PLACE marks every
318
+ // later position. A hoisted call moves as a unit — its args run in a fresh inner eff, and
319
+ // it does NOT advance the outer eff (the whole unit relocates together, order intact).
320
+ const hExpr = (n, pre, cond, eff) => {
321
+ if (!Array.isArray(n) || n[0] === '=>') return n
322
+ if (!cond && !eff.seen && n[0] === '()' && typeof n[1] === 'string' && blockNames.has(n[1])) {
323
+ const call = [n[0], n[1], ...n.slice(2).map(a => hExpr(a, pre, false, { seen: false }))]
324
+ const tmp = `${T}inl${ctx.func.uniq++}_h`
325
+ pre.push(['const', ['=', tmp, call]])
326
+ changed = true
327
+ return [null, tmp]
328
+ }
329
+ if (OPTIONAL_CHAIN.has(n[0])) {
330
+ const out = [n[0], ...n.slice(1).map(c => hExpr(c, pre, true, eff))]
331
+ if (n[0] === '?.()') eff.seen = true // optional CALL may run
332
+ return out
333
+ }
334
+ if (SHORT_CIRCUIT.has(n[0]))
335
+ return [n[0], hExpr(n[1], pre, cond, eff), ...n.slice(2).map(c => hExpr(c, pre, true, eff))]
336
+ const out = [n[0], ...n.slice(1).map(c => hExpr(c, pre, cond, eff))]
337
+ if (n[0] === '()' || ASSIGN_OPS.has(n[0])) eff.seen = true // a call/assign left in place is an effect
338
+ return out
339
+ }
340
+ // A RHS that is DIRECTLY a candidate call is already folded by inlineInStmt's
341
+ // `const X = call` / `X = call` paths — hoisting it would be redundant and (for an
342
+ // object/array-literal `{}`-bodied factory) would break the post-inline alias chain.
343
+ // Only hoist NESTED calls; leave a top-level direct call to those paths.
344
+ const directCall = (e) => Array.isArray(e) && e[0] === '()' && typeof e[1] === 'string' && blockNames.has(e[1])
345
+ const hStmt = (s) => { // → array of statements (hoisted decls prepended)
346
+ if (!Array.isArray(s)) return [s]
347
+ switch (s[0]) {
348
+ case ';': return [[';', ...s.slice(1).flatMap(hStmt)]]
349
+ case '{}': return [['{}', seq(hStmt(s[1]))]]
350
+ case 'if': return [s.length > 3
351
+ ? ['if', s[1], seq(hStmt(s[2])), seq(hStmt(s[3]))]
352
+ : ['if', s[1], seq(hStmt(s[2]))]]
353
+ case 'for': { const i = forLoopBodyIndex(s); return [withForLoopBody(s, seq(hStmt(s[i])))] }
354
+ case 'while': return [['while', s[1], seq(hStmt(s[2]))]]
355
+ case 'let': case 'const': {
356
+ if (s.length === 2 && Array.isArray(s[1]) && s[1][0] === '=' && typeof s[1][1] === 'string' && !directCall(s[1][2])) {
357
+ const pre = []; const rhs = hExpr(s[1][2], pre, false, { seen: false })
358
+ return pre.length ? [...pre, [s[0], ['=', s[1][1], rhs]]] : [s]
359
+ }
360
+ return [s]
361
+ }
362
+ // A computed assign target (`a[i]=…`) evaluates its index BEFORE the RHS, so an effect
363
+ // there (`a[j++]=…`) must block hoisting too — seed eff.seen from the LHS.
364
+ case '=': { if (directCall(s[2])) return [s]; const pre = []; const rhs = hExpr(s[2], pre, false, { seen: containsEffect(s[1]) }); return pre.length ? [...pre, ['=', s[1], rhs]] : [s] }
365
+ case 'return': { if (s.length < 2 || directCall(s[1])) return [s]; const pre = []; const v = hExpr(s[1], pre, false, { seen: false }); return pre.length ? [...pre, ['return', v]] : [s] }
366
+ default: return [s] // unrecognized shape (break/continue/throw/try/switch): leave alone
367
+ }
368
+ }
369
+ const out = hStmt(body)
370
+ return { node: changed ? seq(out) : body, changed }
371
+ }
372
+
262
373
  export const inlineHotInternalCalls = (programFacts, ast) => {
263
374
  const cfg = ctx.transform.optimize
264
375
  if (cfg && cfg.sourceInline === false) return false
376
+ // Transitive candidacy + expression-position hoisting are a size↔speed trade (they
377
+ // pull a large multi-call leaf like noise's perlin fully into its hot caller, where
378
+ // the lower tiers prefer to keep multi-caller helpers outlined for V8 tier-up). Gate
379
+ // both on the speed tier so levels ≤2 keep their conservative inlining policy.
380
+ const speedTier = !!(cfg && cfg.inlineFns)
265
381
 
266
382
  const fixedByFunc = new Map(ctx.func.list.map(func => [func, fixedTypedArraysInBody(func.body)]))
267
383
  const typedByFunc = new Map(ctx.func.list.map(func => [func, analyzeBody(func.body).typedElems]))
@@ -315,7 +431,14 @@ export const inlineHotInternalCalls = (programFacts, ast) => {
315
431
  // entry points, not pulling a leaf INTO an export's hot loop (game-of-life's
316
432
  // step calls rot per cell; pre-Turboshaft wasm tiers never inline calls).
317
433
  const leaves = new Set()
434
+ // Transitive candidacy via fixpoint: a function whose only user-callees are THEMSELVES
435
+ // candidates (so they inline away) can be inlined too. noise's `perlin` calls grad/fade/
436
+ // lerp (loop-free leaves) — once those are candidates, perlin clears the call-bearing-body
437
+ // gate and becomes a leaf candidate. Each pass adds ≥1 or stops, so it's bounded.
438
+ for (let recollect = true; recollect;) {
439
+ recollect = false
318
440
  for (const func of ctx.func.list) {
441
+ if (candidates.has(func.name)) continue
319
442
  const sites = sitesByCallee.get(func.name)
320
443
  // Exported leaf/kernel with exactly one internal caller (e.g. fill→beat in
321
444
  // floatbeat): inline into the caller's loop but keep the export for external
@@ -336,7 +459,13 @@ export const inlineHotInternalCalls = (programFacts, ast) => {
336
459
  const fullyFixedTypedArraySite = hasFullyFixedTypedArraySites(func, sites)
337
460
  const hasLoop = some(func.body, n => LOOP_OPS.has(n[0]))
338
461
  const isTinyLeaf = !hasLoop && nodeSize(func.body) <= 15
339
- if (!sites || sites.length < 1 || (!isTinyLeaf && !fixedTypedArraySite && sites.length > 2) || sites.length > 8) continue
462
+ // A small leaf (no loop, ≤40 nodes) is cheap to splice even when called several times its
463
+ // per-call overhead + lost cross-call fusion dwarfs the ≤8× duplication, and temp-binding +
464
+ // flattenPrefix keep the spliced body bounded (no arg re-evaluation, CSE collapses copies).
465
+ // The 2-site non-tiny-leaf cap would otherwise outline a hot helper like noise's `grad`
466
+ // (~30 nodes, called 4× from perlin) and freeze the call overhead per pixel.
467
+ const isSmallLeaf = !hasLoop && nodeSize(func.body) <= 48
468
+ if (!sites || sites.length < 1 || (!isTinyLeaf && !isSmallLeaf && !fixedTypedArraySite && sites.length > 2) || sites.length > 8) continue
340
469
  const stmts = blockStmts(func.body)
341
470
  // Expression-bodied arrow funcs (`(c) => expr`) have no block — body IS the
342
471
  // return value. Treat as a "tiny leaf" branch handled below; force hasLoop=false.
@@ -356,15 +485,24 @@ export const inlineHotInternalCalls = (programFacts, ast) => {
356
485
  // that get hammered from a hot caller's loop — replacing the call with its
357
486
  // body saves the per-iteration call+reinterpret overhead (tokenizer hot path).
358
487
  if (!hasLoop) {
359
- if (some(func.body, n => n[0] === '()' && typeof n[1] === 'string' && ctx.func.names.has(n[1]))) continue
488
+ // Calls to functions that are THEMSELVES candidates are fine they inline away;
489
+ // only a call to a non-candidate user function blocks (a later fixpoint pass re-checks).
490
+ // Speed-tier only; lower tiers keep the strict "any user call ⇒ outline" rule.
491
+ if (some(func.body, n => n[0] === '()' && typeof n[1] === 'string' && ctx.func.names.has(n[1]) && !(speedTier && candidates.has(n[1])))) continue
360
492
  // Per-iteration call overhead dwarfs body-size bloat when EVERY site sits
361
493
  // inside a caller's loop (game-of-life's rot: ~40 nodes × 2 sites, fired
362
- // for most of 260k cells/frame). V8's pre-Turboshaft wasm tiers never
494
+ // for most of 260k cells/frame; cloth's relax: ~160 nodes × 2 sites, fired
495
+ // per link every relaxation pass). V8's pre-Turboshaft wasm tiers never
363
496
  // inline cross-function, so an out-of-line leaf in a hot loop is a hard
364
497
  // per-cell tax on Node ≤ 22 — and still saves call setup on newer tiers.
498
+ // The in-loop cap is generous because the gate above bounds non-tiny leaves
499
+ // to ≤2 sites, so the spliced duplication is at most ~2× a bounded body.
365
500
  const allSitesInLoop = sites.every(site =>
366
501
  site.callerFunc?.body && containsNode(site.callerFunc.body, site.node, false))
367
- if (nodeSize(func.body) > (allSitesInLoop ? 80 : 30)) continue
502
+ // Non-in-loop cap is 40 (not 30) so a small leaf called from a straight-line but
503
+ // transitively-hot caller still inlines (noise's grad is called from perlin, which has
504
+ // no loop of its own but is itself the per-pixel kernel). Still tightly bounded.
505
+ if (nodeSize(func.body) > (allSitesInLoop ? 200 : 48)) continue
368
506
  }
369
507
  if (some(func.body, n => n[0] === '()' && n[1] === func.name)) continue
370
508
  // Kernels with nested loops (depth ≥ 2) are typically large and the inner
@@ -383,6 +521,8 @@ export const inlineHotInternalCalls = (programFacts, ast) => {
383
521
  forwarders.add(func.name)
384
522
  if (!hasLoop) leaves.add(func.name)
385
523
  candidates.set(func.name, func)
524
+ if (speedTier) recollect = true // only the speed-tier transitive relaxation needs a re-pass
525
+ }
386
526
  }
387
527
  if (!candidates.size) return false
388
528
 
@@ -433,20 +573,38 @@ export const inlineHotInternalCalls = (programFacts, ast) => {
433
573
  // Route those through inlineInExpr so the call is replaced by the inlined
434
574
  // value expression instead.
435
575
  const isExprBody = !Array.isArray(func.body) || func.body[0] !== '{}'
436
- const r = isExprBody
437
- ? inlineInExpr(func.body, activeCandidates)
438
- : inlineInStmt(func.body, activeCandidates)
439
- let body = r.changed ? r.node : func.body
440
- let bodyChanged = r.changed
441
- // Expression-position pass. Exported callers take the leaf-safe subset —
442
- // the same tier-up rationale as the statement path (leaves into exports
443
- // are fine; relocated kernels are not).
576
+ // Expression-position pass takes the leaf-safe subset for exports — the same tier-up
577
+ // rationale as the statement path (leaves into exports are fine; relocated kernels are not).
444
578
  const exprActive = func.exported
445
579
  ? new Map([...exprOnlyCandidates].filter(([n]) => exportedCandidates.has(n)))
446
580
  : exprOnlyCandidates
447
- if (exprActive.size) {
448
- const e = inlineInExpr(body, exprActive)
449
- if (e.changed) { body = e.node; bodyChanged = true }
581
+ // Iterate to a (bounded) fixpoint: inlining a call whose args are themselves candidate calls
582
+ // binds those args to temps (`t0 = grad(a)`); the next pass folds the candidate into the temp
583
+ // decl. Depth is bounded by call nesting (a small constant), capped here so a pathological
584
+ // chain can't loop unbounded.
585
+ // Loop-free block-body LEAVES: the stmt path folds them only at a DIRECT `const X =
586
+ // call`, never nested in an expression. Hoisting such a call to a temp (in the iter
587
+ // fixpoint below) lets inlineInStmt then fold it — the noise `sum + amp*perlin(x)`
588
+ // shape. Restricted to LEAVES (no own loop): a loop kernel called in expression
589
+ // position (e.g. a 2-site `reduce`) was deliberately staying outlined for V8 tier-up,
590
+ // and hoisting it would pull the loop into a cold caller.
591
+ const blockNames = new Set()
592
+ for (const n of activeCandidates.keys()) if (leaves.has(n) && !exprActive.has(n)) blockNames.add(n)
593
+ let body = func.body, bodyChanged = false
594
+ for (let iter = 0; iter < 4; iter++) {
595
+ let iterChanged = false
596
+ if (speedTier && !isExprBody && blockNames.size) {
597
+ const h = hoistNestedCalls(body, blockNames)
598
+ if (h.changed) { body = h.node; iterChanged = true }
599
+ }
600
+ const r = isExprBody ? inlineInExpr(body, activeCandidates) : inlineInStmt(body, activeCandidates)
601
+ if (r.changed) { body = r.node; iterChanged = true }
602
+ if (exprActive.size) {
603
+ const e = inlineInExpr(body, exprActive)
604
+ if (e.changed) { body = e.node; iterChanged = true }
605
+ }
606
+ if (!iterChanged) break
607
+ bodyChanged = true
450
608
  }
451
609
  if (bodyChanged) { func.body = body; changed = true }
452
610
  }