Skip to content

Commit

Permalink
Add swap_local_nodes mutator
Browse files Browse the repository at this point in the history
Add a new mutator that swaps two disjunct and compatible subtrees
inside a single input test.
  • Loading branch information
renatahodovan committed Dec 20, 2024
1 parent d6caa88 commit 808c71a
Showing 1 changed file with 58 additions and 0 deletions.
58 changes: 58 additions & 0 deletions grammarinator/tool/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ def __init__(self, generator_factory, out_format, lock=None, rule=None, limit=No
self.replicate_quantified,
self.shuffle_quantifieds,
self.hoist_rule,
self.swap_local_nodes,
]
self._unrestricted_mutators = [
self.unrestricted_delete,
Expand Down Expand Up @@ -299,6 +300,7 @@ def mutate(self, individual=None):
:meth:`delete_quantified`, :meth:`replicate_quantified`,
:meth:`shuffle_quantifieds`, :meth:`hoist_rule`,
:meth:`unrestricted_delete`, :meth:`unrestricted_hoist_rule`
:meth:`swap_local_nodes`
:param ~grammarinator.runtime.Individual individual: The population item to
be mutated.
Expand Down Expand Up @@ -597,3 +599,59 @@ def unrestricted_hoist_rule(self, individual=None, _=None):
random.choice(options).replace(rule)
return root
return root

def swap_local_nodes(self, individual=None, _=None):
"""
Swap two non-overlapping subtrees at random positions in a single test
where the nodes are compatible with each other (i.e., they share the same node name).
:param ~grammarinator.runtime.Individual individual: The population item to be mutated
:return: The root of the mutated tree.
:rtype: Rule
"""
individual = self._ensure_individual(individual)
root, annot = individual.root, individual.annotations

options = dict(annot.rules_by_name)
options.update(annot.quants_by_name)
options.update(annot.alts_by_name)

for _, nodes in random.sample(list(options.items()), k=len(options)):
# Skip node types without two instances.
if len(nodes) < 2:
continue

shuffled = random.sample(nodes, k=len(nodes))
for i, first_node in enumerate(shuffled[:-1]):
first_node_level = annot.node_levels[first_node]
first_node_depth = annot.node_depths[first_node]
for second_node in shuffled[i + 1:]:
second_node_level = annot.node_levels[second_node]
second_node_depth = annot.node_depths[second_node]
if (first_node_level + second_node_depth > self._limit.depth
and second_node_level + first_node_depth > self._limit.depth):
continue

# Avoid swapping two identical nodes with each other.
if first_node.equalTokens(second_node):
continue

# Ensure the subtrees rooted at recipient and donor nodes are disjunct.
upper_node, lower_node = (first_node, second_node) if first_node_level < second_node_level else (second_node, first_node)
disjunct = True
parent = lower_node.parent
while parent and disjunct:
disjunct = parent != upper_node
parent = parent.parent

if not disjunct:
continue

first_parent = first_node.parent
second_parent = second_node.parent
first_parent.children[first_parent.children.index(first_node)] = second_node
second_parent.children[second_parent.children.index(second_node)] = first_node
first_node.parent = second_parent
second_node.parent = first_parent
return root
return root

0 comments on commit 808c71a

Please sign in to comment.