Skip to content

Commit e4a94ed

Browse files
committed
literaly wrote entire mutation engine in only 3 hours
1 parent fc97b0a commit e4a94ed

File tree

2 files changed

+298
-17
lines changed

2 files changed

+298
-17
lines changed

engine.py

Lines changed: 181 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
from numpy import random
2+
23
# Mutation operators
34
# AOR: Arithmetic Operator Replacement: a + b -> a - b
45
# LCR: Logical Connector Replacement: a and b -> a or b
56
# ROR: Relational Operator Replacement: a > b -> a < b
6-
# UOI: Unary Operator Insertion: a -> not a (only in conditionals)
7-
# SBR: Statement Block Replacement: stmt -> 0
7+
# UOI: Unary Operator Insertion: b -> not b, i -> i + 1
8+
# SBR: Statement Block Replacement: stmt -> 0 TODO
89

910
from typing import Literal, Callable, Any
1011

@@ -27,29 +28,122 @@ def convert_to_add(self, node):
2728
def convert_to_sub(self, node):
2829
raise NotImplementedError
2930

31+
def convert_to_binop_a(self, node):
32+
raise NotImplementedError
33+
34+
def convert_to_binop_b(self, node):
35+
raise NotImplementedError
36+
37+
def convert_to_boolop_a(self, node):
38+
raise NotImplementedError
39+
40+
def convert_to_boolop_b(self, node):
41+
raise NotImplementedError
42+
43+
def convert_to_and(self, node):
44+
raise NotImplementedError
45+
46+
def convert_to_or(self, node):
47+
raise NotImplementedError
48+
49+
def convert_to_true(self, node):
50+
raise NotImplementedError
51+
52+
def convert_to_false(self, node):
53+
raise NotImplementedError
54+
55+
def convert_to_gt(self, node):
56+
raise NotImplementedError
57+
58+
def convert_to_lt(self, node):
59+
raise NotImplementedError
60+
61+
def convert_to_gte(self, node):
62+
raise NotImplementedError
63+
64+
def convert_to_lte(self, node):
65+
raise NotImplementedError
66+
67+
def convert_to_eq(self, node):
68+
raise NotImplementedError
69+
70+
def convert_to_neq(self, node):
71+
raise NotImplementedError
72+
73+
def convert_to_not(self, node):
74+
raise NotImplementedError
75+
76+
def convert_to_increment(self, node):
77+
raise NotImplementedError
78+
79+
def convert_to_decrement(self, node):
80+
raise NotImplementedError
81+
82+
83+
class EngineConfig:
84+
def __init__(
85+
self,
86+
aor_op_rate=0.2,
87+
lcr_op_rate=0.2,
88+
ror_op_rate=0.2,
89+
binop_expr_rate=0.1,
90+
boolop_expr_rate=0.1,
91+
num_lit_rate=0.1,
92+
max_mutations=1,
93+
):
94+
self.aor_op_rate = aor_op_rate
95+
self.lcr_op_rate = lcr_op_rate
96+
self.ror_op_rate = ror_op_rate
97+
self.binop_expr_rate = binop_expr_rate
98+
self.boolop_expr_rate = boolop_expr_rate
99+
self.num_lit_rate = num_lit_rate
100+
self.max_mutations = max_mutations
101+
30102

31103
class Engine:
32-
def __init__(self, mutation_rate: float = 0.1, seed=1337):
33-
self.mutation_rate = mutation_rate
104+
def __init__(
105+
self,
106+
config: EngineConfig = EngineConfig(),
107+
seed=1337
108+
):
109+
self.config = config
34110
self.rng = random.default_rng(seed)
111+
self.mutate_calls = 0
112+
self.already_mutated = []
113+
self.mutations = 0
114+
115+
def new(self):
116+
engine = Engine(config=self.config, seed=self.rng.integers(0, 2 ** 32))
117+
engine.already_mutated = self.already_mutated
118+
return engine
35119

36120
def pick(self, iterable):
37121
return self.rng.choice(iterable)
38122

39-
def mutate(self, node, conv_fn):
40-
if self.rng.random() < self.mutation_rate:
123+
def mutate_node(self, node, conv_fn, rate):
124+
self.mutate_calls += 1
125+
if self.mutations < self.config.max_mutations \
126+
and self.mutate_calls not in self.already_mutated \
127+
and self.rng.random() < rate:
128+
self.mutations += 1
129+
self.already_mutated.append(self.mutate_calls)
41130
return conv_fn(node)
42131
else:
43132
return node
44133

45134

