Source code for grammarinator.runtime.population

# Copyright (c) 2023-2025 Renata Hodovan, Akos Kiss.
#
# Licensed under the BSD 3-Clause License
# <LICENSE.rst or https://opensource.org/licenses/BSD-3-Clause>.
# This file may not be copied, modified, or distributed except
# according to those terms.

from __future__ import annotations

from .rule import ParentRule, Rule, UnlexerRule, UnparserRule, UnparserRuleAlternative, UnparserRuleQuantified, UnparserRuleQuantifier


[docs] class Population: """ Abstract base class of populations that store test cases in tree form (i.e., individuals) and can select trees for mutation or recombination based on some strategy. """
[docs] def empty(self) -> bool: """ Return whether the population is empty. Raises :exc:`NotImplementedError` by default. :return: ``True`` if the population is empty and ``False`` otherwise. """ raise NotImplementedError()
def __bool__(self) -> bool: """ Truth value testing of Populations. :return: ``True`` if the population is not empty and ``False`` otherwise. """ return not self.empty()
[docs] def add_individual(self, root: Rule, path: str | None = None) -> None: """ Add a tree to the population. Raises :exc:`NotImplementedError` by default. :param root: Root of the tree to be added. :param path: The pathname of the test case corresponding to the tree, if it exists. May be used for debugging. """ raise NotImplementedError()
[docs] def select_individual(self, recipient: Individual | None = None) -> Individual: """ Select an individual of the population. Raises :exc:`NotImplementedError` by default. :param recipient: If None, the caller looks for an individual that could be mutated or recombined (i.e., a recipient). If not None, the caller looks for an individual (i.e., a donor) that could be recombined with the given individual (i.e., with the recipient). :return: A single individual of the population. """ raise NotImplementedError()
[docs] class Individual: """ Abstract base class of population individuals. """ def __init__(self, root: Rule | None = None) -> None: """ :param root: Root of the tree of the individual. """ self._root = root self._annot: Annotations | None = None @property def root(self) -> Rule: """ Return the root node of the tree of the individual. :return: Root of the tree. """ assert self._root is not None return self._root @property def annotations(self) -> Annotations: """ Return the associated annotations if available, otherwise compute them immediately. :return: The annotations associated with the tree. """ if not self._annot: self._annot = Annotations(self.root) return self._annot
[docs] class Annotations: """ Class for calculating and managing additional metadata needed by the mutators, particularly to enforce size constraints and facilitate node filtering by rule types. """ def __init__(self, root: Rule) -> None: """ :param root: Root of the tree to be annotated. """ def _annotate(current, level): nonlocal current_rule_name self.node_levels[current] = level node_name = None if isinstance(current, (UnlexerRule, UnparserRule)): if current.name != '<INVALID>': current_rule_name = (current.name,) if current != root and (not isinstance(current, UnlexerRule) or not current.immutable): if current_rule_name not in self.rules_by_name: self.rules_by_name[current_rule_name] = [] self.rules_by_name[current_rule_name].append(current) node_name = current_rule_name else: current_rule_name = None elif current_rule_name: if isinstance(current, UnparserRuleQuantifier): node_name = current_rule_name + ('q', current.idx,) if node_name not in self.quants_by_name: self.quants_by_name[node_name] = [] self.quants_by_name[node_name].append(current) elif isinstance(current, UnparserRuleQuantified): node_name = current_rule_name + ('qd', current.parent.idx,) elif isinstance(current, UnparserRuleAlternative): node_name = current_rule_name + ('a', current.alt_idx,) if node_name: if node_name not in self.nodes_by_name: self.nodes_by_name[node_name] = [] self.nodes_by_name[node_name].append(current) self.node_depths[current] = 0 self.node_tokens[current] = 0 if isinstance(current, ParentRule): for child in current.children: _annotate(child, level + 1) self.node_depths[current] = max(self.node_depths[current], self.node_depths[child] + 1) self.node_tokens[current] += self.node_tokens[child] if isinstance(child, ParentRule) else child.size.tokens + 1 current_rule_name = None self.nodes_by_name: dict[str, list[Rule]] = {} self.rules_by_name: dict[str, list[UnlexerRule | UnparserRule]] = {} self.quants_by_name: dict[str, list[UnparserRuleQuantifier]] = {} self.node_levels: dict[Rule, int] = {} self.node_depths: dict[Rule, int] = {} self.node_tokens: dict[Rule, int] = {} _annotate(root, 0) @property def nodes(self) -> list[Rule]: """ Get all the nodes of the tree. :return: List of Rule nodes. """ return [node for nodes in self.nodes_by_name.values() for node in nodes] @property def rules(self) -> list[UnlexerRule | UnparserRule]: """ Get nodes created from rule nodes. :return: List of rule nodes. """ return [rule for rules in self.rules_by_name.values() for rule in rules] @property def quants(self) -> list[UnparserRuleQuantifier]: """ Get nodes created from quantified expressions. :return: List of quantifier nodes. """ return [quant for quants in self.quants_by_name.values() for quant in quants]