Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

majestic: Type-checking improvements + unifying overload resolution + simple REPL #173

Open
wants to merge 8 commits into
base: majestic
Choose a base branch
from
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ DATA_DIR

stack*.yaml.lock

# Generated source files
src/compiler/api/GF/Grammar/Lexer.hs
src/compiler/api/GF/Grammar/Parser.hs
src/compiler/api/PackageInfo_gf.hs
src/compiler/api/Paths_gf.hs

# Output files for test suite
*.out
gf-tests.html
Expand Down
17 changes: 11 additions & 6 deletions src/compiler/api/GF/Compile/Compute/Concrete.hs
Original file line number Diff line number Diff line change
Expand Up @@ -207,14 +207,19 @@ eval env (S t1 t2) vs = do v1 <- eval env t1 []
eval env (Let (x,(_,t1)) t2) vs = do tnk <- newThunk env t1
eval ((x,tnk):env) t2 vs
eval env (Q q@(m,id)) vs
| m == cPredef = do vs' <- mapM force vs
res <- evalPredef id vs'
case res of
Const res -> return res
RunTime -> return (VApp q vs)
NonExist -> return (VApp (cPredef,cNonExist) [])
| m == cPredef = do vs' <- mapM force vs -- FIXME this does not allow for partial application!
if any isVar vs'
then return (VApp q vs)
else do res <- evalPredef id vs'
case res of
Const res -> return res
RunTime -> return (VApp q vs)
NonExist -> return (VApp (cPredef,cNonExist) [])
| otherwise = do t <- getResDef q
eval env t vs
where
isVar (VGen _ _) = True
isVar _ = False
eval env (QC q) vs = return (VApp q vs)
eval env (C t1 t2) [] = do v1 <- eval env t1 []
v2 <- eval env t2 []
Expand Down
141 changes: 141 additions & 0 deletions src/compiler/api/GF/Compile/Repl.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
{-# LANGUAGE LambdaCase #-}

module GF.Compile.Repl (ReplOpts(..), defaultReplOpts, replOptDescrs, getReplOpts, runRepl, runRepl') where

import Control.Monad (unless, forM_, foldM)
import Control.Monad.IO.Class (MonadIO)
import qualified Data.ByteString.Char8 as BS
import Data.Char (isSpace)
import Data.Function ((&))
import Data.Functor ((<&>))
import qualified Data.Map as Map

import System.Console.GetOpt (ArgOrder(RequireOrder), OptDescr(..), ArgDescr(..), getOpt, usageInfo)
import System.Console.Haskeline (InputT, Settings(..), noCompletion, runInputT, getInputLine, outputStrLn)
import System.Directory (getAppUserDataDirectory)

import GF.Compile (batchCompile)
import GF.Compile.Compute.Concrete (Globals(Gl), stdPredef, normalFlatForm)
import GF.Compile.Rename (renameSourceTerm)
import GF.Compile.TypeCheck.ConcreteNew (inferLType)
import GF.Data.ErrM (Err(..))
import GF.Grammar.Grammar
( Grammar
, mGrammar
, Info
, Module
, ModuleName
, ModuleInfo(..)
, ModuleType(MTResource)
, ModuleStatus(MSComplete)
, OpenSpec(OSimple)
, Location (NoLoc)
, Term
, prependModule
)
import GF.Grammar.Lexer (Posn(..), Lang(GF), runLangP)
import GF.Grammar.Parser (pTerm)
import GF.Grammar.Printer (TermPrintQual(Unqualified), ppTerm)
import GF.Infra.CheckM (Check, runCheck)
import GF.Infra.Ident (moduleNameS)
import GF.Infra.Option (noOptions)
import GF.Infra.UseIO (justModuleName)
import GF.Text.Pretty (render)

data ReplOpts = ReplOpts
{ noPrelude :: Bool
, inputFiles :: [String]
}

defaultReplOpts :: ReplOpts
defaultReplOpts = ReplOpts False []

type Errs a = Either [String] a
type ReplOptsOp = ReplOpts -> Errs ReplOpts

replOptDescrs :: [OptDescr ReplOptsOp]
replOptDescrs =
[ Option ['h'] ["help"] (NoArg $ \o -> Left [usageInfo "gfci" replOptDescrs]) "Display help."
, Option [] ["no-prelude"] (flag $ \o -> o { noPrelude = True }) "Don't load the prelude."
]
where
flag f = NoArg $ \o -> pure (f o)

getReplOpts :: [String] -> Errs ReplOpts
getReplOpts args = case errs of
[] -> foldM (&) defaultReplOpts flags <&> \o -> o { inputFiles = inputFiles }
_ -> Left errs
where
(flags, inputFiles, errs) = getOpt RequireOrder replOptDescrs args

execCheck :: MonadIO m => Check a -> (a -> InputT m ()) -> InputT m ()
execCheck c k = case runCheck c of
Ok (a, warn) -> do
unless (null warn) $ outputStrLn warn
k a
Bad err -> outputStrLn err

replModNameStr :: String
replModNameStr = "<repl>"

replModName :: ModuleName
replModName = moduleNameS replModNameStr

parseThen :: MonadIO m => Grammar -> String -> (Term -> InputT m ()) -> InputT m ()
parseThen g s k = case runLangP GF pTerm (BS.pack s) of
Left (Pn l c, err) -> outputStrLn $ err ++ " (" ++ show l ++ ":" ++ show c ++ ")"
Right t -> execCheck (renameSourceTerm g replModName t) $ \t -> k t

runRepl' :: Globals -> IO ()
runRepl' gl@(Gl g _) = do
historyFile <- getAppUserDataDirectory "gfci_history"
runInputT (Settings noCompletion (Just historyFile) True) repl -- TODO tab completion
where
repl = do
getInputLine "gfci> " >>= \case
Nothing -> repl
Just (':' : l) -> let (cmd, arg) = break isSpace l in command cmd (dropWhile isSpace arg)
Just code -> evalPrintLoop code

command "t" arg = do
parseThen g arg $ \main ->
execCheck (inferLType gl main) $ \(_, ty) ->
outputStrLn $ render (ppTerm Unqualified 0 ty)
outputStrLn "" >> repl

command "q" _ = outputStrLn "Bye!"

command cmd _ = do
outputStrLn $ "Unknown REPL command: " ++ cmd
outputStrLn "" >> repl

evalPrintLoop code = do -- TODO bindings
parseThen g code $ \main ->
execCheck (inferLType gl main >>= \(t, _) -> normalFlatForm gl t) $ \nfs ->
forM_ (zip [1..] nfs) $ \(i, nf) ->
outputStrLn $ show i ++ ". " ++ render (ppTerm Unqualified 0 nf)
outputStrLn "" >> repl

runRepl :: ReplOpts -> IO ()
runRepl (ReplOpts noPrelude inputFiles) = do
-- TODO accept an ngf grammar
let toLoad = if noPrelude then inputFiles else "prelude/Predef.gfo" : inputFiles
(g0, opens) <- case toLoad of
[] -> pure (mGrammar [], [])
_ -> do
(_, (_, g0)) <- batchCompile noOptions Nothing toLoad
pure (g0, OSimple . moduleNameS . justModuleName <$> toLoad)
let
modInfo = ModInfo
{ mtype = MTResource
, mstatus = MSComplete
, mflags = noOptions
, mextend = []
, mwith = Nothing
, mopens = opens
, mexdeps = []
, msrc = replModNameStr
, mseqs = Nothing
, jments = Map.empty
}
runRepl' (Gl (prependModule g0 (replModName, modInfo)) (if noPrelude then Map.empty else stdPredef))
120 changes: 86 additions & 34 deletions src/compiler/api/GF/Compile/TypeCheck/ConcreteNew.hs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{-# LANGUAGE RankNTypes, CPP #-}
module GF.Compile.TypeCheck.ConcreteNew( checkLType, inferLType ) where
{-# LANGUAGE RankNTypes, CPP, TupleSections #-}
module GF.Compile.TypeCheck.ConcreteNew( checkLType, checkLType', inferLType, inferLType' ) where

-- The code here is based on the paper:
-- Simon Peyton Jones, Dimitrios Vytiniotis, Stephanie Weirich.
Expand All @@ -12,26 +12,33 @@ import GF.Grammar.Predef
import GF.Grammar.Lockfield
import GF.Compile.Compute.Concrete
import GF.Infra.CheckM
import GF.Data.Operations
import GF.Data.ErrM ( Err(Ok, Bad) )
import Control.Applicative(Applicative(..))
import Control.Monad(ap,liftM,mplus,foldM,zipWithM,forM)
import Control.Monad(ap,liftM,mplus,foldM,zipWithM,forM,filterM,when)
import Control.Monad.ST
import GF.Text.Pretty
import Data.STRef
import Data.List (nub, (\\), tails)
import qualified Data.Map as Map
import Data.Maybe(fromMaybe,isNothing)
import Data.Maybe(fromMaybe,isNothing,mapMaybe)
import Data.Functor((<&>))
import qualified Control.Monad.Fail as Fail

checkLType :: Globals -> Term -> Type -> Check (Term, Type)
checkLType globals t ty = runEvalOneM globals $ do
checkLType globals t ty = runEvalOneM globals (checkLType' t ty)

checkLType' :: Term -> Type -> EvalM s (Term, Type)
checkLType' t ty = do
vty <- eval [] ty []
(t,_) <- tcRho [] t (Just vty)
t <- zonkTerm [] t
return (t,ty)

inferLType :: Globals -> Term -> Check (Term, Type)
inferLType globals t = runEvalOneM globals $ do
inferLType globals t = runEvalOneM globals (inferLType' t)

inferLType' :: Term -> EvalM s (Term, Type)
inferLType' t = do
(t,ty) <- inferSigma [] t
t <- zonkTerm [] t
ty <- value2term False [] ty
Expand Down Expand Up @@ -64,13 +71,13 @@ tcRho scope t@(Vr v) mb_ty = do -- VAR
Just v_sigma -> instSigma scope t v_sigma mb_ty
Nothing -> evalError ("Unknown variable" <+> v)
tcRho scope t@(Q id) mb_ty = do
(t,ty) <- tcApp scope t t
(t,ty) <- tcApp scope t t []
instSigma scope t ty mb_ty
tcRho scope t@(QC id) mb_ty = do
(t,ty) <- tcApp scope t t
(t,ty) <- tcApp scope t t []
instSigma scope t ty mb_ty
tcRho scope t@(App fun arg) mb_ty = do
(t,ty) <- tcApp scope t t
(t,ty) <- tcApp scope t t []
instSigma scope t ty mb_ty
tcRho scope (Abs bt var body) Nothing = do -- ABS1
(i,tnk) <- newResiduation scope
Expand Down Expand Up @@ -105,7 +112,6 @@ tcRho scope (Abs bt var body) Nothing = do -- ABS1
v2 <- eval ((x,tnk):env) t []
check m (n+1) (b,x:xs) v2
v2 -> check m n st v2
check m (n+1) (b,x:xs) v2
check m n st (VRecType as) = foldM (\st (l,v) -> check m n st v) st as
check m n st (VR as) =
foldM (\st (lbl,tnk) -> follow m n st tnk) st as
Expand Down Expand Up @@ -376,21 +382,28 @@ tcCases scope ((p,t):cs) p_ty res_ty = do
cs <- tcCases scope cs p_ty res_ty
return ((p,t):cs)

tcApp scope t0 t@(App fun (ImplArg arg)) = do -- APP1
(fun,fun_ty) <- tcApp scope t0 fun
tcApp scope t0 (App fun (ImplArg arg)) args = tcApp scope t0 fun ((arg,False):args) -- APP1
tcApp scope t0 (App fun arg) args = tcApp scope t0 fun ((arg,True):args) -- APP2
tcApp scope t0 (Q id) args = resolveOverloads scope t0 id args -- VAR (global)
tcApp scope t0 (QC id) args = resolveOverloads scope t0 id args -- VAR (global)
tcApp scope t0 t args = do
(t,ty) <- tcRho scope t Nothing
reapply scope t ty args

reapply :: Scope s -> Term -> Constraint s -> [(Term,Bool)] -> EvalM s (Term,Rho s)
reapply scope fun fun_ty [] = return (fun,fun_ty)
reapply scope fun fun_ty ((arg,False):args) = do
(bt, x, arg_ty, res_ty) <- unifyFun scope fun_ty
if (bt == Implicit)
then return ()
else evalError (ppTerm Unqualified 0 t <+> "is an implicit argument application, but no implicit argument is expected")
when (bt == Implicit) $ evalError (ppTerm Unqualified 0 (App fun (ImplArg arg)) <+>
"is an implicit argument application, but no implicit argument is expected")
(arg,_) <- tcRho scope arg (Just arg_ty)
res_ty <- case res_ty of
VClosure res_env res_ty -> do env <- scopeEnv scope
tnk <- newThunk env arg
eval ((x,tnk):res_env) res_ty []
res_ty -> return res_ty
return (App fun (ImplArg arg), res_ty)
tcApp scope t0 (App fun arg) = do -- APP2
(fun,fun_ty) <- tcApp scope t0 fun
res_ty -> return res_ty
reapply scope (App fun (ImplArg arg)) res_ty args
reapply scope fun fun_ty ((arg,True):args) = do
(fun,fun_ty) <- instantiate scope fun fun_ty
(_, x, arg_ty, res_ty) <- unifyFun scope fun_ty
(arg,_) <- tcRho scope arg (Just arg_ty)
Expand All @@ -399,22 +412,61 @@ tcApp scope t0 (App fun arg) = do -- APP2
tnk <- newThunk env arg
eval ((x,tnk):res_env) res_ty []
res_ty -> return res_ty
return (App fun arg, res_ty)
tcApp scope t0 (Q id) = do -- VAR (global)
(t,ty) <- getOverload t0 id
vty <- eval [] ty []
return (t,vty)
tcApp scope t0 (QC id) = do -- VAR (global)
(t,ty) <- getOverload t0 id
vty <- eval [] ty []
return (t,vty)
tcApp scope t0 t = tcRho scope t Nothing
reapply scope (App fun arg) res_ty args

resolveOverloads :: Scope s -> Term -> QIdent -> [(Term,Bool)] -> EvalM s (Term,Rho s)
resolveOverloads scope t q args = EvalM $ \gl@(Gl gr _) k mt d r msgs ->
case lookupOverloadTypes gr q of
Bad msg -> return $ Fail (pp msg) msgs
Ok [tty] -> try tty gl k mt d r msgs -- skip overload resolution if there's only one overload
Ok ttys -> do rs <- mapM (\tty -> (tty,) <$> try tty gl k mt d r msgs) ttys
let successes = mapMaybe isSuccess rs
r <- case successes of
[] -> return $ Fail mempty msgs
[(_,r,msgs)] -> return $ Success r msgs
_ -> case unifyOverloads (successes <&> \(tty,_,_) -> tty) of
EvalM f -> f gl k mt d r msgs
return $ case r of
s@(Success _ _) -> s
Fail err msgs -> let h = "Overload resolution failed" $$
"of term " <+> pp t $$
"with types" <+> vcat [ppTerm Terse 0 ty | (_,ty) <- ttys]
in Fail (h $+$ err) msgs
where
try (t,ty) = case eval [] ty [] >>= \vty -> reapply scope t vty args of EvalM f -> f

isSuccess (tty, Success r msg) = Just (tty,r,msg)
isSuccess (_, Fail _ _) = Nothing

tcOverloadFailed t ttys =
evalError ("Overload resolution failed" $$
"of term " <+> pp t $$
"with types" <+> vcat [ppTerm Terse 0 ty | (_,ty) <- ttys])
unifyOverloads ttys = do
ttys <- forM ttys $ \(t,ty) -> do
vty <- eval [] ty []
(t,vty) <- papply scope t vty args
return (t,vty)
(_,tnk) <- newResiduation scope
let mv = VMeta tnk []
mapM_ (\(_,vty) -> unify scope vty mv) ttys
fvty <- force tnk
return (FV (fst <$> ttys), fvty)

papply scope fun fun_ty [] = return (fun,fun_ty)
papply scope fun (VProd Implicit x arg_ty res_ty) ((arg,False):args) = do
(arg,_) <- tcRho scope arg (Just arg_ty)
res_ty <- case res_ty of
VClosure res_env res_ty -> do env <- scopeEnv scope
tnk <- newThunk env arg
eval ((x,tnk):res_env) res_ty []
res_ty -> return res_ty
papply scope (App fun (ImplArg arg)) res_ty args
papply scope fun fun_ty ((arg,True):args) = do
(fun,VProd Explicit x arg_ty res_ty) <- instantiate scope fun fun_ty
(arg,_) <- tcRho scope arg (Just arg_ty)
res_ty <- case res_ty of
VClosure res_env res_ty -> do env <- scopeEnv scope
tnk <- newThunk env arg
eval ((x,tnk):res_env) res_ty []
res_ty -> return res_ty
papply scope (App fun arg) res_ty args

tcPatt scope PW ty0 =
return scope
Expand Down
1 change: 1 addition & 0 deletions src/compiler/api/GF/Interactive.hs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import qualified Data.Sequence as Seq
import qualified Text.ParserCombinators.ReadP as RP
import System.Directory(getAppUserDataDirectory)
import Control.Exception(SomeException,fromException,evaluate,try)
import Control.Monad ((<=<),when,mplus,join)
import Control.Monad.State hiding (void)
import qualified GF.System.Signal as IO(runInterruptibly)
import GF.Command.Messages(welcome)
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/api/GF/Term.hs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module GF.Term (renameSourceTerm,
Globals(..), ConstValue(..), EvalM, stdPredef,
Value(..), showValue, Thunk, newThunk, newEvaluatedThunk,
evalError, evalWarn,
inferLType, checkLType,
inferLType, inferLType', checkLType, checkLType',
normalForm, normalFlatForm, normalStringForm,
unsafeIOToEvalM, force
) where
Expand Down
12 changes: 12 additions & 0 deletions src/compiler/gf-repl.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import GHC.IO.Encoding (setLocaleEncoding, utf8)

import System.Environment (getArgs)
import GF.Compile.Repl (getReplOpts, runRepl)

main :: IO ()
main = do
setLocaleEncoding utf8
args <- getArgs
case getReplOpts args of
Left errs -> mapM_ putStrLn errs
Right opts -> runRepl opts
Loading