Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check cfg edge constraints #1084

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
6b77082
feat: initially implement update_edge_feasibility
Raine-Yang-UofT Sep 17, 2024
0df453a
Merge branch 'master' of https://github.com/Raine-Yang-UofT/pyta into…
Raine-Yang-UofT Sep 17, 2024
820e2a3
tests: update unit tests for test_edge_feasibility
Raine-Yang-UofT Sep 21, 2024
771e419
Merge branch 'master' of https://github.com/pyta-uoft/pyta into check…
Raine-Yang-UofT Sep 21, 2024
9f22aba
docs: fix typo in change log
Raine-Yang-UofT Sep 21, 2024
a4870ed
refactor: reimplement update_edge_feasibility
Raine-Yang-UofT Sep 23, 2024
aa31bda
Merge branch 'master' of https://github.com/Raine-Yang-UofT/pyta into…
Raine-Yang-UofT Sep 23, 2024
5f1b4d7
refactor: remove unused import
Raine-Yang-UofT Sep 24, 2024
71c5f1a
docs: resolve change log merge conflict
Raine-Yang-UofT Sep 25, 2024
842cff3
feat: implement rendering of infeasible CFG edges
Raine-Yang-UofT Sep 26, 2024
42a5338
fix: fix incorrect edge feasibility
Raine-Yang-UofT Sep 26, 2024
ee18274
tests: update testing snapshot
Raine-Yang-UofT Sep 26, 2024
fd5d0ab
Merge branch 'master' into check-CFG-edge-constraints
david-yz-liu Sep 27, 2024
2e4ae0f
test: create unit test with no precondition
Raine-Yang-UofT Sep 27, 2024
b02d72c
Merge branch 'check-CFG-edge-constraints' of https://github.com/Raine…
Raine-Yang-UofT Sep 27, 2024
a090257
Merge branch 'master' into check-CFG-edge-constraints
Raine-Yang-UofT Sep 30, 2024
2617624
refactor: change to relative import
Raine-Yang-UofT Sep 30, 2024
99d9fc6
chores: resolve merge conflict in change log
Raine-Yang-UofT Sep 30, 2024
b1efa1a
Merge branch 'check-CFG-edge-constraints' of https://github.com/Raine…
Raine-Yang-UofT Sep 30, 2024
5b49f71
fix: check for optional z3 dependency
Raine-Yang-UofT Sep 30, 2024
836bf91
fix: allow cfg_generator to work correctly without z3
Raine-Yang-UofT Oct 2, 2024
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- Removed `node` attribute for `Z3Parser`
- Renamed `reduce` method of `Z3Parser` to `parse`
- Renamed `test_expr_wrapper` to `test_z3_parser`
- Added `is_feasible` attribute for `CFGEdge` and implemented update to edge feasibility based on lists of Z3 constraints
- Refactored codebase to use modern type annotations. Replaced `List` with `list`, `Dict` with `dict`, `Set` with `set`, and `Tuple` with `tuple`
- Checked for variable reassignment in `AugAssign` and `AnnAssign` node in parsing edge Z3 constraints
- Rendered logically infeasible control flow graph edges in light grey
- Modified `test_snapshot_to_json_sets_primitive` for Python 3.8 compatibility

## [2.8.1] - 2024-08-19
Expand Down
21 changes: 19 additions & 2 deletions python_ta/cfg/cfg_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@
from astroid import nodes
from astroid.builder import AstroidBuilder

try:
from ..transforms.z3_visitor import Z3Visitor

z3_dependency_available = True
except ImportError:
Z3Visitor = Any
z3_dependency_available = False

from .graph import CFGBlock, ControlFlowGraph
from .visitor import CFGVisitor

