【Rust】電卓に「論理」を。比較演算子の実装と優先順位の階層設計 #6

前回は関数の事前パースを実装してみました。今回はより複雑な論理演算ができるように、Lexerに比較演算子を追加し、Parserの条件を改良していこうと思います。

計算機構築までのワークフロー。比較演算などを追加したくなったので追加

今回は比較演算のパース追加をします。

前回 関数を事前パースする仕組みの実装 : 【Rust】関数の「事前パース」で電卓を高速化!AST管理とデバッグ出力の実装 #5

1. 比較演算子のパース優先度

比較演算子は、=>や <などのことです。比較演算子は、足し算や引き算などよりも最上層に置く(優先順位を低く設定する)のが一般的です。

例えば、1 + 2 < 4 という表記があったとき、足し算と引き算と同じレベルであると、 1 + (2 > 4) と解析されてしまい、意味が分からなくなってしまうため、 (1+2) < 4 と解釈されるようにするためには足し算や引き算よりも前に判断する(優先度が低い)必要があります。

前々回のパースの実装では以下の表のように実装しましたが、この上に比較演算子を解析する層を追加する必要があります。

前々回 parserの実装 : 【Rust】数式を「計算の木」へ:ASTと再帰下降構文解析で電卓エンジンの心臓部を作る #4

表1 前々回のパーステーブル

階層関数名処理内容
最上層expression()加減算 (+, -)
第2層term()乗除算 (*, /)
第3層unary()符号 (-)
第4層power()べき乗 (^) ※右結合
最下層primary()数値、(式)$n、変数、関数呼び出し

比較演算子を一番上に追加すると以下のようなテーブルになります。

表2 比較演算子を追加したパーステーブル

階層関数名処理内容
最上層 (New!)comparison()比較演算 (==, !=, <, >, <=, >=)
第2層 (旧最上層)expression()加減算 (+, -)
第3層 (旧第2層)term()乗除算 (*, /)
第4層 (旧第3層)unary()符号 (-)
第5層 (旧第4層)power()べき乗 (^) ※右結合
最下層primary()数値、(式)$n、変数、関数呼び出し

2. TokenからParserまでの実装

2.1 Tokenに追加

Tokenに 比較演算のトークンを定義します。

pub enum Token {
    Number(f64),
    Ident(String),
    HistoryRef(usize), // $1, $2...
    Plus,
    Minus,
    Star,
    Slash,
    Caret,
    LParen,
    RParen,
    Comma,
    Less,          // < 追加部分
    LessEq,        // <=
    Greater,       // >
    GreaterEq,     // >=
    Equal,         // ==
    NotEq,         // !=
    Assign,        // = (将来の変数代入用にあらかじめ定義しておく)
    EOF,
}

2.2 Lexerを改良

Rustのpeek(1文字先を見て判断させる機能)を使って、比較演算のLexerを追加しました。

// Lexerの中身抜粋
'<' => {
    self.advance(); // '<' を消費
    if self.peek() == Some('=') {
        self.advance(); // '=' を消費
        Token::LessEq
    } else {
        Token::Less
    }
}
'>' => {
    self.advance(); // '>' を消費
    if self.peek() == Some('=') {
        self.advance(); // '=' を消費
        Token::GreaterEq
    } else {
        Token::Greater
    }
}
'=' => {
    self.advance(); // '=' を消費
    if self.peek() == Some('=') {
        self.advance(); // 2つ目の '=' を消費
        Token::Equal
    } else {
        Token::Assign // 単なる '=' 
    }
}
'!' => {
    self.advance(); // '!' を消費
    if self.peek() == Some('=') {
        self.advance(); // '=' を消費
        Token::NotEq
    } else {
        return Err("Expected '=' after '!'".into());
    }
}

2.3 Parserに最上層を追加

前回の実装では、Parserの最上層は加減算演算子でしたが、ここを比較演算子に変えて、比較演算の後に加減算演算子に渡すように変更します。

pub fn parse(&mut self) -> Result<Expr, String> {
    let expr = self.parse_comparison()?;
    if let Some(Token::EOF) = self.tokens.peek() {
        Ok(expr)
    } else {
        Err(format!("Unexpected token: {:?}", self.tokens.peek()))
    }
}

// 0. 比較演算
fn parse_comparison(&mut self) -> Result<Expr, String> {
    let mut left = self.parse_expression()?; // 次は足し算へ

    while let Some(token) = self.tokens.peek() {
        let op = match token {
            Token::Less => BinOp::Less,
            Token::Greater => BinOp::Greater,
            _ => break,
        };
        self.tokens.next();
        let right = self.parse_expression()?;
        left = Expr::BinaryOp {
            left: Box::new(left),
            op,
            right: Box::new(right),
        };
    }
    Ok(left)
}

// 1. 足し算・引き算 (優先順位:低)
fn parse_expression(&mut self) -> Result<Expr, String> { ... }

3. 実行結果

上記の変更などを行い起動、適当に比較演算式を打ち込んでテストしてみました。

比較演算のテスト

想定通りに動作してくれているようです。if文については、中身の定義をしていない(通常関数とは別処理をする必要がある)のでundifined functionとなっていますが、パース自体は成功しました。

比較演算のパース成功

比較演算のパースまでは成功

最後に

今回は、比較演算のif文を扱うために比較演算子のToken定義、LexerとParserでの処理の実装を行いました。これで基本的な、電卓で使いたい機能としてはおおむね受け付けることができるようになったはずです。

次回は、ここまでのパースの結果を評価し、実際に計算可能なのか?(循環参照などがないのかどうか)を評価するEvaluationの実装を行っていこうと思います。

評価機構の実装 : 関連記事は、2026年5月4日に公開予定 (あと3日)

ここまで読んでいただきありがとうございます。

では、次の記事で。 lumenHero

関連記事

第1回 構想編【Rustで作る】ターミナル関数電卓を設計する(アーキテクチャ編) #1
第2回 Lexer実装:【Rust】自作計算機エンジンへの道:Lexer(字句解析)で数式をトークンに分解する #2
第3回 tomlの読み込み:【Rust】TOMLで関数を自由に追加!自作電卓に設定読み込みとホットリロードを実装する #3
第4回 Parserの実装(基礎):【Rust】数式を「計算の木」へ:ASTと再帰下降構文解析で電卓エンジンの心臓部を作る #4
第5回 Parserの実装(関数):【Rust】関数の「事前パース」で電卓を高速化!AST管理とデバッグ出力の実装 #5