Symbolic Execution Deep Dive¶
Time: 30 minutes | Tools: symbolic_execute | Tier: Pro
Learn how symbolic execution explores every possible path through your code.
What You'll Learn¶
- How symbolic execution works
- Finding edge cases automatically
- Understanding path constraints
- Generating test inputs
What is Symbolic Execution?¶
Instead of running code with actual values like x = 5, symbolic execution uses symbolic values that represent all possible inputs.
Concrete Execution¶
Symbolic Execution¶
classify(x) # x is symbolic
# Explores ALL paths:
# - x < 0 → "negative"
# - x == 0 → "zero"
# - x > 0 → "positive"
Step 1: Basic Symbolic Execution¶
Consider this function:
def classify_age(age):
if age < 0:
return "invalid"
elif age < 18:
return "minor"
elif age < 65:
return "adult"
else:
return "senior"
Run symbolic execution:
"Explore all paths through the classify_age function"
{
"tool": "symbolic_execute",
"parameters": {
"code": "def classify_age(age):\n if age < 0:\n return \"invalid\"\n elif age < 18:\n return \"minor\"\n elif age < 65:\n return \"adult\"\n else:\n return \"senior\""
}
}
Step 2: Understand the Results¶
{
"data": {
"paths": [
{
"id": 1,
"constraints": ["age < 0"],
"result": "\"invalid\"",
"concrete_example": {"age": -1},
"path_trace": [
"line 2: if age < 0 → True",
"line 3: return \"invalid\""
]
},
{
"id": 2,
"constraints": ["age >= 0", "age < 18"],
"result": "\"minor\"",
"concrete_example": {"age": 10},
"path_trace": [
"line 2: if age < 0 → False",
"line 4: elif age < 18 → True",
"line 5: return \"minor\""
]
},
{
"id": 3,
"constraints": ["age >= 18", "age < 65"],
"result": "\"adult\"",
"concrete_example": {"age": 30},
"path_trace": [
"line 2: if age < 0 → False",
"line 4: elif age < 18 → False",
"line 6: elif age < 65 → True",
"line 7: return \"adult\""
]
},
{
"id": 4,
"constraints": ["age >= 65"],
"result": "\"senior\"",
"concrete_example": {"age": 70},
"path_trace": [
"line 2: if age < 0 → False",
"line 4: elif age < 18 → False",
"line 6: elif age < 65 → False",
"line 9: return \"senior\""
]
}
],
"total_paths": 4,
"coverage": {
"lines_covered": [2, 3, 4, 5, 6, 7, 8, 9],
"branch_coverage": "100%"
}
}
}
What Each Path Tells You¶
| Path | Constraints | Example | Result |
|---|---|---|---|
| 1 | age < 0 | -1 | "invalid" |
| 2 | 0 ≤ age < 18 | 10 | "minor" |
| 3 | 18 ≤ age < 65 | 30 | "adult" |
| 4 | age ≥ 65 | 70 | "senior" |
Step 3: Find Edge Cases¶
Symbolic execution reveals boundary conditions:
def calculate_discount(amount, is_member):
if amount <= 0:
raise ValueError("Amount must be positive")
if is_member:
if amount >= 100:
return amount * 0.20 # 20% discount
else:
return amount * 0.10 # 10% discount
else:
if amount >= 200:
return amount * 0.05 # 5% discount
else:
return 0 # No discount
"Find all paths and edge cases in calculate_discount"
Edge cases found:
| Path | Conditions | Example | Risk |
|---|---|---|---|
| 1 | amount ≤ 0 | (0, True) | Exception path |
| 2 | member, amount ≥ 100 | (100, True) | Boundary: exactly 100 |
| 3 | member, amount < 100 | (99, True) | Just below boundary |
| 4 | non-member, amount ≥ 200 | (200, False) | Boundary: exactly 200 |
| 5 | non-member, amount < 200 | (199, False) | Just below boundary |
Step 4: Detect Bugs¶
Consider buggy code:
def divide_rewards(total, num_people):
"""Divide rewards equally among people."""
per_person = total / num_people
return per_person
Run symbolic execution:
{
"data": {
"paths": [
{
"id": 1,
"constraints": ["num_people != 0"],
"result": "total / num_people",
"concrete_example": {"total": 100, "num_people": 5}
},
{
"id": 2,
"constraints": ["num_people == 0"],
"result": "EXCEPTION: ZeroDivisionError",
"concrete_example": {"total": 100, "num_people": 0}
}
],
"warnings": [
{
"type": "POTENTIAL_EXCEPTION",
"path": 2,
"exception": "ZeroDivisionError",
"message": "Division by zero when num_people == 0"
}
]
}
}
Bug found! No check for zero people.
Step 5: Multiple Variables¶
Symbolic execution handles multiple variables:
def shipping_cost(weight, distance, express):
base = weight * 0.5
if distance < 100:
rate = 1.0
elif distance < 500:
rate = 1.5
else:
rate = 2.0
cost = base * rate
if express:
cost *= 2
return cost
Paths explored:
| # | weight | distance | express | rate | multiplier |
|---|---|---|---|---|---|
| 1 | any | < 100 | False | 1.0 | 1 |
| 2 | any | < 100 | True | 1.0 | 2 |
| 3 | any | 100-499 | False | 1.5 | 1 |
| 4 | any | 100-499 | True | 1.5 | 2 |
| 5 | any | ≥ 500 | False | 2.0 | 1 |
| 6 | any | ≥ 500 | True | 2.0 | 2 |
Total: 6 paths (2 distance zones × 2 express options × ...)
Step 6: Loop Handling¶
Symbolic execution handles loops with bounded unrolling:
With max_depth=3, explores: - Empty list - 1 positive number - 1 negative number - 2 numbers (combinations) - 3 numbers (combinations)
Supported Types¶
| Type | Support | Notes |
|---|---|---|
int | Full | All operations |
bool | Full | All operations |
float | Basic | Comparisons, arithmetic |
str | Basic | Length, equality |
list | Limited | Length, index |
dict | — | Not supported |
Configuration Options¶
Limit Paths¶
{
"tool": "symbolic_execute",
"parameters": {
"code": "...",
"max_paths": 50 // Stop after 50 paths
}
}
Limit Depth¶
{
"tool": "symbolic_execute",
"parameters": {
"code": "...",
"max_depth": 5 // Max recursion/loop depth
}
}
Try It Yourself¶
Exercise 1: Find All Paths¶
def validate_password(password):
if len(password) < 8:
return "too_short"
if len(password) > 100:
return "too_long"
has_digit = any(c.isdigit() for c in password)
has_upper = any(c.isupper() for c in password)
if not has_digit:
return "needs_digit"
if not has_upper:
return "needs_upper"
return "valid"
"What are all possible return values from validate_password?"
Exercise 2: Find Bugs¶
"What edge cases could cause this to fail?"
Exercise 3: Complex Conditions¶
def categorize(value, threshold, strict):
if strict:
if value > threshold:
return "above"
elif value < threshold:
return "below"
else:
return "equal"
else:
if value >= threshold:
return "above_or_equal"
else:
return "below"
"How many paths are in categorize?"
Key Takeaways¶
- Symbolic execution explores all paths - not just the ones you think of
- Constraints show requirements - what must be true for each path
- Concrete examples are test inputs - ready to use in tests
- Warnings reveal bugs - division by zero, None access, etc.
- Bounded by depth and paths - prevents infinite exploration
Next Tutorial¶
Use the symbolic findings in a broader engineering workflow:
- Configuration Guide - Set limits and governance settings
- Enterprise Deployment Guide - Extend symbolic analysis into governed rollout patterns