Skip to content

Commit e4db022

Browse files
Merge pull request #72 from mojtaba-eshghie/bug_issue_71
Bug #71: Fixed the bugs and added new tests for them.
2 parents 485c5de + 6e85c27 commit e4db022

2 files changed

Lines changed: 73 additions & 27 deletions

File tree

src/sindi/comparator.py

Lines changed: 66 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,11 @@ def _to_sympy_expr(self, ast):
138138
try:
139139
return sp.Number(float(ast.value)) if '.' in ast.value else sp.Number(int(ast.value))
140140
except ValueError:
141+
# Map booleans
142+
if ast.value.lower() == 'true':
143+
return sp.true
144+
if ast.value.lower() == 'false':
145+
return sp.false
141146
return sp.Symbol(ast.value.replace('.', '_'))
142147

143148
# Handle indexed attributes: a[b].c
@@ -157,16 +162,37 @@ def _to_sympy_expr(self, ast):
157162

158163
args = [self._to_sympy_expr(child) for child in ast.children]
159164

165+
166+
# Normalize ==/!= with boolean literals to X / !X
167+
if ast.value in ('==', '!=') and len(args) == 2:
168+
L, R = args
169+
is_L_bool = L is sp.true or L is sp.false
170+
is_R_bool = R is sp.true or R is sp.false
171+
if is_L_bool or is_R_bool:
172+
bval = True if (L is sp.true or R is sp.true) else False
173+
expr = R if is_L_bool else L
174+
if ast.value == '==':
175+
return expr if bval else sp.Not(expr)
176+
else: # '!='
177+
return sp.Not(expr) if bval else expr
178+
160179
if ast.value in ('&&', '||', '!', '==', '!=', '>', '<', '>=', '<='):
161180
return getattr(sp, self._sympy_operator(ast.value))(*args)
162181
elif ast.value == '/':
163182
return sp.Mul(args[0], sp.Pow(args[1], -1))
164183
# # Failed Use sympy.floor to correctly model Solidity's integer division
165184
# return sp.floor(args[0] / args[1])
166185
elif ast.value == '+':
167-
return sp.Add(*args)
186+
# unary plus: +x → x ; n-ary Add otherwise
187+
return args[0] if len(args) == 1 else sp.Add(*args)
168188
elif ast.value == '-':
169-
return sp.Add(args[0], -args[1])
189+
# Support unary negation and binary subtraction
190+
if len(args) == 1:
191+
return sp.Mul(sp.Integer(-1), args[0])
192+
elif len(args) == 2:
193+
return sp.Add(args[0], sp.Mul(sp.Integer(-1), args[1]))
194+
else:
195+
raise ValueError(f"Invalid number of children for '-' node: {len(args)}")
170196
elif ast.value == '*':
171197
return sp.Mul(*args)
172198
elif '()' in ast.value:
@@ -363,15 +389,15 @@ def _implies(self, expr1, expr2, level=0):
363389
if isinstance(expr1, relational_operators) and isinstance(expr2, relational_operators):
364390
printer(f'In relational base cases; expr1: {expr1}, expr2: {expr2}', level)
365391

366-
# Z3+nonneg fast-path before the other branches
367-
# Prefer Z3 under non-negative domain if there are variables
368-
try:
369-
if expr1.free_symbols or expr2.free_symbols:
370-
z3_result = self._z3_implies_with_nonneg(expr1, expr2)
371-
printer(f"Z3 (nonneg) implication {expr1} -> {expr2}: {z3_result}", level)
372-
return z3_result
373-
except Exception as e:
374-
printer(f"Error (Z3 nonneg implication): {e}", level)
392+
# # Z3+nonneg fast-path before the other branches
393+
# # Prefer Z3 under non-negative domain if there are variables
394+
# try:
395+
# if expr1.free_symbols or expr2.free_symbols:
396+
# z3_result = self._z3_implies_with_nonneg(expr1, expr2)
397+
# printer(f"Z3 (nonneg) implication {expr1} -> {expr2}: {z3_result}", level)
398+
# return z3_result
399+
# except Exception as e:
400+
# printer(f"Error (Z3 nonneg implication): {e}", level)
375401

376402
# Check for Eq vs non-Eq comparisons; we don't handle this well, let's return False
377403
if (isinstance(expr1, sp.Eq) and not isinstance(expr2, sp.Eq)) or (not isinstance(expr1, sp.Eq) and isinstance(expr2, sp.Eq)):
@@ -455,38 +481,51 @@ def _implies(self, expr1, expr2, level=0):
455481
printer(f'type of expr2.rhs: {type(expr2.rhs)}')
456482

