19
18

More than 5 years have passed since last update.

Go言語で簡単なパーサー書いてみた

Posted at

Go言語は初心者なのでまずいところあると思う。

今回作ったもの

簡単な四則演算パーサー。

こんな感じのBNFを解析しながら実行する。

parser.bnf
top  ::= "?"? line | ""
line ::= (ident "=")? line | expr
expr ::= term (("+" | "-") term)*
term ::= fact (("*" | "/") fact)*
fact ::= "(" expr ")"
       | ("+" | "-") fact
       | number
       | ident

number :::= [0-9]+
ident  :::= [a-zA-Z]+

説明不要だと思うけど、+-*/は四則演算で、=は代入演算子。
先頭に?が付いている場合は結果を表示する。

というどこかで見たことのあるような四則演算機。

実装

ほぼBNFの名前通りにメソッド名が付けられているから分かりやすいと思う。

parser.go
package main

import (
    "bufio"
    "errors"
    "fmt"
    "os"
    "strconv"
    "unicode"
)

//位置情報
type SrcInfo struct {
    Src string
    Row int
    Col int
}

//エラー
type ParseError struct {
    Info    SrcInfo
    Message string
}

//パースの状態
type ParseState struct {
    buffer []rune
    point  int
    info   SrcInfo
}

//変数の環境(グローバル変数かよ!)
var (
    env map[string]int64
)

//エラー表示用メソッド
func (e SrcInfo) Error() string {
    return fmt.Sprintf("%s(%d,%d)",
        e.Src, e.Row, e.Col)
}

func (e ParseError) Error() string {
    return fmt.Sprintf("%s : %s",
        e.Info.Error(), e.Message)
}

//現在のパース位置から文字を取得
func (p *ParseState) at() rune {
    var ret rune
    l := len(p.buffer)
    if l <= p.point {
        ret = 0
    } else {
        ret = p.buffer[p.point]
    }
    return ret
}

//文字を取得して一つ進める
func (p *ParseState) next() rune {
    c := p.at()
    if c == 0 {
        return c
    } else if c == '\n' {
        p.info.Row += 1
        p.info.Col = 1
    } else {
        p.info.Col += 1
    }
    p.point += 1
    return c
}

//パーサーユーティリティ

//ParseErrorを作る
func (p *ParseState) error(message string) error {
    return ParseError{
        Info:    p.info,
        Message: message,
    }
}

//ParseStateのコピー(バックトラック用)
func (p *ParseState) Copy() (copy *ParseState) {
    copy = new(ParseState)
    *copy = *p
    return
}



func NewParseState(src string, buffer string) ParseState {
    return ParseState{
        buffer: []rune(buffer),
        info: SrcInfo{
            Src: src,
            Col: 1,
            Row: 1,
        },
    }
}

//トークンパーサー達

//空白を読み飛ばす
func (p *ParseState) skip() {
    for c := p.at(); unicode.IsSpace(c); c = p.at() {
        p.next()
    }
}

//指定した文字があるかどうか
func (p *ParseState) char(e rune) bool {
    ret := false
    c := p.at()
    if c == e {
        ret = true
        p.next()
        p.skip()
    }
    return ret
}

//数値リテラル
// number :::= [0-9]+
func (p *ParseState) number() (x int64, err error) {
    begin := p.point
    for c := p.at(); unicode.IsDigit(c); c = p.at() {
        p.next()
    }
    end := p.point
    if begin == end {
        return 0, p.error("expect integer")
    }
    buf := string(p.buffer[begin:end])
    x, err = strconv.ParseInt(buf, 10, 64)
    p.skip()
    return
}

//識別子
// ident :::= [a-zA-Z]+
func (p *ParseState) ident() (buf string, err error) {
    begin := p.point
    for c := p.at(); unicode.IsLetter(c); c = p.at() {
        p.next()
    }
    end := p.point
    if begin == end {
        return "", p.error("expect ident")
    }
    buf = string(p.buffer[begin:end])
    p.skip()
    return
}

