1use soldb_ethdebug::{parse_word, StorageLayout, Word};
17
18use crate::state::{state_value, StorageWords};
19use crate::stepping::{Frame, StepMap};
20use crate::DebugValueStatus;
21
22#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct Condition {
25 groups: Vec<Vec<Comparison>>,
27 text: String,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq)]
31struct Comparison {
32 left: Operand,
33 operator: Option<Operator>,
35 right: Option<Operand>,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39enum Operator {
40 Equal,
41 NotEqual,
42 Less,
43 LessOrEqual,
44 Greater,
45 GreaterOrEqual,
46}
47
48impl Operator {
49 fn parse(text: &str) -> Option<Self> {
50 match text {
51 "==" => Some(Self::Equal),
52 "!=" => Some(Self::NotEqual),
53 "<" => Some(Self::Less),
54 "<=" => Some(Self::LessOrEqual),
55 ">" => Some(Self::Greater),
56 ">=" => Some(Self::GreaterOrEqual),
57 _ => None,
58 }
59 }
60
61 fn holds(self, ordering: std::cmp::Ordering) -> bool {
62 use std::cmp::Ordering;
63 match self {
64 Self::Equal => ordering == Ordering::Equal,
65 Self::NotEqual => ordering != Ordering::Equal,
66 Self::Less => ordering == Ordering::Less,
67 Self::LessOrEqual => ordering != Ordering::Greater,
68 Self::Greater => ordering == Ordering::Greater,
69 Self::GreaterOrEqual => ordering != Ordering::Less,
70 }
71 }
72}
73
74#[derive(Debug, Clone, PartialEq, Eq)]
76enum Operand {
77 Literal(Word),
79 Bool(bool),
80 Text(String),
82 Name(String),
84}
85
86#[derive(Debug, Clone, PartialEq, Eq)]
88pub enum Value {
89 Word(Word, bool),
91 Bool(bool),
92 Text(String),
93}
94
95#[derive(Debug, Clone, PartialEq, Eq)]
97pub enum Evaluation {
98 True,
99 False,
100 Unavailable(String),
102}
103
104impl Condition {
105 pub fn parse(input: &str) -> Result<Self, String> {
107 let text = input.trim().to_owned();
108 if text.is_empty() {
109 return Err("a condition cannot be empty".to_owned());
110 }
111 let mut groups = Vec::new();
112 for group in text.split("||") {
113 let mut terms = Vec::new();
114 for term in group.split("&&") {
115 terms.push(Comparison::parse(term.trim())?);
116 }
117 groups.push(terms);
118 }
119 Ok(Self { groups, text })
120 }
121
122 #[must_use]
124 pub fn text(&self) -> &str {
125 &self.text
126 }
127
128 #[must_use]
130 pub fn evaluate(&self, context: &ConditionContext<'_>) -> Evaluation {
131 let mut unavailable = None;
132 for group in &self.groups {
133 let mut holds = true;
134 for comparison in group {
135 match comparison.evaluate(context) {
136 Evaluation::True => {}
137 Evaluation::False => {
138 holds = false;
139 break;
140 }
141 Evaluation::Unavailable(reason) => {
142 unavailable.get_or_insert(reason);
143 holds = false;
144 break;
145 }
146 }
147 }
148 if holds {
149 return Evaluation::True;
150 }
151 }
152 match unavailable {
153 Some(reason) => Evaluation::Unavailable(reason),
154 None => Evaluation::False,
155 }
156 }
157}
158
159impl Comparison {
160 fn parse(input: &str) -> Result<Self, String> {
161 if input.is_empty() {
162 return Err("a condition cannot have an empty term".to_owned());
163 }
164 for symbol in ["==", "!=", "<=", ">=", "<", ">"] {
166 let Some(index) = find_operator(input, symbol) else {
167 continue;
168 };
169 let (left, rest) = input.split_at(index);
170 let right = &rest[symbol.len()..];
171 let operator = Operator::parse(symbol).expect("listed operator");
172 return Ok(Self {
173 left: Operand::parse(left.trim())?,
174 operator: Some(operator),
175 right: Some(Operand::parse(right.trim())?),
176 });
177 }
178 Ok(Self {
179 left: Operand::parse(input)?,
180 operator: None,
181 right: None,
182 })
183 }
184
185 fn evaluate(&self, context: &ConditionContext<'_>) -> Evaluation {
186 let left = match context.resolve(&self.left) {
187 Ok(value) => value,
188 Err(reason) => return Evaluation::Unavailable(reason),
189 };
190 let (Some(operator), Some(right)) = (self.operator, self.right.as_ref()) else {
191 return match left {
193 Value::Bool(value) => Evaluation::from(value),
194 Value::Word(word, _) => Evaluation::from(word != [0_u8; 32]),
195 Value::Text(text) => Evaluation::from(!text.is_empty()),
196 };
197 };
198 let right = match context.resolve(right) {
199 Ok(value) => value,
200 Err(reason) => return Evaluation::Unavailable(reason),
201 };
202 match compare(&left, &right) {
203 Ok(ordering) => Evaluation::from(operator.holds(ordering)),
204 Err(reason) => Evaluation::Unavailable(reason),
205 }
206 }
207}
208
209impl From<bool> for Evaluation {
210 fn from(value: bool) -> Self {
211 if value {
212 Self::True
213 } else {
214 Self::False
215 }
216 }
217}
218
219fn compare(left: &Value, right: &Value) -> Result<std::cmp::Ordering, String> {
221 match (left, right) {
222 (Value::Bool(left), Value::Bool(right)) => Ok(left.cmp(right)),
223 (Value::Text(left), Value::Text(right)) => Ok(left.cmp(right)),
224 (Value::Word(left, left_signed), Value::Word(right, right_signed)) => {
225 if *left_signed || *right_signed {
226 return Ok(signed_value(left).cmp(&signed_value(right)));
227 }
228 Ok(left.cmp(right))
229 }
230 (Value::Bool(left), Value::Word(right, _)) => Ok(word_of_bool(*left).cmp(right)),
232 (Value::Word(left, _), Value::Bool(right)) => Ok(left.cmp(&word_of_bool(*right))),
233 _ => Err("a number and a string cannot be compared".to_owned()),
234 }
235}
236
237fn signed_value(word: &Word) -> (bool, Word) {
240 let negative = word[0] & 0x80 != 0;
241 (!negative, *word)
244}
245
246fn word_of_bool(value: bool) -> Word {
247 let mut word = [0_u8; 32];
248 word[31] = u8::from(value);
249 word
250}
251
252fn find_operator(input: &str, symbol: &str) -> Option<usize> {
255 let bytes = input.as_bytes();
256 let mut depth = 0_i32;
257 let mut quoted = false;
258 for index in 0..bytes.len() {
259 match bytes[index] {
260 b'"' => quoted = !quoted,
261 b'[' if !quoted => depth += 1,
262 b']' if !quoted => depth -= 1,
263 _ => {}
264 }
265 if quoted || depth != 0 {
266 continue;
267 }
268 if input[index..].starts_with(symbol) {
269 return Some(index);
272 }
273 }
274 None
275}
276
277impl Operand {
278 fn parse(input: &str) -> Result<Self, String> {
279 let input = input.trim();
280 if input.is_empty() {
281 return Err("a comparison is missing one side".to_owned());
282 }
283 if let Some(text) = input
284 .strip_prefix('"')
285 .and_then(|rest| rest.strip_suffix('"'))
286 {
287 return Ok(Self::Text(text.to_owned()));
288 }
289 match input {
290 "true" => return Ok(Self::Bool(true)),
291 "false" => return Ok(Self::Bool(false)),
292 _ => {}
293 }
294 if input.starts_with("0x") || input.bytes().all(|byte| byte.is_ascii_digit()) {
295 return parse_word(input)
296 .map(Self::Literal)
297 .map_err(|error| error.to_string());
298 }
299 Ok(Self::Name(input.to_owned()))
300 }
301}
302
303pub struct ConditionContext<'a> {
305 map: &'a StepMap,
306 step: usize,
307 words: Option<StorageWords<'a>>,
308 op: &'a str,
309 pc: u64,
310 gas: u64,
311 depth: u64,
312 frame: Option<&'a Frame>,
314}
315
316impl<'a> ConditionContext<'a> {
317 #[must_use]
319 pub fn new(
320 map: &'a StepMap,
321 step: usize,
322 trace_step: &'a soldb_core::TraceStep,
323 words: Option<StorageWords<'a>>,
324 ) -> Self {
325 Self {
326 map,
327 step,
328 words,
329 op: &trace_step.op,
330 pc: trace_step.pc,
331 gas: trace_step.gas,
332 depth: trace_step.depth,
333 frame: None,
334 }
335 }
336
337 #[must_use]
339 pub fn with_frame(mut self, frame: Option<&'a Frame>) -> Self {
340 self.frame = frame;
341 self
342 }
343
344 fn resolve(&self, operand: &Operand) -> Result<Value, String> {
345 match operand {
346 Operand::Literal(word) => Ok(Value::Word(*word, false)),
347 Operand::Bool(value) => Ok(Value::Bool(*value)),
348 Operand::Text(text) => Ok(Value::Text(text.clone())),
349 Operand::Name(name) => self.value(name),
350 }
351 }
352
353 fn value(&self, name: &str) -> Result<Value, String> {
356 match name {
357 "pc" => return Ok(Value::Word(word_of_u64(self.pc), false)),
358 "gas" => return Ok(Value::Word(word_of_u64(self.gas), false)),
359 "depth" => return Ok(Value::Word(word_of_u64(self.depth), false)),
360 "step" => {
361 return Ok(Value::Word(word_of_u64(self.step as u64), false));
362 }
363 "op" => return Ok(Value::Text(self.op.to_owned())),
364 _ => {}
365 }
366 if let Some(argument) = self.frame.and_then(|frame| {
367 frame
368 .arguments
369 .iter()
370 .find(|argument| argument.name == name)
371 }) {
372 let word = parse_word(argument.value.raw.as_deref().unwrap_or("0x0"))
373 .map_err(|error| error.to_string())?;
374 return Ok(Value::Word(word, argument.ty.starts_with("int")));
375 }
376 let Some(layout) = self.layout() else {
377 return Err(format!(
378 "`{name}` is not a step value or an argument here, and no storage layout is loaded to look it up as a state variable"
379 ));
380 };
381 let Some(words) = self.words.as_ref() else {
382 return Err(format!(
383 "no storage was recorded, so `{name}` cannot be read"
384 ));
385 };
386 let variable = state_value(layout, words, name)?;
387 if variable.value.status == DebugValueStatus::Unavailable {
388 return Err(format!("`{name}` is {}", variable.value.display));
389 }
390 let Some(raw) = variable.value.raw.as_deref() else {
391 return Err(format!(
392 "`{name}` is a {}, which a condition cannot compare",
393 variable.ty
394 ));
395 };
396 let word = parse_word(raw).map_err(|error| error.to_string())?;
397 Ok(Value::Word(word, variable.ty.starts_with("int")))
398 }
399
400 fn layout(&self) -> Option<&StorageLayout> {
401 self.map
402 .contract_at_step(self.step)?
403 .storage_layout
404 .as_ref()
405 }
406}
407
408fn word_of_u64(value: u64) -> Word {
409 let mut word = [0_u8; 32];
410 word[24..].copy_from_slice(&value.to_be_bytes());
411 word
412}
413
414#[cfg(test)]
415mod tests {
416 use super::{Comparison, Condition, Evaluation, Operand, Operator, Value};
417
418 fn parse(input: &str) -> Condition {
419 Condition::parse(input).expect(input)
420 }
421
422 #[test]
423 fn parses_comparisons_and_groups() {
424 let condition = parse("counter > 4");
425 assert_eq!(condition.text(), "counter > 4");
426 assert_eq!(condition.groups.len(), 1);
427 assert_eq!(
428 condition.groups[0][0],
429 Comparison {
430 left: Operand::Name("counter".to_owned()),
431 operator: Some(Operator::Greater),
432 right: Some(Operand::Literal(super::word_of_u64(4))),
433 }
434 );
435
436 let condition = parse("gas <= 100 && depth >= 1");
438 assert_eq!(condition.groups[0].len(), 2);
439 assert_eq!(condition.groups[0][0].operator, Some(Operator::LessOrEqual));
440 assert_eq!(
441 condition.groups[0][1].operator,
442 Some(Operator::GreaterOrEqual)
443 );
444
445 let condition = parse("balances[0x00000000000000000000000000000000000000aa] == 25");
447 assert_eq!(
448 condition.groups[0][0].left,
449 Operand::Name("balances[0x00000000000000000000000000000000000000aa]".to_owned())
450 );
451
452 let condition = parse("active || counter == 0");
453 assert_eq!(condition.groups.len(), 2);
454 assert_eq!(condition.groups[0][0].operator, None);
455
456 assert!(Condition::parse("").is_err());
457 assert!(Condition::parse("counter >").is_err());
458 assert!(Condition::parse("a && ").is_err());
459 }
460
461 #[test]
462 fn compares_words_bools_and_text() {
463 let one = super::word_of_u64(1);
464 let two = super::word_of_u64(2);
465 assert_eq!(
466 super::compare(&Value::Word(one, false), &Value::Word(two, false)),
467 Ok(std::cmp::Ordering::Less)
468 );
469 let minus_one = [0xff_u8; 32];
471 assert_eq!(
472 super::compare(&Value::Word(minus_one, true), &Value::Word(one, true)),
473 Ok(std::cmp::Ordering::Less)
474 );
475 assert_eq!(
477 super::compare(&Value::Word(minus_one, false), &Value::Word(one, false)),
478 Ok(std::cmp::Ordering::Greater)
479 );
480 assert_eq!(
481 super::compare(&Value::Bool(true), &Value::Word(one, false)),
482 Ok(std::cmp::Ordering::Equal)
483 );
484 assert_eq!(
485 super::compare(
486 &Value::Text("SSTORE".to_owned()),
487 &Value::Text("SSTORE".to_owned())
488 ),
489 Ok(std::cmp::Ordering::Equal)
490 );
491 assert!(super::compare(&Value::Text("a".to_owned()), &Value::Bool(true)).is_err());
492 }
493
494 #[test]
495 fn evaluation_of_groups_short_circuits_and_reports_the_first_reason() {
496 let unavailable = Evaluation::Unavailable("no".to_owned());
497 assert_ne!(unavailable, Evaluation::False);
498 assert_eq!(Evaluation::from(true), Evaluation::True);
499 assert_eq!(Evaluation::from(false), Evaluation::False);
500 }
501}