Expand Down Expand Up @@ -61,6 +69,12 @@ def _generate(

file_name = os.path.splitext(os.path.basename(abs_path))[0]
module = AstroidBuilder().file_build(abs_path)

# invoke Z3Visitor if z3 dependency is available
if z3_dependency_available:
z3v = Z3Visitor()
module = z3v.visitor.visit(module)

visitor = CFGVisitor(options=visitor_options)
module.accept(visitor)

Expand Down Expand Up @@ -140,8 +154,11 @@ def _visit(block: CFGBlock, graph: graphviz.Digraph, visited: set[int], end: CFG
visited.add(node_id)

for edge in block.successors:
color = "black" if edge.is_feasible else "lightgrey"
if edge.get_label() is not None:
graph.edge(node_id, f"{graph.name}_{edge.target.id}", edge.get_label())
graph.edge(
node_id, f"{graph.name}_{edge.target.id}", label=edge.get_label(), color=color
)
else:
graph.edge(node_id, f"{graph.name}_{edge.target.id}")
graph.edge(node_id, f"{graph.name}_{edge.target.id}", color=color)
_visit(edge.target, graph, visited, end)
33 changes: 32 additions & 1 deletion python_ta/cfg/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,15 @@
from typing import Any, Dict, Generator, List, Optional, Set

try:
from z3 import Z3_OP_UNINTERPRETED, ExprRef, Not, Z3Exception, is_const
from z3 import (
Z3_OP_UNINTERPRETED,
ExprRef,
Not,
Solver,
Z3Exception,
is_const,
unsat,
)

from ..z3.z3_parser import Z3ParseException, Z3Parser

Expand All @@ -16,6 +24,8 @@
is_const = Any
Z3_OP_UNINTERPRETED = Any
Z3ParseException = Any
Solver = Any
unsat = Any
z3_dependency_available = False

from astroid import (
Expand Down Expand Up @@ -288,6 +298,25 @@ def _handle_variable_reassignment(self, node: NodeNG, env: Z3Environment) -> Non
elif isinstance(node, (AugAssign, AnnAssign)):
env.assign(node.target.name)

def update_edge_feasibility(self) -> None:
"""Traverse through edges in DFS order and update is_feasible
attribute of each edge. Edges that are unreachable with the given
set of Z3 constraints will have is_feasible set to False
"""
if not z3_dependency_available:
return

def _check_unsat(constraints: List[ExprRef]) -> bool:
solver = Solver()
solver.add(constraints)
return solver.check() == unsat

for edge in self.get_edges():
if len(edge.z3_constraints) > 0:
edge.is_feasible = not all(
_check_unsat(constraints) for constraints in edge.z3_constraints.values()
)


class CFGBlock:
"""A node in a control flow graph.
Expand Down Expand Up @@ -346,6 +375,7 @@ class CFGEdge:
condition: Optional[NodeNG]
negate: Optional[bool]
z3_constraints: Dict[int, List[ExprRef]]
is_feasible: bool

def __init__(
self,
Expand All @@ -363,6 +393,7 @@ def __init__(
self.source.successors.append(self)
self.target.predecessors.append(self)
self.z3_constraints = {}
self.is_feasible = True

def get_label(self) -> Optional[str]:
"""Return the edge label if specified.
Expand Down
1 change: 1 addition & 0 deletions python_ta/cfg/visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ def visit_functiondef(self, func: nodes.FunctionDef) -> None:
if hasattr(func, "z3_constraints"):
self._current_cfg.precondition_constraints = func.z3_constraints
self._current_cfg.update_edge_z3_constraints()
self._current_cfg.update_edge_feasibility()

self._current_block = previous_block
self._current_cfg = previous_cfg
Expand Down
16 changes: 8 additions & 8 deletions tests/test_cfg/snapshots/funcs_only.gv
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,29 @@ digraph funcs_only {
node [fontname="Courier New" shape=box]
subgraph cluster_0 {
cluster_0_0 [label="self\l" fillcolor=white style=filled]
cluster_0_0 -> cluster_0_2
cluster_0_0 -> cluster_0_2 [color=black]
cluster_0_2 [label="print('method')\l" fillcolor=white style=filled]
cluster_0_2 -> cluster_0_1
cluster_0_2 -> cluster_0_1 [color=black]
cluster_0_1 [label="\l" fillcolor=black style=filled]
fontname="Courier New" label="MyClass.foo"
}
subgraph cluster_1 {
cluster_1_0 [label="\l" fillcolor=white style=filled]
cluster_1_0 -> cluster_1_2
cluster_1_0 -> cluster_1_2 [color=black]
cluster_1_2 [label="a = 5\l" fillcolor=white style=filled]
cluster_1_2 -> cluster_1_1
cluster_1_2 -> cluster_1_1 [color=black]
cluster_1_1 [label="\l" fillcolor=black style=filled]
fontname="Courier New" label=foo
}
subgraph cluster_2 {
cluster_2_0 [label="\l" fillcolor=white style=filled]
cluster_2_0 -> cluster_2_2
cluster_2_0 -> cluster_2_2 [color=black]
cluster_2_2 [label="a > 0\l" fillcolor=white style=filled]
cluster_2_2 -> cluster_2_3 [label=True]
cluster_2_2 -> cluster_2_3 [label=True color=black]
cluster_2_3 [label="owl = 'hoo'\l" fillcolor=white style=filled]
cluster_2_3 -> cluster_2_1
cluster_2_3 -> cluster_2_1 [color=black]
cluster_2_1 [label="\l" fillcolor=black style=filled]
cluster_2_2 -> cluster_2_1 [label=False]
cluster_2_2 -> cluster_2_1 [label=False color=black]
fontname="Courier New" label=hoo
}
}
16 changes: 8 additions & 8 deletions tests/test_cfg/snapshots/my_file.gv
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,23 @@ digraph my_file {
node [fontname="Courier New" shape=box]
subgraph cluster_0 {
cluster_0_0 [label="\ldef foo() -> None:\l for i in range(1, 10):\l if i < 5:\l print('hi')\l" fillcolor=white style=filled]
cluster_0_0 -> cluster_0_1
cluster_0_0 -> cluster_0_1 [color=black]
cluster_0_1 [label="\l" fillcolor=black style=filled]
fontname="Courier New" label=__main__
}
subgraph cluster_1 {
cluster_1_0 [label="\l" fillcolor=white style=filled]
cluster_1_0 -> cluster_1_2
cluster_1_0 -> cluster_1_2 [color=black]
cluster_1_2 [label="range(1, 10)\l" fillcolor=white style=filled]
cluster_1_2 -> cluster_1_3
cluster_1_2 -> cluster_1_3 [color=black]
cluster_1_3 [label="i\l" fillcolor=white style=filled]
cluster_1_3 -> cluster_1_5
cluster_1_3 -> cluster_1_5 [color=black]
cluster_1_5 [label="i < 5\l" fillcolor=white style=filled]
cluster_1_5 -> cluster_1_6 [label=True]
cluster_1_5 -> cluster_1_6 [label=True color=black]
cluster_1_6 [label="print('hi')\l" fillcolor=white style=filled]
cluster_1_6 -> cluster_1_3
cluster_1_5 -> cluster_1_3 [label=False]
cluster_1_3 -> cluster_1_1
cluster_1_6 -> cluster_1_3 [color=black]
cluster_1_5 -> cluster_1_3 [label=False color=black]
cluster_1_3 -> cluster_1_1 [color=black]
cluster_1_1 [label="\l" fillcolor=black style=filled]
fontname="Courier New" label=foo
}
Expand Down
11 changes: 11 additions & 0 deletions tests/test_cfg/test_cfg_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""

import os.path
from unittest.mock import patch

import pytest

Expand Down Expand Up @@ -141,3 +142,13 @@ def test_mod_not_str(capsys) -> None:
captured = capsys.readouterr()

assert captured.out == expected


@patch.dict("sys.modules", {"z3": None})
def test_z3_dependency_uninstalled(snapshot, create_cfg) -> None:
"""Test that generate_cfg does not raise any error when z3 is not installed"""
_, _, gv_file_io = create_cfg

# Check that the contents match with the snapshot
snapshot.snapshot_dir = "snapshots"
snapshot.assert_match(gv_file_io.read(), "my_file.gv")
Loading