Skip to content

Optimize replaceAllTypeSynonyms with per-node TypeFlags#11

Merged
kozak merged 5 commits intorestaumaticfrom
synonym-opt
Apr 23, 2026
Merged

Optimize replaceAllTypeSynonyms with per-node TypeFlags#11
kozak merged 5 commits intorestaumaticfrom
synonym-opt

Conversation

@kozak
Copy link
Copy Markdown

@kozak kozak commented Apr 12, 2026

Summary

  • Add a TypeFlags field to every Type constructor, cached per-node, tracking structural properties of the subtree
  • Pattern synonyms auto-compute flags on construction and ignore them on matching — existing code is unchanged
  • Short-circuit replaceAllTypeSynonyms, replaceTypeWildcards, introduceSkolemScope, and substituteType when flags indicate the traversal is unnecessary
  • Custom single-pass traversal for synonym expansion that marks all output nodes as synonym-free

Flags tracked

Flag Meaning Enables
tfHasWildcards Subtree contains TypeWildcard Skip replaceTypeWildcards
tfHasUnscopedForAlls Subtree contains ForAll without SkolemScope Skip introduceSkolemScope
tfSynonymsFree Subtree fully synonym-expanded Skip replaceAllTypeSynonyms
tfHasUnknowns Subtree contains TUnknown Skip substituteType

Profile results (pr-admin, 1758 modules)

Cost Centre Before After
replaceAllTypeSynonyms'.go 16.9% 0.0%
introduceSkolemScope 2.4% 0.0%
replaceTypeWildcards 2.2% 0.0%
everywhereOnTypes.go 3.0% 0.0%
Full build wall time ~72s ~53s (~26% faster)

Test plan

  • All 1340 tests pass (stack test --fast)
  • Full build timing verified on pr-admin corpus
  • Profiled build confirms cost centre reductions
  • Debug assertions (Control.Exception.assert) verify flag correctness in --fast builds

🤖 Generated with Claude Code

Add a TypeFlags field to every Type constructor that caches structural
properties of the subtree. Pattern synonyms auto-compute flags on
construction and ignore them on matching, so existing code is unchanged.

Three flags are tracked:
- tfHasWildcards: subtree contains TypeWildcard nodes
- tfHasUnscopedForAlls: subtree contains ForAll without SkolemScope
- tfSynonymsFree: subtree has been fully synonym-expanded

This enables short-circuiting in three hot traversals:
- replaceAllTypeSynonyms skips types already marked synonym-free
- replaceTypeWildcards skips types with no wildcards
- introduceSkolemScope skips types with no unscoped ForAlls

The synonym expansion also uses a custom single-pass traversal that
both expands synonyms and marks all output nodes as synonym-free,
so subsequent calls on the same type or any subtree return in O(1).

Profile results (pr-admin, 1758 modules):
- replaceAllTypeSynonyms'.go: 16.9% → 0.1%
- introduceSkolemScope: 2.4% → 0.0%
- replaceTypeWildcards: 2.2% → 0.0%
- Full build wall time: ~72s → ~65s (~10% faster)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@kozak kozak changed the base branch from master to restaumatic April 12, 2026 11:31
@kozak kozak requested a review from zyla April 12, 2026 14:20
kozak and others added 3 commits April 12, 2026 14:27
These utility functions were defined during development but ended up
unused — the custom traversal in Synonyms.hs uses raw constructors
directly instead. Weeder flagged them as dead code.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When short-circuiting based on tfSynonymsFree, tfHasWildcards, or
tfHasUnscopedForAlls, assert that a fresh scan of the type confirms the
flag's claim. Catches any bug where a flag is set on a type that actually
contains the nodes it claims to exclude.

Uses Control.Exception.assert, which is active in --fast builds (no -O)
but compiled away in optimized builds, so there is no production cost.

All 1340 tests pass with assertions active, confirming the invariants
hold across the full test corpus.

The key invariant: any construction via pattern synonyms calls
combineFlags, which masks to structural flags only (0x03), so
tfSynonymsFree is always cleared on reconstructed internal nodes.
This means substituteType (which uses everywhereOnTypes and thus
pattern synonyms) cannot sneak an unexpanded synonym past the flag.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a concrete example showing how substitution can introduce a new
synonym application at a parent node even when both children are
synonym-free in isolation. This explains why we conservatively clear
the flag on every reconstruction instead of propagating it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
-- Substitution: u -> TypeConstructor SomeAlias -- standalone, fine
-- After substitution: TypeApp (TypeConstructor SomeAlias) someArg
-- -- now a fully-applied synonym that needs expansion!
-- @
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@zyla does this + assertions address your substitution concern ?

Derive the mask from tfHasWildcards and tfHasUnscopedForAlls so that
adding a new structural flag automatically includes it in propagation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@kozak kozak merged commit f7cf774 into restaumatic Apr 23, 2026
5 of 7 checks passed
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.

1 participant