Compare commits

..

No commits in common. "51d97b15f55939f780385ac815af25c1a8bedaaa" and "3715773adc63267ae6e0bd7b152f603cb5bab345" have entirely different histories.

9 changed files with 73 additions and 171 deletions

View file

@ -80,11 +80,8 @@ The error messages currently aren't terrible, but it would be nice to have, e.g.
*** TODO Improve β-equivalence check *** TODO Improve β-equivalence check
The check for β-equivalence could certainly be a lot smarter. Right now it just fully normalizes both terms and checks if the normal forms are equal, which isn't the best strategy. This is much less of an issue without [[Inductive Definitions][inductive definitions]]/recursion, as far less computation gets done in general, let alone at the type level. That being said, it's certainly not a bad idea. The check for β-equivalence could certainly be a lot smarter. Right now it just fully normalizes both terms and checks if the normal forms are equal, which isn't the best strategy. This is much less of an issue without [[Inductive Definitions][inductive definitions]]/recursion, as far less computation gets done in general, let alone at the type level. That being said, it's certainly not a bad idea.
*** DONE Better testing *** TODO Better testing
Using some kind of testing framework, like [[https://hackage.haskell.org/package/QuickCheck][QuickCheck]] and/or [[https://hackage.haskell.org/package/HUnit][HUnit]] seems like a good idea. I would like to avoid regressions as I keep working on this, and a suite of unit tests would make me feel much more comfortable. Using some kind of testing framework, like [[https://hackage.haskell.org/package/QuickCheck][QuickCheck]] and/or [[https://hackage.haskell.org/package/HUnit][HUnit]] seems like a good idea. I would like to avoid regressions as I keep working on this, and a suite of unit tests would make me feel much more comfortable.
*** DONE Switch to =Text=
Currently I use =String= everywhere. Switching to =Text= should be straightforward, and would improve performance and Unicode support.
*** TODO TUI *** TODO TUI
This is definitely a stretch goal, but I'm imagining a TUI split into two panels. On the left you can see the term you are building with holes in it. On the right you have the focused hole's type as well as the types of everything in scope (like Coq and Lean show while you're in the middle of a proof). Then you can interact with the system by entering commands (e.g. =intros=, =apply=, etc.) which changes the proof term on the left. You'd also just be able to type in the left window as well, and edit the proof term directly. This way you'd get the benefits of working with tactics, making it way faster to construct proof terms, and the benefits of working with proof terms directly, namely transparency and simplicity. I'll probably want to look into [[https://hackage.haskell.org/package/brick][brick]] in order to make this happen. This is definitely a stretch goal, but I'm imagining a TUI split into two panels. On the left you can see the term you are building with holes in it. On the right you have the focused hole's type as well as the types of everything in scope (like Coq and Lean show while you're in the middle of a proof). Then you can interact with the system by entering commands (e.g. =intros=, =apply=, etc.) which changes the proof term on the left. You'd also just be able to type in the left window as well, and edit the proof term directly. This way you'd get the benefits of working with tactics, making it way faster to construct proof terms, and the benefits of working with proof terms directly, namely transparency and simplicity. I'll probably want to look into [[https://hackage.haskell.org/package/brick][brick]] in order to make this happen.

View file

@ -3,17 +3,16 @@ module Main where
import Check import Check
import Expr import Expr
import Parser import Parser
import qualified Data.Text.IO as T
import System.IO import System.IO
main :: IO () main :: IO ()
main = do main = do
_ <- T.putStr "> " _ <- putStr "> "
_ <- hFlush stdout _ <- hFlush stdout
input <- T.getLine input <- getLine
case pAll input of case pAll input of
Left err -> T.putStrLn err Left err -> putStrLn err
Right expr -> case findType [] expr of Right expr -> case findType [] expr of
Right ty -> T.putStrLn $ pretty expr <> " : " <> pretty ty Right ty -> putStrLn $ pretty expr ++ " : " ++ pretty ty
Left err -> print err Left err -> print err
main main

View file

@ -39,8 +39,6 @@ library dependent-lambda-lib
, parser-combinators , parser-combinators
, mtl , mtl
default-language: Haskell2010 default-language: Haskell2010
default-extensions: OverloadedStrings
, GADTs
common warnings common warnings
ghc-options: -Wall ghc-options: -Wall
@ -51,11 +49,8 @@ executable dependent-lambda
build-depends: base ^>=4.19.1.0 build-depends: base ^>=4.19.1.0
, dependent-lambda-lib , dependent-lambda-lib
, text
hs-source-dirs: app hs-source-dirs: app
default-language: Haskell2010 default-language: Haskell2010
default-extensions: OverloadedStrings
, GADTs
test-suite tests test-suite tests
type: exitcode-stdio-1.0 type: exitcode-stdio-1.0
@ -65,9 +60,6 @@ test-suite tests
, CheckTests , CheckTests
build-depends: base ^>=4.19.1.0 build-depends: base ^>=4.19.1.0
, HUnit , HUnit
, text
, dependent-lambda-lib , dependent-lambda-lib
hs-source-dirs: tests hs-source-dirs: tests
default-language: Haskell2010 default-language: Haskell2010
default-extensions: OverloadedStrings
, GADTs

View file

@ -1,23 +1,21 @@
module Check (TypeCheckError (..), CheckResult (..), findType) where module Check (TypeCheckError (..), CheckResult (..), findType) where
import Control.Monad.Except (MonadError (throwError)) import Control.Monad.Except ( MonadError(throwError) )
import Data.List (intercalate, (!?)) import Data.List (intercalate, (!?))
import Data.Text (Text)
import qualified Data.Text as T
import Control.Monad (unless) import Control.Monad (unless)
import Expr import Expr
type Context = [Expr] type Context = [Expr]
data TypeCheckError = SquareUntyped | UnboundVariable Text | NotASort Expr Expr | ExpectedPiType Expr Expr | NotEquivalent Expr Expr Expr deriving (Eq) data TypeCheckError = SquareUntyped | UnboundVariable String | NotASort Expr Expr | ExpectedPiType Expr Expr | NotEquivalent Expr Expr Expr
instance Show TypeCheckError where instance Show TypeCheckError where
show SquareUntyped = "□ does not have a type" show SquareUntyped = "□ does not have a type"
show (UnboundVariable x) = "Unbound variable: " ++ T.unpack x show (UnboundVariable x) = "Unbound variable: " ++ x
show (NotASort x t) = "Expected " ++ prettyS x ++ " to have type * or □, instead found " ++ prettyS t show (NotASort x t) = "Expected " ++ pretty x ++ " to have type * or □, instead found " ++ pretty t
show (ExpectedPiType m a) = prettyS m ++ " : " ++ prettyS a ++ " is not a function" show (ExpectedPiType m a) = pretty m ++ " : " ++ pretty a ++ " is not a function"
show (NotEquivalent a a' e) = "Cannot unify " ++ prettyS a ++ " with " ++ prettyS a' ++ " when evaluating " ++ prettyS e show (NotEquivalent a a' e) = "Cannot unify " ++ pretty a ++ " with " ++ pretty a' ++ " when evaluating " ++ pretty e
type CheckResult = Either TypeCheckError type CheckResult = Either TypeCheckError

View file

@ -1,16 +1,16 @@
{-# LANGUAGE GADTs #-}
module Expr where module Expr where
import Data.Function (on) import Data.Function (on)
import Data.Text (Text)
import qualified Data.Text as T
data Expr where data Expr where
Var :: Integer -> Text -> Expr Var :: Integer -> String -> Expr
Star :: Expr Star :: Expr
Square :: Expr Square :: Expr
App :: Expr -> Expr -> Expr App :: Expr -> Expr -> Expr
Abs :: Text -> Expr -> Expr -> Expr Abs :: String -> Expr -> Expr -> Expr
Pi :: Text -> Expr -> Expr -> Expr Pi :: String -> Expr -> Expr -> Expr
deriving (Show) deriving (Show)
instance Eq Expr where instance Eq Expr where
@ -32,61 +32,58 @@ occursFree n (Pi _ a b) = occursFree n a || occursFree (n + 1) b
{- --------------------- PRETTY PRINTING ----------------------------- -} {- --------------------- PRETTY PRINTING ----------------------------- -}
parenthesize :: Text -> Text parenthesize :: String -> String
parenthesize s = T.concat ["(", s, ")"] parenthesize s = "(" ++ s ++ ")"
collectLambdas :: Expr -> ([(Text, Expr)], Expr) collectLambdas :: Expr -> ([(String, Expr)], Expr)
collectLambdas (Abs x ty body) = ((x, ty) : params, final) collectLambdas (Abs x ty body) = ((x, ty) : params, final)
where where
(params, final) = collectLambdas body (params, final) = collectLambdas body
collectLambdas e = ([], e) collectLambdas e = ([], e)
collectPis :: Expr -> ([(Text, Expr)], Expr) collectPis :: Expr -> ([(String, Expr)], Expr)
collectPis p@(Pi "" _ _) = ([], p) collectPis p@(Pi "" _ _) = ([], p)
collectPis (Pi x ty body) = ((x, ty) : params, final) collectPis (Pi x ty body) = ((x, ty) : params, final)
where where
(params, final) = collectPis body (params, final) = collectPis body
collectPis e = ([], e) collectPis e = ([], e)
groupParams :: [(Text, Expr)] -> [([Text], Expr)] groupParams :: [(String, Expr)] -> [([String], Expr)]
groupParams = foldr addParam [] groupParams = foldr addParam []
where where
addParam :: (Text, Expr) -> [([Text], Expr)] -> [([Text], Expr)] addParam :: (String, Expr) -> [([String], Expr)] -> [([String], Expr)]
addParam (x, t) [] = [([x], t)] addParam (x, t) [] = [([x], t)]
addParam (x, t) l@((xs, s) : rest) addParam (x, t) l@((xs, s) : rest)
| incIndices t == s = (x : xs, t) : rest | incIndices t == s = (x : xs, t) : rest
| otherwise = ([x], t) : l | otherwise = ([x], t) : l
showParamGroup :: ([Text], Expr) -> Text showParamGroup :: ([String], Expr) -> String
showParamGroup (ids, ty) = parenthesize $ T.unwords ids <> " : " <> pretty ty showParamGroup (ids, ty) = parenthesize $ unwords ids ++ " : " ++ pretty ty
helper :: Integer -> Expr -> Text helper :: Integer -> Expr -> String
helper _ (Var _ s) = s helper _ (Var _ s) = s
helper _ Star = "*" helper _ Star = "*"
helper _ Square = "" helper _ Square = ""
helper k (App e1 e2) = if k > 3 then parenthesize res else res helper k (App e1 e2) = if k > 3 then parenthesize res else res
where where
res = helper 3 e1 <> " " <> helper 4 e2 res = helper 3 e1 ++ " " ++ helper 4 e2
helper k (Pi "" t1 t2) = if k > 2 then parenthesize res else res helper k (Pi "" t1 t2) = if k > 2 then parenthesize res else res
where where
res = helper 3 t1 <> " -> " <> helper 2 t2 res = helper 3 t1 ++ " -> " ++ helper 2 t2
helper k e@(Pi{}) = if k > 2 then parenthesize res else res helper k e@(Pi{}) = if k > 2 then parenthesize res else res
where where
(params, body) = collectPis e (params, body) = collectPis e
grouped = showParamGroup <$> groupParams params grouped = showParamGroup <$> groupParams params
res = "" <> T.unwords grouped <> " . " <> pretty body res = "" ++ unwords grouped ++ " . " ++ pretty body
helper k e@(Abs{}) = if k >= 1 then parenthesize res else res helper k e@(Abs{}) = if k >= 1 then parenthesize res else res
where where
(params, body) = collectLambdas e (params, body) = collectLambdas e
grouped = showParamGroup <$> groupParams params grouped = showParamGroup <$> groupParams params
res = "λ " <> T.unwords grouped <> " . " <> pretty body res = "λ " ++ unwords grouped ++ " . " ++ pretty body
pretty :: Expr -> Text pretty :: Expr -> String
pretty = helper 0 pretty = helper 0
prettyS :: Expr -> String
prettyS = T.unpack . pretty
{- --------------- ACTUAL MATH STUFF ---------------- -} {- --------------- ACTUAL MATH STUFF ---------------- -}
isSort :: Expr -> Bool isSort :: Expr -> Bool

View file

@ -7,22 +7,20 @@ import Data.Functor.Identity
import Data.List (elemIndex) import Data.List (elemIndex)
import Data.List.NonEmpty (NonEmpty ((:|))) import Data.List.NonEmpty (NonEmpty ((:|)))
import qualified Data.List.NonEmpty as NE import qualified Data.List.NonEmpty as NE
import Data.Text (Text)
import qualified Data.Text as T
import Expr (Expr (..), incIndices) import Expr (Expr (..), incIndices)
import Text.Megaparsec hiding (State) import Text.Megaparsec hiding (State)
import Text.Megaparsec.Char import Text.Megaparsec.Char
import qualified Text.Megaparsec.Char.Lexer as L import qualified Text.Megaparsec.Char.Lexer as L
type InnerState = [Text] type InnerState = [String]
data CustomErrors = UnboundVariable Text [Text] deriving (Eq, Ord, Show) data CustomErrors = UnboundVariable String [String] deriving (Eq, Ord, Show)
instance ShowErrorComponent CustomErrors where instance ShowErrorComponent CustomErrors where
showErrorComponent (UnboundVariable var bound) = showErrorComponent (UnboundVariable var bound) =
"Unbound variable: " ++ T.unpack var ++ ". Did you mean one of: " ++ T.unpack (T.unwords bound) ++ "?" "Unbound variable: " ++ var ++ ". Did you mean one of: " ++ unwords bound ++ "?"
type Parser = ParsecT CustomErrors Text (State InnerState) type Parser = ParsecT CustomErrors String (State InnerState)
skipSpace :: Parser () skipSpace :: Parser ()
skipSpace = skipSpace =
@ -34,11 +32,11 @@ skipSpace =
lexeme :: Parser a -> Parser a lexeme :: Parser a -> Parser a
lexeme = L.lexeme skipSpace lexeme = L.lexeme skipSpace
pIdentifier :: Parser Text pIdentifier :: Parser String
pIdentifier = label "identifier" $ lexeme $ do pIdentifier = label "identifier" $ lexeme $ do
firstChar <- letterChar <|> char '_' firstChar <- letterChar <|> char '_'
rest <- many $ alphaNumChar <|> char '_' rest <- many $ alphaNumChar <|> char '_'
return $ T.pack (firstChar : rest) -- Still need T.pack here as we're building from chars return $ firstChar : rest
pVar :: Parser Expr pVar :: Parser Expr
pVar = label "variable" $ lexeme $ do pVar = label "variable" $ lexeme $ do
@ -48,10 +46,12 @@ pVar = label "variable" $ lexeme $ do
Just i -> return $ Var (fromIntegral i) var Just i -> return $ Var (fromIntegral i) var
Nothing -> customFailure $ UnboundVariable var binders Nothing -> customFailure $ UnboundVariable var binders
defChoice :: NE.NonEmpty Text -> Parser () defChoice :: NE.NonEmpty String -> Parser ()
defChoice options = lexeme $ label (T.unpack $ NE.head options) $ void $ choice $ NE.map chunk options defChoice options = lexeme $ label labelText $ void $ choice $ NE.map string options
where
labelText = NE.head options
pParamGroup :: Parser [(Text, Expr)] pParamGroup :: Parser [(String, Expr)]
pParamGroup = lexeme $ label "parameter group" $ between (char '(') (char ')') $ do pParamGroup = lexeme $ label "parameter group" $ between (char '(') (char ')') $ do
idents <- some pIdentifier idents <- some pIdentifier
_ <- defChoice $ ":" :| [] _ <- defChoice $ ":" :| []
@ -59,7 +59,7 @@ pParamGroup = lexeme $ label "parameter group" $ between (char '(') (char ')') $
modify (flip (foldl $ flip (:)) idents) modify (flip (foldl $ flip (:)) idents)
pure $ zip idents (iterate incIndices ty) pure $ zip idents (iterate incIndices ty)
pParams :: Parser [(Text, Expr)] pParams :: Parser [(String, Expr)]
pParams = concat <$> some pParamGroup pParams = concat <$> some pParamGroup
pLAbs :: Parser Expr pLAbs :: Parser Expr
@ -112,5 +112,5 @@ pAppTerm = lexeme $ pLAbs <|> pPAbs <|> pApp
pExpr :: Parser Expr pExpr :: Parser Expr
pExpr = lexeme $ try pArrow <|> pAppTerm pExpr = lexeme $ try pArrow <|> pAppTerm
pAll :: Text -> Either Text Expr pAll :: String -> Either String Expr
pAll input = first (T.pack . errorBundlePretty) $ fst $ runIdentity $ runStateT (runParserT pExpr "" input) [] pAll input = first errorBundlePretty $ fst $ runIdentity $ runStateT (runParserT pExpr "" input) []

View file

@ -1,75 +1 @@
module CheckTests (tests) where module CheckTests where
import Check
import Expr (Expr (..))
import Test.HUnit
sort :: Test
sort = TestCase $ assertEqual "*" (Right Square) (findType [] Star)
stlc :: Test
stlc =
TestCase $
assertEqual
"fun (x : A) (y : B) . x"
(Right $ Pi "" (Var 0 "A") (Pi "" (Var 2 "B") (Var 2 "A")))
(findType [Star, Star] $ Abs "x" (Var 0 "A") (Abs "y" (Var 2 "B") (Var 1 "x")))
polyIdent :: Test
polyIdent =
TestCase $
assertEqual
"fun (A : *) (x : A) . x"
(Right $ Pi "A" Star (Pi "" (Var 0 "A") (Var 1 "A")))
(findType [] (Abs "A" Star (Abs "x" (Var 0 "A") (Var 0 "x"))))
typeCons :: Test
typeCons =
TestCase $
assertEqual
"fun (A B : *) . A -> B"
(Right $ Pi "" Star (Pi "" Star Star))
(findType [] (Abs "A" Star (Abs "B" Star (Pi "" (Var 1 "A") (Var 1 "B")))))
useTypeCons :: Test
useTypeCons =
TestCase $
assertEqual
"fun (C : * -> *) (A : *) (x : C A) . x"
(Right $ Pi "C" (Pi "" Star Star) (Pi "A" Star (Pi "" (App (Var 1 "C") (Var 0 "A")) (App (Var 2 "C") (Var 1 "A")))))
(findType [] $ Abs "C" (Pi "" Star Star) (Abs "A" Star (Abs "x" (App (Var 1 "C") (Var 0 "A")) (Var 0 "x"))))
dependent :: Test
dependent =
TestCase $
assertEqual
"fun (S : *) (x : S) . S -> S"
(Right $ Pi "S" Star (Pi "" (Var 0 "S") Star))
(findType [] $ Abs "S" Star (Abs "x" (Var 0 "S") (Pi "" (Var 1 "S") (Var 2 "S"))))
useDependent :: Test
useDependent =
TestCase $
assertEqual
"fun (S : *) (P : S -> *) (x : S) . P x"
(Right $ Pi "S" Star (Pi "" (Pi "" (Var 0 "S") Star) (Pi "" (Var 1 "S") Star)))
(findType [] $ Abs "S" Star (Abs "P" (Pi "" (Var 0 "S") Star) (Abs "x" (Var 1 "S") (App (Var 1 "P") (Var 0 "x")))))
big :: Test
big =
TestCase $
assertEqual
"fun (S : *) (P Q : S -> *) (H : forall (x : S), P x -> Q x) (G : forall (x : S), P x) (x : S) . H x (G x)"
(Right $ Pi "S" Star (Pi "P" (Pi "" (Var 0 "S") Star) (Pi "Q" (Pi "" (Var 1 "S") Star) (Pi "" (Pi "x" (Var 2 "S") (Pi "" (App (Var 2 "P") (Var 0 "x")) (App (Var 2 "Q") (Var 1 "x")))) (Pi "" (Pi "x" (Var 3 "S") (App (Var 3 "P") (Var 0 "x"))) (Pi "x" (Var 4 "S") (App (Var 3 "Q") (Var 0 "x"))))))))
(findType [] $ Abs "S" Star (Abs "P" (Pi "" (Var 0 "S") Star) (Abs "Q" (Pi "" (Var 1 "S") Star) (Abs "H" (Pi "x" (Var 2 "S") (Pi "" (App (Var 2 "P") (Var 0 "x")) (App (Var 2 "Q") (Var 1 "x")))) (Abs "G" (Pi "x" (Var 3 "S") (App (Var 3 "P") (Var 0 "x"))) (Abs "x" (Var 4 "S") (App (App (Var 2 "H") (Var 0 "x")) (App (Var 1 "G") (Var 0 "x")))))))))
tests :: Test
tests =
TestList
[ TestLabel "sort" sort
, TestLabel "λ→" stlc
, TestLabel "λ2" polyIdent
, TestLabel "λω" (TestList [typeCons, useTypeCons])
, TestLabel "λP2" (TestList [dependent, useDependent])
, TestLabel "λC" big
]

View file

@ -1,7 +1,5 @@
module ParserTests (tests) where module ParserTests (tests) where
import Data.Text (Text)
import qualified Data.Text as T
import Expr (Expr (..)) import Expr (Expr (..))
import Parser (pAll) import Parser (pAll)
import Test.HUnit import Test.HUnit

View file

@ -1,18 +1,13 @@
module Main where module Main where
import qualified CheckTests as C
import qualified ExprTests as E import qualified ExprTests as E
import qualified ParserTests as P import qualified ParserTests as P
import qualified System.Exit as Exit import qualified System.Exit as Exit
import Test.HUnit import Test.HUnit
tests :: Test tests :: Test
tests = tests = TestList [ TestLabel "ExprTests" E.tests
TestList , TestLabel "ParserTests" P.tests]
[ TestLabel "ExprTests" E.tests
, TestLabel "ParserTests" P.tests
, TestLabel "CheckTests" C.tests
]
main :: IO () main :: IO ()
main = do main = do