Skip to content

Add INLINABLE to AST traversals — full builds -9%#19

Merged
kozak merged 1 commit into
restaumaticfrom
traversal-inline
May 11, 2026
Merged

Add INLINABLE to AST traversals — full builds -9%#19
kozak merged 1 commit into
restaumaticfrom
traversal-inline

Conversation

@kozak
Copy link
Copy Markdown

@kozak kozak commented May 9, 2026

Summary

Two-line change: add {-# INLINABLE #-} to everywhereOnValuesTopDownM and everywhereOnValuesM in src/Language/PureScript/AST/Traversals.hs.

+ {-# INLINABLE everywhereOnValuesTopDownM #-}
  everywhereOnValuesTopDownM f g h = (f' <=< f, g' <=< g, h' <=< h)
  ...
+ {-# INLINABLE everywhereOnValuesM #-}
  everywhereOnValuesM f g h = (f', g', h')

Why

The hot caller is Entailment.replaceTypeClassDictionaries (src/Language/PureScript/TypeChecker/Entailment.hs:118-139). It runs once per typechecked declaration with monad m = WriterT (Any, [...]) (StateT InstanceContext TypeCheckM) — four transformer layers, each with its own >>= per recursive descent over Expr trees.

The traversal helpers were polymorphic over Monad m =>. Without INLINABLE, GHC can specialise within the defining module but cannot see the unfolding when compiling other modules — so callers go through the runtime Monad dictionary on every recursive bind. Adding INLINABLE exposes the unfolding in the .hi file; GHC's specialiser at each call site then collapses the polymorphic helper into a flat, monad-specialised loop.

Measurements

Two interleaved-measurement runs (exp run, median-of-4 after warm-up) against the pre-change baseline (c84101d8) on the pr-admin workload (1758 modules):

Scenario Run 1 (load 4–7) Run 2 (load 1.85)
full −9.6% −8.9%
nochange +8.4% −3.0%
prelude −0.5% +1.6%
leaf −4.3% −1.5%

Per-round full deltas (Run 2): −10.1%, −15.2%, −9.6%, −8.9% — even the worst round is −8.9%, so the full-build win is robust. nochange/prelude/leaf are within noise floor (Run 1's nochange +8.4% was a load-contamination artefact on a 643 ms wallclock; Run 2's −3.0% confirms it).

Cost

  • Diff size: 2 lines.
  • Binary size: 48,625,952 → 48,839,456 bytes (+213 KB / +0.4%) — GHC emits a few specialised copies at heavy callers.
  • stack build time: unchanged within noise.
  • Tests: stack test --fast passes 1340 examples.

Test plan

  • stack build (optimised) — green
  • stack test --fast — 1340 examples, 0 failures
  • Full pr-admin compile via exp run --scenarios all --runs 5 — robust ~−9% on full builds across two runs

🤖 Generated with Claude Code

…OnValuesM

Two-line change: add {-# INLINABLE #-} pragmas to the polymorphic
traversal helpers so callers (notably Entailment.replaceTypeClass-
Dictionaries with its WriterT/StateT/TypeCheckM stack) can specialise
the dictionary at use site.

Test suite passes (1340 examples).

Initial measurement (load avg ~4-7, baseline c84101d):
  full      -9.6%  (58.4s -> 52.8s)
  nochange  +8.4%  (0.643s -> 0.697s) -- regression to investigate
  prelude   -0.5%
  leaf      -4.3%

Per-round full deltas: -4.0, -10.1, -10.3, -8.7 -- consistent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@kozak kozak requested a review from zyla May 9, 2026 08:53
Copy link
Copy Markdown
Collaborator

@zyla zyla left a comment

Choose a reason for hiding this comment

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

LG

I was super confused at first when I saw this - my thought was "Wait, didn't we add INLINE to the traversals a long time ago?". The answer is: yes, but to the type traversals, not full AST.

@kozak kozak merged commit d3f69f9 into restaumatic May 11, 2026
5 of 7 checks passed
@seastian
Copy link
Copy Markdown

Hi! the work you are doing here is very good! Does it make sense to port this to the official one?

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.

3 participants