Skip to content

perf: drive optimizer fixpoints by change flags instead of whole-module Eq - #156

Merged
Unisay merged 2 commits into
mainfrom
issue-144/fixpoint-change-flag
Jul 4, 2026
Merged

perf: drive optimizer fixpoints by change flags instead of whole-module Eq#156
Unisay merged 2 commits into
mainfrom
issue-144/fixpoint-change-flag

Conversation

@Unisay

@Unisay Unisay commented Jul 4, 2026

Copy link
Copy Markdown
Collaborator

Closes #144. Builds on the bottom-up driver (#155, merged).

fixpointM detected convergence by deep Eq on the whole UberModule every round. Now that the rewrite driver reports precisely whether it rewrote anything, the passes can just say so:

  • Per review feedback, the signal is a dedicated domain type instead of Any: data WasRewritten = Rewritten | Unmodified with a "Rewritten wins" Semigroup, so use sites read tell Rewritten and signatures read SupplyM (Exp, WasRewritten).
  • passRun returns (UberModule, WasRewritten). Unmodified asserts the output is structurally identical to the input; Rewritten says it may differ. The fixpoint members report precisely: optimize aggregates the driver signal across all expressions plus every top-level inlining, and dce combines its driver signal with top-level binding/foreign drops and ForeignImport name-list pruning. Run-once passes report a conservative Rewritten, which nothing consumes.
  • A fixpoint stops on the first round that reports Unmodified, bounded by maxFixpointIterations (100). No runner compares whole modules anymore.
  • The two failure directions land in different runners. Under-reporting (changed but said it didn't) would silently under-optimize, so the checked runner (the whole test suite and --lint-ir) compares the one module a pass claims unchanged against its input and fails with PassUnreportedChange. Over-reporting (said it changed but didn't) would spin the loop, so at the cap the checked runner fails with FixpointDivergence while the production runner accepts the module reached so far. Every pass is semantics-preserving, so an early stop costs optimization, never correctness, and production can no longer hang.
  • idempotently stays as the semantic reference: a property test checks that the signal-driven fixpoint computes exactly what the Eq-driven reference computes when the signal is precise. fixpointM is gone.

Since the golden harness runs the checked pipeline, every fixpoint round of every golden test now asserts the signal's precision for free. No golden moved: the fixpoint stops at exactly the round the Eq oracle did, drawing the same supply names.

To be honest about the payoff: the raw perf win is modest, since Eq short-circuits on the first difference and only pays a full traversal on the final converged round. The real gains are a precise per-pass signal, loud failures when it misreports, and the per-pass delta that #142/#143 can build on.

@Unisay
Unisay force-pushed the issue-144/fixpoint-change-flag branch from c932082 to b91aad2 Compare July 4, 2026 15:21
Base automatically changed from issue-145/bottom-up-rewrite-driver to main July 4, 2026 15:29
@Unisay
Unisay requested a review from Copilot July 4, 2026 15:29
@Unisay Unisay self-assigned this Jul 4, 2026
@Unisay
Unisay marked this pull request as ready for review July 4, 2026 15:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the IR pass framework and optimizer fixpoint execution to stop iterating based on per-pass change flags rather than deep structural Eq of whole UberModules, eliminating repeated whole-module equality checks while adding explicit checks for dishonest change reporting in the checked runner.

Changes:

  • Extend Pass.passRun to return (UberModule, Any) and implement flag-driven fixpoint looping with a hard cap (maxFixpointIterations).
  • Add checked-runner enforcement for under-reporting (PassUnreportedChange) and non-converging fixpoints (FixpointDivergence), with corresponding CLI rendering and tests.
  • Update Optimizer/DCE to report precise change flags (optimize aggregates rewrite-driver flags + top-level inlining; DCE reports expression rewrites plus drops/prunes).

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.

