hydra-0.15.0: src/main/haskell/Hydra/Sources/Test/Substitution.hs
-- | Test cases for type and term substitution operations
module Hydra.Sources.Test.Substitution where
-- Standard imports for shallow DSL tests
import Hydra.Kernel
import Hydra.Dsl.Meta.Testing as Testing
import Hydra.Sources.Kernel.Types.All
import qualified Hydra.Dsl.Meta.Core as Core
import Hydra.Dsl.Meta.Phantoms as Phantoms hiding ((++))
import qualified Hydra.Dsl.Meta.Types as T
import qualified Hydra.Dsl.Meta.Lib.Maps as Maps
import qualified Hydra.Sources.Test.TestGraph as TestGraph
import qualified Hydra.Sources.Test.TestTerms as TestTerms
import qualified Hydra.Sources.Test.TestTypes as TestTypes
import qualified Data.List as L
import qualified Data.Map as M
import Hydra.Testing
import qualified Hydra.Sources.Kernel.Terms.Show.Core as ShowCore
import qualified Hydra.Sources.Kernel.Terms.Substitution as SubstitutionModule
ns :: Namespace
ns = Namespace "hydra.test.substitution"
module_ :: Module
module_ = Module {
moduleNamespace = ns,
moduleDefinitions = definitions,
moduleTermDependencies = [ShowCore.ns, SubstitutionModule.ns],
moduleTypeDependencies = kernelTypesNamespaces,
moduleDescription = (Just "Test cases for type and term substitution operations")}
where
definitions = [Phantoms.toDefinition allTests]
define :: String -> TTerm a -> TTermDefinition a
define = definitionInModule module_
-- Helper to build names
nm :: String -> TTerm Name
nm s = Core.name $ string s
-- Helper for building substitution pairs
subst :: [(String, TTerm Type)] -> TTerm [(Name, Type)]
subst pairs = list [pair (nm n) t | (n, t) <- pairs]
-- | Apply substInType and show the result as a string
showSubstInType :: [(String, TTerm Type)] -> TTerm Type -> TTerm String
showSubstInType pairs inputType =
ShowCore.type_ @@ (SubstitutionModule.substInType @@
(wrap _TypeSubst (Maps.fromList (subst pairs))) @@ inputType)
-- | Universal substInType test case
substInTypeCase :: String -> [(String, TTerm Type)] -> TTerm Type -> TTerm Type -> TTerm TestCaseWithMetadata
substInTypeCase cname pairs input output =
universalCase cname (showSubstInType pairs input) (ShowCore.type_ @@ output)
-- | Build a TypeScheme DSL value
scheme :: [String] -> TTerm Type -> TTerm TypeScheme
scheme vars body = Core.typeScheme (list [nm v | v <- vars]) body nothing
-- | Apply substInTypeScheme and render just the scheme's body type, which is
-- where the capture-avoidance behavior shows up. The quantifier list is
-- preserved unchanged so we test it separately via showSubstInTypeSchemeVars.
showSubstInTypeSchemeBody :: [(String, TTerm Type)] -> TTerm TypeScheme -> TTerm String
showSubstInTypeSchemeBody pairs inputScheme =
ShowCore.type_ @@ (Core.typeSchemeBody $ SubstitutionModule.substInTypeScheme @@
(wrap _TypeSubst (Maps.fromList (subst pairs))) @@ inputScheme)
substInTypeSchemeBodyCase :: String -> [(String, TTerm Type)] -> TTerm TypeScheme -> TTerm Type -> TTerm TestCaseWithMetadata
substInTypeSchemeBodyCase cname pairs input expectedBody =
universalCase cname (showSubstInTypeSchemeBody pairs input)
(ShowCore.type_ @@ expectedBody)
-- ============================================================
-- substInType tests
-- ============================================================
substInTypeTests :: TTerm TestGroup
substInTypeTests = subgroup "substInType" [
-- Empty substitution returns type unchanged
substInTypeCase "empty substitution returns type unchanged"
[]
T.string
T.string,
-- Substitute a type variable with a concrete type
substInTypeCase "substitute type variable with int32"
[("a", T.int32)]
(T.var "a")
T.int32,
-- Non-matching variable is unchanged
substInTypeCase "non-matching variable unchanged"
[("a", T.int32)]
(T.var "b")
(T.var "b"),
-- Substitute in function type
substInTypeCase "substitute in function domain"
[("a", T.int32)]
(T.function (T.var "a") T.string)
(T.function T.int32 T.string),
substInTypeCase "substitute in function codomain"
[("a", T.string)]
(T.function T.int32 (T.var "a"))
(T.function T.int32 T.string),
-- Substitute in list type
substInTypeCase "substitute in list element type"
[("a", T.int32)]
(T.list (T.var "a"))
(T.list T.int32),
-- Substitute in optional type
substInTypeCase "substitute in optional type"
[("a", T.string)]
(T.optional (T.var "a"))
(T.optional T.string),
-- Substitute in pair type
substInTypeCase "substitute in pair type both sides"
[("a", T.int32)]
(T.pair (T.var "a") (T.var "a"))
(T.pair T.int32 T.int32),
-- Substitute in either type
substInTypeCase "substitute in either type"
[("a", T.string)]
(T.either_ (T.var "a") T.int32)
(T.either_ T.string T.int32),
-- Substitute in map type
substInTypeCase "substitute in map key type"
[("k", T.string)]
(T.map (T.var "k") T.int32)
(T.map T.string T.int32),
-- Substitute in set type
substInTypeCase "substitute in set type"
[("a", T.int32)]
(T.set (T.var "a"))
(T.set T.int32),
-- Nested substitution
substInTypeCase "nested substitution in list of pairs"
[("a", T.int32)]
(T.list (T.pair (T.var "a") T.string))
(T.list (T.pair T.int32 T.string)),
-- Multiple substitutions
substInTypeCase "multiple substitutions"
[("a", T.int32), ("b", T.string)]
(T.pair (T.var "a") (T.var "b"))
(T.pair T.int32 T.string),
-- Forall type: bound variable should not be substituted in body
substInTypeCase "forAll bound variable not substituted"
[("a", T.int32)]
(T.forAll "a" (T.function (T.var "a") (T.var "a")))
(T.forAll "a" (T.function (T.var "a") (T.var "a"))),
-- Forall type: free variable in body should be substituted
substInTypeCase "forAll free variable substituted"
[("b", T.string)]
(T.forAll "a" (T.function (T.var "a") (T.var "b")))
(T.forAll "a" (T.function (T.var "a") T.string))]
-- ============================================================
-- substInTypeScheme tests: quantifier-shadowing
-- ============================================================
substInTypeSchemeTests :: TTerm TestGroup
substInTypeSchemeTests = subgroup "substInTypeScheme" [
-- Bound variable in scheme's quantifier list must shadow the substitution.
-- Without proper shadowing, {t0 -> Foo} applied to `forall [t0]. t0 -> t0`
-- would incorrectly rewrite the body to `Foo -> Foo`, leaving the scheme
-- with quantifier [t0] and body Foo -> Foo — a scheme with no free t0
-- and an escaped Foo. Regression test for the incremental-inference
-- unification bug (2026-04-24).
substInTypeSchemeBodyCase "quantified variable shadows substitution"
[("a", T.int32)]
(scheme ["a"] (T.function (T.var "a") (T.var "a")))
(T.function (T.var "a") (T.var "a")),
-- Free variable in scheme body (not in quantifier list) is substituted.
substInTypeSchemeBodyCase "free variable in body is substituted"
[("b", T.string)]
(scheme ["a"] (T.function (T.var "a") (T.var "b")))
(T.function (T.var "a") T.string),
-- Mixed: substitute frees, not quantified
substInTypeSchemeBodyCase "mixed: free substituted, quantified shadowed"
[("a", T.int32), ("b", T.string)]
(scheme ["a"] (T.function (T.var "a") (T.var "b")))
(T.function (T.var "a") T.string),
-- Multiple quantifiers: all are shadowed
substInTypeSchemeBodyCase "multiple quantifiers shadow substitution"
[("a", T.int32), ("b", T.string)]
(scheme ["a", "b"] (T.pair (T.var "a") (T.var "b")))
(T.pair (T.var "a") (T.var "b")),
-- Empty quantifier list: substitution applies normally
substInTypeSchemeBodyCase "empty quantifiers: normal substitution"
[("a", T.int32)]
(scheme [] (T.var "a"))
T.int32]
-- ============================================================
-- All tests
-- ============================================================
allTests :: TTermDefinition TestGroup
allTests = define "allTests" $
doc "Test cases for type and term substitution operations" $
supergroup "substitution" [
substInTypeTests,
substInTypeSchemeTests]