summaryrefslogtreecommitdiff
path: root/source/Felix/Test/Unit/Core.hs
diff options
context:
space:
mode:
Diffstat (limited to 'source/Felix/Test/Unit/Core.hs')
-rw-r--r--source/Felix/Test/Unit/Core.hs670
1 files changed, 670 insertions, 0 deletions
diff --git a/source/Felix/Test/Unit/Core.hs b/source/Felix/Test/Unit/Core.hs
new file mode 100644
index 0000000..0388c7f
--- /dev/null
+++ b/source/Felix/Test/Unit/Core.hs
@@ -0,0 +1,670 @@
+{-# LANGUAGE NoImplicitPrelude #-}
+
+module Felix.Test.Unit.Core (unitTests) where
+
+import Base hiding (Empty)
+import Felix.Checking.Core
+import Felix.Checking.Foundation qualified as Foundation
+import Felix.Checking.SetConstruction
+
+import Control.DeepSeq (NFData(..), force)
+import Hedgehog
+import Hedgehog.Gen qualified as Gen
+import Hedgehog.Range qualified as Range
+import Data.Set qualified as Set
+import Test.Tasty
+import Test.Tasty.HUnit hiding (assert)
+import Test.Tasty.Hedgehog (testPropertyNamed)
+
+
+data TestGlobal
+ = TestGlobal
+ | TestPairGlobal
+ deriving (Show, Eq, Ord)
+
+instance NFData TestGlobal where
+ rnf _global =
+ ()
+
+testGlobalType :: TestGlobal -> Maybe CoreType
+testGlobalType TestGlobal =
+ Just TySet
+testGlobalType TestPairGlobal =
+ Just (TySet `TyArrow` (TySet `TyArrow` TySet))
+
+unitTests :: TestTree
+unitTests =
+ testGroup "Checked core"
+ [ testCase
+ "freezes explicit binder positions"
+ freezesExplicitBinderPositions
+ , testCase
+ "freezes generalized substitution without capture"
+ freezesGeneralizedSubstitution
+ , testCase
+ "rejects ill-typed and open terms"
+ rejectsInvalidTerms
+ , testCase
+ "rechecks canonical terms without unchecked construction"
+ rechecksCanonicalTerms
+ , testCase
+ "checks scoped canonical weakening and substitution"
+ checksScopedCanonicalOperations
+ , testCase
+ "builds set-induction hypotheses from the complete property"
+ buildsSetInductionHypotheses
+ , testCase
+ "specializes the checked separation characteristic"
+ specializesCheckedSeparationCharacteristic
+ , testCase
+ "specializes the checked replacement characteristic"
+ specializesCheckedReplacementCharacteristic
+ , testCase
+ "derives named construction views from checked components"
+ derivesNamedConstructionViews
+ , testCase
+ "thaws checked closed terms without changing them"
+ thawsCheckedClosedTerms
+ , testPropertyNamed
+ "optimized freeze agrees with bounded reference"
+ "prop_freezeAgreesWithReference"
+ prop_freezeAgreesWithReference
+ ]
+
+freezesExplicitBinderPositions :: Assertion
+freezesExplicitBinderPositions = do
+ let x = 0 :: Int
+ y = 1 :: Int
+ freeze body = do
+ checked <-
+ either
+ (assertFailure . show)
+ pure
+ (checkClosedCore
+ testGlobalType
+ (coreLambda TySet x
+ (coreLambda TyProp y body)))
+ either
+ (assertFailure . show)
+ (pure . frozenCoreTerm)
+ (freezeClosed checked)
+ nearest <-
+ freeze (coreLocal y)
+ outer <-
+ freeze (coreLocal x)
+ global <-
+ freeze (coreGlobal TestGlobal)
+ assertEqual
+ "nearest binder"
+ (CLam TySet (CLam TyProp (CBound 0)))
+ nearest
+ assertEqual
+ "outer binder"
+ (CLam TySet (CLam TyProp (CBound 1)))
+ outer
+ assertEqual
+ "global with both vacuous binders"
+ (CLam TySet (CLam TyProp (CGlobal TestGlobal)))
+ global
+
+freezesGeneralizedSubstitution :: Assertion
+freezesGeneralizedSubstitution = do
+ let outer = 0 :: Int
+ inner = 1 :: Int
+ placeholder = 2 :: Int
+ innerTerm =
+ coreLambda TySet inner (coreLocal placeholder)
+ substituted =
+ innerTerm >>= \local ->
+ if local == placeholder
+ then coreLocal outer
+ else coreLocal local
+ term =
+ coreLambda TyProp outer substituted
+ checked <-
+ either
+ (assertFailure . show)
+ pure
+ (checkClosedCore testGlobalType term)
+ optimized <-
+ either
+ (assertFailure . show)
+ pure
+ (freezeClosed checked)
+ reference <-
+ either
+ (assertFailure . show)
+ pure
+ (referenceFreezeClosed checked)
+ assertEqual "reference result" reference optimized
+ assertEqual
+ "outer variable remains outside the inner binder"
+ (CLam TyProp (CLam TySet (CBound 1)))
+ (frozenCoreTerm optimized)
+
+rejectsInvalidTerms :: Assertion
+rejectsInvalidTerms = do
+ assertEqual
+ "free local"
+ (Left UnboundCoreLocal)
+ (checkedCoreType
+ <$> checkClosedCore
+ testGlobalType
+ (coreLocal (0 :: Int)))
+ assertEqual
+ "application argument"
+ (Left
+ (ApplicationArgumentTypeMismatch
+ TySet
+ TyProp))
+ (checkedCoreType
+ <$> checkClosedCore
+ testGlobalType
+ (coreApply
+ (coreIntrinsic FamilyUnion)
+ coreFalsum))
+ assertEqual
+ "equality operand"
+ (Left
+ (EqualityOperandTypeMismatch
+ TySet
+ TyProp))
+ (checkedCoreType
+ <$> checkClosedCore
+ testGlobalType
+ (coreEquality
+ TySet
+ coreFalsum
+ (coreGlobal TestGlobal)))
+
+rechecksCanonicalTerms :: Assertion
+rechecksCanonicalTerms = do
+ let term =
+ CForall TySet
+ (CEq TySet
+ (CBound 0)
+ (CBound 0))
+ assertEqual
+ "well-typed canonical proposition"
+ (Right (TyProp, term))
+ ( (\checked ->
+ ( frozenCoreType checked
+ , frozenCoreTerm checked
+ ))
+ <$> checkCanonicalCore testGlobalType term
+ )
+ assertEqual
+ "out-of-scope de Bruijn index"
+ (Left (UnboundCoreIndex 1))
+ (frozenCoreType
+ <$> checkCanonicalCore
+ testGlobalType
+ (CForall TySet (CBound 1)))
+ assertEqual
+ "canonical application still checks argument types"
+ (Left
+ (ApplicationArgumentTypeMismatch
+ TySet
+ TyProp))
+ (frozenCoreType
+ <$> checkCanonicalCore
+ testGlobalType
+ (CApp
+ (CIntrinsic FamilyUnion)
+ CFalsum))
+
+checksScopedCanonicalOperations :: Assertion
+checksScopedCanonicalOperations = do
+ scoped <-
+ either
+ (assertFailure . show)
+ pure
+ (checkScopedCanonicalCore
+ testGlobalType
+ [TySet]
+ (CBound 0))
+ weakened <-
+ either
+ (assertFailure . show)
+ pure
+ (weakenScopedCore
+ testGlobalType
+ TyProp
+ scoped)
+ assertEqual
+ "nearest binder insertion shifts the prior local"
+ ( [TyProp, TySet]
+ , TySet
+ , CBound 1
+ )
+ ( scopedCoreContext weakened
+ , scopedCoreType weakened
+ , scopedCoreTerm weakened
+ )
+ assertEqual
+ "top-level binder substitution removes its index"
+ (CEq TySet
+ (CIntrinsic Empty)
+ (CIntrinsic Empty))
+ (instantiateCanonical
+ (CIntrinsic Empty
+ :: CanonicalTerm TestGlobal)
+ (CEq TySet
+ (CBound 0)
+ (CBound 0)))
+ root <-
+ either
+ (assertFailure . show)
+ pure
+ (checkScopedCanonicalCore
+ testGlobalType
+ []
+ (CGlobal TestGlobal))
+ assertEqual
+ "empty scoped context closes"
+ (Just
+ (TySet, CGlobal TestGlobal))
+ ( (\closed ->
+ ( frozenCoreType closed
+ , frozenCoreTerm closed
+ ))
+ <$> closeScopedCore root
+ )
+
+buildsSetInductionHypotheses :: Assertion
+buildsSetInductionHypotheses = do
+ let propertyTerm =
+ CImp
+ (CEq TySet
+ (CBound 0)
+ (CIntrinsic Empty))
+ (CEq TySet
+ (CBound 0)
+ (CBound 0))
+ claimProperty <-
+ either
+ (assertFailure . show)
+ pure
+ (checkScopedCanonicalCore
+ testGlobalType
+ [TySet]
+ propertyTerm)
+ (predicate, hypothesis, step, result) <-
+ maybe
+ (assertFailure "set-induction instance was not constructed")
+ pure
+ (scopedSetInductionInstance 0 claimProperty)
+ let abstractedProperty =
+ CImp
+ (CEq TySet
+ (CBound 0)
+ (CIntrinsic Empty))
+ (CEq TySet
+ (CBound 0)
+ (CBound 0))
+ memberHypothesis =
+ CForall TySet
+ (CImp
+ (CApp
+ (CApp
+ (CIntrinsic Member)
+ (CBound 0))
+ (CBound 1))
+ abstractedProperty)
+ assertEqual
+ "set induction abstracts the selected property once"
+ (CLam TySet abstractedProperty)
+ (scopedCoreTerm predicate)
+ assertEqual
+ "antecedent and goal are both generalized over the member"
+ memberHypothesis
+ (scopedCoreTerm hypothesis)
+ assertEqual
+ "set-induction step owns its member-wise hypothesis"
+ (CForall TySet
+ (CImp memberHypothesis abstractedProperty))
+ (scopedCoreTerm step)
+ assertEqual
+ "set-induction result closes the complete property"
+ (CForall TySet abstractedProperty)
+ (scopedCoreTerm result)
+
+specializesCheckedSeparationCharacteristic :: Assertion
+specializesCheckedSeparationCharacteristic = do
+ foundation <-
+ either
+ (assertFailure . show)
+ pure
+ Foundation.checkedFoundation
+ let bound = CIntrinsic Empty
+ predicate =
+ CLam TySet
+ (CEq TySet (CBound 0) (CBound 0))
+ separation =
+ CApp
+ (CApp (CIntrinsic Sep) bound)
+ predicate
+ body <-
+ either
+ (assertFailure . show)
+ pure
+ (checkScopedCanonicalCore
+ testGlobalType
+ []
+ separation)
+ definition <-
+ maybe
+ (assertFailure "separation did not form a set definition")
+ pure
+ (scopedSetDefinition
+ (Foundation.foundationAxiomFrozen
+ foundation
+ Foundation.SeparationCharacteristic)
+ body)
+ let generated =
+ instantiateCanonical
+ separation
+ (scopedCoreTerm definition)
+ expected =
+ betaNormalize
+ (specializeForall predicate
+ (specializeForall bound
+ (mapCanonicalGlobals absurd
+ (frozenCoreTerm
+ (Foundation.foundationAxiomFrozen
+ foundation
+ Foundation.SeparationCharacteristic)))))
+ assertEqual
+ "local characteristic is the checked rule specialization"
+ expected
+ generated
+
+specializesCheckedReplacementCharacteristic :: Assertion
+specializesCheckedReplacementCharacteristic = do
+ foundation <-
+ either
+ (assertFailure . show)
+ pure
+ Foundation.checkedFoundation
+ pair <- checked [] (CGlobal TestPairGlobal)
+ domain <- checked [] (CIntrinsic Empty)
+ value <- checked [TySet] (CBound 0)
+ (graph, checkedDomain, function) <-
+ maybe
+ (assertFailure "checked values did not form a replacement graph")
+ pure
+ (scopedReplacementGraph pair domain value)
+ definition <-
+ maybe
+ (assertFailure "replacement graph did not form a definition")
+ pure
+ (scopedCharacteristicDefinition
+ (Foundation.foundationAxiomFrozen
+ foundation
+ Foundation.ReplacementCharacteristic)
+ graph
+ (checkedDomain :| [function]))
+ let generated =
+ instantiateCanonical
+ (scopedCoreTerm graph)
+ (scopedCoreTerm definition)
+ expected =
+ betaNormalize
+ (specializeForall (scopedCoreTerm function)
+ (specializeForall (scopedCoreTerm checkedDomain)
+ (mapCanonicalGlobals absurd
+ (frozenCoreTerm
+ (Foundation.foundationAxiomFrozen
+ foundation
+ Foundation.ReplacementCharacteristic)))))
+ assertEqual
+ "local graph characteristic is the checked rule specialization"
+ expected
+ generated
+ where
+ checked context term =
+ either
+ (assertFailure . show)
+ pure
+ (checkScopedCanonicalCore testGlobalType context term)
+
+derivesNamedConstructionViews :: Assertion
+derivesNamedConstructionViews = do
+ foundation <-
+ either (assertFailure . show) pure Foundation.checkedFoundation
+ bound <- checked [] (CIntrinsic Empty)
+ predicate <- checked [TySet] (CEq TySet (CBound 0) (CBound 0))
+ separation <-
+ maybe
+ (assertFailure "checked separation descriptor failed")
+ pure
+ (checkedSeparationConstruction testGlobalType bound predicate)
+ (separationView, separationEquation) <-
+ maybe
+ (assertFailure "checked separation views failed")
+ pure
+ (namedSetConstructionLocalViews
+ (checkedFoundationSetConstruction foundation)
+ separation)
+ expectedSeparationView <-
+ maybe
+ (assertFailure "checked separation characteristic failed")
+ pure
+ (scopedSetDefinition
+ (Foundation.foundationAxiomFrozen foundation
+ Foundation.SeparationCharacteristic)
+ (namedSetConstructionTerm separation))
+ assertEqual "separation view is the checked specialization"
+ expectedSeparationView separationView
+ assertEqual "separation view is first-order"
+ (Set.singleton Foundation.EmptyCharacteristic)
+ (Foundation.foundationAxiomDependencies
+ (scopedCoreTerm separationView))
+ assertEqual "separation equation retains exact construction"
+ (Set.fromList
+ [ Foundation.EmptyCharacteristic
+ , Foundation.SeparationCharacteristic
+ ])
+ (Foundation.foundationAxiomDependencies
+ (scopedCoreTerm separationEquation))
+
+ firstDomain <- checked [] (CIntrinsic Empty)
+ singleValue <- checked [TySet] (CBound 0)
+ singleCondition <- checked [TySet]
+ (CEq TySet (CBound 0) (CBound 0))
+ singleReplacement <-
+ maybe
+ (assertFailure "checked one-domain replacement failed")
+ pure
+ (checkedFunctionalReplacementConstruction
+ testGlobalType
+ (firstDomain :| [])
+ singleValue
+ (Just singleCondition))
+ assertEqual "one-domain replacement has one canonical term"
+ (CApp
+ (CApp
+ (CIntrinsic Repl)
+ (CApp
+ (CApp (CIntrinsic Sep) (CIntrinsic Empty))
+ (CLam TySet
+ (CEq TySet (CBound 0) (CBound 0)))))
+ (CLam TySet (CBound 0)))
+ (scopedCoreTerm
+ (namedSetConstructionTerm singleReplacement))
+
+ secondDomain <- checked [TySet] (CBound 0)
+ value <- checked [TySet, TySet] (CBound 0)
+ condition <- checked [TySet, TySet]
+ (CEq TySet (CBound 0) (CBound 0))
+ replacement <-
+ maybe
+ (assertFailure "checked replacement descriptor failed")
+ pure
+ (checkedFunctionalReplacementConstruction
+ testGlobalType
+ (firstDomain :| [secondDomain])
+ value
+ (Just condition))
+ (replacementView, replacementEquation) <-
+ maybe
+ (assertFailure "checked replacement views failed")
+ pure
+ (namedSetConstructionLocalViews
+ (checkedFoundationSetConstruction foundation)
+ replacement)
+ let terminal =
+ andP
+ (CEq TySet (CBound 0) (CBound 0))
+ (CEq TySet (CBound 2) (CBound 0))
+ secondWitness =
+ existsP
+ (andP
+ (memberP (CBound 0) (CBound 1))
+ terminal)
+ firstWitness =
+ existsP
+ (andP
+ (memberP (CBound 0) (CIntrinsic Empty))
+ secondWitness)
+ expectedReplacementTerm =
+ CForall TySet
+ (CEq TyProp
+ (memberP (CBound 0) (CBound 1))
+ firstWitness)
+ expectedReplacementView <- checked [TySet] expectedReplacementTerm
+ assertEqual "replacement view preserves bounds and condition"
+ expectedReplacementView replacementView
+ assertEqual "flattened replacement view is first-order"
+ (Set.singleton Foundation.EmptyCharacteristic)
+ (Foundation.foundationAxiomDependencies
+ (scopedCoreTerm replacementView))
+ assertEqual "replacement equation retains every helper"
+ (Set.fromList
+ [ Foundation.FamilyUnionCharacteristic
+ , Foundation.EmptyCharacteristic
+ , Foundation.SeparationCharacteristic
+ , Foundation.ReplacementCharacteristic
+ ])
+ (Foundation.foundationAxiomDependencies
+ (scopedCoreTerm replacementEquation))
+ where
+ checked context term =
+ either
+ (assertFailure . show)
+ pure
+ (checkScopedCanonicalCore testGlobalType context term)
+
+ memberP element set =
+ CApp (CApp (CIntrinsic Member) element) set
+
+ notP proposition = CImp proposition CFalsum
+
+ andP left right =
+ notP (CImp left (notP right))
+
+ existsP proposition =
+ notP (CForall TySet (notP proposition))
+
+specializeForall
+ :: CanonicalTerm global
+ -> CanonicalTerm global
+ -> CanonicalTerm global
+specializeForall argument = \case
+ CForall _binderType body ->
+ instantiateCanonical argument body
+ _ ->
+ error "the checked characteristic lost a binder"
+
+betaNormalize :: CanonicalTerm global -> CanonicalTerm global
+betaNormalize = \case
+ CApp function argument ->
+ case betaNormalize function of
+ CLam _binderType body ->
+ betaNormalize
+ (instantiateCanonical
+ (betaNormalize argument)
+ body)
+ normalizedFunction ->
+ CApp normalizedFunction (betaNormalize argument)
+ CLam binderType body ->
+ CLam binderType (betaNormalize body)
+ CImp premise conclusion ->
+ CImp
+ (betaNormalize premise)
+ (betaNormalize conclusion)
+ CEq operandType left right ->
+ CEq operandType
+ (betaNormalize left)
+ (betaNormalize right)
+ CForall binderType body ->
+ CForall binderType (betaNormalize body)
+ term -> term
+
+thawsCheckedClosedTerms :: Assertion
+thawsCheckedClosedTerms = do
+ let source =
+ coreLambda TySet (0 :: Int)
+ (coreForall TySet 1
+ (coreEquality
+ TySet
+ (coreLocal 0)
+ (coreLocal 1)))
+ freeze syntax = do
+ checked <-
+ either
+ (assertFailure . show)
+ pure
+ (checkClosedCore testGlobalType syntax)
+ either
+ (assertFailure . show)
+ pure
+ (freezeClosed checked)
+ original <-
+ freeze source
+ roundTrip <-
+ freeze (thawFrozenCore original)
+ assertEqual "frozen term" original roundTrip
+ assertEqual
+ "global inventory"
+ mempty
+ (frozenCoreGlobals original)
+
+prop_freezeAgreesWithReference :: Property
+prop_freezeAgreesWithReference = property do
+ depth <-
+ forAll (Gen.int (Range.linear 1 10))
+ binderTypes <-
+ forAll
+ (Gen.list
+ (Range.singleton depth)
+ (Gen.element
+ [ TyProp
+ , TySet
+ , TySet `TyArrow` TySet
+ ]))
+ selected <-
+ forAll
+ (Gen.maybe
+ (Gen.int (Range.linear 0 (depth - 1))))
+ let binders =
+ zip [0 :: Int ..] binderTypes
+ body =
+ maybe
+ (coreGlobal TestGlobal)
+ coreLocal
+ selected
+ term =
+ foldr
+ (\(local, binderType) ->
+ coreLambda binderType local)
+ body
+ binders
+ checked <-
+ evalEither
+ (checkClosedCore testGlobalType term)
+ optimized <-
+ evalEither (force <$> freezeClosed checked)
+ reference <-
+ evalEither (force <$> referenceFreezeClosed checked)
+ optimized === reference