traverse_codegen/
invariant_breaker.rs

1//! # Invariant Breaker Module
2//!
3//! Finds counterexamples to Solidity invariant expressions by generating random variable assignments
4//! that make the expression evaluate to `false`.
5//!
6//! ## Usage
7//!
8//! The invariant breaker provides async functions to find counterexamples to Solidity expressions.
9//! Use `break_invariant()` for simple cases or `break_invariant_with_config()` for custom configuration.
10//!
11//! ## Supported Types
12//!
13//! - `Bool`: true/false values
14//! - `UInt`: 0 to u64::MAX  
15//! - `Int`: i64::MIN to i64::MAX
16//! - `String`: random ASCII strings
17//! - `Address`: 20-byte Ethereum addresses (hex)
18//! - `Bytes`: variable-length byte arrays (hex)
19//!
20//! ## Examples
21//!
22//! | Expression | Sample Counterexample |
23//! |------------|----------------------|
24//! | `x > 10` | `x = 5` → `(5 > 10)` |
25//! | `a && b` | `a = false, b = true` → `(false && true)` |
26//! | `balance >= amount` | `balance = 50, amount = 100` → `(50 >= 100)` |
27//!
28//! The module uses the Solidity parser, interpreter, and writer from the `solidity` crate
29//! to parse expressions, evaluate them with random values, and generate concrete output.
30
31use tracing::debug;
32
33use rand::Rng;
34use serde::{Deserialize, Serialize};
35use traverse_solidity::ast::*;
36use traverse_solidity::{
37    format_value_for_expression, parse_expression, write_expression_to_string, SolidityInterpreter,
38    Value,
39};
40use std::collections::HashMap;
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct InvariantBreakerResult {
44    pub success: bool,
45    pub error: Option<String>,
46    pub entries: Vec<InvariantBreakerEntry>,
47    pub original_expression: String,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct InvariantBreakerEntry {
52    pub variables: HashMap<String, InvariantBreakerValue>,
53    pub concrete_expression: String,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub enum InvariantBreakerValue {
58    Bool(bool),
59    UInt(u64),
60    Int(i64),
61    String(String),
62    Address(String),
63    Bytes(Vec<u8>),
64}
65
66impl From<Value> for InvariantBreakerValue {
67    fn from(value: Value) -> Self {
68        match value {
69            Value::Bool(b) => InvariantBreakerValue::Bool(b),
70            Value::UInt(n) => InvariantBreakerValue::UInt(n),
71            Value::Int(n) => InvariantBreakerValue::Int(n),
72            Value::String(s) => InvariantBreakerValue::String(s),
73            Value::Address(addr) => InvariantBreakerValue::Address(addr),
74            Value::Bytes(b) => InvariantBreakerValue::Bytes(b),
75            Value::Null => InvariantBreakerValue::Bool(false), // Convert null to false
76        }
77    }
78}
79
80impl From<InvariantBreakerValue> for Value {
81    fn from(value: InvariantBreakerValue) -> Self {
82        match value {
83            InvariantBreakerValue::Bool(b) => Value::Bool(b),
84            InvariantBreakerValue::UInt(n) => Value::UInt(n),
85            InvariantBreakerValue::Int(n) => Value::Int(n),
86            InvariantBreakerValue::String(s) => Value::String(s),
87            InvariantBreakerValue::Address(addr) => Value::Address(addr),
88            InvariantBreakerValue::Bytes(b) => Value::Bytes(b),
89        }
90    }
91}
92
93impl std::fmt::Display for InvariantBreakerValue {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        match self {
96            InvariantBreakerValue::Bool(b) => write!(f, "{}", b),
97            InvariantBreakerValue::UInt(n) => write!(f, "{}", n),
98            InvariantBreakerValue::Int(n) => write!(f, "{}", n),
99            InvariantBreakerValue::String(s) => write!(f, "\"{}\"", s),
100            InvariantBreakerValue::Address(addr) => write!(f, "{}", addr),
101            InvariantBreakerValue::Bytes(b) => write!(f, "0x{}", hex::encode(b)),
102        }
103    }
104}
105
106#[derive(Debug, Clone)]
107pub struct InvariantBreakerConfig {
108    pub max_attempts: usize,
109    pub max_results: usize,
110    pub seed: Option<u64>,
111}
112
113impl Default for InvariantBreakerConfig {
114    fn default() -> Self {
115        Self {
116            max_attempts: 1000,
117            max_results: 10,
118            seed: None,
119        }
120    }
121}
122
123pub async fn break_invariant(
124    expression: &str,
125) -> Result<InvariantBreakerResult, Box<dyn std::error::Error + Send + Sync>> {
126    break_invariant_with_config(expression, InvariantBreakerConfig::default()).await
127}
128
129pub async fn break_invariant_with_config(
130    expression: &str,
131    config: InvariantBreakerConfig,
132) -> Result<InvariantBreakerResult, Box<dyn std::error::Error + Send + Sync>> {
133    let parsed_expr = match parse_expression(expression) {
134        Ok(expr) => expr,
135        Err(e) => {
136            return Ok(InvariantBreakerResult {
137                success: false,
138                error: Some(format!("Failed to parse expression: {}", e)),
139                entries: vec![],
140                original_expression: expression.to_string(),
141            });
142        }
143    };
144
145    let variables = extract_variables(&parsed_expr);
146
147    if variables.is_empty() {
148        let interpreter = SolidityInterpreter::new();
149        match interpreter.evaluate_predicate(&parsed_expr) {
150            Ok(true) => {
151                return Ok(InvariantBreakerResult {
152                    success: false,
153                    error: Some("Expression is always true (no variables to modify)".to_string()),
154                    entries: vec![],
155                    original_expression: expression.to_string(),
156                });
157            }
158            Ok(false) => {
159                return Ok(InvariantBreakerResult {
160                    success: true,
161                    error: None,
162                    entries: vec![InvariantBreakerEntry {
163                        variables: HashMap::new(),
164                        concrete_expression: expression.to_string(),
165                    }],
166                    original_expression: expression.to_string(),
167                });
168            }
169            Err(e) => {
170                return Ok(InvariantBreakerResult {
171                    success: false,
172                    error: Some(format!("Failed to evaluate expression: {}", e)),
173                    entries: vec![],
174                    original_expression: expression.to_string(),
175                });
176            }
177        }
178    }
179
180    let mut rng = if let Some(seed) = config.seed {
181        use rand::SeedableRng;
182        rand::rngs::StdRng::seed_from_u64(seed)
183    } else {
184        use rand::SeedableRng;
185        rand::rngs::StdRng::from_entropy()
186    };
187
188    // Heuristic: Identify variables likely to be uints based on comparisons with non-negative literals
189    let mut uint_candidates = std::collections::HashSet::new();
190    collect_comparison_vars_recursive(&parsed_expr, &variables, &mut uint_candidates);
191
192    debug!("[INV_BREAK DEBUG] Original Expression: {}", expression);
193    debug!("[INV_BREAK DEBUG] Extracted Variables: {:?}", variables);
194    debug!("[INV_BREAK DEBUG] Uint Candidates: {:?}", uint_candidates);
195
196    let mut entries = Vec::new();
197    let mut attempts = 0;
198
199    while attempts < config.max_attempts && entries.len() < config.max_results {
200        attempts += 1;
201
202        let mut variable_assignments = HashMap::new();
203        for var_name in &variables {
204            let value = if uint_candidates.contains(var_name) {
205                // Strongly prefer UInt, especially 0 and 1 for these candidates
206                let choice = rng.gen_range(0..10); // 10% for 0, 10% for 1
207                if choice == 0 {
208                    Value::UInt(0)
209                } else if choice == 1 {
210                    Value::UInt(1)
211                } else {
212                    Value::UInt(rng.gen_range(2..5000))
213                } // Other small positive uints, expanded range
214            } else {
215                generate_random_value(&mut rng) // Existing general random generation
216            };
217            variable_assignments.insert(var_name.clone(), value);
218        }
219
220        let mut interpreter = SolidityInterpreter::new();
221        for (name, value) in &variable_assignments {
222            interpreter
223                .context_mut()
224                .set_variable(name.clone(), value.clone());
225        }
226
227        debug!(
228            "[INV_BREAK DEBUG] Attempt {}: Variable Assignments: {:?}",
229            attempts,
230            variable_assignments
231                .iter()
232                .map(|(k, v)| (k, format!("{}", v)))
233                .collect::<HashMap<_, _>>()
234        );
235
236        match interpreter.evaluate_predicate(&parsed_expr) {
237            Ok(false) => {
238                debug!(
239                    "[INV_BREAK DEBUG] Attempt {}: Found counterexample (predicate returned false)",
240                    attempts
241                );
242                let concrete_expr =
243                    substitute_variables_in_expression(&parsed_expr, &variable_assignments)?;
244                entries.push(InvariantBreakerEntry {
245                    variables: variable_assignments
246                        .into_iter()
247                        .map(|(k, v)| (k, v.into()))
248                        .collect(),
249                    concrete_expression: concrete_expr,
250                });
251            }
252            Ok(true) => {
253                debug!(
254                    "[INV_BREAK DEBUG] Attempt {}: Predicate returned true",
255                    attempts
256                );
257                // Expression evaluated to true, continue searching
258            }
259            Err(e) => {
260                debug!(
261                    "[INV_BREAK DEBUG] Attempt {}: Error evaluating predicate: {}. Skipping.",
262                    attempts, e
263                );
264                // Error evaluating expression, skip this assignment
265                continue;
266            }
267        }
268    }
269
270    if entries.is_empty() {
271        debug!(
272            "[INV_BREAK DEBUG] No counterexamples found after {} attempts for expression: '{}'",
273            attempts, expression
274        );
275    } else {
276        debug!(
277            "[INV_BREAK DEBUG] Found {} counterexamples for expression: '{}'",
278            entries.len(),
279            expression
280        );
281    }
282
283    Ok(InvariantBreakerResult {
284        success: !entries.is_empty(),
285        error: if entries.is_empty() {
286            Some(format!(
287                "No counterexamples found after {} attempts",
288                attempts
289            ))
290        } else {
291            None
292        },
293        entries,
294        original_expression: expression.to_string(),
295    })
296}
297
298fn extract_variables(expr: &Expression) -> Vec<String> {
299    let mut variables = Vec::new();
300    extract_variables_recursive(expr, &mut variables);
301    variables.sort();
302    variables.dedup();
303    variables
304}
305
306fn collect_comparison_vars_recursive(
307    expr: &Expression,
308    all_vars: &Vec<String>,
309    candidates: &mut std::collections::HashSet<String>,
310) {
311    match expr {
312        Expression::Binary(bin_expr) => {
313            // Check if the current binary expression involves an identifier and a non-negative number literal
314            if matches!(
315                bin_expr.operator,
316                BinaryOperator::GreaterThan
317                    | BinaryOperator::GreaterThanOrEqual
318                    | BinaryOperator::LessThan
319                    | BinaryOperator::LessThanOrEqual
320                    | BinaryOperator::Equal
321                    | BinaryOperator::NotEqual
322            ) {
323                // Case 1: Identifier on the left, Literal on the right
324                if let Expression::Identifier(ref var_name) = *bin_expr.left {
325                    if all_vars.contains(var_name) {
326                        if let Expression::Literal(Literal::Number(ref num_lit)) = *bin_expr.right {
327                            if num_lit.value.parse::<u64>().is_ok()
328                                || num_lit.value.parse::<i64>().is_ok_and(|n| n >= 0)
329                            {
330                                candidates.insert(var_name.clone());
331                            }
332                        }
333                    }
334                }
335                // Case 2: Literal on the left, Identifier on the right
336                if let Expression::Identifier(ref var_name) = *bin_expr.right {
337                    if all_vars.contains(var_name) {
338                        if let Expression::Literal(Literal::Number(ref num_lit)) = *bin_expr.left {
339                            if num_lit.value.parse::<u64>().is_ok()
340                                || num_lit.value.parse::<i64>().is_ok_and(|n| n >= 0)
341                            {
342                                candidates.insert(var_name.clone());
343                            }
344                        }
345                    }
346                }
347            }
348
349            // Recursively check sub-expressions
350            collect_comparison_vars_recursive(&bin_expr.left, all_vars, candidates);
351            collect_comparison_vars_recursive(&bin_expr.right, all_vars, candidates);
352        }
353        Expression::Unary(unary_expr) => {
354            collect_comparison_vars_recursive(&unary_expr.operand, all_vars, candidates);
355        }
356        Expression::FunctionCall(call_expr) => {
357            // It's unlikely the function name itself is a candidate, so we focus on arguments
358            // collect_comparison_vars_recursive(&call_expr.function, all_vars, candidates);
359            for arg in &call_expr.arguments {
360                collect_comparison_vars_recursive(arg, all_vars, candidates);
361            }
362        }
363        Expression::Conditional(cond_expr) => {
364            collect_comparison_vars_recursive(&cond_expr.condition, all_vars, candidates);
365            collect_comparison_vars_recursive(&cond_expr.true_expr, all_vars, candidates);
366            collect_comparison_vars_recursive(&cond_expr.false_expr, all_vars, candidates);
367        }
368        Expression::Assignment(assign_expr) => {
369            // Typically, we are interested in conditions, but assignments might contain comparisons
370            collect_comparison_vars_recursive(&assign_expr.left, all_vars, candidates);
371            collect_comparison_vars_recursive(&assign_expr.right, all_vars, candidates);
372        }
373        Expression::Tuple(tuple_expr) => {
374            for element in &tuple_expr.elements {
375                if let Some(expr_element) = element {
376                    collect_comparison_vars_recursive(expr_element, all_vars, candidates);
377                }
378            }
379        }
380        Expression::Array(array_expr) => {
381            for element in &array_expr.elements {
382                collect_comparison_vars_recursive(element, all_vars, candidates);
383            }
384        }
385        Expression::TypeConversion(conv_expr) => {
386            collect_comparison_vars_recursive(&conv_expr.expression, all_vars, candidates);
387        }
388        // Base cases like Identifier, Literal, New don't need further recursion for this purpose.
389        _ => {}
390    }
391}
392
393fn extract_variables_recursive(expr: &Expression, variables: &mut Vec<String>) {
394    match expr {
395        Expression::Identifier(name) => {
396            if !is_builtin_identifier(name) {
397                variables.push(name.clone());
398            }
399        }
400        Expression::Binary(bin_expr) => {
401            extract_variables_recursive(&bin_expr.left, variables);
402            extract_variables_recursive(&bin_expr.right, variables);
403        }
404        Expression::Unary(unary_expr) => {
405            extract_variables_recursive(&unary_expr.operand, variables);
406        }
407        Expression::FunctionCall(call_expr) => {
408            extract_variables_recursive(&call_expr.function, variables);
409            for arg in &call_expr.arguments {
410                extract_variables_recursive(arg, variables);
411            }
412        }
413        Expression::MemberAccess(member_expr) => {
414            extract_variables_recursive(&member_expr.object, variables);
415        }
416        Expression::IndexAccess(index_expr) => {
417            extract_variables_recursive(&index_expr.object, variables);
418            if let Some(index) = &index_expr.index {
419                extract_variables_recursive(index, variables);
420            }
421        }
422        Expression::Conditional(cond_expr) => {
423            extract_variables_recursive(&cond_expr.condition, variables);
424            extract_variables_recursive(&cond_expr.true_expr, variables);
425            extract_variables_recursive(&cond_expr.false_expr, variables);
426        }
427        Expression::Assignment(assign_expr) => {
428            extract_variables_recursive(&assign_expr.left, variables);
429            extract_variables_recursive(&assign_expr.right, variables);
430        }
431        Expression::Tuple(tuple_expr) => {
432            for element in &tuple_expr.elements {
433                if let Some(expr) = element {
434                    extract_variables_recursive(expr, variables);
435                }
436            }
437        }
438        Expression::Array(array_expr) => {
439            for element in &array_expr.elements {
440                extract_variables_recursive(element, variables);
441            }
442        }
443        Expression::TypeConversion(conv_expr) => {
444            extract_variables_recursive(&conv_expr.expression, variables);
445        }
446        Expression::New(_new_expr) => {
447            // New expressions don't contain variables in the type name
448        }
449        Expression::Literal(_) => {
450            // Literals don't contain variables
451        }
452    }
453}
454
455fn is_builtin_identifier(name: &str) -> bool {
456    matches!(
457        name,
458        "true"
459            | "false"
460            | "msg"
461            | "block"
462            | "tx"
463            | "now"
464            | "keccak256"
465            | "sha256"
466            | "ripemd160"
467            | "ecrecover"
468            | "this"
469            | "super"
470            | "selfdestruct"
471            | "revert"
472            | "require"
473            | "assert"
474            // Solidity types that can be used in function-like casts
475            | "address"
476            | "bool"
477            | "string"
478            | "bytes"
479            | "byte"
480            | "int"
481            | "uint"
482            // Common fixed-size byte arrays
483            | "bytes1" | "bytes2" | "bytes3" | "bytes4" | "bytes5" | "bytes6" | "bytes7" | "bytes8"
484            | "bytes9" | "bytes10" | "bytes11" | "bytes12" | "bytes13" | "bytes14" | "bytes15" | "bytes16"
485            | "bytes17" | "bytes18" | "bytes19" | "bytes20" | "bytes21" | "bytes22" | "bytes23" | "bytes24"
486            | "bytes25" | "bytes26" | "bytes27" | "bytes28" | "bytes29" | "bytes30" | "bytes31" | "bytes32"
487            // Common integer types ( Solidity allows uint8 to uint256, int8 to int256 in steps of 8)
488            // Adding a few common ones, though a regex might be better for full coverage
489            | "uint8" | "uint16" | "uint32" | "uint64" | "uint128" | "uint256"
490            | "int8" | "int16" | "int32" | "int64" | "int128" | "int256"
491            // Other global functions/objects that might appear in expressions but aren't variables to assign
492            | "abi" // e.g. abi.encode
493            | "type" // e.g. type(MyContract).creationCode
494    )
495}
496
497fn generate_random_value(rng: &mut impl Rng) -> Value {
498    match rng.gen_range(0..6) {
499        0 => Value::Bool(rng.gen()),
500        1 => Value::UInt(rng.gen_range(0..5000)), // Expanded range
501        2 => Value::Int(rng.gen_range(-500..500)),
502        3 => {
503            let strings = ["", "hello", "world", "test", "value"];
504            Value::String(strings[rng.gen_range(0..strings.len())].to_string())
505        }
506        4 => {
507            let addresses = [
508                "0x0000000000000000000000000000000000000000",
509                "0x1234567890123456789012345678901234567890",
510                "0xabcdefabcdefabcdefabcdefabcdefabcdefabcdef",
511            ];
512            Value::Address(addresses[rng.gen_range(0..addresses.len())].to_string())
513        }
514        5 => {
515            let byte_arrays = [vec![], vec![0x12, 0x34], vec![0xff, 0x00, 0xaa, 0xbb]];
516            Value::Bytes(byte_arrays[rng.gen_range(0..byte_arrays.len())].clone())
517        }
518        _ => Value::Bool(false),
519    }
520}
521
522fn substitute_variables_in_expression(
523    expr: &Expression,
524    assignments: &HashMap<String, Value>,
525) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
526    Ok(substitute_expression_recursive(expr, assignments))
527}
528
529fn substitute_expression_recursive(
530    expr: &Expression,
531    assignments: &HashMap<String, Value>,
532) -> String {
533    match expr {
534        Expression::Identifier(name) => {
535            if let Some(value) = assignments.get(name) {
536                format_value_for_expression(value)
537            } else {
538                name.clone()
539            }
540        }
541        Expression::Binary(bin_expr) => {
542            let left = substitute_expression_recursive(&bin_expr.left, assignments);
543            let right = substitute_expression_recursive(&bin_expr.right, assignments);
544            let op = bin_expr.operator.to_string();
545            format!("({} {} {})", left, op, right)
546        }
547        Expression::Unary(unary_expr) => {
548            let operand = substitute_expression_recursive(&unary_expr.operand, assignments);
549            let op = unary_expr.operator.to_string();
550            if unary_expr.is_prefix {
551                format!("({}{})", op, operand)
552            } else {
553                format!("({}{})", operand, op)
554            }
555        }
556        _ => {
557            // For other expression types, use the solidity writer
558            write_expression_to_string(expr)
559        }
560    }
561}
562
563#[cfg(test)]
564mod tests {
565    use super::*;
566    use tokio;
567
568    #[tokio::test]
569    async fn test_break_invariant_newvalue_gt_zero() {
570        let expression = "_newValue > 0";
571        let config = InvariantBreakerConfig {
572            max_attempts: 250, // Increased attempts for robustness, especially if 0 isn't hit early
573            max_results: 1,
574            seed: None, // Let's rely on the random generation and prioritization
575        };
576
577        let result = break_invariant_with_config(expression, config)
578            .await
579            .unwrap();
580
581        assert!(
582            result.success,
583            "Invariant breaker should succeed for '{}'. Error: {:?}",
584            expression, result.error
585        );
586        assert!(
587            !result.entries.is_empty(),
588            "Should find at least one counterexample for '{}'",
589            expression
590        );
591
592        let entry = &result.entries[0];
593        assert!(
594            entry.variables.contains_key("_newValue"),
595            "Counterexample should contain '_newValue'. Variables: {:?}",
596            entry.variables
597        );
598
599        let new_value_opt = entry.variables.get("_newValue");
600        assert!(new_value_opt.is_some(), "_newValue not found in variables");
601
602        match new_value_opt.unwrap() {
603            InvariantBreakerValue::UInt(val) => {
604                assert_eq!(
605                    *val, 0,
606                    "For '_newValue > 0', the counterexample _newValue should be 0. Got {}",
607                    val
608                );
609                assert!(
610                    entry.concrete_expression.contains("0 > 0")
611                        || entry.concrete_expression.contains("0>0"),
612                    "Concrete expression '{}' did not match expected pattern for _newValue = 0.",
613                    entry.concrete_expression
614                );
615            }
616            other_type => {
617                panic!(
618                    "'_newValue' should be a UInt, but got {:?}. Variables: {:?}",
619                    other_type, entry.variables
620                );
621            }
622        }
623    }
624
625    #[tokio::test]
626    async fn test_break_invariant_simple_expression() {
627        let result = break_invariant("x > 10").await.unwrap();
628        assert!(result.success);
629        assert!(!result.entries.is_empty());
630
631        // Check that at least one entry has x <= 10
632        let has_counterexample = result.entries.iter().any(|entry| {
633            if let Some(InvariantBreakerValue::UInt(x)) = entry.variables.get("x") {
634                *x <= 10
635            } else if let Some(InvariantBreakerValue::Int(x)) = entry.variables.get("x") {
636                *x <= 10
637            } else {
638                false
639            }
640        });
641        assert!(has_counterexample);
642    }
643
644    #[tokio::test]
645    async fn test_break_invariant_boolean_expression() {
646        let result = break_invariant("isActive && hasPermission").await.unwrap();
647        assert!(result.success);
648        assert!(!result.entries.is_empty());
649
650        // Check that at least one entry has either isActive=false or hasPermission=false
651        let has_counterexample = result.entries.iter().any(|entry| {
652            let is_active = entry.variables.get("isActive");
653            let has_permission = entry.variables.get("hasPermission");
654
655            matches!(is_active, Some(InvariantBreakerValue::Bool(false)))
656                || matches!(has_permission, Some(InvariantBreakerValue::Bool(false)))
657        });
658        assert!(has_counterexample);
659    }
660
661    #[tokio::test]
662    async fn test_break_invariant_always_true_expression() {
663        let result = break_invariant("true").await.unwrap();
664        assert!(!result.success);
665        assert!(result.error.is_some());
666    }
667
668    #[tokio::test]
669    async fn test_break_invariant_always_false_expression() {
670        let result = break_invariant("false").await.unwrap();
671        assert!(result.success);
672        assert_eq!(result.entries.len(), 1);
673        assert!(result.entries[0].variables.is_empty());
674    }
675
676    #[tokio::test]
677    async fn test_extract_variables() {
678        // Create a simple binary expression: x > y
679        let expr = Expression::Binary(BinaryExpression {
680            left: Box::new(Expression::Identifier("x".to_string())),
681            operator: BinaryOperator::GreaterThan,
682            right: Box::new(Expression::Identifier("y".to_string())),
683        });
684
685        let variables = extract_variables(&expr);
686        assert_eq!(variables, vec!["x", "y"]);
687    }
688
689    #[tokio::test]
690    async fn test_substitute_variables() {
691        let expr = Expression::Binary(BinaryExpression {
692            left: Box::new(Expression::Identifier("x".to_string())),
693            operator: BinaryOperator::GreaterThan,
694            right: Box::new(Expression::Identifier("y".to_string())),
695        });
696
697        let mut assignments = HashMap::new();
698        assignments.insert("x".to_string(), Value::UInt(15));
699        assignments.insert("y".to_string(), Value::UInt(10));
700
701        let result = substitute_variables_in_expression(&expr, &assignments).unwrap();
702        assert_eq!(result, "(15 > 10)");
703    }
704}