Proof Assistantを作る・実装編 その4

これは一人Computer Scienceアドベントカレンダー 21日目の記事です。


Proof Assistant 「Claire」の実装について説明していきます。

リポジトリはこちら: myuon/claire

昨日に引き続いてClaireの宣言(Decl)について説明していきます。

Declarations

まずはtoplevelMの定義から。

  toplevelM :: (Monad m, MonadIO m) => Coroutine DeclSuspender (StateT Env m) ()
  toplevelM = forever $ do
    let typecheck fml u k = do {
      env <- lift get;
      utyp <- liftIO $ try $ infer env fml;
      case utyp of
        Left err -> suspend $ DeclError "typecheck" (toException $ TypeError fml err) (return ())
        Right typ | u == typ -> k
        Right typ -> suspend $ DeclError "typecheck" (toException $ TypeError fml (toException $ UnificationFailed u typ)) (return ())
    }
 
    decl <- suspend (DeclAwait return)
    env <- lift get
    case decl of
      ここに実装を書く

Claireは実は(貧弱ながら)型システムを備えていて、型チェックを一応行います。 とりあえずそれは今はおいておいて、toplevelMはDeclを受け取って実行するのを繰り返すだけの単純なステートマシンです。

ThmD thmindex formula proof

定理と証明を宣言します。

  ThmD idx fml (Proof coms) -> typecheck fml Prop $ do
    lift $ modify $ \env -> env { proof = [] }
    runThmD idx fml coms

    where
      runThmD :: (Monad m, MonadIO m) => ThmIndex -> Formula -> [Command] -> Coroutine DeclSuspender (StateT Env m) ()
      runThmD idx fml coms = do
        env <- lift get
        go (commandM env) (newGoal fml) coms
        lift $ modify $ insertThm idx fml

        where
          go :: (Monad m) => Coroutine ComSuspender (StateT [Judgement] m) () -> [Judgement] -> [Command] -> Coroutine DeclSuspender (StateT Env m) ()
          go machine js coms = do
            env <- lift get
            (result,js') <- lift $ lift $ runStateT (resume machine) js

            case result of
              Right () -> return ()
              Left (ComAwait cont) -> do
                case coms of
                  [] -> do
                    com' <- suspend $ ProofNotFinished js' return
                    go (suspend $ ComAwait cont) js' [com']
                  (c:cs) -> do
                    go (cont c) js' cs
              Left (z@(CommandError idt err cont)) -> do
                suspend $ RunCommandError idt err (return ())
                go cont js coms

ThmDは中でcommandMを走らせ、その結果によって挙動を決めます。

commandMが問題なく終了した時(=与えられたProofが与えられた命題の証明を完成させた時)、示した論理式を環境に追加して終了します。 commandMがコマンドを要求するComAwaitで終了したとき、toplevelM全体をProofNotFinishedという証明が完了していないことを表すsuspenderでsuspendします。 commandMがエラーになった時toplevelM全体をRunCommandErrorで返します。

syntaxは次のような感じです。

  theorem hoge: a ==> a
  proof
    apply ImpR
    apply I
  qed

AxiomD thmindex formula

公理として指定された論理式を追加します。

      AxiomD idx fml -> typecheck fml Prop $ do
        lift $ modify $ insertThm idx fml

環境に定理として追加するだけ。

ImportD path

他の証明ファイルをインポートします。

      ImportD path -> do
        env <- lift get
        env' <- liftIO $ claire env . (\(Laire ds) -> ds) . pLaire =<< readFile path
        lift $ put $ env'

あとで定義しますがclaireというClaireの証明ファイル(というかClaire型の値)そのものを解釈して動くproofchecker machineをあとで定義するのでそれを使います。 そしてそれを実行後に環境を現在の環境に上書きします。

ConstD identifier type

ある型のtermを追加します。 これは型チェッカーの話をする時についでに説明しますが、ADTみたいなsyntaxが今のところないのでconstructorはこのdeclarationによって追加します。

      ConstD p typ -> do
        lift $ modify $ \env -> env { types = M.insert p typ (types env) }

Print_Proof

直前の証明をprintします。 これは証明を入力するたびにそれを証明に追加していくという方法で実装しています。詳細はまた後ほど。

      PrintProof -> do
        env <- lift get
        liftIO $ putStrLn $ print_proof env

HsFile, NewDecl

この辺はマクロ関係の機能なので今は保留。

      HsFile file -> ...
      NewDecl dec args -> ...

Claire Proofchecker

