Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/sindi/comparator.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,9 @@ def _to_sympy_expr(self, ast):
return base[index]

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


if ast.value == '&' and len(args) == 2:
return sp.Function('BITAND')(*args)

# Normalize ==/!= with boolean literals to X / !X
if ast.value in ('==', '!=') and len(args) == 2:
Expand Down
2 changes: 1 addition & 1 deletion src/sindi/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ def term(self) -> ASTNode:
node = self.factor()
#debug_print(f"Parsed factor in term: {node}")

while self.position < len(self.tokens) and self.tokens[self.position][1] in ('MULTIPLY', 'DIVIDE', 'MODULUS'):
while self.position < len(self.tokens) and self.tokens[self.position][1] in ('MULTIPLY', 'DIVIDE', 'MODULUS', 'BITWISE_AND'):
operator = self.tokens[self.position]
#debug_print(f"Parsing operator in term: {operator}")
self.position += 1
Expand Down
38 changes: 27 additions & 11 deletions src/sindi/rewriter.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,6 @@ class Rewriter:

# Bare _owner / owner -> owner()
_BARE__OWNER = re.compile(r"(?<!\.)\b_owner\b")
# Bare 'owner' (not member access foo.owner, not a call owner(), not part of ownerOf)
# Preceding char must NOT be dot or a word char; following must NOT start a '('.
# This matches start-of-string, whitespace, operators, etc.
_BARE_OWNER = re.compile(r"(?<![\.\w])owner(?!\s*\()", flags=0)

# Interface ID literals
Expand All @@ -39,7 +36,6 @@ class Rewriter:
re.compile(r"\b0x80ac58cd\b", re.IGNORECASE): "type(IERC721).interfaceId",
re.compile(r"\b0xd9b67a26\b", re.IGNORECASE): "type(IERC1155).interfaceId",
}
# Normalize any spaced form of type(IERCX).interfaceId to a single canonical text
_TYPE_IFACE_NORMALIZER = re.compile(
r"\btype\s*\(\s*(IERC20|IERC721|IERC1155)\s*\)\s*\.\s*interfaceId\b"
)
Expand All @@ -51,7 +47,7 @@ class Rewriter:
_SM_DIV = re.compile(r"\bSafeMath\s*\.\s*div\s*\(\s*([^,()]+?)\s*,\s*([^)]+?)\s*\)")
_SM_MOD = re.compile(r"\bSafeMath\s*\.\s*mod\s*\(\s*([^,()]+?)\s*,\s*([^)]+?)\s*\)")

# SafeMath (extension methods) — works with a.balances[idx].add(x) etc.
# SafeMath (extension methods)
_EXT_ADD = re.compile(r"(\b[A-Za-z_]\w*(?:\[[^\]]+\])?(?:\.[A-Za-z_]\w*(?:\[[^\]]+\])?)*)\s*\.s*add\s*\(\s*([^)]+?)\s*\)")
_EXT_SUB = re.compile(r"(\b[A-Za-z_]\w*(?:\[[^\]]+\])?(?:\.[A-Za-z_]\w*(?:\[[^\]]+\])?)*)\s*\.s*sub\s*\(\s*([^)]+?)\s*\)")
_EXT_MUL = re.compile(r"(\b[A-Za-z_]\w*(?:\[[^\]]+\])?(?:\.[A-Za-z_]\w*(?:\[[^\]]+\])?)*)\s*\.s*mul\s*\(\s*([^)]+?)\s*\)")
Expand All @@ -60,6 +56,18 @@ class Rewriter:

_ETH_MULT = {"ether": 10**18, "gwei": 10**9, "wei": 1}

# ---------------- NEW: parenthesized assignment and finalization mask ----------------
# Replace occurrences of '(var = expr)' with 'expr' (single '=' only).
_PAREN_ASSIGN = re.compile(
r"\(\s*([A-Za-z_]\w*)\s*=\s*(.*?)\s*\)"
)

# (X & MarketplaceLib.FLAG_MASK_FINALIZED) == 0 --> !MarketplaceLib.isFinalized(X)
_FINALIZED_CLEAR = re.compile(
r"\(\s*(?P<x>[^()]+?)\s*&\s*MarketplaceLib\.FLAG_MASK_FINALIZED\s*\)\s*==\s*0"
)
# -------------------------------------------------------------------------------------

def apply(self, s: str) -> str:
# 1) Trivial textual normalizations
s = self._NOW.sub("block.timestamp", s)
Expand All @@ -73,7 +81,6 @@ def apply(self, s: str) -> str:
# 3) Interface IDs (hex -> type(...).interfaceId)
for pat, repl in self._IFACE_MAP.items():
s = pat.sub(repl, s)
# Normalize any spaced `type(IERCX).interfaceId`
s = self._TYPE_IFACE_NORMALIZER.sub(lambda m: f"type({m.group(1)}).interfaceId", s)

