Skip to content

Skip redundant unifyTypes in entailment solver (-15% full build)#14

Merged
kozak merged 4 commits intorestaumaticfrom
skip-redundant-entailment-unify
Apr 21, 2026
Merged

Skip redundant unifyTypes in entailment solver (-15% full build)#14
kozak merged 4 commits intorestaumaticfrom
skip-redundant-entailment-unify

Conversation

@kozak
Copy link
Copy Markdown

@kozak kozak commented Apr 17, 2026

Summary

  • Skip redundant unifyTypes calls during entailment functional dependency enforcement when the inferred type is structurally equal to the target type
  • For wide row types (e.g. HasField on a 667-field Translations record), this avoids O(n log n) row sorting + O(n) alignment that produces no new information
  • Two-line change in Entailment.hs — adds unless (eqType inferredType t2) guard before the existing unifyTypes call

Background

The entailment solver's Solved branch (Entailment.hs:295-298) enforces functional dependencies by calling:

zipWithM_ (\t1 t2 -> do
  let inferredType = replaceAllTypeVars (M.toList subst') t1
  unifyTypes inferredType t2) (tcdInstanceTypes tcd) tys''

When the instance types are TypeVars (common — e.g. HasField label (Record row) ty), the substitution maps them back to the original constraint types. So inferredType is often structurally identical to t2.

For small types this is harmless — unifyTypes on identical small types is fast. But for wide row types like a 667-field Translations record, unifyTypes dispatches to unifyRows which calls alignRowsWith, which:

  1. Calls rowToSortedList twice — O(n log n) each
  2. Aligns the sorted lists — O(n)
  3. Recursively unifies each field type — O(n) calls

All for a no-op result (identical types always unify successfully with no new bindings).

The eqType guard does a simple O(n) structural comparison that short-circuits on the first mismatch — much cheaper than the full unification path, and free for the common case where types differ (different constructors → O(1) False).

Measurements

Measured on pr-admin (1758 modules, -O2 optimised build, median of 3 runs):

Scenario Before After Delta
Full build 74.1s 62.6s -15.5%
No-change rebuild 1.24s 1.29s ~0% (noise)
Prelude comment edit 5.25s 5.44s ~0% (noise)
Leaf module edit 2.03s 2.00s ~0% (noise)

Test plan

  • All 1340 tests pass (stack test --fast)
  • Full pr-admin build succeeds
  • No regression on incremental scenarios (nochange, prelude, leaf)

🤖 Generated with Claude Code

When the entailment solver enforces functional dependencies after
matching an instance, it unifies each inferred type with the
corresponding constraint type. For constraints on wide row types
(e.g. HasField on a 667-field record), this triggers O(n) row
alignment via alignRowsWith — even when both sides are structurally
identical (the inferred type IS the constraint type after variable
substitution).

Add an eqType guard before unifyTypes to skip the unification when
the inferred type already equals the target type. eqType is a
simple structural equality check (O(n) but no sorting, no recursive
unification, no cache insertion) compared to the full unifyRows path
(rowToSortedList O(n log n) + alignment O(n) + recursive unifications).

Measured on pr-admin (1758 modules, optimised -O2 build):

  full build:  74s -> 63s  (-15%)
  no-change:   1.2s        (unchanged)
  prelude-edit:5.3s        (unchanged)
  leaf-edit:   2.0s        (unchanged)

All 1340 tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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.

Great stuff.

One thing I'd suggest: add a comment why the eqType special case is there (i.e. for performance) - otherwise looks odd.

BTW, we should fix CI on the fork - currently it fails to build deps so doesn't even run tests

kozak and others added 3 commits April 20, 2026 10:58
The build step uses --haddock --no-haddock-deps, which affects the
install-path hash. Downstream steps (glob-test, build-package-set,
libtinfo check) called stack exec/path without those flags, resolving
to a different install hash and failing to find the purs binary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
stack path --local-doc-root doesn't inherit flags from the outer
stack exec invocation, so it resolved to a different install hash
than the build artifacts. Since stack exec already puts the binary
on PATH, use command -v / which instead.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@kozak kozak merged commit bb6850b into restaumatic Apr 21, 2026
4 of 6 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.

2 participants