練習: 数値をパースして表示する
ここからは実践編.学んだ 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 というパイプラインの骨格ができた.次の章からこの骨格に演算子を追加していく.
次の章では,足し算を実装する.