Rust言語のChumskyでパーサ入門 2
前回の続き
前回に引き続きchumskyをいろいろと試します。
今回は、ariadneでエラー表示を試したいと思いますが、その前に前回は大事なことを忘れていました。
それは、 ASTを評価する評価器(eval)を実装し、パースした結果から計算結果を表示することです。
evalの実装
というわけで、evalを実装していきます。
match式で以下の様に処理を分岐し、再帰的に評価するだけで簡単に実装できます。
-
Num()
: ASTが数値の場合に、その数値を返す -
Op()
: opの計算対象を再度eval()
で評価し、再帰します
fn eval(ast:&Expr) -> i64 {
match ast {
Expr::Num(n) => *n,
Expr::Op(expr1, op, expr2) => {
let p1 = eval(expr1);
let p2 = eval(expr2);
let fop = match op {
Op::Add => |x:i64, y:i64| -> i64 {x + y},
Op::Sub => |x:i64, y:i64| -> i64 {x - y},
Op::Mul => |x:i64, y:i64| -> i64 {x * y},
Op::Div => |x:i64, y:i64| -> i64 {x / y},
};
fop(p1, p2)
},
}
}
テストも追加します。
#[cfg(test)]
mod tests {
use chumsky::prelude::*;
use super::*;
#[test]
fn test_calc() {
let parser = parser();
let input = "10 + 2 * 3";
let expect = 10 + 2 * 3;
let ast = parser.parse(input).into_result().unwrap();
assert_eq!(eval(&ast), expect);
let input = "(10 + 2) * 3";
let expect = (10 + 2) * 3;
let ast = parser.parse(input).into_result().unwrap();
assert_eq!(eval(&ast), expect);
let input = "10 / 2 - 5 * 2 + 100";
let expect = 10 / 2 - 5 * 2 + 100;
let ast = parser.parse(input).into_result().unwrap();
assert_eq!(eval(&ast), expect);
}
}
テスト結果
$ cargo test
Compiling chumsky-sample v0.1.0 (/home/test/rust/chumsky-sample)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.74s
Running unittests src/main.rs (target/debug/deps/chumsky_sample-557406b9f4a066aa)
running 1 test
test tests::test_calc ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
ariadneでエラー表示
ariadne自体は単体でも使用できるエラー表示ライブラリとなっていますが、作者がchumskyと同じこともあり簡単に連携できるようになっています。
chumskyでパースした結果、Result
型がErr
の場合、エラー箇所などの情報がベクターで保存されています。
このエラーの型はパーサの作成時の型となっています。
- パーサの型指定
fn parser<'a>() -> impl Parser<'a, &'a str, Expr, extra::Err<Simple<'a, char>>> {
- エラー表示関数の呼び出し
let input = "10 + 2 * 3"; // 10 + (2 * 3) = 16
println!("Input: \"{}\"", input);
match parser.parse(input).into_result() {
Ok(ast) => println!("Parsed AST: {:?}\nResult: {}\n", ast, eval(&ast)),
Err(errors) => error_print(input, errors),
}
- エラー表示関数
Report::build
でエラーレポートを作成します。
今回はextra::Err<Simple<...>>
エラー型を使用しており、パース中にエラーの種類などを指定していないのでエラー表示側ではエラーの種類の特定はできていません。
もっと複雑なエラー表示を行いたい場合にはextra::Err<Rich<...>>
を使用する必要がります。
e.span().into_range()
でエラー箇所のレンジが取得でき e.found()
でエラー箇所のトークンが取得できます。トークンが取得できない場合は入力の終端のエラーとなります。
fn error_print(source: &str, errors: Vec<Simple<'_, char>>) {
let src_id = "test";
for e in errors {
Report::build(ReportKind::Error, (src_id, e.span().into_range()))
.with_message("parse error")
.with_label(
Label::new((src_id, e.span().into_range()))
.with_message(
match e.found() {
Some(c) => format!("unexpected token: {}", c),
None => format!("end of input"),
}
)
.with_color(Color::Red),
)
.finish()
.eprint((src_id, Source::from(source)))
.unwrap();
}
}
- コードの出力例
Input: "10 + * 2"
Error: parse error
╭─[ test:1:6 ]
│
1 │ 10 + * 2
│ ┬
│ ╰── unexpected token: *
───╯
コード全体
上記のコードの全体部分をのせておきます。
use chumsky::prelude::*;
use ariadne::{Color, Label, Report, ReportKind, Source};
#[derive(Debug, PartialEq, Clone)]
enum Op {
Add,
Sub,
Mul,
Div,
}
#[derive(Debug, PartialEq, Clone)]
enum Expr {
Num(i64),
Op(Box<Expr>, Op, Box<Expr>),
}
fn parser<'a>() -> impl Parser<'a, &'a str, Expr, extra::Err<Simple<'a, char>>> {
// 整数をパースするパーサ
let num = just('-')
.or_not()
.then(text::int(10))
.to_slice()
.padded()
.map(|s: &str| Expr::Num(s.parse().unwrap()));
// 括弧をパースするパーサを遅延評価で定義
let expr = recursive(|expr| {
// 最も単純な項(整数または括弧で囲まれた式)
let term = num.or(expr.delimited_by(just('('), just(')'))).padded();
// 乗除算のパーサ
let factor = term.clone().foldl(
just('*')
.to(Op::Mul)
.or(just('/').to(Op::Div))
.then(term)
.repeated(),
|lhs, (op, rhs)| Expr::Op(Box::new(lhs), op, Box::new(rhs)),
);
// 加減算のパーサ
factor.clone().foldl(
just('+')
.to(Op::Add)
.or(just('-').to(Op::Sub))
.then(factor)
.repeated(),
|lhs, (op, rhs)| Expr::Op(Box::new(lhs), op, Box::new(rhs)),
)
});
expr.then_ignore(end())
}
fn eval(ast:&Expr) -> i64 {
match ast {
Expr::Num(n) => *n,
Expr::Op(expr1, op, expr2) => {
let p1 = eval(expr1);
let p2 = eval(expr2);
let fop = match op {
Op::Add => |x:i64, y:i64| -> i64 {x + y},
Op::Sub => |x:i64, y:i64| -> i64 {x - y},
Op::Mul => |x:i64, y:i64| -> i64 {x * y},
Op::Div => |x:i64, y:i64| -> i64 {x / y},
};
fop(p1, p2)
},
}
}
fn error_print(source: &str, errors: Vec<Simple<'_, char>>) {
let src_id = "test";
for e in errors {
Report::build(ReportKind::Error, (src_id, e.span().into_range()))
.with_message("parse error")
.with_label(
Label::new((src_id, e.span().into_range()))
.with_message(
match e.found() {
Some(c) => format!("unexpected token: {}", c),
None => format!("end of input"),
}
)
.with_color(Color::Red),
)
.finish()
.eprint((src_id, Source::from(source)))
.unwrap();
}
}
fn main() {
let parser = parser();
// 実行例1: 優先順位が考慮される
let input = "10 + 2 * 3"; // 10 + (2 * 3) = 16
println!("Input: \"{}\"", input);
match parser.parse(input).into_result() {
Ok(ast) => println!("Parsed AST: {:?}\nResult: {}\n", ast, eval(&ast)),
Err(errors) => error_print(input, errors),
}
// 実行例2: 括弧の処理
let input = "(10 + 2) * 3"; // 12 * 3 = 36
println!("Input: \"{}\"", input);
match parser.parse(input).into_result() {
Ok(ast) => println!("Parsed AST: {:?}\nResult: {}\n", ast, eval(&ast)),
Err(errors) => error_print(input, errors),
}
// 実行例3: 複雑な式
let input = "10 / 2 - 5 * 2 + 100"; // (10 / 2) - (5 * 2) + 100 = 5 - 10 + 100 = 95
println!("Input: \"{}\"", input);
match parser.parse(input).into_result() {
Ok(ast) => println!("Parsed AST: {:?}\nResult: {}\n", ast, eval(&ast)),
Err(errors) => error_print(input, errors),
}
// 実行例4: 無効な式
let input = "10 + * 2";
println!("Input: \"{}\"", input);
match parser.parse(input).into_result() {
Ok(ast) => println!("Parsed AST: {:?}\nResult: {}\n", ast, eval(&ast)),
Err(errors) => error_print(input, errors),
}
// 実行例5: 無効な式 - 閉じ括弧なし
let input = "10 * (5 + 2";
println!("Input: \"{}\"", input);
match parser.parse(input).into_result() {
Ok(ast) => println!("Input: \"{}\"\nParsed AST: {:?}\n", input, ast),
Err(errors) => error_print(input, errors),
}
// 実行例6: 無効な式 - 開き括弧なし
let input = "10 * 5) + 2";
println!("Input: \"{}\"", input);
match parser.parse(input).into_result() {
Ok(ast) => println!("Input: \"{}\"\nParsed AST: {:?}\n", input, ast),
Err(errors) => error_print(input, errors),
}
// 実行例7: 無効な式 - 実数未対応
let input = "10 * 5.2";
println!("Input: \"{}\"", input);
match parser.parse(input).into_result() {
Ok(ast) => println!("Input: \"{}\"\nParsed AST: {:?}\n", input, ast),
Err(errors) => error_print(input, errors),
}
// 実行例8: 無効な式 - 複数エラー
let input = "10 * 5 ) / - 2.2";
println!("Input: \"{}\"", input);
match parser.parse(input).into_result() {
Ok(ast) => println!("Input: \"{}\"\nParsed AST: {:?}\n", input, ast),
Err(errors) => error_print(input, errors),
}
}
#[cfg(test)]
mod tests {
use chumsky::prelude::*;
use super::*;
#[test]
fn test_calc() {
let parser = parser();
let input = "10 + 2 * 3";
let expect = 10 + 2 * 3;
let ast = parser.parse(input).into_result().unwrap();
assert_eq!(eval(&ast), expect);
let input = "(10 + 2) * 3";
let expect = (10 + 2) * 3;
let ast = parser.parse(input).into_result().unwrap();
assert_eq!(eval(&ast), expect);
let input = "10 / 2 - 5 * 2 + 100";
let expect = 10 / 2 - 5 * 2 + 100;
let ast = parser.parse(input).into_result().unwrap();
assert_eq!(eval(&ast), expect);
let input = "(1) + (2) / 2 - 5 * 2 + 100";
let expect = (1) + (2) / 2 - 5 * 2 + 100;
let ast = parser.parse(input).into_result().unwrap();
assert_eq!(eval(&ast), expect);
}
}
cargo run
の出力
Input: "10 + 2 * 3"
Parsed AST: Op(Num(10), Add, Op(Num(2), Mul, Num(3)))
Result: 16
Input: "(10 + 2) * 3"
Parsed AST: Op(Op(Num(10), Add, Num(2)), Mul, Num(3))
Result: 36
Input: "10 / 2 - 5 * 2 + 100"
Parsed AST: Op(Op(Op(Num(10), Div, Num(2)), Sub, Op(Num(5), Mul, Num(2))), Add, Num(100))
Result: 95
Input: "10 + * 2"
Error: parse error
╭─[ test:1:6 ]
│
1 │ 10 + * 2
│ ┬
│ ╰── unexpected token: *
───╯
Input: "(1) + (2) / 2 - 5 * 2 + 100"
Parsed AST: Op(Op(Op(Num(1), Add, Op(Num(2), Div, Num(2))), Sub, Op(Num(5), Mul, Num(2))), Add, Num(100))
Input: "10 * (5 + 2"
Error: parse error
╭─[ test:1:12 ]
│
1 │ 10 * (5 + 2
│ │
│ ╰─ end of input
───╯
Input: "10 * 5) + 2"
Error: parse error
╭─[ test:1:7 ]
│
1 │ 10 * 5) + 2
│ ┬
│ ╰── unexpected token: )
───╯
Input: "10 * 5.2"
Error: parse error
╭─[ test:1:7 ]
│
1 │ 10 * 5.2
│ ┬
│ ╰── unexpected token: .
───╯
Input: "10 * 5 ) / - 2.2"
Error: parse error
╭─[ test:1:8 ]
│
1 │ 10 * 5 ) / - 2.2
│ ┬
│ ╰── unexpected token: )
───╯
最後に
簡単にariadneを使用したエラー表示を行うことができました。
しかし、実用するには、エラーの種類表示やリカバリー処理、複数エラーの対応など、まだまだ難しい部分があります。もっと豊富で典型的なパースの例があれば、より理解しやすいと思うところです。
Discussion