On this page:
13.1 Example:   Path Conditions
13.2 Coming soon
8.17

13 Symbolic Execution🔗

    13.1 Example: Path Conditions

    13.2 Coming soon

13.1 Example: Path Conditions🔗


import ast 
from pprint import pprint
from z3 import *
code = """
def f(x,y):
  if x > 5: 
    if y > 1:
      return 2
    else:
      return y
    y = 2
  else:
    if  x > 5:
        return x
    else:
      return 2

"""

tree= ast.parse(code)

class Path:
    def __init__(self, constraints, returns):
        self.constraints = constraints  # list of Z3 conditions
        self.returns = returns          # return value

def eval_expr(expr, symbols):
    "Convert Python AST expression to Z3 expression."
    if isinstance(expr, ast.Compare):
        left = eval_expr(expr.left, symbols)
        right = eval_expr(expr.comparators[0], symbols)
        op = expr.ops[0]
        if isinstance(op, ast.Gt): return left > right
        if isinstance(op, ast.Lt): return left < right
        if isinstance(op, ast.GtE): return left >= right
        if isinstance(op, ast.LtE): return left <= right
        if isinstance(op, ast.Eq): return left == right
        if isinstance(op, ast.NotEq): return left != right
    elif isinstance(expr, ast.Name):
        return symbols[expr.id]
    elif isinstance(expr, ast.Constant):
        return expr.value
    raise NotImplementedError(f"Unsupported: {ast.dump(expr)}")

def walk(node, path_constraints, paths, symbols):
    if isinstance(node, ast.If):
        cond = eval_expr(node.test, symbols)
        # Then branch
        walk(node.body[0], path_constraints + [cond], paths, symbols)

        # Else branch
        walk(node.orelse[0], path_constraints + [Not(cond)], paths, symbols)

    elif isinstance(node, ast.Return):
        paths.append(Path(path_constraints, node.value))


def visit(nodes, counter, path_constraints, paths, symbols):
    node = nodes[counter]
    if isinstance(node, ast.If):
        walk(node, path_constraints, paths, symbols)
    elif isinstance(node, ast.Return):
        walk(node, path_constraints, paths, symbols)
    else:
       visit(nodes, counter+1, path_constraints, paths, symbols)



print(code)
path_constraint = []
paths = []
symbol_x = Int('x')
symbol_y = Int('y')
symbols = {'x': symbol_x, 'y': symbol_y}

func_def = tree.body[0]  # get the function node
visit(func_def.body,0, [], paths, symbols)
#walk(func_def.body[0], [], paths, symbols)

for i, path in enumerate(paths):
    print("Path Constraint", i+1, ":", path.constraints)

# ---------- Step 4: Solve Path Conditions ----------
print("Generated Test Inputs:")
for i, path in enumerate(paths):
    solver = Solver()
    solver.add(path.constraints)
    if solver.check() == sat:
        model = solver.model()
        print(f"✅ Path {i+1}: return '{ast.unparse(path.returns)}' | input: x = {model[symbol_x]}, y={model[symbol_y]}")
    else:
        print(f"❌ Path {i+1} (return {ast.unparse(path.returns)}) is unreachable")

shell

> python3 path-condition.py

def f(x,y):
  if x > 5: 
    if y > 1:
      return 10
    else:
      return y
    y = 2
  else:
    if  x > 5:
        #Invariant: Execution must not reach here.
        return x
    else:
      return 20


Path Constraint 1 : [x > 5, y > 1]
Path Constraint 2 : [x > 5, Not(y > 1)]
Path Constraint 3 : [Not(x > 5), x > 5]
Path Constraint 4 : [Not(x > 5), Not(x > 5)]

Generated Test Inputs:
Path 1: return '10' | input: x = 6, y=2
Path 2: return 'y' | input: x = 6, y=1
Path 3 (return x) is unreachable
Path 4: return '20' | input: x = 5, y=None

13.2 Coming soon🔗