色々説明していないこともありますがまぁそれはそれとして、Claireのproofcheckerを定義します。

  claire :: Env -> [Decl] -> IO Env
  claire = go toplevelM where
    go :: Coroutine DeclSuspender (StateT Env IO) () -> Env -> [Decl] -> IO Env
    go machine env decls = do
      (result,env') <- flip runStateT env (resume machine)
      case result of
        Left (DeclAwait cont) -> case decls of
          [] -> return env'
          (d:ds) -> go (cont d) env' ds
        Left z -> do
          print z
          return env'

これは基本的には、受け取ったDeclのリストをtoplevelMに食わしながらループをぐるぐる回すだけですね。 toplevelMは(foreverで定義されているのでresumeすると常にDeclAwaitが出現し続けるという)無限の深さの構造をもつので、戻り値は必ずLeftナントカの形になります。

DeclAwaitになったらそのまま実行を続け、そうでなければエラーなので画面に出力してそこでproofcheckerを止めます。

Main.hs

さて、今までの機能を使ってとにかく動くところまでやりたいので、Main.hsの実装を説明します。

app/Main.hs

  main :: IO ()
  main = do
    xs <- getArgs
    case (xs /= []) of
      True -> do
        env <- claire defEnv . (\(Laire ds) -> ds) . pLaire =<< readFile (head xs)
        putStrLn "= Constants ="
        mapM_ print $ M.assocs $ types env
        putStrLn "= Proved Theorems ="
        mapM_ print $ M.assocs $ thms env
      False -> do
        mapM_ putStrLn $
          [ "========================="
          , "=== Welcome to Claire ==="
          , "========================="
          , ""
          ]
        clairepl defEnv

main関数は、ファイルを受け取ってproofcheckerを走らせるか、インタラクティブシェルを起動するかのいずれかです。 proofcheckerは先程説明したclaireを実行するだけですね。

インタラクティブシェルは次のような実装になっています。

  clairepl :: Env -> IO ()
  clairepl env = go env toplevelM where
    go :: Env -> Coroutine DeclSuspender (StateT Env IO) () -> IO ()
    go env k = do
      (result,env') <- flip runStateT env $ resume k

      case result of
        Right () -> go env' k
        Left (DeclAwait k) -> do
          t <- safep (putStr "decl>" >> hFlush stdout) pDecl
          go env' (k t)
        Left (ProofNotFinished js cont) -> do
          mapM_ print js
          (t,raw) <- safep (putStr "command>" >> hFlush stdout) (\s -> let s' = pCommand s in s' `seq` (s',s))
          let addProof env k = env { proof = proof env ++ [k] }
          go (addProof env' (t,raw)) (cont t)
        Left (z@(RunCommandError idt err cont)) -> do
          print z
          let unaddProof env | length (proof env) >= 1 = env { proof = init (proof env) }
              unaddProof env = env
          go (unaddProof env') cont
        Left (z@(DeclError idt err cont)) -> do
          print z
          go env cont

    safep :: IO () -> (String -> a) -> IO a
    safep ma p = ma >> (p <$!> getLine) `catch` (\err -> print (err :: ErrorCall) >> safep ma p)

これはtoplevelMを使って実際にどのように実行するかを記述するものです。

toplevelMを実行すると、DeclAwait, ProofNotFinished, RunCommandError, DeclErrorなどの様々な状態が返ってきます。 DeclAwaitが返ってきたらDeclをユーザーに入力させ、ProofNotFinishedが返ってきたらCommandをユーザーに入力させる、みたいなことをしているのが上のコードです。 コマンドの入力は証明の入力なので、上でも触れましたがPrint_Proofができるようにするために入力した証明を環境に追加しておきます。

ところで、ユーザーの入力が例えばパース不可能な文字列である場合があるので、そのような場合にはエラーを出力しもう一度入力をさせるための関数がsafepです。

toplevelMの結果エラーが返ってきたらエラー内容を画面に出力して、もう一度入力をさせます。

注目してほしいこととして、証明チェックに関する実装は全てtoplevelMが請け負っていて、claireplもclaireも共通したコードで実装ができているということですね。 Coroutineモナドを使ったことにより、証明の実装自体は共通化させ、実際にマシーンが停止状態に入った時にエラーを画面に表示するか、それともユーザーからの入力を受け付けるのか、というUI部分は後から実装を与えて挙動を変えているというのが大事なところです。

実行

以上のような仕組みで、Claireのproofcheckerとインタラクティブシェルが動きます。

例えば実行してみると、次のような操作で証明を記述することが出来ます。

  =========================
  === Welcome to Claire ===
  =========================

  decl>theorem id: a ==> a

  [] |- [Pred "a" [] :==>: Pred "a" []]

  command>apply ImpR

  [Pred "a" []] |- [Pred "a" []]

  command>apply I

  decl>print_proof

  = proof of the previous theorem =
  proof
    apply ImpR
    apply I
  qed

decl>, command> の部分がユーザーの入力です。

まとめ

はてさて、だいぶ形になってきたように思います。 一応今回でClaireの証明チェックに関するコアの部分の説明は終わりです。

あと、型システムの説明と(と言っても特別なことはないので説明することもないですが)、マクロ機能の説明をしたらClaireの解説はおしまいになります。

というわけでお疲れ様でした。