# 4) Owner forms
Expand All @@ -83,6 +90,19 @@ def apply(self, s: str) -> str:
# 5) Ether unit canonicalization to raw wei integer
s = self._canon_ether_units(s)

# ---------------- NEW: strip parenthesized assignments ----------------
# Apply repeatedly in case there are multiple occurrences.
while True:
new_s = self._PAREN_ASSIGN.sub(lambda m: m.group(2), s)
if new_s == s:
break
s = new_s
# ----------------------------------------------------------------------

# ---------------- NEW: bitmask → library predicate canonicalization ----
s = self._FINALIZED_CLEAR.sub(lambda m: f"!MarketplaceLib.isFinalized({m.group('x').strip()})", s)
# ----------------------------------------------------------------------

# 6) SafeMath → operators (iterate a few times to catch nested cases)
for _ in range(4):
before = s
Expand All @@ -104,27 +124,23 @@ def apply(self, s: str) -> str:
return s

# ----- helpers -----

def _canon_ether_units(self, s: str) -> str:
# 10**K wei → integer
def _pow10_to_int(m):
k = int(m.group(1))
return str(10 ** k)

s = self._WEI_POW10.sub(_pow10_to_int, s)

# 1e18 wei → integer
def _sci_to_int(m):
n = m.group(1)
return str(int(Decimal(n)))

s = self._WEI_SCI.sub(_sci_to_int, s)

# N ether/gwei/wei → integer wei
def _simple_to_wei(m):
n = int(m.group(1))
unit = m.group(2).lower()
return str(n * self._ETH_MULT[unit])

s = self._ETHER_SIMPLE.sub(_simple_to_wei, s)
return s
return s
41 changes: 29 additions & 12 deletions src/sindi/tokenizer.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import re
from typing import List, Tuple


class Tokenizer:
def __init__(self):
self.token_patterns = [
Expand All @@ -17,7 +16,7 @@ def __init__(self):
(r'&&', 'AND'),
(r'\|\|', 'OR'),
(r'\!', 'NOT'),
(r'&', 'BITWISE_AND'),
(r'\&', 'BITWISE_AND'),
(r'\?', 'QUESTION'),
(r':', 'COLON'),
(r'\(', 'LPAREN'),
Expand All @@ -33,16 +32,19 @@ def __init__(self):
(r'\[', 'LBRACKET'),
(r'\]', 'RBRACKET'),
(r'\"[^\"]*\"', 'STRING_LITERAL'),
(r'\b\d+\.\d+\b', 'FLOAT'),
(r'\b\d+\b', 'INTEGER'),

# ---- Numbers (order matters: scientific before float/int) ----
(r'\b\d(?:_?\d)*(?:\.\d(?:_?\d)*)?[eE][+-]?\d+(?:_?\d)*\b', 'SCIENTIFIC'),
(r'\b\d(?:_?\d)*\.\d(?:_?\d)*\b', 'FLOAT'),
(r'\b\d(?:_?\d)*\b', 'INTEGER'),

(r'\btrue\b', 'TRUE'),
(r'\bfalse\b', 'FALSE'),
(r'0x[0-9a-fA-F]{40}', 'ADDRESS_LITERAL'),
(r'0x[0-9a-fA-F]+', 'BYTES_LITERAL'),
(r'\b\d+\s*(seconds|minutes|hours|days|weeks)\b', 'TIME_UNIT'), # Handle time units
(r'\b\d(?:_?\d)*\s*(seconds|minutes|hours|days|weeks)\b', 'TIME_UNIT'),
(r'[a-zA-Z_]\w*', 'IDENTIFIER'),
(r'\d+e\d+', 'SCIENTIFIC'), # Handle scientific notation
(r'\s+', None), # Let's ignore whitespace(s)
(r'\s+', None),
]
self.time_units = {
'seconds': 1,
Expand Down Expand Up @@ -74,13 +76,28 @@ def tokenize(self, predicate: str) -> List[Tuple[str, str]]:
if match:
if tag:
value = match.group(0)

if tag == 'TIME_UNIT':
number, unit = re.match(r'(\d+)\s*(\w+)', value).groups()
value = str(int(number) * self.time_units[unit])
tag = 'INTEGER'
elif tag == 'SCIENTIFIC':
value = str(int(float(value)))
num, unit = re.match(r'(\d(?:_?\d)*)\s*(\w+)', value).groups()
num = int(num.replace('_', ''))
value = str(num * self.time_units[unit])
tag = 'INTEGER'

elif tag in ('SCIENTIFIC', 'FLOAT', 'INTEGER'):
# Strip underscores from numeric tokens
value = value.replace('_', '')
if tag == 'SCIENTIFIC':
# Normalize to INTEGER by evaluating (safe enough for our use)
# Example: 9e18 -> 9000000000000000000
try:
from decimal import Decimal
value = str(int(Decimal(value)))
tag = 'INTEGER'
except Exception:
# Fall back to float path if Decimal fails
value = str(int(float(value)))
tag = 'INTEGER'

tokens.append((value, tag))
position = match.end()
break
Expand Down