match
match
Level 2 — Control Flow & Data Structures Pattern matching expression; must be exhaustive over all possible variants.
1. Prerequisites
if/else— The basic branching logic thatmatchoften replaces when things get complex.- Expressions — Because
matchis an expression, it can return a value directly to a variable.
2. Term Category
Rust-specific (the strict safety): match is the Rust equivalent of the switch statement found in C, Java, or JavaScript. However, Rust elevates it by enforcing strict exhaustiveness (you must handle every possible case) and by making it an expression.
3. Explanation
(1) Design Motivation — "Why did we design this?"
The traditional switch statement in older languages is infamous for causing bugs. Two major issues exist:
- Fallthrough: If you forget to write the
breakkeyword at the end of aswitchcase, the code accidentally "falls through" and executes the next case too. - Missing cases: You can easily forget to handle a specific value, leading to unexpected runtime behavior.
Rust designed the match expression to completely eliminate these bugs.
First, there is no "fallthrough" in Rust. Once a match arm is chosen, it executes that block and immediately exits.
Second, match is exhaustive. The compiler will literally refuse to compile your code if it detects that you haven't handled every single possible value. To handle "everything else", Rust uses the _ (underscore) character as a catch-all.
(2) Reality Metaphor
Imagine you are a postal worker sorting physical mail.
A traditional switch statement is like having a few specific slots for standard letters. If a weirdly shaped package arrives, you might just throw it on the floor because you forgot to build a slot for it.
A Rust match statement is like having a strict postmaster hovering over your shoulder. They demand that every single piece of mail has a designated slot. To guarantee this, they force you to put a large "Catch-All" bin (_) at the end of your desk so that absolutely nothing is dropped on the floor.
(3) Rust Code Examples
Short Snippet
let dice_roll = 4;
// Matching against a number.
match dice_roll {
1 => println!("Critical failure!"),
6 => println!("Critical success!"),
// The `_` is the catch-all. It handles 2, 3, 4, 5,
// and literally any other integer.
_ => println!("A normal roll."),
}
Fuller Example
fn main() {
let traffic_light = "Yellow";
// Because `match` is an expression, we can assign the result
// directly to the `action` variable.
let action = match traffic_light {
"Green" => "Go",
"Yellow" => "Slow down",
"Red" => "Stop",
// If the string is anything else (e.g. "Purple" or "Broken"),
// the catch-all handles it safely.
_ => "Proceed with caution",
};
println!("The light is {}. Action: {}", traffic_light, action);
}
4. Common Mistakes & Pitfalls
Mistake 1: Misunderstanding Match Scoping and Lifecycle Rules
The mistake: Assuming Match instances remain valid beyond their declaring scope block or across asynchronous boundaries without explicit lifetime tracking.
Why it's wrong: Rust strictly enforces lexical scope boundaries and non-lexical lifetimes (NLL) at compile time. Accessing dropped values or failing to handle variable drop order results in compiler errors such as E0597 or E0382.
Incorrect:
fn get_ref() -> &str {
let s = String::from("match_data");
&s // ❌ Error E0106/E0515: returns a reference to data owned by the current function
}
Fix:
fn get_string() -> String {
let s = String::from("match_data");
s // Ownership of the String is transferred directly to the caller
}
Mistake 2: Mutating Match State Without Exclusive Ownership or mut Borrowing
The mistake: Attempting to mutate data associated with Match through an immutable reference &T or without specifying mut in variable declarations.
Why it's wrong: Rust's aliasing XOR mutability rule (&T for shared immutable access, &mut T for exclusive mutable access) prohibits mutating state through shared references unless interior mutability patterns (e.g. RefCell, Mutex) are explicitly used.
Incorrect:
fn update_val(data: &i32) {
// *data += 1; // ❌ Error E0594: cannot assign to `*data`, which is behind a `&` reference
}
Fix:
fn update_val(data: &mut i32) {
*data += 1; // Correct: exclusive mutable reference permits mutation
}
Mistake 3: Concurrent Access to Match Across Threads Without Send / Sync Guards
The mistake: Sharing non-thread-safe Match instances across OS threads via std::thread::spawn.
Why it's wrong: Types that do not implement Send or Sync marker traits cannot safely cross thread boundaries. The compiler prevents data races by raising compile errors E0277 (trait Send is not implemented).
Incorrect:
use std::rc::Rc;
use std::thread;
let rc = Rc::new(42);
// thread::spawn(move || { println!("{}", rc); }); // ❌ Error E0277: `Rc` cannot be sent between threads safely
Fix:
use std::sync::Arc;
use std::thread;
let arc = Arc::new(42);
thread::spawn(move || {
println!("{}", arc); // Correct: `Arc` implements `Send` and `Sync`
});
5. Practice Exercises
Exercise 1: Telemetry Network Packet Dispatcher & Filter Engine
Scenario: You are building an edge IoT gateway that ingests telemetry packet frames over TCP/UDP network sockets. The gateway must classify incoming frames and emit routing directives to backend microservices.
Define the telemetry structures and write a function route_frame(frame: &NetworkFrame) -> RoutingDirective that uses pattern matching with destructuring, ranges, pattern binding (@), and match guards (if) to enforce the following business rules:
- Telemetry Frames:
- If
retry_count > 5, quarantine the node with reason"Excessive retries". - If
metricisTemperatureandvalueis out of safe operational range (greater than100.0or less than-40.0), issue aCriticalAlarmcontaining a formatted string message. - If
metricisVoltageandvalueis within nominal operating range (11.4to12.6), issueLogNormal. - All other telemetry frames default to
LogNormal.
- If
- Control Commands:
- Priority
8..=10: route toHighPriorityDispatch. - Priority
1..=7: route toStandardQueue. - Priority
0or any out-of-range priority:DropFrame.
- Priority
- Heartbeat Frames:
- If
status_flags & 0x80 != 0(hardware error bit set), quarantine the node with reason"Hardware fault flag set". - Otherwise, issue
LogNormal.
- If
- Malformed Frames:
- Error code in range
400..=499: quarantine node 0 with reason"Client protocol violation". - Error code in range
500..=599: issueRetryPayloadpreserving the error code. - Any other error code:
DropFrame.
- Error code in range
Answer
Implementation
#[derive(Debug, PartialEq, Clone)]
pub enum MetricType {
Temperature,
Voltage,
Vibration,
}
#[derive(Debug, PartialEq, Clone)]
pub enum NetworkFrame {
Telemetry {
sensor_id: u32,
metric: MetricType,
value: f64,
retry_count: u8,
},
ControlCommand {
client_id: u64,
priority: u8,
},
Heartbeat {
node_id: u32,
status_flags: u8,
},
Malformed {
raw_bytes: Vec<u8>,
error_code: u16,
},
}
#[derive(Debug, PartialEq, Clone)]
pub enum RoutingDirective {
CriticalAlarm { sensor_id: u32, message: String },
LogNormal,
HighPriorityDispatch { client_id: u64 },
StandardQueue { client_id: u64 },
QuarantineNode { id: u32, reason: &'static str },
RetryPayload { code: u16 },
DropFrame,
}
pub fn route_frame(frame: &NetworkFrame) -> RoutingDirective {
match frame {
// 1. Telemetry: Check for excessive retries first via guard
NetworkFrame::Telemetry { sensor_id, retry_count, .. } if *retry_count > 5 => {
RoutingDirective::QuarantineNode {
id: *sensor_id,
reason: "Excessive retries",
}
}
// Telemetry: Temperature out-of-bounds alarm
NetworkFrame::Telemetry {
sensor_id,
metric: MetricType::Temperature,
value,
..
} if *value > 100.0 || *value < -40.0 => {
RoutingDirective::CriticalAlarm {
sensor_id: *sensor_id,
message: format!("Temperature out of bounds: {:.1}", value),
}
}
// Telemetry: Nominal Voltage check using guard range
NetworkFrame::Telemetry {
metric: MetricType::Voltage,
value,
..
} if (11.4..=12.6).contains(value) => RoutingDirective::LogNormal,
// Telemetry fallback
NetworkFrame::Telemetry { .. } => RoutingDirective::LogNormal,
// 2. Control Commands: Sub-range pattern matching with @ binding
NetworkFrame::ControlCommand {
client_id,
priority: 8..=10,
} => RoutingDirective::HighPriorityDispatch { client_id: *client_id },
NetworkFrame::ControlCommand {
client_id,
priority: 1..=7,
} => RoutingDirective::StandardQueue { client_id: *client_id },
NetworkFrame::ControlCommand { .. } => RoutingDirective::DropFrame,
// 3. Heartbeat: Bitwise flag evaluation via guard
NetworkFrame::Heartbeat {
node_id,
status_flags,
} if (status_flags & 0x80) != 0 => RoutingDirective::QuarantineNode {
id: *node_id,
reason: "Hardware fault flag set",
},
NetworkFrame::Heartbeat { .. } => RoutingDirective::LogNormal,
// 4. Malformed: Range patterns and code binding
NetworkFrame::Malformed {
error_code: 400..=499,
..
} => RoutingDirective::QuarantineNode {
id: 0,
reason: "Client protocol violation",
},
NetworkFrame::Malformed {
error_code: code @ 500..=599,
..
} => RoutingDirective::RetryPayload { code: *code },
NetworkFrame::Malformed { .. } => RoutingDirective::DropFrame,
}
}
fn main() {
let frame = NetworkFrame::Telemetry {
sensor_id: 101,
metric: MetricType::Temperature,
value: 105.5,
retry_count: 1,
};
let directive = route_frame(&frame);
println!("Routing result: {:?}", directive);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_telemetry_routing() {
let high_temp = NetworkFrame::Telemetry {
sensor_id: 42,
metric: MetricType::Temperature,
value: 120.0,
retry_count: 0,
};
let directive = route_frame(&high_temp);
assert!(matches!(directive, RoutingDirective::CriticalAlarm { .. }));
assert_eq!(
directive,
RoutingDirective::CriticalAlarm {
sensor_id: 42,
message: "Temperature out of bounds: 120.0".to_string()
}
);
let normal_voltage = NetworkFrame::Telemetry {
sensor_id: 43,
metric: MetricType::Voltage,
value: 12.0,
retry_count: 2,
};
assert_eq!(route_frame(&normal_voltage), RoutingDirective::LogNormal);
let excessive_retries = NetworkFrame::Telemetry {
sensor_id: 44,
metric: MetricType::Vibration,
value: 1.0,
retry_count: 6,
};
assert_eq!(
route_frame(&excessive_retries),
RoutingDirective::QuarantineNode {
id: 44,
reason: "Excessive retries"
}
);
}
#[test]
fn test_control_and_heartbeat_routing() {
let cmd_high = NetworkFrame::ControlCommand {
client_id: 99,
priority: 9,
};
let cmd_low = NetworkFrame::ControlCommand {
client_id: 99,
priority: 0,
};
assert_eq!(
route_frame(&cmd_high),
RoutingDirective::HighPriorityDispatch { client_id: 99 }
);
assert_ne!(route_frame(&cmd_high), route_frame(&cmd_low));
assert_eq!(route_frame(&cmd_low), RoutingDirective::DropFrame);
let hb_fault = NetworkFrame::Heartbeat {
node_id: 7,
status_flags: 0b1000_0001,
};
assert!(matches!(
route_frame(&hb_fault),
RoutingDirective::QuarantineNode { id: 7, .. }
));
}
#[test]
fn test_malformed_frame_routing() {
let client_err = NetworkFrame::Malformed {
raw_bytes: vec![0xDE, 0xAD],
error_code: 404,
};
let server_err = NetworkFrame::Malformed {
raw_bytes: vec![0xBE, 0xEF],
error_code: 503,
};
assert_eq!(
route_frame(&client_err),
RoutingDirective::QuarantineNode {
id: 0,
reason: "Client protocol violation"
}
);
assert_eq!(
route_frame(&server_err),
RoutingDirective::RetryPayload { code: 503 }
);
}
}
Technical Explanation
-
Pattern Matching & Borrowing Semantics: Passing
&NetworkFrameintomatch framecauses pattern variables to bind by reference (&u32,&f64,&u8). To compare primitive values inside match guards, we explicitly dereference parameters (*retry_count > 5,*value > 100.0). -
Match Guard Mechanics: Match guards (
if condition) execute dynamically at runtime after the structural pattern matches. If a match guard evaluates tofalse, control falls through to subsequent match arms. -
Sub-Range & Pattern Binding (
@): The syntaxcode @ 500..=599binds the value oferror_codeto the local variablecodeif and only if it falls within the closed range500..=599. -
Exhaustiveness and Fallthrough Guarantee: Rust compiler guarantees exhaustiveness. Wildcard pattern arms
NetworkFrame::Telemetry { .. }and catch-all arms ensure that all enum variants and all potential integer/floating-point values are handled cleanly.
Exercise 2: Algorithmic Trading Risk Engine & Batch Order Processor
Scenario: An electronic trading execution engine processes incoming orders (OrderRequest) and validates risk controls using multi-variable pattern matching and slice recursion.
Requirements:
-
validate_order(order: &OrderRequest) -> ValidationResult: Match on the tuple(&order.tier, &order.order_type, order.quantity):- Retail accounts:
Marketorders withquantity > 1000: reject with reason"Retail market order size exceeded".Limitorders where total value (price * quantity) exceedsaccount_balance: reject with reason"Insufficient balance".StopLossorders: requireManualReviewRequired.- Standard valid orders:
Approved.
- VIP accounts:
Marketorders withquantity > 50_000: reject with reason"VIP market order size exceeded".Limitorders exceedingaccount_balance: reject with reason"Insufficient balance".StopLossorders:Approved.- Standard valid orders:
Approved.
- Institutional accounts:
quantity > 1_000_000:ManualReviewRequiredfor compliance audit.- Standard valid orders:
Approved.
- Retail accounts:
-
process_batch_slice(orders: &[OrderRequest]) -> (usize, usize, usize): Use slice pattern matching ([]and[head, tail @ ..]) to recursively evaluate order slices and aggregate counts(approved_count, rejected_count, review_count).
Answer
Implementation
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum AccountTier {
Retail,
VIP,
Institutional,
}
#[derive(Debug, PartialEq, Clone)]
pub enum OrderType {
Market { side: &'static str },
Limit { side: &'static str, price: u64 },
StopLoss { trigger_price: u64 },
}
#[derive(Debug, PartialEq, Clone)]
pub struct OrderRequest {
pub order_id: u64,
pub account_id: u64,
pub tier: AccountTier,
pub order_type: OrderType,
pub quantity: u32,
pub account_balance: u64,
}
#[derive(Debug, PartialEq, Clone)]
pub enum ValidationResult {
Approved { order_id: u64 },
RejectedRiskLimit { order_id: u64, reason: &'static str },
ManualReviewRequired { order_id: u64 },
}
pub fn validate_order(order: &OrderRequest) -> ValidationResult {
let id = order.order_id;
match (&order.tier, &order.order_type, order.quantity) {
// Retail Rules
(AccountTier::Retail, OrderType::Market { .. }, qty) if qty > 1000 => {
ValidationResult::RejectedRiskLimit {
order_id: id,
reason: "Retail market order size exceeded",
}
}
(AccountTier::Retail, OrderType::Limit { price, .. }, qty)
if (*price as u128 * qty as u128) > order.account_balance as u128 =>
{
ValidationResult::RejectedRiskLimit {
order_id: id,
reason: "Insufficient balance",
}
}
(AccountTier::Retail, OrderType::StopLoss { .. }, _) => {
ValidationResult::ManualReviewRequired { order_id: id }
}
// VIP Rules
(AccountTier::VIP, OrderType::Market { .. }, qty) if qty > 50_000 => {
ValidationResult::RejectedRiskLimit {
order_id: id,
reason: "VIP market order size exceeded",
}
}
(AccountTier::VIP, OrderType::Limit { price, .. }, qty)
if (*price as u128 * qty as u128) > order.account_balance as u128 =>
{
ValidationResult::RejectedRiskLimit {
order_id: id,
reason: "Insufficient balance",
}
}
// Institutional Rules
(AccountTier::Institutional, _, qty) if qty > 1_000_000 => {
ValidationResult::ManualReviewRequired { order_id: id }
}
// Default approval for all valid combinations
_ => ValidationResult::Approved { order_id: id },
}
}
pub fn process_batch_slice(orders: &[OrderRequest]) -> (usize, usize, usize) {
match orders {
[] => (0, 0, 0),
[head, tail @ ..] => {
let (app, rej, rev) = match validate_order(head) {
ValidationResult::Approved { .. } => (1, 0, 0),
ValidationResult::RejectedRiskLimit { .. } => (0, 1, 0),
ValidationResult::ManualReviewRequired { .. } => (0, 0, 1),
};
let (t_app, t_rej, t_rev) = process_batch_slice(tail);
(app + t_app, rej + t_rej, rev + t_rev)
}
}
}
fn main() {
let req = OrderRequest {
order_id: 1,
account_id: 88,
tier: AccountTier::VIP,
order_type: OrderType::Market { side: "BUY" },
quantity: 500,
account_balance: 10_000_000,
};
println!("Order outcome: {:?}", validate_order(&req));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_retail_orders() {
let retail_market_large = OrderRequest {
order_id: 1,
account_id: 100,
tier: AccountTier::Retail,
order_type: OrderType::Market { side: "BUY" },
quantity: 5000,
account_balance: 100_000,
};
let res = validate_order(&retail_market_large);
assert!(matches!(res, ValidationResult::RejectedRiskLimit { .. }));
assert_eq!(
res,
ValidationResult::RejectedRiskLimit {
order_id: 1,
reason: "Retail market order size exceeded"
}
);
let retail_stop_loss = OrderRequest {
order_id: 2,
account_id: 100,
tier: AccountTier::Retail,
order_type: OrderType::StopLoss { trigger_price: 150 },
quantity: 500,
account_balance: 50_000,
};
assert_eq!(
validate_order(&retail_stop_loss),
ValidationResult::ManualReviewRequired { order_id: 2 }
);
}
#[test]
fn test_batch_slice_processing() {
let orders = vec![
OrderRequest {
order_id: 10,
account_id: 200,
tier: AccountTier::VIP,
order_type: OrderType::Market { side: "BUY" },
quantity: 1000,
account_balance: 1_000_000,
},
OrderRequest {
order_id: 11,
account_id: 201,
tier: AccountTier::Retail,
order_type: OrderType::Market { side: "SELL" },
quantity: 2000,
account_balance: 10_000,
},
OrderRequest {
order_id: 12,
account_id: 202,
tier: AccountTier::Institutional,
order_type: OrderType::Limit { side: "BUY", price: 10 },
quantity: 5_000_000,
account_balance: 100_000_000,
},
];
let counts = process_batch_slice(&orders);
assert_eq!(counts, (1, 1, 1));
assert_ne!(counts, (3, 0, 0));
}
}
Technical Explanation
-
Multi-Variable Tuple Destructuring: By matching on
(&order.tier, &order.order_type, order.quantity), Rust evaluates multiple orthogonal dimensions of state simultaneously without requiring deeply nestedif/elsestatements. -
Slice Pattern Matching &
@Binding: The pattern[head, tail @ ..]splits a slice reference&[T]into its first elementhead: &Tand the remaining sub-slicetail: &[T]. This allows recursive batch evaluation without dynamic memory allocation or vector reallocation. -
Numeric Overflow Prevention: When multiplying
price * quantityin match guard checks, values are safely cast tou128(*price as u128 * qty as u128) to prevent unexpected integer overflow panics during financial limit calculations. -
Exhaustive Wildcards (
_): The fallback arm_ => ValidationResult::Approved { order_id: id }guarantees that all unflagged combinations pass risk validation cleanly.
Exercise 3: Abstract Syntax Tree (AST) Compiler Optimizer & Evaluator
Scenario: Design a constant-folding pass and evaluation engine for a domain-specific mathematical language represented as an AST Expr.
Requirements:
-
fold_constants(expr: Expr) -> Expr: Recursively optimizes the tree using pattern matching overExpr:Binary:- First fold
leftandright. - Evaluate arithmetic operations on two
Literalnodes:Add,Sub,Mul,Div(when divisor != 0). - Apply algebraic identities:
x + 0or0 + x->xx - 0->xx * 0or0 * x->Literal(0)x * 1or1 * x->x
- Otherwise return optimized
Binarynode.
- First fold
Negate:- Fold
inner. Negate(Literal(n))->Literal(-n).Negate(Negate(x))->x(double negation cancellation).- Otherwise return optimized
Negatenode.
- Fold
IfZero:- Fold
cond. - If
condisLiteral(0), return foldedthen_branch. - If
condisLiteral(n)wheren != 0, return foldedelse_branch. - Otherwise return optimized
IfZeronode with folded sub-expressions.
- Fold
Literal(n)andVariable(name)remain unchanged.
-
eval(expr: &Expr, env: &std::collections::HashMap<String, i64>) -> Result<i64, String>: Evaluates an AST node against a variable context environment using pattern matching.
Answer
Implementation
use std::collections::HashMap;
#[derive(Debug, PartialEq, Clone)]
pub enum Operator {
Add,
Sub,
Mul,
Div,
}
#[derive(Debug, PartialEq, Clone)]
pub enum Expr {
Literal(i64),
Variable(String),
Binary {
op: Operator,
left: Box<Expr>,
right: Box<Expr>,
},
Negate(Box<Expr>),
IfZero {
cond: Box<Expr>,
then_branch: Box<Expr>,
else_branch: Box<Expr>,
},
}
pub fn fold_constants(expr: Expr) -> Expr {
match expr {
Expr::Binary { op, left, right } => {
let l_folded = fold_constants(*left);
let r_folded = fold_constants(*right);
match (op, l_folded, r_folded) {
// Constant arithmetic folding
(Operator::Add, Expr::Literal(a), Expr::Literal(b)) => Expr::Literal(a + b),
(Operator::Sub, Expr::Literal(a), Expr::Literal(b)) => Expr::Literal(a - b),
(Operator::Mul, Expr::Literal(a), Expr::Literal(b)) => Expr::Literal(a * b),
(Operator::Div, Expr::Literal(a), Expr::Literal(b)) if b != 0 => Expr::Literal(a / b),
// Algebraic identity optimizations
(Operator::Add, l, Expr::Literal(0)) | (Operator::Add, Expr::Literal(0), l) => l,
(Operator::Sub, l, Expr::Literal(0)) => l,
(Operator::Mul, _, Expr::Literal(0)) | (Operator::Mul, Expr::Literal(0), _) => Expr::Literal(0),
(Operator::Mul, l, Expr::Literal(1)) | (Operator::Mul, Expr::Literal(1), l) => l,
// Non-foldable binary expression
(op, l, r) => Expr::Binary {
op,
left: Box::new(l),
right: Box::new(r),
},
}
}
Expr::Negate(inner) => {
let folded_inner = fold_constants(*inner);
match folded_inner {
Expr::Literal(n) => Expr::Literal(-n),
Expr::Negate(nested) => *nested, // Double negation cancellation
other => Expr::Negate(Box::new(other)),
}
}
Expr::IfZero {
cond,
then_branch,
else_branch,
} => {
let folded_cond = fold_constants(*cond);
match folded_cond {
Expr::Literal(0) => fold_constants(*then_branch),
Expr::Literal(_) => fold_constants(*else_branch),
other_cond => Expr::IfZero {
cond: Box::new(other_cond),
then_branch: Box::new(fold_constants(*then_branch)),
else_branch: Box::new(fold_constants(*else_branch)),
},
}
}
Expr::Literal(val) => Expr::Literal(val),
Expr::Variable(name) => Expr::Variable(name),
}
}
pub fn eval(expr: &Expr, env: &HashMap<String, i64>) -> Result<i64, String> {
match expr {
Expr::Literal(n) => Ok(*n),
Expr::Variable(var_name) => env
.get(var_name)
.copied()
.ok_or_else(|| format!("Undefined variable: {}", var_name)),
Expr::Negate(inner) => eval(inner, env).map(|val| -val),
Expr::Binary { op, left, right } => {
let l_val = eval(left, env)?;
let r_val = eval(right, env)?;
match op {
Operator::Add => Ok(l_val + r_val),
Operator::Sub => Ok(l_val - r_val),
Operator::Mul => Ok(l_val * r_val),
Operator::Div => {
if r_val == 0 {
Err("Division by zero".to_string())
} else {
Ok(l_val / r_val)
}
}
}
}
Expr::IfZero {
cond,
then_branch,
else_branch,
} => {
let c_val = eval(cond, env)?;
if c_val == 0 {
eval(then_branch, env)
} else {
eval(else_branch, env)
}
}
}
}
fn main() {
let expr = Expr::Binary {
op: Operator::Add,
left: Box::new(Expr::Literal(5)),
right: Box::new(Expr::Literal(10)),
};
let folded = fold_constants(expr);
println!("Folded AST: {:?}", folded);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ast_constant_folding() {
// Construct: (x * 1) + (10 + 20)
let unoptimized = Expr::Binary {
op: Operator::Add,
left: Box::new(Expr::Binary {
op: Operator::Mul,
left: Box::new(Expr::Variable("x".to_string())),
right: Box::new(Expr::Literal(1)),
}),
right: Box::new(Expr::Binary {
op: Operator::Add,
left: Box::new(Expr::Literal(10)),
right: Box::new(Expr::Literal(20)),
}),
};
let optimized = fold_constants(unoptimized.clone());
assert_ne!(unoptimized, optimized);
// Should optimize to: x + 30
let expected = Expr::Binary {
op: Operator::Add,
left: Box::new(Expr::Variable("x".to_string())),
right: Box::new(Expr::Literal(30)),
};
assert_eq!(optimized, expected);
}
#[test]
fn test_ast_eval_and_dead_branch_elimination() {
// IfZero(Literal(0), Literal(42), Variable("dead"))
let dead_branch_expr = Expr::IfZero {
cond: Box::new(Expr::Literal(0)),
then_branch: Box::new(Expr::Literal(42)),
else_branch: Box::new(Expr::Variable("dead".to_string())),
};
let folded = fold_constants(dead_branch_expr);
assert!(matches!(folded, Expr::Literal(42)));
assert_eq!(folded, Expr::Literal(42));
let mut env = HashMap::new();
env.insert("x".to_string(), 12);
let res = eval(&folded, &env);
assert_eq!(res, Ok(42));
}
}
Technical Explanation
-
Deep Nested Structural Matching: Rust's
matchcan inspect recursively nested enum payloads in a single arm (e.g.,(Operator::Add, Expr::Literal(a), Expr::Literal(b))). This makes tree transformation passes concise and clear. -
Recursive Ownership Transfers: Because
fold_constantstakesexpr: Exprby value, matching destructures innerBox<Expr>values. Dereferencing*leftmoves the heap-allocated sub-expression out of the box into the recursive call safely. -
Dead Branch Elimination & Algebraic Simplification: When
IfZerohas a known constant conditionLiteral(0)orLiteral(non_zero), pattern matching collapses the entire conditional branch at compile/optimization time, discarding dead sub-trees completely. -
Error Propagation Pattern: In
eval,Result<i64, String>combinesmatchover operators with the?operator for clean recursive error propagation when evaluating missing variables or division by zero.
6. Related Terms
if let/while let— Syntactic sugar for when amatchonly cares about one specific pattern and ignores all others.- Enum — The custom data structure that
matchwas practically built to work hand-in-hand with. if/else— Related concept:if/else.matches!Macro — Related concept:matches!Macro.todo!/unimplemented!/unreachable!— Related concept:todo!/unimplemented!/unreachable!.- Declarative Macros (
macro_rules!) — Related concept: Declarative Macros (macro_rules!).
7. Key Takeaways
matchis a safer, more powerful alternative to longif / else ifchains (and replacesswitch).- It is an expression, meaning it can return a value.
- Every possible value must be handled (exhaustiveness).
- Use the underscore
_as a "catch-all" or "default" case. - There is no "fallthrough" in Rust; only the matching arm is executed, and it automatically breaks.