phorth---(その1.1)AST+Parser

Harrorthを適当にいじった結果なんとなく雰囲気が分かったので,好きなようにHarrothを真似て作ってみます.

文法を(いい加減に)考える

forthの文法では,何かのコード片に名前をつけるのは

:NAME <code>;

とします.:(コロン)の後,;(セミコロン)の前の空白は無視されます.またansiのforth仕様書(のドラフト)を眺めると,各種の命令が大文字だったり数字記号交じりだったりします.

個人的に大文字オンリーの名前や数字や記号が入り込んだものはちょっといやなのと,:と;で挟み込んで名前をつけるのも好きではないので,ばっさり整理してしまいます.必要ならあとで整理しましょう.

  • 何かの操作や計算を行うものを「オペレータ」(operator)と呼ぶ
  • オペレータの操作対象を「オペランド」(operand)と呼ぶ
  • オペレータはスタックからオペランドを取得し,結果をスタックにプッシュする
  • オペレータは次の正規表現で表記される文字列とする:[a-z]+|==|\[|\]

とりあえず,これくらいを決めておきます.

また,コード片に名前をつける文法をPostScriptと同じに次のようにします.以降,コード片を「ブロック」と呼ぶことにします.

/name { <code> } def

これによって,に名前nameをつけることにします.defは名前付けのためのオペレータとします./nameと「/」をつけることで「名前」であることを明示します.

いわゆる「リテラル」は整数だけとします.

ASTを考える

Harrorth--のASTを書き換えます.

type AST = [Exp]
data Exp = Push Literal | Invoke Word deriving Show
type Literal = Integer
type Word = String

でした.Expは「定義」のために拡張します.

type AST = [Exp]
data Exp =   Push Literal
           | Invoke Word 
           | NewWord Name AST
           deriving Show                        
type Literal = Integer
type Word    = String
type Name    = String

新しく「定義」を行うために,データコンストラクタNewWordを用意し,名前とブロック内のコードに対応するASTをおきます.こうして

1 2 /a { 3 4 add } def

とすることで,

[Push 1,
 Push 2,
 NewWord "a" 
         [Push 3,
          Push 4,
          Invoke "add"
         ]
]

というASTができあがることを目指します.

パーサ(ほとんど同じ)

まずはほとんど同じなので,Harroth--のものを流用します.ここで,phorthProgの最初の空白の読み飛ばしskipMany spaceをパーサphorthの方に移します.これで「前後の空白」をスキップできます.

phorthProg::Parser AST
phorthProg = do ast <- phorth
                eof
                return ast

phorth::Parser AST
phorth =  do skipMany space
             sepEndBy1 phorthExp (skipMany1 space)

phorthExp::Parser Exp
phorthExp = literal <|> word <|> newWord

literal::Parser Exp
literal = (many1 digit) >>= \x ->return (Push (read x))

word::Parser Exp
word = many1 letter >>= \x ->return (Invoke x)

ただし,今度は新しいオペレータの定義ために,newWord::Parser Expを追加します.

newWord::Parser Exp
newWord =do char '/'
            name <- many1 letter
            skipMany space
            char '{'
            ast <- phorth
            char '}'
            skipMany space
            string "def"
            return $ NewWord name ast

とりあえずまとめます.

{--
phorth.hs 
--}
import Text.ParserCombinators.Parsec

-- AST
type AST = [Exp]
data Exp =   Push Literal
           | Invoke Word 
           | NewWord Name AST
           deriving Show
type Literal = Integer
type Word = String
type Name = String

-- Parser
phorthProg::Parser AST
phorthProg = do -- skipMany space
                ast <- phorth
                eof
                return ast

phorth::Parser AST
phorth = do skipMany space
            sepEndBy1 phorthExp (skipMany1 space)

phorthExp::Parser Exp
phorthExp = literal <|> word <|> newWord

literal::Parser Exp
literal = (many1 digit) >>= \x ->return (Push (read x))

word::Parser Exp
word = many1 letter >>= \x ->return (Invoke x)

newWord::Parser Exp
newWord =do char '/'
            name <- many1 letter
            skipMany space
            char '{'
            ast <- phorth
            char '}'
            skipMany space
            string "def"
            return $ NewWord name ast

このコードで

ghci
GHCi, version 6.8.3: http://www.haskell.org/ghc/  :? for help
Loading package base ... linking ... done.
Prelude> :l phorth.hs
[1 of 1] Compiling Main             ( phorth.hs, interpreted )
Ok, modules loaded: Main.
*Main> parseTest phorthProg " 1 2 /a { 3 4 add } def"
Loading package parsec-2.1.0.1 ... linking ... done.
[Push 1,Push 2,NewWord "a" [Push 3,Push 4,Invoke "add"]]

となります.これは期待したものです.仕様的にはかなりアレなのですが,とりあえずそのまま.