//パーサー

// top ::= "?"? line | ""
func (p *ParseState) Parse() (display bool, x int64, err error) {
    p.skip()
    if p.at() == 0 {
        return
    }
    display = p.char('?')
    x, err = p.line()
    if err == nil && p.at() != 0 {
        err = p.error("expect operator")
    }
    return
}

// line ::= ident "=" line | expr
func (p *ParseState) line() (x int64, err error) {
    var id string
    p_ := p.Copy()
    if id, err = p.ident(); err == nil {
        if p.char('=') {
            if x, err = p.line(); err != nil {
                return
            }
            env[id] = x
            return
        } else {
            *p = *p_
        }
    }
    return p.expr()
}

// expr ::= term (("+" | "-") term)*
func (p *ParseState) expr() (x int64, err error) {
    x, err = p.term()
    for true {
        var y int64
        if p.char('+') {
            y, err = p.term()
            if err != nil {
                return
            }
            x += y
        } else if p.char('-') {
            y, err = p.term()
            if err != err {
                return
            }
            x -= y
        } else {
            p.skip()
            break
        }
    }
    return
}

// term ::= fact (("*" | "/") fact)*
func (p *ParseState) term() (x int64, err error) {
    x, err = p.fact()
    for true {
        var y int64
        if p.char('*') {
            y, err = p.fact()
            if err != nil {
                return
            }
            x *= y
        } else if p.char('/') {
            y, err = p.fact()
            if err != err {
                return
            }
            if y == 0 {
                return 0, errors.New("error : zero division")
            }
            x /= y
        } else {
            p.skip()
            break
        }
    }
    return
}

// fact ::= "(" expr ")"
//        | ("+" | "-") fact
//        | number
//        | ident
func (p *ParseState) fact() (x int64, err error) {
    if p.char('+') {
        return p.fact()
    }
    if p.char('-') {
        x, err = p.fact()
        x = -x
        return
    }
    if p.char('(') {
        x, err = p.expr()
        if err == nil && !p.char(')') {
            err = p.error("expect ')'")
        }
        return
    }
    if x, err = p.number(); err == nil {
        return
    }
    var id string
    if id, err = p.ident(); err == nil {
        var ok bool
        if x, ok = env[id]; !ok {
            return 0, errors.New("error : undeclared variable '" + id + "'")
        }
        return
    }
    return 0, p.error("except number or ident")
}

//REPLのReadとLoopを司るところ
func PromptLoop(prompt string, loop func(string) error) {
    stdin := bufio.NewReader(os.Stdin)
    for err := error(nil); err == nil; {
        var line string
        fmt.Printf("%s", prompt)
        line, err = stdin.ReadString('\n')
        if err != nil {
            return
        }
        err = loop(line)
    }
}

//初期化
func init() {
    env = make(map[string]int64)
}


func main() {
    fmt.Print(`calculator
Use Ctrl-Z plus return to exit
`)
    PromptLoop("# ", func(line string) error {
        p := NewParseState("prompt", line)
        display, x, err := p.Parse()
        if err != nil {
            fmt.Fprintf(os.Stdout, "%s\n", err.Error())
        } else if display {
            fmt.Printf(">> %d\n", x)
        }
        return nil
    })
    fmt.Print(`
see you
`)
}

エラー処理とか作り込んだら思ったより長くなってしまった。
それでも、C言語で書くよりは短く書けたはず。それ以上に、やや特徴的なオブジェクト指向のおけげで、Javaで同じ事をやろうとしたら設計がかなり面倒な作業になるところも、試行錯誤しながら作っていきやすかった。、Go言語のスケーラブルな仕様はいいと思う。

最初にも書いた通り、Go言語はずぶの素人なので、どなたか添削していただけるとありがたいです。

19
18
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
18