hydra-0.15.0: src/main/haskell/Hydra/Sources/Test/Generation.hs
{-# LANGUAGE FlexibleContexts #-}
-- | Test cases for code generation operations, in particular the contract
-- between `inferModules` and `inferModulesGiven`: incremental inference of a
-- subset of the universe must produce the same inferred type schemes for the
-- target modules' term bindings as a full inference run over the same universe.
module Hydra.Sources.Test.Generation where
import Hydra.Kernel hiding (inferModules)
import Hydra.Dsl.Meta.Testing as Testing
import qualified Hydra.Dsl.Meta.Terms as Terms
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.Eithers as Eithers
import qualified Hydra.Dsl.Meta.Lib.Lists as Lists
import qualified Hydra.Dsl.Meta.Lib.Maybes as Maybes
import qualified Hydra.Dsl.Meta.Lib.Strings as Strings
import qualified Hydra.Dsl.Packaging as Packaging
import qualified Hydra.Sources.Test.TestGraph as TestGraph
import qualified Data.List as L
import qualified Data.Map as M
import qualified Hydra.Sources.Kernel.Terms.Generation as Generation
import qualified Hydra.Sources.Kernel.Terms.Show.Core as ShowCore
ns :: Namespace
ns = Namespace "hydra.test.generation"
module_ :: Module
module_ = Module {
moduleNamespace = ns,
moduleDefinitions = definitions,
moduleTermDependencies = [Generation.ns, ShowCore.ns, TestGraph.ns],
moduleTypeDependencies = kernelTypesNamespaces,
moduleDescription = (Just "Test cases for code generation operations such as inferModules and inferModulesGiven")}
where
definitions = [Phantoms.toDefinition allTests]
define :: String -> TTerm a -> TTermDefinition a
define = definitionInModule module_
-- Local alias for polymorphic application.
(#) :: (AsTerm f (a -> b), AsTerm g a) => f -> g -> TTerm b
(#) = (Phantoms.@@)
infixl 1 #
----------------------------------------
-- Toy modules used as test input.
--
-- Two modules in a fictional `hydra.testInput.*` namespace, deliberately
-- minimal:
--
-- hydra.testInput.a: idA = \x. x
-- hydra.testInput.b: useId = idA 42 (depends on hydra.testInput.a)
--
-- The cross-module reference from `useId` to `idA` is the property we care
-- about: it forces inference for `b` to consult information about `a`, which
-- is exactly the path `inferModulesGiven` exercises differently from
-- `inferModules` (the former only infers `targetBindings`, the latter infers
-- the full universe).
nsA :: TTerm Namespace
nsA = Packaging.namespace (Phantoms.string "hydra.testInput.a")
nsB :: TTerm Namespace
nsB = Packaging.namespace (Phantoms.string "hydra.testInput.b")
nameIdA :: TTerm Name
nameIdA = Core.name (Phantoms.string "hydra.testInput.a.idA")
nameUseId :: TTerm Name
nameUseId = Core.name (Phantoms.string "hydra.testInput.b.useId")
-- An untyped term definition (Maybe TypeScheme = nothing) so inference will
-- assign a fresh scheme.
untypedTermDef :: TTerm Name -> TTerm Term -> TTerm Definition
untypedTermDef nm tm = Packaging.definitionTerm
(Packaging.termDefinition nm tm Phantoms.nothing)
-- A pre-annotated term definition, simulating a universe binding whose type
-- scheme was populated by a prior inference run (the future caching layer).
-- `inferModulesGiven` uses these schemes via `modulesToGraph`'s `boundTypes`
-- seeding to resolve cross-module references without re-inferring the
-- universe.
typedTermDef :: TTerm Name -> TTerm Term -> TTerm TypeScheme -> TTerm Definition
typedTermDef nm tm ts = Packaging.definitionTerm
(Packaging.termDefinition nm tm (Phantoms.just ts))
-- The scheme carried on `modA.idA`: forall a. a -> a.
idAScheme :: TTerm TypeScheme
idAScheme = T.poly ["a"] (T.function (T.var "a") (T.var "a"))
modA :: TTerm Module
modA = Packaging.module_
Phantoms.nothing
nsA
(Phantoms.list ([] :: [TTerm Namespace]))
(Phantoms.list ([] :: [TTerm Namespace]))
(Phantoms.list [
typedTermDef nameIdA (Terms.lambda "x" (Terms.var "x")) idAScheme])
modB :: TTerm Module
modB = Packaging.module_
Phantoms.nothing
nsB
(Phantoms.list [nsA])
(Phantoms.list ([] :: [TTerm Namespace]))
(Phantoms.list [
untypedTermDef nameUseId (Terms.apply (Terms.var "hydra.testInput.a.idA") (Terms.int32 42))])
universeMods :: TTerm [Module]
universeMods = Phantoms.list [modA, modB]
----------------------------------------
-- Second toy universe: a "clean" module carrying a scheme with vacuous
-- quantifiers (type variables that appear only in the domain, never in the
-- codomain), plus a "stale" target that applies the function. This shape is
-- actually produced by a prior inference run on the real kernel, e.g. the
-- stored scheme for `hydra.eval.lib.eithers.either` after round-tripping
-- through JSON. `inferModules` and `inferModulesGiven` must agree on the
-- rendered inferred modules for any such universe.
--
-- hydra.testInput.v.funky :: forall t0 t1. t0 -> t1 -> int32 -> int32
-- (the body is irrelevant; only the scheme matters for seeding.)
-- hydra.testInput.w.useFunky = funky "foo" 7 100
nsV :: TTerm Namespace
nsV = Packaging.namespace (Phantoms.string "hydra.testInput.v")
nsW :: TTerm Namespace
nsW = Packaging.namespace (Phantoms.string "hydra.testInput.w")
nameFunky :: TTerm Name
nameFunky = Core.name (Phantoms.string "hydra.testInput.v.funky")
nameUseFunky :: TTerm Name
nameUseFunky = Core.name (Phantoms.string "hydra.testInput.w.useFunky")
-- forall t0 t1 t2. t0 -> t1 -> t2 -> t2
-- This is the canonical scheme that inferModules produces when funky is
-- inferred alone (without useFunky constraining it). The body `\x.\y.\z. z`
-- gives 3 free type variables; a caching layer would store exactly this.
funkyScheme :: TTerm TypeScheme
funkyScheme = T.poly ["t0", "t1", "t2"]
(T.function (T.var "t0")
(T.function (T.var "t1")
(T.function (T.var "t2") (T.var "t2"))))
-- Body: `\x. \y. \z. z`. Three args, returns the third.
funkyTerm :: TTerm Term
funkyTerm = Terms.lambda "x" (Terms.lambda "y" (Terms.lambda "z" (Terms.var "z")))
modV :: TTerm Module
modV = Packaging.module_
Phantoms.nothing
nsV
(Phantoms.list ([] :: [TTerm Namespace]))
(Phantoms.list ([] :: [TTerm Namespace]))
(Phantoms.list [typedTermDef nameFunky funkyTerm funkyScheme])
-- useFunky = funky "foo" 7 100
modW :: TTerm Module
modW = Packaging.module_
Phantoms.nothing
nsW
(Phantoms.list [nsV])
(Phantoms.list ([] :: [TTerm Namespace]))
(Phantoms.list [
untypedTermDef nameUseFunky
(Terms.apply
(Terms.apply
(Terms.apply (Terms.var "hydra.testInput.v.funky")
(Terms.string "foo"))
(Terms.int32 7))
(Terms.int32 100))])
vacuousUniverse :: TTerm [Module]
vacuousUniverse = Phantoms.list [modV, modW]
----------------------------------------
-- Show helpers.
--
-- Render a `[Module]` deterministically as the concatenation, in module order,
-- of `"<binding-name> :: <type-scheme>\n"` for every term definition. Type
-- definitions are skipped (they aren't touched by inference).
-- | Render a definition as "<name> :: <scheme> = <term>\n". The term component
-- is included so that two runs producing the same final type but different
-- inferred term bodies (e.g. different TypeApplication wrappers) fail the
-- equality check.
showDef :: TTerm Definition -> TTerm String
showDef d = Phantoms.cases _Definition d Nothing [
_Definition_type>>: "td" ~> Phantoms.string "",
_Definition_term>>: "td" ~>
Strings.concat [
Core.unName (Packaging.termDefinitionName (var "td")),
Phantoms.string " :: ",
Maybes.maybe
(Phantoms.string "<no scheme>")
("ts" ~> ShowCore.typeScheme # var "ts")
(Packaging.termDefinitionTypeScheme (var "td")),
Phantoms.string " = ",
ShowCore.term # (Packaging.termDefinitionTerm (var "td")),
Phantoms.string "\n"]]
showModule :: TTerm Module -> TTerm String
showModule m = Strings.cat (Lists.map ("d" ~> showDef (var "d")) (Packaging.moduleDefinitions m))
showModules :: TTerm [Module] -> TTerm String
showModules ms = Strings.cat (Lists.map ("m" ~> showModule (var "m")) ms)
showResult :: TTerm (Either Error [Module]) -> TTerm String
showResult r = Eithers.either_
("e" ~> Phantoms.string "<<inference error>>")
("ms" ~> showModules (var "ms"))
r
----------------------------------------
-- Test cases.
-- | Property: incremental inference of a strict subset of the universe
-- produces the same inferred type schemes for the target modules' term
-- bindings as a full inference run over the same universe.
incrementalSubsetCase :: TTerm TestCaseWithMetadata
incrementalSubsetCase = universalCase "incremental inference of subset matches full inference"
actual
expected
where
target = Phantoms.list [modB]
actual = showResult (Generation.inferModulesGiven
# TestGraph.testContext # TestGraph.testGraph # universeMods # target)
expected = showResult (Generation.inferModules
# TestGraph.testContext # TestGraph.testGraph # universeMods # target)
-- | Property: when target = universe, `inferModulesGiven` is equivalent to
-- `inferModules`.
incrementalFullCase :: TTerm TestCaseWithMetadata
incrementalFullCase = universalCase "incremental inference of full universe matches full inference"
actual
expected
where
actual = showResult (Generation.inferModulesGiven
# TestGraph.testContext # TestGraph.testGraph # universeMods # universeMods)
expected = showResult (Generation.inferModules
# TestGraph.testContext # TestGraph.testGraph # universeMods # universeMods)
-- | Property: when a clean universe module carries a pre-inferred scheme,
-- `inferModulesGiven` uses that scheme verbatim rather than re-inferring it.
-- References from stale targets to the clean module are instantiated at the
-- scheme's full quantifier count (routed through `inferTypeOfVariable`'s
-- `graphBoundTypes` branch), even if the scheme has vacuous quantifiers that
-- a fresh inference pass would collapse.
--
-- This is the behaviour that lets `inferModulesGiven` skip inference for
-- clean modules without losing soundness: the cached scheme is the source of
-- truth, and Phase 6 of `inferTypeOfLetNormalized` only rewrites references
-- to currently let-bound names, so there is no quantifier-count mismatch.
--
-- The divergence from `inferModules` (which re-infers the clean binding and
-- may collapse quantifiers) is expected and correct. Byte-identical output
-- against the real kernel JSON at regeneration time is what certifies the
-- partition; this case just pins the per-reference AST shape for the toy
-- vacuous-quantifier universe.
vacuousQuantifierCase :: TTerm TestCaseWithMetadata
vacuousQuantifierCase = universalCase
"incremental inference uses cached scheme verbatim on vacuous-quantifier universe"
actual
(Phantoms.string $
"hydra.testInput.w.useFunky :: (int32) = (" ++
"hydra.testInput.v.funky⟨string⟩⟨int32⟩⟨int32⟩" ++
" @ \"foo\" @ 7:int32 @ 100:int32)\n")
where
target = Phantoms.list [modW]
actual = showResult (Generation.inferModulesGiven
# TestGraph.testContext # TestGraph.testGraph # vacuousUniverse # target)
allTests :: TTermDefinition TestGroup
allTests = define "allTests" $
Phantoms.doc "Test cases for code generation operations" $
supergroup "generation" [
subgroup "inferModulesGiven" [
incrementalSubsetCase,
incrementalFullCase,
vacuousQuantifierCase]]