English

実践: 数式パーサの作成

LuneScript の強力な型システム、特に代数的データ型 (Algebric Data Type) とパターンマッチを活用して、数式パーサ(計算機)を作成してみましょう。
このチュートリアルでは、"1 + 2 * 3" のような文字列を解析し、計算結果を出すプログラムをステップ・バイ・ステップで作成します。

目標

以下の機能を実装します。

Step 1: トークン定義

まずは、入力文字列を意味のある単位(トークン)に分解するための型を定義します。
LuneScript の alge (Algebric Data Type) が最適です。

// @lnsFront: skip
// Token 型の定義
alge Token {
    Num( val: int ),      // 数字: 123
    Op( val: str ),       // 演算子: +, -, *, / (, )
    Eof,                  // 文字列の終端
}

Step 2: 字句解析 (Lexer)

文字列を読み取って Token を生成する Lexer クラスを作成します。

// @lnsFront: skip
class Lexer {
    let text: str;
    let len: int;
    let mut pos: int;

    pub fn __init( text:str ) {
        self.text = text;
        self.len = #text;
        self.pos = 1;
    }

    // 現在の文字を取得 (範囲外なら 0 の文字コード)
    fn getChar(): int {
        if self.pos > self.len { return 0; }
        return self.text.byte( self.pos );
    }

    // 次のトークンを取得
    pub fn next() mut: Token {
        // 空白スキップ
        while self.getChar() == ?  {
            self.pos = self.pos + 1;
        }
        
        let ch = self.getChar();
        if ch == 0 { return .Eof; }

        // 数字の場合
        if ch >= ?0 and ch <= ?9 {
            let mut numVal = 0;
            while true {
                let cur = self.getChar();
                if cur < ?0 or cur > ?9 { break; }
                numVal = numVal * 10 + ( cur - ?0 );
                self.pos = self.pos + 1;
            }
            return Token.Num( numVal );
        }

        // 演算子の場合
        self.pos = self.pos + 1;
        return Token.Op( "%c" ( ch ) );
    }
}
? は文字コードリテラルです (?A は 65)。

Step 3: 構文木 (AST) の定義

構文解析の結果を格納する木構造を定義します。ここでも alge が活躍します。

// @lnsFront: skip
alge Ast {
    Val( val: int ),                      // 数値
    Bin( op: str, left: Ast, right: Ast ), // 二項演算 (左辺 op 右辺)
}

Step 4: 構文解析 (Parser)

再帰下降構文解析法 (Recursive Descent Parsing) を用いて、トークン列を AST に変換します。
優先順位を扱うため、Expr (式), Term (項), Factor (因子) にメソッドを分割します。

// @lnsFront: skip
class Parser {
    let mut lexer: Lexer;
    let mut cur: Token;

    pub fn __init( text: str ) {
        self.lexer = new Lexer( text );
        self.cur = self.lexer.next();
    }

    // トークンを進める
    fn next() mut {
        self.cur = self.lexer.next();
    }

    // 因子: 数値 または (式)
    fn parseFactor() mut: Ast {
        match self.cur {
            case .Num( val ) {
                self.next();
                return .Val( val );
            }
            case .Op( op ) {
                if op == "(" {
                    self.next();
                    let node = self.parseExpr();
                    match self.cur {
                       case .Op( nextOp ) {
                           if nextOp == ")" {
                               self.next();
                               return node;
                           }
                       }
                       default {}
                    }
                    print( "Error: missing )" );
                    return node;
                }
            }
            default {
                print( "Error: unexpected token" );
            }
        }
        return .Val( 0 );
    }

    // 項: 掛け算、割り算
    fn parseTerm() mut: Ast {
        let mut left = self.parseFactor();
        while true {
            match self.cur {
                case .Op( op ) {
                    if op == "*" or op == "/" {
                        self.next();
                        let right = self.parseFactor();
                        left = .Bin( op, left, right );
                    } else {
                        break;
                    }
                }
                default { break; }
            }
        }
        return left;
    }

    // 式: 足し算、引き算
    pub fn parseExpr() mut: Ast {
        let mut left = self.parseTerm();
        while true {
            match self.cur {
                case .Op( op ) {
                    if op == "+" or op == "-" {
                        self.next();
                        let right = self.parseTerm();
                        left = .Bin( op, left, right );
                    } else {
                        break;
                    }
                }
                default { break; }
            }
        }
        return left;
    }
}