457483

484+
# Detect ANY non-trivial numeric scaling (not just fractions) anywhere:
485+
# - Multiplicative numeric factor != ±1 (e.g., *2, *1.5, *1e18, *1/2)
486+
# - Negative powers (division), e.g., 2**-1
487+
def _has_numeric_scale(e) -> bool:
488+
try:
489+
# Direct negative power (e.g., 2**-1) => division
490+
if isinstance(e, sp.Pow) and e.exp.is_number and e.exp < 0:
491+
return True
492+
# Multiplicative factors with a numeric coefficient or negative powers
493+
if isinstance(e, sp.Mul):
494+
for aa in e.args:
495+
if isinstance(aa, sp.Pow) and aa.exp.is_number and aa.exp < 0:
496+
return True
497+
# Any numeric factor not ±1 (covers integers, floats, rationals)
498+
if isinstance(aa, sp.Number) and aa not in (1, -1):
499+
return True
500+
# Recurse within Mul arguments
501+
return any(_has_numeric_scale(aa) for aa in e.args)
502+
# Recurse through additive structures without flagging bare integers
503+
if isinstance(e, sp.Add):
504+
return any(_has_numeric_scale(aa) for aa in e.args)
505+
except Exception:
506+
pass
507+
return False
458508

459-
# even if one of the above lhs and rhs's is sympy.core.mul.Mul and then one of its args is a number or a float bigger or lower than 1, we should switch to z3; we are not handling "1" case since it is working with sympy already, don't want to break a working prototype
460-
if any(isinstance(arg, sp.Mul) and any(isinstance(a, (sp.Number, sp.Float)) and (a > 1 or a < 1) for a in arg.args) for arg in [expr1.lhs, expr1.rhs, expr2.lhs, expr2.rhs]):
461-
printer(f'One of the arguments is a Mul, switching to z3 ...', level)
462-
509+
if any(_has_numeric_scale(arg) for arg in [expr1.lhs, expr1.rhs, expr2.lhs, expr2.rhs]):
510+
printer(f'Numeric scaling detected; switching to z3 with non-negativity.', level)
463511

464512
z3_expr1 = self.sympy_to_z3(expr1)
465513
z3_expr2 = self.sympy_to_z3(expr2)
466514

467515
variables = {str(sym) for sym in expr1.free_symbols.union(expr2.free_symbols)}
468516
z3_vars = {var: z3.Real(var) for var in variables} # Convert to Z3 Reals
469517

470-
471-
472518
solver = z3.Solver()
473-
474-
# Add constraints to ensure all variables are greater than 0
519+
# Solidity-like domains for this numeric monotonicity reasoning
475520
for var in z3_vars.values():
476-
solver.add(var >= 0)
521+
solver.add(var >= 0)
477522

523+
# Check UNSAT of expr1 ∧ ¬expr2
478524
solver.add(z3_expr1, z3.Not(z3_expr2))
479-
480-
481-
482-
483-
# Check satisfiability
484525
if solver.check() == z3.sat:
485-
# If satisfiable, implication does not hold
486526
printer(f"Implies {expr1} to {expr2}: False", level=0)
487527
return False
488528
else:
489-
# If unsatisfiable, implication holds
490529
printer(f"Implies {expr1} to {expr2}: True", level=0)
491530
return True
492531
else:

tests/comparator_test_set.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
[
22
{
33
"The first predicate is stronger.": [
4+
["x > (-1)", "x != (-10)"],
5+
["x > -1", "x != -1"],
6+
["x > 1", "x != 1"],
47
["a > b", "a >= b"],
8+
["a > 0", "a >= 0"],
9+
["a > 0", "a != 0"],
510
["msg.sender == msg.origin && a >= b", "msg.sender == msg.origin"],
611
["msg.sender == msg.origin", "msg.sender == msg.origin || a < b"],
712
["a == 1", "a >= 1"],
@@ -55,6 +60,8 @@
5560
["x == 0x80ac58cd", "x == type(IERC721).interfaceId"]
5661
],
5762
"The predicates are not equivalent and neither is stronger.": [
63+
["a > 0", "a < 0"],
64+
["a > 0", "a != 1"],
5865
["(a > b) && (a <= c)", "(a >= b) && (a < c)"],
5966
["msg.sender != msg.origin", "a >= b"],
6067
["ethBalances[_msgSender()]<=9e18", "tokens<=remainingTokens"],

0 commit comments

Comments
 (0)