Qiita の言語実装 advent Calendar 2016 の21日目用の記事です. (1日遅れですいません)

前に書いた その1 に続いて,「Ruby で学ぶ Ruby」中の MinRuby を Haskell で実装していきたいと思います.

ソースコードはコチラ

MinRuby

ascii.jp にて連載中の 「Ruby で学ぶ Ruby」 でステップバイステップに作成中の処理系. 名前の通り,Ruby のサブクラスになってる.

自分はそれを,大好きな Haskell で実装してみようと試みてる.

連載の方は Ripper というライブラリを使って構文解析を省略してるが,Haskell にそんなライブラリが無いので構文解析から少しずつ書いてる.

今のところ連載の方は,整数演算,論理演算,変数,条件分岐,ループ,組み込み関数が使える. 自分の方は今のところ分岐もどきまで...

ステップバイステップ

その1では雑な二項演算までしか書いてなかったので,少しずつ改良してく.

型

まずは最も重要な型から.

構文解析というのは文字列(String) から構文木に変換するもの. その1ではめんどくさくて文字列の木(Tree String ,Tree は containers から)を使ったが,後々に論理演算を使うためにも,ちゃんと和型を定義してく.

data Value = SVal { getString :: String }
           | IVal { getInt :: Int }
           | BVal { getBool :: Bool }
           | UVal ()
           deriving (Eq)

Ruby の場合は,このあたりをへテロリストで,なんも気にせず返せるのズルい.

p(minruby_parse("4 * (56 / 7 + 8 + 9)"))
#=> ["*", [["lit", 4], ["+", ["+", ["/", ["lit", 56], ["lit", 7]], ["lit", 8]], ["lit", 9]]]]

こっちは Tree Value 型を返す(イロイロ省略してる).

> minrubyParse()
Node "*" [Node "lit" [Node 4 []], Node "+" [Node "/" [Node "lit" [Node 56 []], Node "lit" [Node 7 []]], Node "+" [Node "lit" [Node 8 []], Node "lit" [Node 9 []]]]]

パーサー

長くなるので今回は割愛.

整数演算

連載の方は第4回目で電卓を作り始めた(その前の回は Ruby 入門的なの).

自分もまずはそこから.

意味解析はこんな感じ.

import MinRuby (Value(..), minrubyParse)
import Data.Tree (Tree(..))

main :: IO ()
main = do
  input <- getLine
  let tree = minrubyParse input
  print $ evaluate tree

evaluate :: Tree Value -> Value
evaluate (Node v ls) =
  if null ls then v else
  case getString v of
    "lit" -> evaluate $ ls !! 0
    "+"   -> intOp (+) (evaluate $ ls !! 0) (evaluate $ ls !! 1)
    "-"   -> intOp (-) (evaluate $ ls !! 0) (evaluate $ ls !! 1)
    "*"   -> intOp (*) (evaluate $ ls !! 0) (evaluate $ ls !! 1)
    "/"   -> intOp div (evaluate $ ls !! 0) (evaluate $ ls !! 1)
    "%"   -> intOp mod (evaluate $ ls !! 0) (evaluate $ ls !! 1)
    "**"  -> intOp (^) (evaluate $ ls !! 0) (evaluate $ ls !! 1)
    _     -> error ("undefined : " `mappend` show v)

intOp :: (Int -> Int -> Int) -> Value -> Value -> Value
intOp f (IVal v1) (IVal v2) = IVal $ f v1 v2
intOp f v1 v2 = error $ "unmatch type " `mappend` concatMap show [v1,v2]
$ stack exec -- interp
4 * (56 / 7 + 8 + 9)
100

論理演算

ここからがめんどい.

論理演算の型が多相的だからだ.

今回は単純に型の差分を食ってくれる関数を書いた.

