Bitterless Rust

練習: 数値をパースして表示する

ここからは実践編.学んだ Rust の知識を総動員して,四則演算ができる電卓をインクリメンタルに作っていく.

最終的にはこうなる:

"1 + 2 * 3" → [Lexer] → トークン列 → [Parser] → AST → [Eval] → 7.0

でもいきなり全部は作らない.この章ではまず,数値を 1 つ読み取って表示するところから始める.

プロジェクトを作る

cargo new calc
cd calc

以降,src/main.rs を編集していく.

Step 1: Token を定義する

Lexer は文字列を「トークン」に分解するもの.まずはトークンの種類を enum で定義する.今は数値だけ:

#[derive(Debug, Clone, PartialEq)]
enum Token {
    Number(f64),
}

Number(f64) — 数値を表すトークン.中に f64 の値を持つ.

Step 2: Lexer を作る

struct Lexer {
    input: Vec<char>,
    pos: usize,
}

文字列を Vec<char> に変換して 1 文字ずつ処理する.pos が「今どこを読んでいるか」.

なぜ Vec<char> にするのか: Rust の String はインデックスで 1 文字ずつアクセスしにくい(UTF-8 エンコーディングの都合).Vec<char> にすると input[0], input[1] のように簡単にアクセスできる.

impl Lexer {
    fn new(input: String) -> Lexer {
        Lexer {
            input: input.chars().collect(),
            pos: 0,
        }
    }

    fn tokenize(&mut self) -> Vec<Token> {
        let mut tokens = Vec::new();

        while self.pos < self.input.len() {
            let ch = self.input[self.pos];

            match ch {
                // 空白はスキップ
                ' ' | '\t' => {
                    self.pos += 1;
                }

                // 数字が来たら数値を読む
                '0'..='9' => {
                    let token = self.read_number();
                    tokens.push(token);
                }

                // それ以外もスキップ(今は)
                _ => {
                    self.pos += 1;
                }
            }
        }

        tokens
    }

    fn read_number(&mut self) -> Token {
        let start = self.pos;

        // 数字か小数点が続く限り読み進める
        while self.pos < self.input.len()
            && (self.input[self.pos].is_ascii_digit() || self.input[self.pos] == '.')
        {
            self.pos += 1;
        }

        // 読んだ範囲を文字列にして f64 に変換
        let num_str: String = self.input[start..self.pos].iter().collect();
        let num: f64 = num_str.parse().unwrap();

        Token::Number(num)
    }
}

ポイント:

  • '0'..='9' は「'0' から '9' までの文字」を表すパターン
  • read_number は数字と小数点が続く限り読み進めて,最後に f64 に変換する
  • .parse().unwrap() は文字列を数値に変換.失敗したらパニックするが,数字しか読んでいないので大丈夫

Step 3: AST と parse

AST(Abstract Syntax Tree: 抽象構文木)は式の構造を表す木.今は数値だけなので木もへったくれもないけど,後の章で拡張する土台を作っておく:

#[derive(Debug, Clone)]
enum Expr {
    Number(f64),
}

パースも今はシンプル — トークン列の最初の数値を取り出すだけ:

fn parse(tokens: Vec<Token>) -> Expr {
    let token = tokens[0].clone();
    match token {
        Token::Number(n) => Expr::Number(n),
    }
}

Step 4: eval

AST を受け取って計算結果を返す関数.今は数値をそのまま返すだけ:

fn eval(expr: Expr) -> f64 {
    match expr {
        Expr::Number(n) => n,
    }
}

Step 5: 全部つなげる

fn main() {
    let input = String::from("42");

    let mut lexer = Lexer::new(input);
    let tokens = lexer.tokenize();
    println!("Tokens: {:?}", tokens);

    let ast = parse(tokens);
    println!("AST: {:?}", ast);

    let result = eval(ast);
    println!("Result: {}", result);
}
cargo run
Tokens: [Number(42.0)]
AST: Number(42.0)
Result: 42

小数もいける:

fn main() {
    let input = String::from("3.14");

    let mut lexer = Lexer::new(input);
    let tokens = lexer.tokenize();
    let ast = parse(tokens);
    let result = eval(ast);

    println!("{}", result); // 3.14
}

この章の完成コード

#[derive(Debug, Clone, PartialEq)]
enum Token {
    Number(f64),
}

struct Lexer {
    input: Vec<char>,
    pos: usize,
}

impl Lexer {
    fn new(input: String) -> Lexer {
        Lexer {
            input: input.chars().collect(),
            pos: 0,
        }
    }

    fn tokenize(&mut self) -> Vec<Token> {
        let mut tokens = Vec::new();

        while self.pos < self.input.len() {
            let ch = self.input[self.pos];

            match ch {
                ' ' | '\t' => {
                    self.pos += 1;
                }
                '0'..='9' => {
                    let token = self.read_number();
                    tokens.push(token);
                }
                _ => {
                    self.pos += 1;
                }
            }
        }

        tokens
    }

    fn read_number(&mut self) -> Token {
        let start = self.pos;

        while self.pos < self.input.len()
            && (self.input[self.pos].is_ascii_digit() || self.input[self.pos] == '.')
        {
            self.pos += 1;
        }

        let num_str: String = self.input[start..self.pos].iter().collect();
        let num: f64 = num_str.parse().unwrap();

        Token::Number(num)
    }
}

#[derive(Debug, Clone)]
enum Expr {
    Number(f64),
}

fn parse(tokens: Vec<Token>) -> Expr {
    let token = tokens[0].clone();
    match token {
        Token::Number(n) => Expr::Number(n),
    }
}

fn eval(expr: Expr) -> f64 {
    match expr {
        Expr::Number(n) => n,
    }
}

fn main() {
    let input = String::from("42");

    let mut lexer = Lexer::new(input);
    let tokens = lexer.tokenize();
    println!("Tokens: {:?}", tokens);

    let ast = parse(tokens);
    println!("AST: {:?}", ast);

    let result = eval(ast);
    println!("Result: {}", result);
}

やっていることは地味だけど,Lexer → Parser → Eval というパイプラインの骨格ができた.次の章からこの骨格に演算子を追加していく.


次の章では,足し算を実装する.

関連コンテンツ

本に戻る