46135
class Mutator(Converter):
47136
AOR_OPS = ["+", "-", "*", "/"]
137+
BIN_OPS = ["a", "b"]
138+
BOOL_OPS = ["a", "b", "true", "false", "not"]
139+
LCR_OPS = ["and", "or"]
140+
ROR_OPS = [">", "<", ">=", "<=", "==", "!="]
141+
NUM_LIT_OP = ["++", "--"]
48142

49143
def __init__(self, engine: Engine):
50144
self.engine = engine
51145

52-
def mutate_aor(self, node, current: Literal["+", "-", "*", "/"]):
146+
def mutate_aor_op(self, node, current: Literal["+", "-", "*", "/"]):
53147
possible = [op for op in self.AOR_OPS if op != current]
54148
picked = self.engine.pick(possible)
55149

@@ -65,4 +159,83 @@ def mutate_aor(self, node, current: Literal["+", "-", "*", "/"]):
65159
else:
66160
raise Exception("Unknown operator")
67161

68-
return self.engine.mutate(node, conv_fn)
162+
return self.engine.mutate_node(node, conv_fn, self.engine.config.aor_op_rate)
163+
164+
def mutate_binop_expr(self, node):
165+
picked = self.engine.pick(self.BIN_OPS)
166+
167+
conv_fn = None
168+
if picked == "a":
169+
conv_fn = self.convert_to_binop_a
170+
elif picked == "b":
171+
conv_fn = self.convert_to_binop_b
172+
else:
173+
raise Exception("Unknown operator")
174+
175+
return self.engine.mutate_node(node, conv_fn, self.engine.config.binop_expr_rate)
176+
177+
def mutate_lcr(self, node, current: Literal["and", "or"]):
178+
possible = [op for op in self.LCR_OPS if op != current]
179+
picked = self.engine.pick(possible)
180+
181+
conv_fn = None
182+
if picked == "and":
183+
conv_fn = self.convert_to_and
184+
elif picked == "or":
185+
conv_fn = self.convert_to_or
186+
else:
187+
raise Exception("Unknown operator")
188+
189+
return self.engine.mutate_node(node, conv_fn, self.engine.config.lcr_op_rate)
190+
191+
def mutate_boolop_expr(self, node):
192+
picked = self.engine.pick(self.BOOL_OPS)
193+
194+
conv_fn = None
195+
if picked == "a":
196+
conv_fn = self.convert_to_boolop_a
197+
elif picked == "b":
198+
conv_fn = self.convert_to_boolop_b
199+
elif picked == "true":
200+
conv_fn = self.convert_to_true
201+
elif picked == "false":
202+
conv_fn = self.convert_to_false
203+
elif picked == "not":
204+
conv_fn = self.convert_to_not
205+
else:
206+
raise Exception("Unknown operator")
207+
208+
return self.engine.mutate_node(node, conv_fn, self.engine.config.boolop_expr_rate)
209+
210+
def mutate_ror(self, node, current: Literal[">", "<", ">=", "<=", "==", "!="]):
211+
possible = [op for op in self.ROR_OPS if op != current]
212+
picked = self.engine.pick(possible)
213+
214+
conv_fn = None
215+
if picked == ">":
216+
conv_fn = self.convert_to_gt
217+
elif picked == "<":
218+
conv_fn = self.convert_to_lt
219+
elif picked == ">=":
220+
conv_fn = self.convert_to_gte
221+
elif picked == "<=":
222+
conv_fn = self.convert_to_lte
223+
elif picked == "==":
224+
conv_fn = self.convert_to_eq
225+
elif picked == "!=":
226+
conv_fn = self.convert_to_neq
227+
else:
228+
raise Exception("Unknown operator")
229+
230+
return self.engine.mutate_node(node, conv_fn, self.engine.config.ror_op_rate)
231+
232+
def mutate_number_literal(self, node):
233+
picked = self.engine.pick(self.NUM_LIT_OP)
234+
235+
conv_fn = None
236+
if picked == "++":
237+
conv_fn = self.convert_to_increment
238+
elif picked == "--":
239+
conv_fn = self.convert_to_decrement
240+
241+
return self.engine.mutate_node(node, conv_fn, self.engine.config.num_lit_rate)

python.py

Lines changed: 117 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,25 +19,121 @@ def convert_to_add(self, node):
1919
def convert_to_sub(self, node):
2020
return ast.Sub()
2121