Show a summary per file
File Description
lib/Language/PureScript/Backend/IR/Pass.hs Reworks pass runner API and fixpoint convergence to use change flags; adds checked-runner failures and iteration cap.
lib/Language/PureScript/Backend/IR/Optimizer.hs Threads change flags through optimize pass via WriterT Any; updates pipeline pass definitions for new passRun signature.
lib/Language/PureScript/Backend/IR/DCE.hs Makes DCE return (UberModule, Any) and aggregates precise flags for rewrites/drops/prunes.
test/Language/PureScript/Backend/IR/Pass/Spec.hs Updates tests for new PassCheckFailure shape and adds coverage for under-reporting and divergence behavior.
test/Language/PureScript/Backend/IR/DCE/Spec.hs Updates DCE tests to assert both output and correctness of the change flag.
changelog.d/20260704_140000_unisay_fixpoint_change_flag.md Documents the behavioral change and new checked-runner failure modes.

-- The binding is dropped from the module in favor of the
-- substituted copies: a change even when it had no
-- occurrences left to substitute.
tell (Any True)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this expression is not telling about its purpose: I'd like to see a self documenting code, something like:

registerRewrite

where registerRewrite could be defined at the bottom or elsewhere as registerRewrite = tell (Any True)
or even better, avoid "boolean blindness" and use a dedicated data type with its own monoid instance, i.e.

data Rewrite = Rewritten | Unmodified
instance Monoid Rewrite ...

-- then
tell Rewritten -- <- reads much better, no explanatory comment needed 

optimizedExpressionM = fmap fst . optimizedExpressionFlagM

-- | 'optimizedExpressionM' plus the driver's change flag (issue #144).
optimizedExpressionFlagM ∷ Exp → SupplyM (Exp, Any)

@Unisay Unisay Jul 4, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow up to the comment above, this reads much nicer:

optimizedExpressionFlagM ∷ Exp → SupplyM (Exp, Rewrite)

Try to find a better name for the datatype that reflects the property of AST that it has been rewritten in one word, or maybe WasRewritten is really non-ambigous

Unisay added 2 commits July 4, 2026 17:50
…back)

Boolean blindness: 'tell (Any True)' needed a comment to say what the
boolean means. Introduce a dedicated type next to the drivers —

  data WasRewritten = Rewritten | Unmodified

with a 'Rewritten wins' Semigroup and 'Unmodified' mempty — so the
drivers return (RawExp ann, WasRewritten) and use sites read
'tell Rewritten' / case-match on the outcome without commentary.

Also trims issue references from comments down to genuine bug
pointers, per the same review.
…le Eq

Closes #144. Every passRun now returns (UberModule, Any): 'Any False'
asserts the output is structurally identical to the input, 'Any True'
says it may differ. The fixpoint members report precisely — optimize
aggregates the driver flag across all expressions plus every top-level
inlining; dce combines its driver flag with top-level binding/foreign
drops and ForeignImport name-list pruning. Run-once passes report a
conservative 'Any True', which nothing consumes.

A fixpoint stops on the first round reporting no change, bounded by
maxFixpointIterations (100); no runner compares whole modules anymore.
The dishonesty directions are split per runner: the checked runner
(test suite, --lint-ir) fails with PassUnreportedChange when a pass
changes the module while claiming it didn't (the one surviving,
targeted Eq — paid only in checked mode and only on a no-change
claim), and with FixpointDivergence at the cap; the production runner
accepts the module reached at the cap — every pass is
semantics-preserving, so an early stop costs optimization, never
correctness, and production can no longer hang.

'idempotently' stays as the semantic reference (a property test pins
the flag-driven fixpoint to it for honest flags); fixpointM is gone.
Since the golden harness runs the checked pipeline, every fixpoint
round of every golden now asserts flag honesty. No golden moved: with
honest flags the fixpoint stops at exactly the round the Eq oracle
did.
@Unisay
Unisay force-pushed the issue-144/fixpoint-change-flag branch from b91aad2 to 5f85684 Compare July 4, 2026 15:59
@Unisay
Unisay merged commit 2f940bf into main Jul 4, 2026
2 checks passed
@Unisay
Unisay deleted the issue-144/fixpoint-change-flag branch July 4, 2026 16:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

idempotently detects the fixpoint by deep Eq on the whole UberModule

2 participants