Skip to content

Commit 5c22f70

Browse files
author
Andrew Clark
authored
Merge pull request #6 from AndrewC19/issue-#528
Issue sixty-north#528
2 parents 59f0156 + da0c49f commit 5c22f70

File tree

2 files changed

+247
-49
lines changed

2 files changed

+247
-49
lines changed

src/cosmic_ray/operators/variable_inserter.py

+128-18
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Implementation of the variable-inserter operator."""
22
import random
33
import parso.python.tree
4-
from parso.python.tree import Name, PythonNode
4+
from parso.python.tree import Name, PythonNode, IfStmt, ExprStmt
55
from .operator import Operator
66
from .example import Example
77

@@ -14,31 +14,141 @@ def __init__(self, cause_variable, effect_variable):
1414
self.effect_variable = effect_variable
1515

1616
def mutation_positions(self, node):
17-
"""Find expressions or terms that define the effect variable. These nodes can be
18-
mutated to introduce an effect of the cause variable.
17+
"""Insert usages of the cause variable to statements of the effect variable that are currently unaffected.
18+
19+
This method identifies all 'suites' that are used to define the value of the effect variable, where a 'suite'
20+
is a body of code that follows an if statement. The entire suite is later replaced with a copy in which all
21+
statements of the effect variable are now affected by the cause variable. This is achieved by either adding
22+
or subtracting the cause variable from the statement.
23+
24+
:param node: node of parso parse tree that is a potential candidate for mutation.
25+
:return (start_pos, end_pos): A pair representing the position in the abstract syntax tree to mutate (only if
26+
the mutation operator is applicable at this position).
1927
"""
20-
if isinstance(node, PythonNode) and (node.type == "arith_expr" or node.type == "term"):
2128

22-
if node.get_previous_sibling() == '=': # We only want to mutate the LHS once
23-
expr_node = node.search_ancestor('expr_stmt')
24-
if expr_node:
25-
effect_variable_names = [v.value for v in expr_node.get_defined_names()]
26-
if self.effect_variable in effect_variable_names:
27-
cause_variables = list(self._get_causes_from_expr_node(expr_node))
28-
if node not in cause_variables:
29-
yield (node.start_pos, node.end_pos)
29+
if isinstance(node, PythonNode) and node.type == "suite" and isinstance(node.parent, IfStmt):
30+
31+
# This node is the body of an if-statement.
32+
# We are only interested in the body of the outer-most if statements that have no else branch.
33+
if 'else' not in node.parent.children:
34+
causes, effects = self._get_cause_and_effect_nodes_from_suite_node(node)
35+
named_causes = [cause.value for cause in causes]
36+
named_effects = [effect.value for effect in effects]
37+
if (self.effect_variable in named_effects) and (self.cause_variable not in named_causes):
38+
print(f"{self.cause_variable} _||_ {self.effect_variable}")
39+
yield node.start_pos, node.end_pos
3040

