Skip to content

perf: single-pass FileProcessor (drop do-while reprint loop) - #8825

Closed
TomasVotruba wants to merge 3 commits into
mainfrom
tv-file-processor-single-pass
Closed

TomasVotruba wants to merge 3 commits into
mainfrom
tv-file-processor-single-pass

Conversation

@TomasVotruba

Copy link
Copy Markdown
Member

What

Replace the do { traverse rules; post-rectors; print } while (changed) fixpoint loop in FileProcessor::processFile() with a single pass. The old loop reprinted the whole file (format-preserving print, the expensive step) once per pass until the AST stabilized. This runs the rule traversal and post-rectors once and prints at most once.

Also removes the now-unused File::changeHasChanged() and keeps File::hasChanged() backed by the real content-change flag (set during print), not by the line-change counter - the two are different signals:

  • rectorWithLineChanges !== [] - a rule reported a change (gates diff-object creation)
  • hasChanged flag - printed content actually differs (authoritative for caching / result)

Status - DRAFT, CI will be red on purpose

This surfaces the perf idea plus its hard drawbacks. It does not pass CI as-is.

Drawbacks (known failing tests)

The loop existed to converge multi-step transformations. Single pass stops one step short in 6 tests/Issues fixtures:

Rule-chain (two rules, A output must reach B):

  • AlwaysTrueIfInDeadConstructor - RemoveAlwaysTrueIf empties the ctor, RemoveEmptyClassMethod should then delete it. Pre-order dispatch visits ClassMethod before the inner if is removed, so the ctor is not empty yet.
  • DeadInstanceFlip - ExplicitNullable produces an instanceof, RemoveDeadInstanceOf should strip it. ExplicitNullable runs last in the visitor list, so RemoveDeadInstanceOf (already run) never sees the produced node.
  • InlineIfReplaceBlock - ReplaceBlock unwraps a bare block via an array return (spliced through $doNodes, never re-traversed that pass), so InlineIf never sees the exposed statements.
  • IndexedStmt - synthetic engine regression: statement index shifts after a removal; inherently needs a re-pass.

Post-rector convergence (legitimately multi-pass):

  • NamespacedUseAutoImport x2 - name import adds a use, then it is normalized on a later pass. This is import convergence in the post-rector chain, not a single rule.

Why per-rule idempotency fixes do not cover these: all four rule-chain cases are cross-rule compositions. A fix inside rule A cannot make rule B fire. Folding only works when a rule chains on its own same-class output.

Proposed path to make it work - deferred last-phase rules

Give cleanup / dead-code rules (RemoveEmptyClassMethod, RemoveDeadInstanceOf, ...) a "run last" position: a dedicated final sweep over the tree after the main sweep, instead of an unbounded reprint loop. Precedent: post-rectors are already ordered and run after rules.

  • Converges the transform-then-cleanup cases (AlwaysTrueIf, DeadInstanceFlip, InlineIfReplaceBlock) in a fixed, small number of ordered sweeps, printing once.
  • Note: plain reordering within a single sweep is not enough for the parent-before-child case (AlwaysTrueIf) - "last" has to mean a deferred sweep, not just a later slot in the same traversal.
  • Does not cover pure re-index (IndexedStmt) or deep A->B->A chains.
  • Import post-rectors still need their own internal fixpoint.

Perf note

Not yet benchmarked. The win is dropping repeated format-preserving prints on files that need more than one pass; single-pass files are unaffected. Needs a before/after measurement before this is more than a draft.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants