Skip to content

Commit

Permalink
Merge pull request #19 from joszamama/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
joszamama authored Oct 16, 2024
2 parents 7f307b1 + b7a283c commit af4c698
Show file tree
Hide file tree
Showing 8 changed files with 321 additions and 102 deletions.
11 changes: 7 additions & 4 deletions simple.fan
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
<a> ::= "a" | <b>;
<b> ::= "b" | "b" <b> :: "bb";
<start> ::= <ab>;
<ab> ::=
"a" <ab>
| <ab> "b"
| ""
;

<a> == "a";
<b> == "b";
'a' not in str(<ab>) and |<ab>| > 2;
6 changes: 4 additions & 2 deletions src/fandango/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,10 @@ def main(*args: str, stdout=sys.stdout, stderr=sys.stderr):
LOGGER.setLevel(logging.DEBUG)

if args.command == INTERACTIVE:
interactive = Interactive(args.fan, args.grammar, args.constraints, args.python)
interactive.run()
interactive_ = Interactive(
args.fan, args.grammar, args.constraints, args.python
)
interactive_.run()


if __name__ == "__main__":
Expand Down
147 changes: 128 additions & 19 deletions src/fandango/cli/interactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from fandango.constraints import predicates
from fandango.constraints.fitness import GeneticBase
from fandango.evolution.algorithm import FANDANGO
from fandango.language.convert import (
FandangoSplitter,
GrammarProcessor,
Expand All @@ -21,6 +22,15 @@
from fandango.language.symbol import NonTerminal
from fandango.language.tree import DerivationTree

_PARAMETERS = {
"population": "_population_size",
"generations": "_max_generations",
"elitism": "_elitism_rate",
"crossover": "_crossover_rate",
"tournament": "_tournament_size",
"mutation": "_mutation_rate",
}


class InteractiveCommands(enum.Enum):
EXIT = "exit"
Expand All @@ -35,6 +45,7 @@ class InteractiveCommands(enum.Enum):
DEL = "del"
CLEAR = "clear"
START = "start"
SET = "set"

def usage(self):
return {
Expand All @@ -45,11 +56,12 @@ def usage(self):
InteractiveCommands.PYTHON: "python [<code>]",
InteractiveCommands.TEST: 'test[-<identifier>][-<non-terminal>] ("<string>" | <identifier>)',
InteractiveCommands.FUZZ: "fuzz [<non-terminal>] [= <identifier>]",
InteractiveCommands.FANDANGO: "fandango",
InteractiveCommands.FANDANGO: "fandango [= <identifier>]",
InteractiveCommands.SHOW: "show [<non-terminal> | <identifier>]",
InteractiveCommands.DEL: "del <identifier>",
InteractiveCommands.CLEAR: "clear",
InteractiveCommands.START: "start <non-terminal>",
InteractiveCommands.SET: "set <parameter> <value>",
}[self]

def description(self):
Expand All @@ -73,12 +85,16 @@ def description(self):
"the fuzzed string will be assigned to that identifier to use in further "
"commands. If a non-terminal is specified, the string will be fuzzed from "
"that non-terminal instead of the start symbol",
InteractiveCommands.FANDANGO: "Run the fandango algorithm with the current specification",
InteractiveCommands.FANDANGO: "Run the fandango algorithm with the current specification. If an identifier "
"is specified, the result will be assigned to that identifier to use in "
"further commands",
InteractiveCommands.SHOW: "Show the rule for a non-terminal or the constraint with the specified "
"identifier. If no argument is specified, show all rules and constraints",
InteractiveCommands.DEL: "Delete a constraint with the specified identifier",
InteractiveCommands.CLEAR: "Clear all rules, constraints and python code",
InteractiveCommands.START: "Set the start symbol for fuzzing and testing, defaults to <start>",
InteractiveCommands.SET: "Set a parameter for the fandango algorithm. Parameters are: "
+ ", ".join(_PARAMETERS.keys()),
}[self]