3141
def mutate(self, node, index):
3242
"""Join the node with cause variable using a randomly sampled arithmetic operator."""
33-
assert isinstance(node, PythonNode)
34-
assert (node.type == "arith_expr" or node.type == "term")
43+
assert isinstance(node, PythonNode) and node.type == "suite", "Error: node is not a suite."
44+
print("PRE-MUTATION: ", node.get_code())
45+
node_with_causes = self._add_causes_to_suite(node)
46+
print("POST-MUTATION: ", node_with_causes.get_code())
47+
# arith_operator = random.choice(['+', '-'])
48+
# arith_operator_node_start_pos = self._iterate_col(node.end_pos)
49+
# cause_node_start_pos = self._iterate_col(arith_operator_node_start_pos)
50+
# arith_operator_node = parso.python.tree.Operator(arith_operator, start_pos=arith_operator_node_start_pos)
51+
# cause_node = Name(self.cause_variable, start_pos=cause_node_start_pos)
52+
# replacement_node = parso.python.tree.PythonNode("arith_expr", [node, arith_operator_node, cause_node])
53+
return node_with_causes
54+
55+
def _get_cause_and_effect_nodes_from_suite_node(self, suite_node):
56+
causes = [] # Variables that appear on RHS of expressions/statements OR in the predicate of an if statement
57+
effects = [] # Variables appearing on LHS of expressions/statements
58+
expr_nodes = [] # These are expressions/statements
59+
60+
for child_node in suite_node.children:
61+
if isinstance(child_node, ExprStmt):
62+
expr_nodes.append(child_node)
63+
elif isinstance(child_node, PythonNode):
64+
expr_nodes.append(child_node.children[0])
65+
elif isinstance(child_node, IfStmt):
66+
for grandchild_node in child_node.children:
67+
if isinstance(grandchild_node, PythonNode) and grandchild_node.type == "suite":
68+
gc_causes, gc_effects = self._get_cause_and_effect_nodes_from_suite_node(grandchild_node)
69+
causes += gc_causes
70+
effects += gc_effects
71+
elif isinstance(grandchild_node, PythonNode) and grandchild_node.type == "comparison":
72+
gc_comparison_causes = list(self._flatten_comparison(grandchild_node))
73+
causes += gc_comparison_causes
74+
75+
for expr_node in expr_nodes:
76+
causes += list(self._get_causes_from_expr_node(expr_node))
77+
effects += expr_node.get_defined_names()
78+
return causes, effects
79+
80+
def _flatten_expr(self, expr):
81+
for item in expr:
82+
# Convert PythonNode to list of its children
83+
try:
84+
item_to_flatten = item.children
85+
except AttributeError:
86+
item_to_flatten = item
87+
try:
88+
yield from self._flatten_expr(item_to_flatten)
89+
except TypeError:
90+
yield item_to_flatten
91+
92+
def _flatten_comparison(self, conditional):
93+
try:
94+
# PythonNode (has children)
95+
to_iterate = conditional.children
96+
except AttributeError:
97+
# Not PythonNode (has no children)
98+
to_iterate = conditional
99+
100+
for child_node in to_iterate:
101+
try:
102+
# If the current node has children, flatten these
103+
item_to_flatten = child_node.children
104+
except AttributeError:
105+
# Otherwise flatted the node itself
106+
item_to_flatten = child_node
107+
108+
try:
109+
yield from self._flatten_comparison(item_to_flatten)
110+
except TypeError:
111+
# Non-iterable (leaf node)
112+
yield item_to_flatten
113+
114+
def _add_causes_to_suite(self, node):
115+
expr_nodes = []
116+
for child_node in node.children:
117+
# Expression/statement
118+
if isinstance(child_node, ExprStmt):
119+
expr_nodes.append(child_node)
120+
elif isinstance(child_node, PythonNode):
121+
expr_nodes.append(child_node.children[0])
122+
# If statement
123+
elif isinstance(child_node, IfStmt):
124+
for grandchild_node in child_node.children:
125+
# Predicate of if statement
126+
if isinstance(grandchild_node, PythonNode) and grandchild_node.type == "comparison":
127+
arith_expr = grandchild_node.children[0]
128+
new_arith_expr = self._add_cause_to_node(arith_expr)
129+
grandchild_node.children[0] = new_arith_expr
130+
# Expressions/statements in true/false branch of if statement
131+
elif isinstance(grandchild_node, PythonNode) and grandchild_node.type == "suite":
132+
grandchild_node = self._add_causes_to_suite(grandchild_node)
133+
134+
# Replace usages of cause variable in expression nodes
135+
for expr_node in expr_nodes:
136+
rhs = expr_node.get_rhs()
137+
new_rhs = self._add_cause_to_node(rhs)
138+
expr_node.children[2] = new_rhs
139+
140+
return node
35141

142+
def _add_cause_to_node(self, arith_expr_node):
36143
arith_operator = random.choice(['+', '-'])
37-
arith_operator_node_start_pos = self._iterate_col(node.end_pos)
144+
arith_operator_node_start_pos = self._iterate_col(arith_expr_node.end_pos)
38145
cause_node_start_pos = self._iterate_col(arith_operator_node_start_pos)
39-
arith_operator_node = parso.python.tree.Operator(arith_operator, start_pos=arith_operator_node_start_pos)
40-
cause_node = Name(self.cause_variable, start_pos=cause_node_start_pos)
41-
replacement_node = parso.python.tree.PythonNode("arith_expr", [node, arith_operator_node, cause_node])
146+
arith_operator_node = parso.python.tree.Operator(arith_operator,
147+
start_pos=arith_operator_node_start_pos,
148+
prefix=' ')
149+
cause_node = Name(self.cause_variable, start_pos=cause_node_start_pos, prefix=' ')
150+
replacement_node = parso.python.tree.PythonNode("arith_expr",
151+
[arith_expr_node, arith_operator_node, cause_node])
42152
return replacement_node
43153

