Infix to Postfix in Rust

A complete infix to postfix converter in Rust, plus a postfix evaluator. Both compile warning-free under rustc -O and were run before publishing.

The complete Rust program

Save this as infix_to_postfix.rs. It uses only the standard library, handles multi-character operands, all six operators and nested brackets, and includes a postfix evaluator so you can check a conversion numerically.

infix_to_postfix.rs
//! infix_to_postfix.rs — convert an infix expression to postfix (RPN)
//! using Dijkstra's shunting-yard algorithm.
//!
//! Build and run:
//!     rustc -O infix_to_postfix.rs
//!     ./infix_to_postfix "A + B * C"

use std::env;
use std::fmt;

#[derive(Debug)]
pub enum ConvertError {
    UnbalancedOpen,
    UnbalancedClose,
    UnexpectedChar(char, usize),
    MalformedPostfix,
    NotANumber(String),
}

impl fmt::Display for ConvertError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            ConvertError::UnbalancedOpen => write!(f, "unbalanced brackets: missing \")\""),
            ConvertError::UnbalancedClose => write!(f, "unbalanced brackets: missing \"(\""),
            ConvertError::UnexpectedChar(c, i) => {
                write!(f, "unexpected character '{}' at position {}", c, i)
            }
            ConvertError::MalformedPostfix => write!(f, "malformed postfix expression"),
            ConvertError::NotANumber(t) => write!(f, "'{}' is not a number", t),
        }
    }
}

/// Binding power. Higher wins; equal levels are settled by associativity.
fn precedence(op: char) -> Option<u8> {
    match op {
        '+' | '-' => Some(1),
        '*' | '/' | '%' => Some(2),
        '^' => Some(3),
        _ => None,
    }
}

/// Only `^` groups right to left.
fn is_right_associative(op: char) -> bool {
    op == '^'
}

fn is_operand_char(c: char) -> bool {
    c.is_alphanumeric() || c == '_' || c == '.'
}

/// Convert an infix expression to its postfix form.
pub fn to_postfix(expression: &str) -> Result<String, ConvertError> {
    let chars: Vec<char> = expression.chars().collect();
    let mut output: Vec<String> = Vec::new();
    let mut stack: Vec<char> = Vec::new();
    let mut i = 0;

    while i < chars.len() {
        let ch = chars[i];

        if ch.is_whitespace() {
            i += 1;
            continue;
        }

        // Operands may be several characters: 12, 3.5, count, x1.
        if is_operand_char(ch) {
            let start = i;
            while i < chars.len() && is_operand_char(chars[i]) {
                i += 1;
            }
            output.push(chars[start..i].iter().collect());
            continue;
        }

        if ch == '(' {
            stack.push(ch);
        } else if ch == ')' {
            while let Some(&top) = stack.last() {
                if top == '(' {
                    break;
                }
                output.push(top.to_string());
                stack.pop();
            }
            if stack.pop().is_none() {
                return Err(ConvertError::UnbalancedClose);
            }
        } else {
            let this = precedence(ch).ok_or(ConvertError::UnexpectedChar(ch, i))?;
            while let Some(&top) = stack.last() {
                if top == '(' {
                    break;
                }
                let top_precedence = precedence(top).unwrap();
                let higher = top_precedence > this;
                let equal_left = top_precedence == this && !is_right_associative(ch);
                if !higher && !equal_left {
                    break;
                }
                output.push(top.to_string());
                stack.pop();
            }
            stack.push(ch);
        }

        i += 1;
    }

    while let Some(top) = stack.pop() {
        if top == '(' {
            return Err(ConvertError::UnbalancedOpen);
        }
        output.push(top.to_string());
    }

    Ok(output.join(" "))
}