def help(self):
Expand Down Expand Up @@ -155,11 +171,18 @@ def __init__(
for constraint in constraints:
self._add_constraint(constraint)

self._fandango = None
self._fandango_object = None
self._updated_grammar = False
self._updated_constraints = False
self._start_symbol = NonTerminal("<start>")
self._scope = {}
self._population_size: int = 50
self._max_generations: int = 100
self._elitism_rate: float = 0.1
self._crossover_rate: float = 0.8
self._tournament_size: float = 0.05
self._mutation_rate: float = 0.2
self._updated_parameters = False

def _rule(self, rule: str, single: bool = False):
parser = FandangoParser(CommonTokenStream(FandangoLexer(InputStream(rule))))
Expand Down Expand Up @@ -208,7 +231,7 @@ def _clear(self):
self._constraints = {}
self.constraints_identifier = 0
self._grammar = Grammar.dummy()
self._fandango = None
self._fandango_object = None
self._updated_grammar = False
self._updated_constraints = False
self._local_vars = predicates.__dict__
Expand Down Expand Up @@ -279,23 +302,37 @@ def _test(
except Exception:
print("Failed to evaluate string or identifier")
return
if isinstance(string, DerivationTree):
tree = string
if isinstance(string, list):
trees = string
elif isinstance(string, tuple):
trees = list(string)
elif isinstance(string, set):
trees = list(string)
else:
tree = self._grammar.parse(string, start)
if tree is None:
print(f"Failed to parse string from start symbol {start}")
return
for constraint in constraints:
print(
("PASSED" if constraint.fitness(tree).success else "FAILED")
+ f" {constraint}"
)
trees = [string]
for s in trees:
if isinstance(s, DerivationTree):
tree = s
else:
try:
tree = self._grammar.parse(s)
except:
tree = None
if tree is None:
print(f"Failed to parse string from start symbol {start}")
return
print(f"Testing {tree}")
for constraint in constraints:
print(
("PASSED" if constraint.fitness(tree).success else "FAILED")
+ f" {constraint}"
)
print()

def _start(self, non_terminal: str):
self._start_symbol = NonTerminal(non_terminal)

def fuzz(
def _fuzz(
self, identifier: Optional[str] = None, non_terminal: Optional[str] = None
):
if non_terminal:
Expand All @@ -307,6 +344,64 @@ def fuzz(
self._scope[identifier] = tree
print(tree)

def _set(self, parameter: str, value: str):
if parameter in _PARAMETERS:
if parameter in {"population", "generations"}:
value = int(value)
if value < 1:
print("Value must be greater than 0")
return
else:
value = float(value)
if value < 0 or value > 1:
print("Value must be between 0 and 1")
return
setattr(self, f"_{_PARAMETERS[parameter]}", float(value))
self._updated_parameters = True
else:
print(f"Parameter {parameter} not found")

def _fandango(self, identifier: Optional[str] = None):
if self._fandango_object is None:
self._fandango_object = FANDANGO(
grammar=self._grammar,
constraints=list(self._constraints.values()),
population_size=self._population_size,
mutation_rate=self._mutation_rate,
crossover_rate=self._crossover_rate,
max_generations=self._max_generations,
elitism_rate=self._elitism_rate,
verbose=False,
)
population = self._fandango_object.evolve()
elif (
self._updated_grammar
or self._updated_constraints
or self._updated_parameters
):
if self._updated_grammar:
self._grammar.update_parser()
self._updated_grammar = False
self._fandango_object = FANDANGO(
grammar=self._grammar,
constraints=list(self._constraints.values()),
population_size=self._population_size,
mutation_rate=self._mutation_rate,
crossover_rate=self._crossover_rate,
max_generations=self._max_generations,
elitism_rate=self._elitism_rate,
verbose=False,
)
self._updated_constraints = False
self._updated_parameters = False
population = self._fandango_object.evolve()
else:
population = self._fandango_object.population
for individual in population:
print(individual)
if identifier:
self._scope[identifier] = population

def run(self):
try:
while True:
Expand Down Expand Up @@ -373,13 +468,13 @@ def run(self):
elif command.startswith(InteractiveCommands.FUZZ.value):
command = command.split(" ", 1)
if len(command) == 1:
self.fuzz()
self._fuzz()
else:
target = command[1].split("=")
if len(target) == 2:
self.fuzz(target[1].strip(), target[0].strip())
self._fuzz(target[1].strip(), target[0].strip())
else:
self.fuzz(command[0].strip())
self._fuzz(command[0].strip())
elif command.startswith(InteractiveCommands.SHOW.value):
command = command.split(" ", 1)
if len(command) == 1:
Expand All @@ -400,6 +495,20 @@ def run(self):
print("Missing non-terminal")
else:
self._start(command[1])
elif command.startswith(InteractiveCommands.SET.value):
command = command.split(" ")
if len(command) < 3:
print("Missing parameter or value")
elif len(command) > 3:
print("Too many arguments")
else:
self._set(command[1], command[2])
elif command.startswith(InteractiveCommands.FANDANGO.value):
command = command.split("=", 1)
if len(command) == 1:
self._fandango()
else:
self._fandango(command[1].strip())
else:
print(f"Command not found {command}")
except KeyboardInterrupt:
Expand Down
45 changes: 27 additions & 18 deletions src/fandango/constraints/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,13 @@ def fitness(
values = []
for combination in self.combinations(tree, scope):
local_variables = self.local_variables.copy()
local_variables.update({name: node for name, node in combination})
for _, node in combination:
if node not in trees:
trees.append(FailingTree(node, self))
local_variables.update(
{name: container.evaluate() for name, container in combination}
)
for _, container in combination:
for node in container.get_trees():
if node not in trees:
trees.append(FailingTree(node, self))
try:
values.append(
eval(self.expression, self.global_variables, local_variables)
Expand Down Expand Up @@ -99,14 +102,17 @@ def fitness(
for combination in self.combinations(tree, scope):
has_combinations = True
local_variables = self.local_variables.copy()
local_variables.update({name: node for name, node in combination})
local_variables.update(
{name: container.evaluate() for name, container in combination}
)
try:
if eval(self.expression, self.global_variables, local_variables):
solved += 1
else:
for _, node in combination:
if node not in failing_trees:
failing_trees.append(FailingTree(node, self))
for _, container in combination:
for node in container.get_trees():
if node not in failing_trees:
failing_trees.append(FailingTree(node, self))
except:
pass
total += 1
Expand Down Expand Up @@ -148,7 +154,9 @@ def fitness(
for combination in self.combinations(tree, scope):
has_combinations = True
local_variables = self.local_variables.copy()
local_variables.update({name: node for name, node in combination})
local_variables.update(
{name: container.evaluate() for name, container in combination}
)
try:
left = eval(self.left, self.global_variables, local_variables)
right = eval(self.right, self.global_variables, local_variables)
Expand Down Expand Up @@ -218,11 +226,12 @@ def fitness(
if is_solved:
solved += 1
else:
for _, node in combination:
if node not in failing_trees:
failing_trees.append(
FailingTree(node, self, suggestions=suggestions)
)
for _, container in combination:
for node in container.get_trees():
if node not in failing_trees:
failing_trees.append(
FailingTree(node, self, suggestions=suggestions)
)
except:
pass
total += 1
Expand Down Expand Up @@ -391,8 +400,8 @@ def fitness(
return copy(self.cache[tree_hash])
fitness_values = list()
scope = scope or dict()
for dt in self.search.find(tree, scope=scope):
scope[self.bound] = dt
for container in self.search.find(tree, scope=scope):
scope[self.bound] = container.evaluate()
fitness = self.statement.fitness(tree, scope)
fitness_values.append(fitness)
if self.lazy and fitness.success:
Expand Down Expand Up @@ -442,8 +451,8 @@ def fitness(
return copy(self.cache[tree_hash])
fitness_values = list()
scope = scope or dict()
for dt in self.search.find(tree, scope=scope):
scope[self.bound] = dt
for container in self.search.find(tree, scope=scope):
scope[self.bound] = container.evaluate()
fitness = self.statement.fitness(tree, scope)
fitness_values.append(fitness)
if self.lazy and not fitness.success:
Expand Down
10 changes: 9 additions & 1 deletion src/fandango/constraints/fitness.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ def __hash__(self):
def __eq__(self, other):
return self.tree == other.tree and self.cause == other.cause

def __repr__(self):
return f"FailingTree({self.tree}, {self.cause}, {self.suggestions})"

def __str__(self):
return self.__repr__()


class Fitness(abc.ABC):
def __init__(self, success: bool, failing_trees: List[FailingTree] = None):
Expand Down Expand Up @@ -136,7 +142,9 @@ def combinations(
):
nodes: List[List[Tuple[str, DerivationTree]]] = []
for name, search in self.searches.items():
nodes.append([(name, node) for node in search.find(tree, scope=scope)])
nodes.append(
[(name, container) for container in search.find(tree, scope=scope)]
)
return itertools.product(*nodes)

def check(
Expand Down
Loading

0 comments on commit af4c698

Please sign in to comment.