{-# LANGUAGE Rank2Types #-}

boolOp :: (forall a . Ord a => a -> a -> Bool) -> Value -> Value -> Value
boolOp f (SVal v1) (SVal v2) = BVal $ f v1 v2
boolOp f (IVal v1) (IVal v2) = BVal $ f v1 v2
boolOp f (BVal v1) (BVal v2) = BVal $ f v1 v2
boolOp _ _ _ = BVal False

勉強不足のため,あんまり詳しくは分かってないので説明は割愛するが,このページの技術を使ってる.

鍵となるのは1引数目の型 forall a . Ord a => a -> a -> Bool . String,Int,Bool に対する別々の型の論理演算を共通の形で利用するためには,このように書いて,いわゆる存在型を使う必要がある.

で,これを使って evaluate を拡張する.

evaluate :: Tree Value -> Value
evaluate (Node v ls) =
  if null ls then v else
  case getString v of
    "lit" -> evaluate $ ls !! 0
    "+"   -> intOp (+) (evaluate $ ls !! 0) (evaluate $ ls !! 1)
    "-"   -> intOp (-) (evaluate $ ls !! 0) (evaluate $ ls !! 1)
    "*"   -> intOp (*) (evaluate $ ls !! 0) (evaluate $ ls !! 1)
    "/"   -> intOp div (evaluate $ ls !! 0) (evaluate $ ls !! 1)
    "%"   -> intOp mod (evaluate $ ls !! 0) (evaluate $ ls !! 1)
    "**"  -> intOp (^) (evaluate $ ls !! 0) (evaluate $ ls !! 1)
    "<"   -> boolOp (<) (evaluate $ ls !! 0) (evaluate $ ls !! 1)
    "<="  -> boolOp (<=) (evaluate $ ls !! 0) (evaluate $ ls !! 1)
    "=="  -> boolOp (==) (evaluate $ ls !! 0) (evaluate $ ls !! 1)
    "!="  -> boolOp (/=) (evaluate $ ls !! 0) (evaluate $ ls !! 1)
    ">"   -> boolOp (>) (evaluate $ ls !! 0) (evaluate $ ls !! 1)
    ">="  -> boolOp (>=) (evaluate $ ls !! 0) (evaluate $ ls !! 1)
    _     -> error ("undefined : " `mappend` show v)

複文と標準出力

> minrubyParse "1+2\n3*4"
Node "stmt" [Node "+" [Node "lit" [Node 1 []], Node "lit" [Node 2 []]], Node "*" [Node "lit" [Node 3 []], Node "lit" [Node 4 []]]]
> minrubyParse "p(1+2)"
Node "func_call" [Node "p" [], Node "+" [Node "lit" [Node 1 []], Node "lit" [Node 2 []]]]

問題は標準出力の方. Haskell は IO がつらい. まぁ,Applicative のおかげで大木は変更しなくてよいのだが.

明示的に撮り歩かう必要があるので型を変更する.

import MinRuby

main :: IO ()
main = do
  input <- minrubyLoad
  let tree = minrubyParse input
  evaluate tree
  return ()

evaluate :: Tree Value -> IO Value
evaluate (Node v ls) = do
  if null ls then return v else
  case getString v of
    "func_call" -> UVal <$> (print =<< evaluate (ls !! 1))
    "stmt" -> foldM (const evaluate) (UVal ()) ls
    "lit" -> evaluate $ ls !! 0
    "+"   -> intOp (+) <$> evaluate (ls !! 0) <*> evaluate (ls !! 1)
    "-"   -> intOp (-) <$> evaluate (ls !! 0) <*> evaluate (ls !! 1)
    ...
    _     -> error ("undefined : " `mappend` show v)

func_call の処理は,連載に習ってとりあえず標準出力にしている.

変数

変数と値をマッピングした,環境(Environment)を定義し,状態として持ちまわす.

連想配列には hashmap ライブラリを,状態型クラスには mtl を使う.

import Control.Monad.State (StateT)
import Data.HashMap (Map)

type Env = Map String Value
type Eval a = StateT Env IO a

構文木は

> minrubyParse "x = 1"
Node "var_assign" [Node "x" [], Node "lit" [Node 1[]]]
> minrubyParse "x"
Node "var_ref" [Node "x" []]

もともと IO を使ってたおかげで,あんまりもとのコードは書き換えなくて済む.

import Control.Monad (join, foldM)
import Control.Monad.State.Strict (StateT, evalStateT, modify, gets)
import Control.Monad.Trans (lift)
import Data.HashMap (Map, empty, insert, findWithDefault)

main :: IO ()
main = do
  input <- minrubyLoad
  let tree = minrubyParse input
  evalStateT (evaluate tree) empty
  return ()

evaluate :: Tree Value -> Eval Value
evaluate (Node v ls) = do
  if null ls then return v else
  case getString v of
    "var_assign" -> join $ assign <$> evaluate (ls !! 0) <*> evaluate (ls !! 1)
    "var_ref" -> refer =<< evaluate (ls !! 0)
    "func_call" -> UVal <$> (lift . print =<< evaluate (ls !! 1))
    "stmt" -> foldM (const evaluate) (UVal ()) ls
    "lit" -> evaluate $ ls !! 0
    "+"   -> intOp (+) <$> evaluate (ls !! 0) <*> evaluate (ls !! 1)
    "-"   -> intOp (-) <$> evaluate (ls !! 0) <*> evaluate (ls !! 1)
    ...
    _     -> error ("undefined : " `mappend` show v)

assign :: Value -> Value -> Eval Value
assign k v = modify (insert (getString k) v) *> return v

refer :: Value -> Eval Value
refer k = gets $ findWithDefault emassage (getString k)
  where
    emassage = error $ "undefined : " `mappend` getString k

assign のとこ,join 使って無理やり畳み込んでる. なんかいい方法ないのかしら...

条件分岐

そもそも,パースがうまくいかない. Node "if" [bexp, exp1, exp2] といった感じに返ってくるが,exp1 や exp2 に複文が書けない. 難しい...

evaluate :: Tree Value -> Eval Value
evaluate (Node v ls) = do
  if null ls then return v else
  case getString v of
    "if"   -> evaluate (ls !! 0) >>=
                \b -> evaluate $ if getBool b then ls !! 1 else ls !! 2
    ...
    _     -> error ("undefined : " `mappend` show v)

Haskell って,if式を綺麗に書けないんだよなぁ. 参考演算子をデフォで入れてくれればいいのに.

実行

一応,こういう Ruby コードは実行できる.

x = 1
y = x + 1
if 0 == 0
  p(y)
else
  p(x)
end
>stack exec -- interp test.rb
2

おしまい

Parsec 使って,それっぽくパーサーを書いてるが,いよいよ限界が来た. ちゃんと Ruby の BNF 見て書き写さなきゃダメかなぁ.

そしたら,Alex と Happy の方が楽な気がする...

頑張って,完成形まで持ってきたいです.