/// Evaluate a postfix expression whose operands are numbers.
pub fn evaluate_postfix(postfix: &str) -> Result<f64, ConvertError> {
    let mut stack: Vec<f64> = Vec::new();

    for token in postfix.split_whitespace() {
        let mut it = token.chars();
        let first = it.next().unwrap_or(' ');

        if token.chars().count() == 1 && precedence(first).is_some() {
            let b = stack.pop().ok_or(ConvertError::MalformedPostfix)?;
            let a = stack.pop().ok_or(ConvertError::MalformedPostfix)?;
            stack.push(match first {
                '+' => a + b,
                '-' => a - b,
                '*' => a * b,
                '/' => a / b,
                '%' => (a as i64 % b as i64) as f64,
                '^' => a.powf(b),
                _ => unreachable!(),
            });
            continue;
        }

        let number: f64 = token
            .parse()
            .map_err(|_| ConvertError::NotANumber(token.to_string()))?;
        stack.push(number);
    }

    if stack.len() != 1 {
        return Err(ConvertError::MalformedPostfix);
    }
    Ok(stack[0])
}

fn main() {
    let args: Vec<String> = env::args().skip(1).collect();
    let expression = if args.is_empty() {
        "A + B * C - (D / E)".to_string()
    } else {
        args.join(" ")
    };

    match to_postfix(&expression) {
        Ok(postfix) => {
            println!("Infix:   {}", expression);
            println!("Postfix: {}", postfix);
        }
        Err(e) => {
            eprintln!("error: {}", e);
            std::process::exit(1);
        }
    }
}

Build and run it

rustc -O infix_to_postfix.rs
./infix_to_postfix "A + B * C - (D / E)"

Sample output

Infix:   A + B * C - (D / E)
Postfix: A B C * + D E / -

Run it with no arguments and it converts that same default expression, which is a quick way to confirm the build works before trusting it with your own input.

How it works

The algorithm keeps two collections: an output list that only ever grows, and a stack holding operators and left brackets while they wait for their operands. Every token falls into one of four cases.

  • Operand — goes straight to the output; it never touches the stack.
  • Left bracket — pushed, where it acts as a fence nothing pops past.
  • Right bracket — pop to the output until the matching left bracket surfaces, then discard both. Brackets never appear in postfix.
  • Operator — pop while the operator on top binds at least as tightly, then push. That one condition is where precedence and associativity live.

Tokenizing as a separate step is what allows multi-character operands. Walking the string one character at a time would read x1 as two operands and quietly produce the wrong answer.

The stack in Rust

Vec<char> is the stack. push and pop are inherent methods, and last() peeks without moving anything out — the borrow checker is what forces that distinction, and it is the right one.

stack.push(ch);                        // push
while let Some(&top) = stack.last() {  // peek, without taking ownership
    // ...
    stack.pop();                       // pop
}

What is idiomatic here

Errors are a type, not a string

ConvertError is an enum with a variant per failure, and Display turns it into a message. Callers can match on the variant instead of parsing English.

? and ok_or

precedence(ch).ok_or(ConvertError::UnexpectedChar(ch, i))? turns "not an operator" into an early return in one line, which is why the main loop stays flat.

The two rules that decide correctness

Nearly every wrong implementation gets one of these two wrong, and both live in the same loop condition.

Right-associativity of ^

A ^ B ^ C means A ^ (B ^ C), so the postfix form is A B C ^ ^ — not A B ^ C ^. That is why the loop pops on equal precedence only when the incoming operator is not right-associative.

Where % belongs

Modulo shares precedence with * and /, not with ^. Putting it on the wrong level changes the answer for any expression that mixes it with multiplication.

Evaluating the postfix result

Evaluation is the easier half: push operands, and on an operator pop two values, apply, push the result. Order matters — the second value popped is the left operand, which is what makes - and / come out the right way round. Converting and then evaluating 3 + 4 * 2 / (1 - 5) ^ 2 gives 3.5.

FAQ

Why collect into a vector of chars rather than indexing the string directly?

Rust strings are UTF-8, so they cannot be indexed by position. Collecting into Vec<char> once gives O(1) indexing and keeps positions meaningful in error messages.

Does this handle multi-digit numbers and variable names?

Yes. is_operand_char accepts alphanumerics, underscore and dot, and the loop consumes a whole run of them as one operand.

Is there any unsafe code?

None. The whole listing is safe Rust, and it compiles with no warnings.

Check your output: paste any expression into the converter to see the same result with the full stack trace, or compare implementations in C · C++ · Python · Java · JavaScript · Go · C#.