Step 5: 評価と実行

最後に、生成された AST を再帰的に処理して計算結果を求める関数を作成します。
match 式を使うことで、簡潔に記述できます。

// @lnsFront: skip
fn eval( node: Ast ): int {
    match node {
        case .Val( val ) {
            return val;
        }
        case .Bin( op, left, right ) {
            let l = eval( left );
            let r = eval( right );
            switch op {
                case "+" { return l + r; }
                case "-" { return l - r; }
                case "*" { return l * r; }
                case "/" { return l / r; }
            }
        }
    }
    return 0;
}

// メイン処理
let input = "1 + 2 * (3 + 4)";
let mut parser = new Parser( input );
let ast = parser.parseExpr();
print( "%s = %d" ( input, eval( ast ) ) );

これを実行すると 1 + 2 * (3 + 4) = 15 が出力されます!


完成コード

すべてのコードをまとめたものが以下になります。

alge Token {
    Num( val: int ),
    Op( val: str ),
    Eof,
}

class Lexer {
    let text: str;
    let len: int;
    let mut pos: int;

    pub fn __init( text:str ) {
        self.text = text;
        self.len = #text;
        self.pos = 1;
    }

    fn getChar(): int {
        if self.pos > self.len { return 0; }
        return unwrap self.text.byte( self.pos ) default 0;
    }

    pub fn next() mut: Token {
        while self.getChar() == ?  {
            self.pos = self.pos + 1;
        }
        
        let ch = self.getChar();
        if ch == 0 { return .Eof; }

        if ch >= ?0 and ch <= ?9 {
            let mut numVal = 0;
            while true {
                let cur = self.getChar();
                if cur < ?0 or cur > ?9 { break; }
                numVal = numVal * 10 + ( cur - ?0 );
                self.pos = self.pos + 1;
            }
            return Token.Num( numVal );
        }

        self.pos = self.pos + 1;
        return Token.Op( "%c" ( ch ) );
    }
}

alge Ast {
    Val( val: int ),
    Bin( op: str, left: Ast, right: Ast ),
}

class Parser {
    let mut lexer: Lexer;
    let mut cur: Token;

    pub fn __init( text: str ) {
        self.lexer = new Lexer( text );
        self.cur = self.lexer.next();
    }

    fn next() mut {
        self.cur = self.lexer.next();
    }

    fn parseFactor() mut: Ast {
        match self.cur {
            case .Num( val ) {
                self.next();
                return .Val( val );
            }
            case .Op( op ) {
                if op == "(" {
                    self.next();
                    let node = self.parseExpr();
                    match self.cur {
                       case .Op( nextOp ) {
                           if nextOp == ")" {
                               self.next();
                               return node;
                           }
                       }
                       default {}
                    }
                    print( "Error: missing )" );
                    return node;
                }
            }
            default {
                print( "Error: unexpected token" );
            }
        }
        return .Val( 0 );
    }

    fn parseTerm() mut: Ast {
        let mut left = self.parseFactor();
        while true {
            match self.cur {
                case .Op( op ) {
                    if op == "*" or op == "/" {
                        self.next();
                        let right = self.parseFactor();
                        left = .Bin( op, left, right );
                    } else {
                        break;
                    }
                }
                default { break; }
            }
        }
        return left;
    }

    pub fn parseExpr() mut: Ast {
        let mut left = self.parseTerm();
        while true {
            match self.cur {
                case .Op( op ) {
                    if op == "+" or op == "-" {
                        self.next();
                        let right = self.parseTerm();
                        left = .Bin( op, left, right );
                    } else {
                        break;
                    }
                }
                default { break; }
            }
        }
        return left;
    }
}

fn eval( node: Ast ): int {
    match node {
        case .Val( val ) {
            return val;
        }
        case .Bin( op, left, right ) {
            let l = eval( left );
            let r = eval( right );
            switch op {
                case "+" { return l + r; }
                case "-" { return l - r; }
                case "*" { return l * r; }
                case "/" { return l / r; }
            }
        }
    }
    return 0;
}

// メイン処理
let input = "1 + 2 * (3 + 4)";
let mut parser = new Parser( input );
let ast = parser.parseExpr();
print( "%s = %d" ( input, eval( ast ) ) );