Pythia is a static analysis framework for Python that:
- Translates Python bytecode to Three Address Code (TAC)
- Builds and analyzes Control Flow Graphs (CFGs)
- Performs data flow analysis (liveness, typed pointers)
- Type inference
- Code instrumentation for efficient checkpointing
Purpose: Determine which variables are "dirty" (modified) in loops to enable efficient checkpointing - only save what changed.
| Module | Purpose |
|---|---|
tac.py |
Bytecode -> TAC translation. Defines TAC instructions (Assign, Jump, For, Return, etc.) |
analysis.py |
Abstract interpretation engine. Runs fixpoint computation over CFG |
domains.py |
Abstract domain interfaces (Lattice, InstructionLattice, ValueLattice) |
dom_typed_pointer.py |
Main analysis - tracks pointer/type info and dirty fields |
dom_liveness.py |
Liveness analysis for variables |
dom_concrete.py |
Concrete domain implementations (Set, Map) |
type_system.py |
Static type system, type expressions, joins, subscripting |
graph_utils.py |
CFG utilities |
instruction_cfg.py |
Builds CFGs from Python bytecode |
ast_transform.py |
AST transformation for instrumentation |
strategy.py |
Iteration strategies for fixpoint |
disassemble.py |
Bytecode disassembly utilities |
analyze.py- Analyze a Python file/functioninstrument.py- Instrument code for runtime analysis
- 293 tests, all passing
- Tests in
tests/directory
- Python 3.12, 3.13, and 3.14 supported
- All 259 tests pass
- Branch:
master - Key features:
- Phase 4: BoundCall + Call separation - Separates function resolution from call execution
pythia/tac.py- Added BoundCall expression type, LIST_EXTEND/SET_UPDATE bytecode supportpythia/dom_typed_pointer.py- Added BoundCall analysis case, bound method trackingtypeshed_mini/builtins.pyi- Added max() overload with default parameter
- Bytecode support: BUILD_CONST_KEY_MAP, BUILD_MAP (dict literals), UNPACK_EX (starred unpacking), CALL_KW (Python 3.13)
- Type system: Complete Access type handling in squeeze/join/meet/is_immutable
- Code cleanup: Fixed "builtings" typo, removed unreachable code, consolidated version checks
- Documentation: Documented debugging artifact in dom_typed_pointer.py
- Code quality: Fixed mutable default argument in
immutable(), removed duplicate "/" case inbinop_to_dunder_method - Test bug fix: Fixed duplicate
test_overloadfunction name (second was shadowing first, so overload call resolution test never ran) - Test coverage: Added tests for dunder method mappings,
find_dirty_roots(),unop_to_str,predefined,build_args_typed_dict
1dom_typed_pointer.py - Multiple TODOs:
- Line 989, 1062:
# TODO: class through type - Line 1073:
# TODO: assert not direct_objs ?? - Line 1175:
# TODO: point from exact literal when possible - Line 609:
# TODO: minus one only for self. Should be fixed on binding- Investigated: This is a workaround for method binding not adjusting argument indices. The-1compensates for self being index 0 in type annotations but not inarg_objects. Proper fix would require changes tobind_self_functionintype_system.py. 2type_system.py- Commented out TODOs about function compatibility
- The
-1offset for bound methods indom_typed_pointer.py:609should be fixed at binding time intype_system.py:bind_self_function
- Add property-based testing?
- Test coverage for edge cases in type system
- Type system doesn't know about some numpy functions (e.g.,
np.random.rand) - causes assertion error "Expected Overloaded type, got BOT"
Abstract Domains (Appendix: Pointer Analysis)
- Pointer Domain (P): O -> K -> 2^O (maps objects to field-keyed sets of objects)
- Type Domain (T): O -> TypeExpr (maps objects to type expressions)
- Dirty Domain (D): O -> 2^K (maps objects to sets of dirty fields)
Strong vs Weak Updates:
Upd(P, O_tgt, f, O_new) =
if |O_tgt| = 1: P[o][f] <- O_new (strong update)
otherwise: P[o][f] <- P[o][f] ∪ O_new (weak update)
Current Modification Analysis:
The modified TypeMap.__setitem__ always uses ts.join (weak update) instead of direct assignment for singleton cases. This is MORE CONSERVATIVE (sound) but LESS PRECISE than the paper's approach.
Interestingly, Pointer.__setitem__ still uses strong updates (direct assignment) at lines 222 and 234. This asymmetry may be intentional or a bug.
Analysis of TypeMap join behavior:
signature()at line 930 doestp.types[pointed] = twithin the transfer function- For most cases,
pointedis a new location object, so join vs assign doesn't matter - The change affects line 760:
new_tp.types[self_obj] = side_effect.update[0]for mutating operations - Join is more conservative (sound) but may lose precision for mutation tracking
- Tests still pass, suggesting this doesn't break existing behavior
- Status: Leaving as-is; further investigation needed to understand original motivation
DirtyRoots Formula (from paper):
DirtyRoots(Σ, R) = { x ∈ Live | ∃o ∈ Reach({P[LOCALS][x]}, P). D[o] ≠ ∅ }
Assumptions:
- No dynamic code evaluation (eval/exec)
- Statically-resolvable calls
- Explicit generic instantiations (e.g.,
list[int]()) - No closures mutating globals
- No generators, coroutines, context managers
The analysis determines which local variables are "dirty" at loop checkpoints. Comparing:
Naive approach (naive.py): Saves ALL local variables modified anywhere in the loop
transaction.commit(counter, curr, neighbour, parent, r, root_to_leaf_path, world) # 7 varsWith analysis (instrumented.py): Saves only variables that ESCAPE the loop (are live after)
transaction.commit(counter, root_to_leaf_path) # 2 vars!Variables like curr, neighbour, parent, r, world are modified but their values don't persist meaningfully across iterations - they're recomputed each time.
Loops are marked for checkpointing with a type comment:
for r in range(10**100): # type: int <-- This marks the loop!The analysis finds these loops via annotated_for_labels() in ast_transform.py.
The experiment/ folder contains empirical evaluation benchmarks comparing different checkpointing strategies. Each experiment has multiple variants:
| File | Description |
|---|---|
main.py |
Original algorithm without checkpointing |
instrumented.py |
Uses Pythia's static analysis to checkpoint only dirty variables |
naive.py |
Naive checkpointing that saves all modified variables |
proc.py |
Process-level snapshotting |
vm.py |
VM-level snapshotting |
- k_means: K-means clustering algorithm on random data
- omp: Original OMP (Orthogonal Matching Pursuit) implementation
- pivoter: Graph pivoting algorithm for clique enumeration
- trivial: Simple test case
- worst: Worst-case scenario for analysis
These experiments demonstrate the effectiveness of static analysis for reducing checkpoint size compared to naive approaches.