Skip to main content

soldb_debugger/
condition.rs

1//! Conditions on a breakpoint: `break TestContract.sol:30 if counter > 4`.
2//!
3//! A condition is a comparison, or several joined by `&&` and `||`, over values the
4//! debugger can actually read at a step: the local variables in scope (inferred from the
5//! legacy stack layout, see [`StepMap::locals_at`]), state variables through the storage
6//! layout (including `balances[0xabc]` and `config.limit`), the arguments of the frame
7//! being entered, enum literals such as `Color.Red`, and a few facts about the step
8//! itself (`pc`, `gas`, `depth`, `op`, `step`). There is no arithmetic and no calls: a
9//! debugger that evaluated Solidity would have to execute it, and this crate never
10//! executes anything.
11//!
12//! A condition that cannot be evaluated — a slot the transaction never touched, a name
13//! nothing defines, a comparison between a number and a string — does not stop, and says
14//! why through [`Evaluation::Unavailable`]. That is deliberate: stopping on an unreadable
15//! condition would be a false positive, and silently treating it as false would hide the
16//! reason it never fires.
17
18use soldb_core::TransactionTrace;
19use soldb_ethdebug::{parse_word, StorageLayout, Word};
20
21use crate::state::{state_value, StorageWords};
22use crate::stepping::{ContractDebugInfo, Frame, StepMap};
23use crate::DebugValueStatus;
24
25/// A parsed breakpoint condition.
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct Condition {
28    /// `||` groups of `&&` terms: the condition holds when any group holds.
29    groups: Vec<Vec<Comparison>>,
30    text: String,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
34struct Comparison {
35    left: Operand,
36    /// `None` for a bare operand, which holds when it is true or non-zero.
37    operator: Option<Operator>,
38    right: Option<Operand>,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42enum Operator {
43    Equal,
44    NotEqual,
45    Less,
46    LessOrEqual,
47    Greater,
48    GreaterOrEqual,
49}
50
51impl Operator {
52    fn parse(text: &str) -> Option<Self> {
53        match text {
54            "==" => Some(Self::Equal),
55            "!=" => Some(Self::NotEqual),
56            "<" => Some(Self::Less),
57            "<=" => Some(Self::LessOrEqual),
58            ">" => Some(Self::Greater),
59            ">=" => Some(Self::GreaterOrEqual),
60            _ => None,
61        }
62    }
63
64    fn holds(self, ordering: std::cmp::Ordering) -> bool {
65        use std::cmp::Ordering;
66        match self {
67            Self::Equal => ordering == Ordering::Equal,
68            Self::NotEqual => ordering != Ordering::Equal,
69            Self::Less => ordering == Ordering::Less,
70            Self::LessOrEqual => ordering != Ordering::Greater,
71            Self::Greater => ordering == Ordering::Greater,
72            Self::GreaterOrEqual => ordering != Ordering::Less,
73        }
74    }
75}
76
77/// One side of a comparison.
78#[derive(Debug, Clone, PartialEq, Eq)]
79enum Operand {
80    /// A number, address, or `0x` word written in the condition.
81    Literal(Word),
82    Bool(bool),
83    /// A quoted string, which only compares equal to another string.
84    Text(String),
85    /// A name to look up: a local variable, a state variable path, a frame argument, an
86    /// enum literal, or a step fact.
87    Name(String),
88}
89
90/// A value a condition can compare.
91#[derive(Debug, Clone, PartialEq, Eq)]
92pub enum Value {
93    /// A 256-bit word, and whether its type is signed.
94    Word(Word, bool),
95    Bool(bool),
96    Text(String),
97}
98
99/// What a condition evaluated to at one step.
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub enum Evaluation {
102    True,
103    False,
104    /// The condition could not be evaluated here, and why.
105    Unavailable(String),
106}
107
108impl Condition {
109    /// Parses `counter > 4`, `amount == 5 && pc != 12`, or `active`.
110    pub fn parse(input: &str) -> Result<Self, String> {
111        let text = input.trim().to_owned();
112        if text.is_empty() {
113            return Err("a condition cannot be empty".to_owned());
114        }
115        let mut groups = Vec::new();
116        for group in text.split("||") {
117            let mut terms = Vec::new();
118            for term in group.split("&&") {
119                terms.push(Comparison::parse(term.trim())?);
120            }
121            groups.push(terms);
122        }
123        Ok(Self { groups, text })
124    }
125
126    /// The condition as the user wrote it.
127    #[must_use]
128    pub fn text(&self) -> &str {
129        &self.text
130    }
131
132    /// Whether the condition holds at the step `context` describes.
133    #[must_use]
134    pub fn evaluate(&self, context: &ConditionContext<'_>) -> Evaluation {
135        let mut unavailable = None;
136        for group in &self.groups {
137            let mut holds = true;
138            for comparison in group {
139                match comparison.evaluate(context) {
140                    Evaluation::True => {}
141                    Evaluation::False => {
142                        holds = false;
143                        break;
144                    }
145                    Evaluation::Unavailable(reason) => {
146                        unavailable.get_or_insert(reason);
147                        holds = false;
148                        break;
149                    }
150                }
151            }
152            if holds {
153                return Evaluation::True;
154            }
155        }
156        match unavailable {
157            Some(reason) => Evaluation::Unavailable(reason),
158            None => Evaluation::False,
159        }
160    }
161}
162
163impl Comparison {
164    fn parse(input: &str) -> Result<Self, String> {
165        if input.is_empty() {
166            return Err("a condition cannot have an empty term".to_owned());
167        }
168        // The two-character operators first, so `<=` does not parse as `<`.
169        for symbol in ["==", "!=", "<=", ">=", "<", ">"] {
170            let Some(index) = find_operator(input, symbol) else {
171                continue;
172            };
173            let (left, rest) = input.split_at(index);
174            let right = &rest[symbol.len()..];
175            let operator = Operator::parse(symbol).expect("listed operator");
176            return Ok(Self {
177                left: Operand::parse(left.trim())?,
178                operator: Some(operator),
179                right: Some(Operand::parse(right.trim())?),
180            });
181        }
182        Ok(Self {
183            left: Operand::parse(input)?,
184            operator: None,
185            right: None,
186        })
187    }
188
189    fn evaluate(&self, context: &ConditionContext<'_>) -> Evaluation {
190        let left = match context.resolve(&self.left) {
191            Ok(value) => value,
192            Err(reason) => return Evaluation::Unavailable(reason),
193        };
194        let (Some(operator), Some(right)) = (self.operator, self.right.as_ref()) else {
195            // A bare operand: true, or a non-zero word.
196            return match left {
197                Value::Bool(value) => Evaluation::from(value),
198                Value::Word(word, _) => Evaluation::from(word != [0_u8; 32]),
199                Value::Text(text) => Evaluation::from(!text.is_empty()),
200            };
201        };
202        let right = match context.resolve(right) {
203            Ok(value) => value,
204            Err(reason) => return Evaluation::Unavailable(reason),
205        };
206        match compare(&left, &right) {
207            Ok(ordering) => Evaluation::from(operator.holds(ordering)),
208            Err(reason) => Evaluation::Unavailable(reason),
209        }
210    }
211}
212
213impl From<bool> for Evaluation {
214    fn from(value: bool) -> Self {
215        if value {
216            Self::True
217        } else {
218            Self::False
219        }
220    }
221}
222
223/// Compares two values, or says why they cannot be compared.
224fn compare(left: &Value, right: &Value) -> Result<std::cmp::Ordering, String> {
225    match (left, right) {
226        (Value::Bool(left), Value::Bool(right)) => Ok(left.cmp(right)),
227        (Value::Text(left), Value::Text(right)) => Ok(left.cmp(right)),
228        (Value::Word(left, left_signed), Value::Word(right, right_signed)) => {
229            if *left_signed || *right_signed {
230                return Ok(signed_value(left).cmp(&signed_value(right)));
231            }
232            Ok(left.cmp(right))
233        }
234        // A bool compares with 0 and 1, which is how a user writes `active == 1`.
235        (Value::Bool(left), Value::Word(right, _)) => Ok(word_of_bool(*left).cmp(right)),
236        (Value::Word(left, _), Value::Bool(right)) => Ok(left.cmp(&word_of_bool(*right))),
237        _ => Err("a number and a string cannot be compared".to_owned()),
238    }
239}
240
241/// A word as a signed 256-bit number, for ordering: the sign bit inverts the order of the
242/// two halves, so a negative value sorts below every non-negative one.
243fn signed_value(word: &Word) -> (bool, Word) {
244    let negative = word[0] & 0x80 != 0;
245    // Negatives compare among themselves in the same direction as their two's complement
246    // bit patterns, so only the halves need separating.
247    (!negative, *word)
248}
249
250fn word_of_bool(value: bool) -> Word {
251    let mut word = [0_u8; 32];
252    word[31] = u8::from(value);
253    word
254}
255
256/// Finds `symbol` outside a `[...]` index or a quoted string, so `balances[0x1] == 2`
257/// splits at the right place.
258fn find_operator(input: &str, symbol: &str) -> Option<usize> {
259    let bytes = input.as_bytes();
260    let mut depth = 0_i32;
261    let mut quoted = false;
262    for index in 0..bytes.len() {
263        match bytes[index] {
264            b'"' => quoted = !quoted,
265            b'[' if !quoted => depth += 1,
266            b']' if !quoted => depth -= 1,
267            _ => {}
268        }
269        if quoted || depth != 0 {
270            continue;
271        }
272        if input[index..].starts_with(symbol) {
273            // `<` must not match the `<` of `<=`, which the caller tries first, and `=`
274            // of `==` must not split `!=`.
275            return Some(index);
276        }
277    }
278    None
279}
280
281impl Operand {
282    fn parse(input: &str) -> Result<Self, String> {
283        let input = input.trim();
284        if input.is_empty() {
285            return Err("a comparison is missing one side".to_owned());
286        }
287        if let Some(text) = input
288            .strip_prefix('"')
289            .and_then(|rest| rest.strip_suffix('"'))
290        {
291            return Ok(Self::Text(text.to_owned()));
292        }
293        match input {
294            "true" => return Ok(Self::Bool(true)),
295            "false" => return Ok(Self::Bool(false)),
296            _ => {}
297        }
298        if input.starts_with("0x") || input.bytes().all(|byte| byte.is_ascii_digit()) {
299            return parse_word(input)
300                .map(Self::Literal)
301                .map_err(|error| error.to_string());
302        }
303        Ok(Self::Name(input.to_owned()))
304    }
305}
306
307/// Everything a condition can read at one step.
308pub struct ConditionContext<'a> {
309    map: &'a StepMap,
310    step: usize,
311    words: Option<StorageWords<'a>>,
312    op: &'a str,
313    pc: u64,
314    gas: u64,
315    depth: u64,
316    /// The whole trace, for reading the locals in scope at the step.
317    trace: Option<&'a TransactionTrace>,
318    /// The frame being entered or executing, for reading its arguments by name.
319    frame: Option<&'a Frame>,
320}
321
322impl<'a> ConditionContext<'a> {
323    /// The context at `step`, given the trace step's own facts.
324    #[must_use]
325    pub fn new(
326        map: &'a StepMap,
327        step: usize,
328        trace_step: soldb_core::StepView<'a>,
329        words: Option<StorageWords<'a>>,
330    ) -> Self {
331        Self {
332            map,
333            step,
334            words,
335            op: trace_step.mnemonic(),
336            pc: trace_step.pc(),
337            gas: trace_step.gas(),
338            depth: trace_step.depth(),
339            trace: None,
340            frame: None,
341        }
342    }
343
344    /// Reads the local variables in scope at the step, and paths through them, from
345    /// the trace; see [`StepMap::local_condition_value`].
346    #[must_use]
347    pub fn with_trace(mut self, trace: &'a TransactionTrace) -> Self {
348        self.trace = Some(trace);
349        self
350    }
351
352    /// Reads the arguments of this frame by name, when the trace proved them.
353    #[must_use]
354    pub fn with_frame(mut self, frame: Option<&'a Frame>) -> Self {
355        self.frame = frame;
356        self
357    }
358
359    fn resolve(&self, operand: &Operand) -> Result<Value, String> {
360        match operand {
361            Operand::Literal(word) => Ok(Value::Word(*word, false)),
362            Operand::Bool(value) => Ok(Value::Bool(*value)),
363            Operand::Text(text) => Ok(Value::Text(text.clone())),
364            Operand::Name(name) => self.value(name),
365        }
366    }
367
368    /// The value of a name: a fact about the step, a local variable in scope, an argument
369    /// of the frame, a state variable read through the storage layout, or an enum literal.
370    fn value(&self, name: &str) -> Result<Value, String> {
371        match name {
372            "pc" => return Ok(Value::Word(word_of_u64(self.pc), false)),
373            "gas" => return Ok(Value::Word(word_of_u64(self.gas), false)),
374            "depth" => return Ok(Value::Word(word_of_u64(self.depth), false)),
375            "step" => {
376                return Ok(Value::Word(word_of_u64(self.step as u64), false));
377            }
378            "op" => return Ok(Value::Text(self.op.to_owned())),
379            _ => {}
380        }
381        if let Some(local) = self.local(name) {
382            return local;
383        }
384        if let Some(argument) = self.frame.and_then(|frame| {
385            frame
386                .arguments
387                .iter()
388                .find(|argument| argument.name == name)
389        }) {
390            let word = parse_word(argument.value.raw.as_deref().unwrap_or("0x0"))
391                .map_err(|error| error.to_string())?;
392            return Ok(Value::Word(word, argument.ty.starts_with("int")));
393        }
394        if let Some(index) = self.contract().and_then(|contract| {
395            contract
396                .types
397                .enum_literal(self.map.scope_at_step(self.step), name)
398        }) {
399            return Ok(Value::Word(word_of_u64(index), false));
400        }
401        let Some(layout) = self.layout() else {
402            return Err(format!(
403                "`{name}` is not a step value, a local, or an argument here, and no storage layout is loaded to look it up as a state variable"
404            ));
405        };
406        let Some(words) = self.words.as_ref() else {
407            return Err(format!(
408                "no storage was recorded, so `{name}` cannot be read"
409            ));
410        };
411        let variable = state_value(layout, words, name)?;
412        if variable.value.status == DebugValueStatus::Unavailable {
413            return Err(format!("`{name}` is {}", variable.value.display));
414        }
415        let Some(raw) = variable.value.raw.as_deref() else {
416            return Err(format!(
417                "`{name}` is a {}, which a condition cannot compare",
418                variable.ty
419            ));
420        };
421        let word = parse_word(raw).map_err(|error| error.to_string())?;
422        Ok(Value::Word(word, variable.ty.starts_with("int")))
423    }
424
425    /// A local variable in scope at the step, or a path through one such as
426    /// `item.tags[1]`, read off the stack and through memory, storage, or calldata as its
427    /// type says: a value type as its word, a `string` as its text, a `bytes` as its hex
428    /// text. `None` when no local of that name is in scope; `Some(Err)` when one is but
429    /// cannot be compared.
430    fn local(&self, name: &str) -> Option<Result<Value, String>> {
431        let trace = self.trace?;
432        self.map
433            .local_condition_value(trace, self.step, self.words.as_ref(), name)
434    }
435
436    fn contract(&self) -> Option<&ContractDebugInfo> {
437        self.map.contract_at_step(self.step)
438    }
439
440    fn layout(&self) -> Option<&StorageLayout> {
441        self.contract()?.storage_layout.as_ref()
442    }
443}
444
445fn word_of_u64(value: u64) -> Word {
446    let mut word = [0_u8; 32];
447    word[24..].copy_from_slice(&value.to_be_bytes());
448    word
449}
450
451#[cfg(test)]
452mod tests {
453    use super::{Comparison, Condition, Evaluation, Operand, Operator, Value};
454
455    fn parse(input: &str) -> Condition {
456        Condition::parse(input).expect(input)
457    }
458
459    #[test]
460    fn parses_comparisons_and_groups() {
461        let condition = parse("counter > 4");
462        assert_eq!(condition.text(), "counter > 4");
463        assert_eq!(condition.groups.len(), 1);
464        assert_eq!(
465            condition.groups[0][0],
466            Comparison {
467                left: Operand::Name("counter".to_owned()),
468                operator: Some(Operator::Greater),
469                right: Some(Operand::Literal(super::word_of_u64(4))),
470            }
471        );
472
473        // `<=` and `>=` are not read as `<` and `>`.
474        let condition = parse("gas <= 100 && depth >= 1");
475        assert_eq!(condition.groups[0].len(), 2);
476        assert_eq!(condition.groups[0][0].operator, Some(Operator::LessOrEqual));
477        assert_eq!(
478            condition.groups[0][1].operator,
479            Some(Operator::GreaterOrEqual)
480        );
481
482        // An index is not split at its contents.
483        let condition = parse("balances[0x00000000000000000000000000000000000000aa] == 25");
484        assert_eq!(
485            condition.groups[0][0].left,
486            Operand::Name("balances[0x00000000000000000000000000000000000000aa]".to_owned())
487        );
488
489        let condition = parse("active || counter == 0");
490        assert_eq!(condition.groups.len(), 2);
491        assert_eq!(condition.groups[0][0].operator, None);
492
493        assert!(Condition::parse("").is_err());
494        assert!(Condition::parse("counter >").is_err());
495        assert!(Condition::parse("a && ").is_err());
496    }
497
498    #[test]
499    fn compares_words_bools_and_text() {
500        let one = super::word_of_u64(1);
501        let two = super::word_of_u64(2);
502        assert_eq!(
503            super::compare(&Value::Word(one, false), &Value::Word(two, false)),
504            Ok(std::cmp::Ordering::Less)
505        );
506        // Signed values order by their sign first: -1 is below 1.
507        let minus_one = [0xff_u8; 32];
508        assert_eq!(
509            super::compare(&Value::Word(minus_one, true), &Value::Word(one, true)),
510            Ok(std::cmp::Ordering::Less)
511        );
512        // Unsigned, the same word is the largest there is.
513        assert_eq!(
514            super::compare(&Value::Word(minus_one, false), &Value::Word(one, false)),
515            Ok(std::cmp::Ordering::Greater)
516        );
517        assert_eq!(
518            super::compare(&Value::Bool(true), &Value::Word(one, false)),
519            Ok(std::cmp::Ordering::Equal)
520        );
521        assert_eq!(
522            super::compare(
523                &Value::Text("SSTORE".to_owned()),
524                &Value::Text("SSTORE".to_owned())
525            ),
526            Ok(std::cmp::Ordering::Equal)
527        );
528        assert!(super::compare(&Value::Text("a".to_owned()), &Value::Bool(true)).is_err());
529    }
530
531    #[test]
532    fn evaluation_of_groups_short_circuits_and_reports_the_first_reason() {
533        let unavailable = Evaluation::Unavailable("no".to_owned());
534        assert_ne!(unavailable, Evaluation::False);
535        assert_eq!(Evaluation::from(true), Evaluation::True);
536        assert_eq!(Evaluation::from(false), Evaluation::False);
537    }
538}