22+
def convert_to_and(self, node):
23+
return ast.And()
24+
25+
def convert_to_or(self, node):
26+
return ast.Or()
27+
28+
def convert_to_binop_a(self, node: ast.BinOp):
29+
return node.left
30+
31+
def convert_to_binop_b(self, node: ast.BinOp):
32+
return node.right
33+
34+
def convert_to_boolop_a(self, node: ast.BoolOp):
35+
return node.values[0]
36+
37+
def convert_to_boolop_b(self, node: ast.BoolOp):
38+
return node.values[1]
39+
40+
def convert_to_true(self, node):
41+
return ast.NameConstant(value=True)
42+
43+
def convert_to_false(self, node):
44+
return ast.NameConstant(value=False)
45+
46+
def convert_to_not(self, node):
47+
return ast.UnaryOp(op=ast.Not(), operand=node)
48+
49+
def convert_to_gt(self, node):
50+
return ast.Gt()
51+
52+
def convert_to_lt(self, node):
53+
return ast.Lt()
54+
55+
def convert_to_gte(self, node):
56+
return ast.GtE()
57+
58+
def convert_to_lte(self, node):
59+
return ast.LtE()
60+
61+
def convert_to_eq(self, node):
62+
return ast.Eq()
63+
64+
def convert_to_neq(self, node):
65+
return ast.NotEq()
66+
67+
68+
def convert_to_increment(self, node: ast.Constant):
69+
return ast.Constant(value=node.value + 1)
70+
71+
def convert_to_decrement(self, node: ast.Constant):
72+
return ast.Constant(value=node.value - 1)
73+
74+
def visit_BinOp(self, node: ast.BinOp):
75+
mutated = self.mutate_binop_expr(node)
76+
if mutated == node:
77+
return self.generic_visit(node)
78+
else:
79+
return mutated
80+
81+
def visit_BoolOp(self, node: ast.BoolOp):
82+
mutated = self.mutate_boolop_expr(node)
83+
if mutated == node:
84+
return self.generic_visit(node)
85+
else:
86+
return mutated
87+
88+
def visit_Constant(self, node: ast.Constant):
89+
if type(node.value) == int:
90+
return self.mutate_number_literal(node)
91+
else: # TODO: mutate bool? i think we shouldn't
92+
return self.generic_visit(node)
93+
2294
def visit_Sub(self, node):
23-
return self.mutate_aor(node, "-")
95+
return self.mutate_aor_op(node, "-")
2496

2597
def visit_Add(self, node):
26-
return self.mutate_aor(node, "+")
98+
return self.mutate_aor_op(node, "+")
2799

28100
def visit_Mult(self, node):
29-
return self.mutate_aor(node, "*")
101+
return self.mutate_aor_op(node, "*")
30102

31103
def visit_Div(self, node):
32-
return self.mutate_aor(node, "/")
104+
return self.mutate_aor_op(node, "/")
105+
106+
def visit_And(self, node):
107+
return self.mutate_lcr(node, "and")
108+
109+
def visit_Or(self, node):
110+
return self.mutate_lcr(node, "or")
111+
112+
def visit_Gt(self, node):
113+
return self.mutate_ror(node, ">")
114+
115+
def visit_Lt(self, node):
116+
return self.mutate_ror(node, "<")
117+
118+
def visit_GtE(self, node):
119+
return self.mutate_ror(node, ">=")
120+
121+
def visit_LtE(self, node):
122+
return self.mutate_ror(node, "<=")
123+
124+
def visit_Eq(self, node):
125+
return self.mutate_ror(node, "==")
126+
127+
def visit_NotEq(self, node):
128+
return self.mutate_ror(node, "!=")
33129

34130

35131
if __name__ == "__main__":
36132
import os
37133
CODE = """
38134
def func(a, b):
39135
bleh = 1337
40-
if a > b:
136+
if a > b > b and True and True or False:
41137
return a - b
42138
elif a < b:
43139
return b - a
@@ -46,7 +142,19 @@ def func(a, b):
46142
"""
47143

48144
tree = ast.parse(CODE)
49-
engine = Engine(0.9, int(os.urandom(4).hex(), 16))
50-
mutator = PythonMutator(engine)
51-
tree = mutator.visit(tree)
52-
print(astunparse.unparse(tree))
145+
print(ast.dump(tree))
146+
engine = Engine(
147+
seed=int(os.urandom(4).hex(), 16)
148+
)
149+
mutated_set = set()
150+
for i in range(100):
151+
mutator = PythonMutator(engine)
152+
tree = ast.parse(CODE)
153+
mutated = mutator.visit(tree)
154+
if engine.mutations == 0:
155+
continue
156+
mutated_set.add(astunparse.unparse(mutated))
157+
engine = engine.new()
158+
159+
for m in mutated_set:
160+
print(m)

0 commit comments

Comments
 (0)