1+ # src/sindi/cli.py
12#!/usr/bin/env python3
23import argparse
34import io
1011from .parser import Parser , ASTNode
1112from .simplifier import Simplifier
1213from .comparator import Comparator
14+ from .witness import DomainSpec
1315from .utils import printer , set_quiet , set_debug
1416from .comparator_light import ComparatorRulesOnly
1517import os
@@ -42,6 +44,8 @@ def read_predicate(value: str, is_file: bool) -> str:
4244 with open (value , "r" , encoding = "utf-8" ) as f :
4345 return f .read ().strip ()
4446
47+ # --- Existing Commands (Rewrite, Tokenize, Parse, Simplify) ---
48+
4549def cmd_rewrite (args : argparse .Namespace ) -> int :
4650 _configure_logging (args )
4751 rw = Rewriter ()
@@ -91,9 +95,7 @@ def cmd_simplify(args: argparse.Namespace) -> int:
9195 tokens = tk .tokenize (s )
9296 ast = Parser (tokens ).parse ()
9397 simplified = sp .simplify (ast )
94-
9598 out : Dict [str , Any ] = {"simplified_ast" : ast_to_dict (simplified )}
96-
9799 if args .show_sympy :
98100 try :
99101 out ["sympy_expr_original" ] = str (sp ._to_sympy (ast )) # type: ignore[attr-defined]
@@ -103,7 +105,6 @@ def cmd_simplify(args: argparse.Namespace) -> int:
103105 out ["sympy_expr_simplified" ] = str (sp ._to_sympy (simplified )) # type: ignore[attr-defined]
104106 except Exception as e :
105107 out ["sympy_expr_simplified_error" ] = str (e )
106-
107108 if args .json :
108109 print (json .dumps (out , ensure_ascii = False ))
109110 else :
@@ -120,31 +121,25 @@ def cmd_compare(args: argparse.Namespace) -> int:
120121 _configure_logging (args )
121122 rw = Rewriter ()
122123 tk = Tokenizer ()
123-
124124 p1 = read_predicate (args .predicate1 , args .p1_file )
125125 p2 = read_predicate (args .predicate2 , args .p2_file )
126-
127126 if args .light :
128127 cmp = ComparatorRulesOnly (verbose = args .verbose )
129128 else :
130129 cmp = Comparator ()
131-
132130 sink = io .StringIO ()
133131 with redirect_stdout (sink ):
134132 verdict = cmp .compare (p1 , p2 )
135-
136133 if not args .verbose and not args .json :
137134 print (verdict )
138135 return 0
139-
140136 out : Dict [str , Any ] = {"verdict" : verdict }
141137 rp1 = rw .apply (p1 )
142138 rp2 = rw .apply (p2 )
143139 out ["rewritten" ] = {"p1" : rp1 , "p2" : rp2 }
144140 ast1 = Parser (tk .tokenize (rp1 )).parse ()
145141 ast2 = Parser (tk .tokenize (rp2 )).parse ()
146142 out ["ast" ] = {"p1" : ast_to_dict (ast1 ), "p2" : ast_to_dict (ast2 )}
147-
148143 if args .json :
149144 print (json .dumps (out , ensure_ascii = False ))
150145 else :
@@ -156,55 +151,143 @@ def cmd_compare(args: argparse.Namespace) -> int:
156151 print_tree (ast2 )
157152 return 0
158153
154+ # --- NEW: Witness Command ---
155+
156+ def parse_domains (domain_str : str ) -> Dict [str , DomainSpec ]:
157+ """Parses 'val:uint8,flag:bool' into domain dict."""
158+ if not domain_str :
159+ return {}
160+ domains = {}
161+ for part in domain_str .split (',' ):
162+ if ':' not in part :
163+ continue
164+ var , spec = part .split (':' , 1 )
165+ var = var .strip ()
166+ spec = spec .strip ().lower ()
167+
168+ if spec == 'bool' :
169+ domains [var ] = DomainSpec (kind = 'bool' )
170+ elif spec .startswith ('uint' ):
171+ # handle uint, uint8, uint256
172+ bits = 256
173+ if len (spec ) > 4 :
174+ try : bits = int (spec [4 :])
175+ except : pass
176+ domains [var ] = DomainSpec (kind = 'uint' , bits = bits )
177+ elif spec .startswith ('int' ):
178+ bits = 256
179+ if len (spec ) > 3 :
180+ try : bits = int (spec [3 :])
181+ except : pass
182+ domains [var ] = DomainSpec (kind = 'int' , bits = bits )
183+ elif spec == 'address' :
184+ domains [var ] = DomainSpec (kind = 'address' )
185+ return domains
186+
187+ def cmd_witness (args : argparse .Namespace ) -> int :
188+ _configure_logging (args )
189+
190+ # Read Predicates
191+ new_pred = read_predicate (args .new_pred , args .new_file )
192+ old_pred = read_predicate (args .old_pred , args .old_file )
193+
194+ # Parse Domains
195+ domains = parse_domains (args .domains ) if args .domains else None
196+
197+ # Solve
198+ comp = Comparator ()
199+ res = comp .witness_solve (new_pred , old_pred , domains = domains )
200+
201+ # Output
202+ out = {
203+ "sat" : res .sat ,
204+ "model" : res .model ,
205+ "error" : res .error ,
206+ "unconstrained" : res .unconstrained
207+ }
208+
209+ if args .verbose :
210+ out ["z3_formula" ] = res .z3_formula
211+ out ["simplified_new" ] = res .simplified_new
212+ out ["simplified_old" ] = res .simplified_old
213+
214+ if args .json :
215+ print (json .dumps (out , indent = 2 , ensure_ascii = False ))
216+ else :
217+ if res .sat is True :
218+ print ("Status: SAT (Weakening Found)" )
219+ print ("Witness Model:" )
220+ for k , v in res .model .items ():
221+ print (f" { k } = { v } " )
222+ elif res .sat is False :
223+ print ("Status: UNSAT (No weakening found / Equivalent)" )
224+ else :
225+ print (f"Status: ERROR/UNKNOWN ({ res .error } )" )
226+
227+ return 0 if res .sat is not None else 1
228+
159229def build_parser () -> argparse .ArgumentParser :
160230 p = argparse .ArgumentParser (
161231 prog = "sindi" ,
162- description = "SInDi CLI: rewrite, tokenize, parse, simplify, and compare Solidity predicates."
232+ description = "SInDi CLI: rewrite, tokenize, parse, simplify, compare, and generate witnesses for Solidity predicates."
163233 )
164234 sub = p .add_subparsers (dest = "cmd" , required = True )
165-
235+
236+ # Rewrite
166237 pr = sub .add_parser ("rewrite" , help = "Apply rewrite rules and print the result." )
167238 pr .add_argument ("predicate" )
168239 pr .add_argument ("--from-file" , action = "store_true" )
169240 pr .set_defaults (func = cmd_rewrite )
170-
241+
242+ # Tokenize
171243 pt = sub .add_parser ("tokenize" , help = "Tokenize (optionally after rewrite)." )
172244 pt .add_argument ("predicate" )
173245 pt .add_argument ("--from-file" , action = "store_true" )
174246 pt .add_argument ("--skip-rewrite" , action = "store_true" )
175247 pt .add_argument ("--json" , action = "store_true" )
176248 pt .set_defaults (func = cmd_tokenize )
177-
249+
250+ # Parse
178251 pp = sub .add_parser ("parse" , help = "Parse into AST (optionally after rewrite)." )
179252 pp .add_argument ("predicate" )
180253 pp .add_argument ("--from-file" , action = "store_true" )
181254 pp .add_argument ("--skip-rewrite" , action = "store_true" )
182255 pp .add_argument ("--tree" , action = "store_true" )
183256 pp .add_argument ("--json" , action = "store_true" )
184257 pp .set_defaults (func = cmd_parse )
185-
258+
259+ # Simplify
186260 ps = sub .add_parser ("simplify" , help = "Simplify AST (SymPy-backed)." )
187261 ps .add_argument ("predicate" )
188262 ps .add_argument ("--from-file" , action = "store_true" )
189263 ps .add_argument ("--skip-rewrite" , action = "store_true" )
190264 ps .add_argument ("--show-sympy" , action = "store_true" )
191265 ps .add_argument ("--json" , action = "store_true" )
192266 ps .set_defaults (func = cmd_simplify )
193-
267+
268+ # Compare
194269 pc = sub .add_parser ("compare" , help = "Compare two predicates and print verdict." )
195270 pc .add_argument ("predicate1" )
196271 pc .add_argument ("predicate2" )
197272 pc .add_argument ("--p1-file" , action = "store_true" )
198273 pc .add_argument ("--p2-file" , action = "store_true" )
199- pc .add_argument ("--light" , action = "store_true" ,
200- help = "Use solver-free ComparatorRulesOnly." )
201- pc .add_argument ("--verbose" , action = "store_true" ,
202- help = "Show rewritten predicates and ASTs." )
274+ pc .add_argument ("--light" , action = "store_true" , help = "Use solver-free ComparatorRulesOnly." )
275+ pc .add_argument ("--verbose" , action = "store_true" , help = "Show rewritten predicates and ASTs." )
203276 pc .add_argument ("--json" , action = "store_true" )
204- pc .add_argument ("--debug-logs" , action = "store_true" ,
205- help = "Do not silence internal debug prints." )
277+ pc .add_argument ("--debug-logs" , action = "store_true" , help = "Do not silence internal debug prints." )
206278 pc .set_defaults (func = cmd_compare )
207279
280+ # Witness (NEW)
281+ pw = sub .add_parser ("witness" , help = "Find a variable assignment satisfying New AND NOT Old." )
282+ pw .add_argument ("new_pred" , help = "The new (weaker?) predicate" )
283+ pw .add_argument ("old_pred" , help = "The old (stronger?) predicate" )
284+ pw .add_argument ("--new-file" , action = "store_true" , help = "Read new_pred from file" )
285+ pw .add_argument ("--old-file" , action = "store_true" , help = "Read old_pred from file" )
286+ pw .add_argument ("--domains" , help = "Comma-separated domains, e.g. 'x:uint8,flag:bool'" )
287+ pw .add_argument ("--json" , action = "store_true" , help = "Output JSON result" )
288+ pw .add_argument ("--verbose" , action = "store_true" , help = "Include debug formulas in JSON" )
289+ pw .set_defaults (func = cmd_witness )
290+
208291 return p
209292
210293def main () -> int :
@@ -220,4 +303,4 @@ def main() -> int:
220303 return 1
221304
222305if __name__ == "__main__" :
223- sys .exit (main ())
306+ sys .exit (main ())
0 commit comments