From e3425f4d8fe3e2000b40b15f9c327c632e468b10 Mon Sep 17 00:00:00 2001 From: Michal Kozakiewicz Date: Wed, 29 Apr 2026 17:23:49 +0000 Subject: [PATCH 1/2] unify-leaf-no-hash: leaf fast-path + drop S.Set cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pre-type-hash baseline 799e8208. Apply the same leaf fast-path as unify-leaf-fast-path, plus drop the S.Set unificationCache wrapper. The catch-all unifyTypes now calls unifyTypes' directly through substituteType / withErrorMessageHint. Result vs 5713e832 (current shipped tip with type-hash + HashSet cache): full -0.4% nochange -1.4% prelude +1.4% leaf +2.6% Result vs 799e8208 (direct base, pre-type-hash with S.Set cache): full -18.6% (vs type-hash's -15.4% standalone) nochange +3.9% prelude -0.9% leaf -0.1% The leaf fast-path absorbs essentially all of type-hash's value on full builds. We can drop type-hash machinery (per-node hash at construction, Hashable instance, HashSet bookkeeping — ~488 KB of binary) for a 5-line leaf fast-path with essentially equivalent performance. unificationCache field stays in CheckState (unread/unwritten); removal would be follow-up cleanup if shipped. --- src/Language/PureScript/TypeChecker/Unify.hs | 28 ++++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/Language/PureScript/TypeChecker/Unify.hs b/src/Language/PureScript/TypeChecker/Unify.hs index 68e5bb8992..b7863ad621 100644 --- a/src/Language/PureScript/TypeChecker/Unify.hs +++ b/src/Language/PureScript/TypeChecker/Unify.hs @@ -17,7 +17,7 @@ module Language.PureScript.TypeChecker.Unify import Prelude import Control.Exception (assert) -import Control.Monad (forM_, void, when) +import Control.Monad (forM_, void) import Control.Monad.Error.Class (MonadError(..)) import Control.Monad.State.Class (MonadState(..), gets, modify, state) import Control.Monad.Writer.Class (MonadWriter(..)) @@ -34,7 +34,6 @@ import Language.PureScript.TypeChecker.Kinds (elaborateKind, instantiateKind, un import Language.PureScript.TypeChecker.Monad (CheckState(..), Substitution(..), UnkLevel(..), Unknown, getLocalContext, guardWith, lookupUnkName, withErrorMessageHint, TypeCheckM) import Language.PureScript.TypeChecker.Skolems (newSkolemConstant, skolemize) import Language.PureScript.Types (Constraint(..), pattern REmptyKinded, RowListItem(..), SourceType, Type(..), WildcardData(..), alignRowsWith, everythingOnTypes, everywhereOnTypes, everywhereOnTypesM, getAnnForType, hasFlag, mkForAll, rowFromList, srcTUnknown, tfHasWildcards, typeFlags) -import Data.Set qualified as S -- | Generate a fresh type variable with an unknown kind. Avoid this if at all possible. freshType :: TypeCheckM SourceType @@ -112,16 +111,29 @@ unknownsInType t = everythingOnTypes (.) go t [] go _ = id -- | Unify two types, updating the current substitution +-- +-- Pre-substitute leaf fast-path: trivially-equal leaves +-- (TypeConstructor/TypeVar/TypeLevelString/TypeLevelInt/Skolem +-- with equal payload) short-circuit before substituteType / +-- withErrorMessageHint. Per unify-cache-anatomy on post-type-hash +-- baseline 5713e832: 86% of cache hits were on 1-2-node pairs +-- of this shape — the pattern is structural to recursive descent +-- so the same survey shape applies pre-type-hash. +-- +-- The S.Set unification cache is dropped — relying on the leaf +-- fast-path to catch the dominant recurrence pattern. Combined +-- effect tested against 799e8208 (pre-type-hash, S.Set) baseline +-- and 5713e832 (post-type-hash, HashSet) tip. unifyTypes :: SourceType -> SourceType -> TypeCheckM () +unifyTypes (TypeConstructor _ c1) (TypeConstructor _ c2) | c1 == c2 = pure () +unifyTypes (TypeVar _ v1) (TypeVar _ v2) | v1 == v2 = pure () +unifyTypes (TypeLevelString _ s1) (TypeLevelString _ s2) | s1 == s2 = pure () +unifyTypes (TypeLevelInt _ n1) (TypeLevelInt _ n2) | n1 == n2 = pure () +unifyTypes (Skolem _ _ _ s1 _) (Skolem _ _ _ s2 _) | s1 == s2 = pure () unifyTypes t1 t2 = do sub <- gets checkSubstitution - withErrorMessageHint (ErrorUnifyingTypes t1 t2) $ unifyTypes'' (substituteType sub t1) (substituteType sub t2) + withErrorMessageHint (ErrorUnifyingTypes t1 t2) $ unifyTypes' (substituteType sub t1) (substituteType sub t2) where - unifyTypes'' t1' t2'= do - cache <- gets unificationCache - when (S.notMember (t1', t2') cache) $ do - modify $ \st -> st { unificationCache = S.insert (t1', t2') cache } - unifyTypes' t1' t2' unifyTypes' (TUnknown _ u1) (TUnknown _ u2) | u1 == u2 = return () unifyTypes' (TUnknown _ u) t = solveType u t unifyTypes' t (TUnknown _ u) = solveType u t From 35ad956a3c5e0195f5a2282b8b2cd78c7cfc12f8 Mon Sep 17 00:00:00 2001 From: Michal Kozakiewicz Date: Wed, 29 Apr 2026 20:30:12 +0000 Subject: [PATCH 2/2] Trim unifyTypes comment to non-obvious why --- src/Language/PureScript/TypeChecker/Unify.hs | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/src/Language/PureScript/TypeChecker/Unify.hs b/src/Language/PureScript/TypeChecker/Unify.hs index b7863ad621..3264a8b6b8 100644 --- a/src/Language/PureScript/TypeChecker/Unify.hs +++ b/src/Language/PureScript/TypeChecker/Unify.hs @@ -110,20 +110,11 @@ unknownsInType t = everythingOnTypes (.) go t [] go (TUnknown ann u) = ((ann, u) :) go _ = id --- | Unify two types, updating the current substitution +-- | Unify two types, updating the current substitution. -- --- Pre-substitute leaf fast-path: trivially-equal leaves --- (TypeConstructor/TypeVar/TypeLevelString/TypeLevelInt/Skolem --- with equal payload) short-circuit before substituteType / --- withErrorMessageHint. Per unify-cache-anatomy on post-type-hash --- baseline 5713e832: 86% of cache hits were on 1-2-node pairs --- of this shape — the pattern is structural to recursive descent --- so the same survey shape applies pre-type-hash. --- --- The S.Set unification cache is dropped — relying on the leaf --- fast-path to catch the dominant recurrence pattern. Combined --- effect tested against 799e8208 (pre-type-hash, S.Set) baseline --- and 5713e832 (post-type-hash, HashSet) tip. +-- Equal-leaf cases short-circuit before substituteType, which +-- is a no-op on these constructors, and the error-hint bracket, +-- which can't fire on equal leaves. unifyTypes :: SourceType -> SourceType -> TypeCheckM () unifyTypes (TypeConstructor _ c1) (TypeConstructor _ c2) | c1 == c2 = pure () unifyTypes (TypeVar _ v1) (TypeVar _ v2) | v1 == v2 = pure ()