44154
def _get_causes_from_expr_node(self, expr_node):

src/cosmic_ray/operators/variable_replacer.py

+119-31
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Implementation of the variable-replacement operator."""
22
from .operator import Operator
33
from .example import Example
4-
from parso.python.tree import Number, ExprStmt, Leaf
4+
from parso.python.tree import Number, ExprStmt, Leaf, PythonNode, IfStmt
55
from random import randint
66

77

@@ -13,38 +13,103 @@ def __init__(self, cause_variable, effect_variable=None):
1313
self.effect_variable = effect_variable
1414

1515
def mutation_positions(self, node):
16-
"""Mutate usages of the specified cause variable. If an effect variable is also
17-
specified, then only mutate usages of the cause variable in definitions of the
18-
effect variable."""
19-
20-
if isinstance(node, ExprStmt):
21-
# Confirm that name node is used on right hand side of the expression
22-
cause_variables = list(self._get_causes_from_expr_node(node))
23-
cause_variable_names = [cause_variable.value for cause_variable in cause_variables]
24-
if self.cause_variable in cause_variable_names:
25-
mutation_position = (node.start_pos, node.end_pos)
26-
27-
# If an effect variable is specified, confirm that it appears on left hand
28-
# side of the expression
29-
if self.effect_variable:
30-
effect_variable_names = [v.value for v in node.get_defined_names()]
31-
if self.effect_variable in effect_variable_names:
32-
yield mutation_position
33-
34-
# If no effect variable is specified, any occurrence of the cause variable
35-
# on the right hand side of an expression can be mutated
36-
else:
37-
yield mutation_position
16+
"""Replace usages of the cause variable with a constant to remove its causal effect on the effect variable.
17+
18+
This method identifies all 'suites' that are used to define the value of the effect variable, where a 'suite'
19+
is a body of code that follows an if statement. The entire suite is later replaced with a copy in which all
20+
usages of the cause variable are replaced with a randomly sampled numeric constant.
21+
22+
:param node: node of parso parse tree that is a potential candidate for mutation.
23+
:return (start_pos, end_pos): A pair representing the position in the abstract syntax tree to mutate (only if
24+
the mutation operator is applicable at this position).
25+
"""
26+
27+
if isinstance(node, PythonNode) and node.type == "suite" and isinstance(node.parent, IfStmt):
28+
29+
# This node is the body of an if-statement.
30+
# We are only interested in the body of the outer-most if statements that have no else branch.
31+
if 'else' not in node.parent.children:
32+
causes, effects = self._get_cause_and_effect_nodes_from_suite_node(node)
33+
named_causes = [cause.value for cause in causes]
34+
named_effects = [effect.value for effect in effects]
35+
if (self.effect_variable in named_effects) and (self.cause_variable in named_causes):
36+
print(f"{self.cause_variable} --> {self.effect_variable}")
37+
yield node.start_pos, node.end_pos
3838

3939
def mutate(self, node, index):
40-
"""Replace cause variable with random constant."""
41-
assert isinstance(node, ExprStmt)
42-
# Find all occurrences of the cause node in the ExprStatement and replace with a random number
43-
rhs = node.get_rhs()
44-
new_rhs = self._replace_named_variable_in_expr(rhs, self.cause_variable)
45-
node.children[2] = new_rhs
40+
"""Replace 'suite' defining the effect variable with a copy in which the cause variable is absent.
41+
42+
There are three parts of the 'suite' in which the cause variable can have an effect on the effect variable:
43+
(1) The predicate of the if statement: if (X1 + X2 + X3) >= 10:
44+
(2) The statement of the true branch: Y1 = X2 + 10
45+
else:
46+
(3) The statement of the false branch: Y1 = X2 + X3 + 4
47+
48+
In the above example, X1 --> Y1 via the predicate, X2 --> Y1 via the true and false branches, and X3 --> Y1 via
49+
only the false branch.
50+
51+
This method finds usages of the specified cause variable in either (1), (2), or (3), and replaces them
52+
simultaneously with a randomly sampled numeric constant.
53+
"""
54+
assert isinstance(node, PythonNode) and node.type == "suite", "Error: Node is not a suite."
55+
print("PRE-MUTATION CODE: ", node.get_code())
56+
no_causes_node = self._replace_causes_in_suite(node)
57+
print("POST-MUTATION CODE: ", no_causes_node.get_code())
58+
return no_causes_node
59+
60+
def _replace_causes_in_suite(self, node):
61+
expr_nodes = []
62+
for child_node in node.children:
63+
# Expression/statement
64+
if isinstance(child_node, ExprStmt):
65+
expr_nodes.append(child_node)
66+
elif isinstance(child_node, PythonNode):
67+
expr_nodes.append(child_node.children[0])
68+
# If statement
69+
elif isinstance(child_node, IfStmt):
70+
for grandchild_node in child_node.children:
71+
# Predicate of if statement
72+
if isinstance(grandchild_node, PythonNode) and grandchild_node.type == "comparison":
73+
arith_expr = grandchild_node.children[0]
74+
new_arith_expr = self._replace_named_variable_in_expr(arith_expr, self.cause_variable)
75+
grandchild_node.children[0] = new_arith_expr
76+
# Expressions/statements in true/false branch of if statement
77+
elif isinstance(grandchild_node, PythonNode) and grandchild_node.type == "suite":
78+
grandchild_node = self._replace_named_variable_in_expr(grandchild_node, self.cause_variable)
79+
80+
# Replace usages of cause variable in expression nodes
81+
for expr_node in expr_nodes:
82+
rhs = expr_node.get_rhs()
83+
new_rhs = self._replace_named_variable_in_expr(rhs, self.cause_variable)
84+
expr_node.children[2] = new_rhs
85+
4686
return node
4787

88+
def _get_cause_and_effect_nodes_from_suite_node(self, suite_node):
89+
causes = [] # Variables that appear on RHS of expressions/statements OR in the predicate of an if statement
90+
effects = [] # Variables appearing on LHS of expressions/statements
91+
expr_nodes = [] # These are expressions/statements
92+
93+
for child_node in suite_node.children:
94+
if isinstance(child_node, ExprStmt):
95+
expr_nodes.append(child_node)
96+
elif isinstance(child_node, PythonNode):
97+
expr_nodes.append(child_node.children[0])
98+
elif isinstance(child_node, IfStmt):
99+
for grandchild_node in child_node.children:
100+
if isinstance(grandchild_node, PythonNode) and grandchild_node.type == "suite":
101+
gc_causes, gc_effects = self._get_cause_and_effect_nodes_from_suite_node(grandchild_node)
102+
causes += gc_causes
103+
effects += gc_effects
104+
elif isinstance(grandchild_node, PythonNode) and grandchild_node.type == "comparison":
105+
gc_comparison_causes = list(self._flatten_comparison(grandchild_node))
106+
causes += gc_comparison_causes
107+
108+
for expr_node in expr_nodes:
109+
causes += list(self._get_causes_from_expr_node(expr_node))
110+
effects += expr_node.get_defined_names()
111+
return causes, effects
112+
48113
def _get_causes_from_expr_node(self, expr_node):
49114
rhs = expr_node.get_rhs().children
50115
return self._flatten_expr(rhs)
@@ -56,16 +121,39 @@ def _flatten_expr(self, expr):
56121
item_to_flatten = item.children
57122
except AttributeError:
58123
item_to_flatten = item
59-
#
60124
try:
61125
yield from self._flatten_expr(item_to_flatten)
62126
except TypeError:
63127
yield item_to_flatten
64128

129+
def _flatten_comparison(self, conditional):
130+
try:
131+
# PythonNode (has children)
132+
to_iterate = conditional.children
133+
except AttributeError:
134+
# Not PythonNode (has no children)
135+
to_iterate = conditional
136+
137+
for child_node in to_iterate:
138+
try:
139+
# If the current node has children, flatten these
140+
item_to_flatten = child_node.children
141+
except AttributeError:
142+
# Otherwise flatted the node itself
143+
item_to_flatten = child_node
144+
145+
try:
146+
yield from self._flatten_comparison(item_to_flatten)
147+
except TypeError:
148+
# Non-iterable (leaf node)
149+
yield item_to_flatten
150+
65151
def _replace_named_variable_in_expr(self, node, variable_name):
66152
if isinstance(node, Leaf):
67153
if node.value == variable_name:
68-
return Number(start_pos=node.start_pos, value=str(randint(-100, 100)))
154+
print(node)
155+
print(node.start_pos, node.end_pos)
156+
return Number(start_pos=node.start_pos, value=str(randint(-100, 100)), prefix=' ')
69157
else:
70158
return node
71159

0 commit comments

Comments
 (0)