from pathlib import Path
from dataclasses import dataclass
from typing import Optional
from lark import Lark, ParseTree, Tree, Visitor, Token, Transformer_NonRecursive
from kappybara.pattern import Site, Agent, Pattern, SiteType, Partner
from kappybara.rule import Rule, KappaRule, KappaRuleUnimolecular, KappaRuleBimolecular
from kappybara.algebra import Expression
[docs]
class KappaParser:
"""Parser for Kappa language files and expressions.
Note:
Don't instantiate directly: use the global kappa_parser instance.
"""
def __init__(self):
"""Initialize the Lark parser with Kappa grammar."""
self._parser = Lark.open(
str(Path(__file__).parent / "kappa.lark"),
rel_to=__file__,
parser="earley",
# The basic lexer isn't required and isn't usually recommended
lexer="dynamic",
start="kappa_input",
# Disabling these slightly improves speed
propagate_positions=False,
maybe_placeholders=False,
)
[docs]
def parse(self, text: str) -> ParseTree:
return self._parser.parse(text)
[docs]
def parse_file(self, filepath: str) -> ParseTree:
with open(filepath, "r") as file:
return self._parser.parse(file.read())
kappa_parser = KappaParser()
[docs]
@dataclass
class SiteBuilder(Visitor):
"""Builds Site objects from Lark parse trees.
Attributes:
parsed_site_name: Name of the site being built.
parsed_state: Internal state of the site.
parsed_partner: Partner specification for the site.
"""
parsed_site_name: str
parsed_state: str
parsed_partner: Partner
def __init__(self, tree: ParseTree):
super().__init__()
self.parsed_agents: list["Agent"] = []
assert tree.data == "site"
self.visit(tree)
# Visitor method for Lark
[docs]
def site_name(self, tree: ParseTree) -> None:
self.parsed_site_name = str(tree.children[0])
# Visitor method for Lark
[docs]
def state(self, tree: ParseTree) -> None:
match tree.children[0]:
case "#":
self.parsed_state = "#"
case str(state):
self.parsed_state = str(state)
case Tree(data="unspecified"):
self.parsed_state = "?"
case _:
raise ValueError(
f"Unexpected internal state in site parse tree: {tree}"
)
# Visitor method for Lark
[docs]
def partner(self, tree: ParseTree) -> None:
match tree.children:
case ["#"]:
self.parsed_partner = "#"
case ["_"]:
self.parsed_partner = "_"
case ["."]:
self.parsed_partner = "."
case [Token("INT", x)]:
self.parsed_partner = int(x)
case [
Tree(data="site_name", children=[site_name]),
Tree(data="agent_name", children=[agent_name]),
]:
self.parsed_partner = SiteType(str(site_name), str(agent_name))
case [Tree(data="unspecified")]:
self.parsed_partner = "?"
case _:
raise ValueError(f"Unexpected link state in site parse tree: {tree}")
@property
def object(self) -> Site:
return Site(
label=self.parsed_site_name,
state=self.parsed_state,
partner=self.parsed_partner,
)
[docs]
@dataclass
class AgentBuilder(Visitor):
"""Builds Agent objects from Lark parse trees.
Attributes:
parsed_type: Type name of the agent.
parsed_interface: List of sites belonging to the agent.
"""
parsed_type: str
parsed_interface: list[Site]
def __init__(self, tree: ParseTree):
super().__init__()
self.parsed_type = None
self.parsed_interface: list[Site] = []
assert tree.data == "agent"
self.visit(tree)
# Visitor method for Lark
[docs]
def agent_name(self, tree: ParseTree) -> None:
self.parsed_type = str(tree.children[0])
# Visitor method for Lark
[docs]
def site(self, tree: ParseTree) -> None:
self.parsed_interface.append(SiteBuilder(tree).object)
@property
def object(self) -> Agent:
agent = Agent(type=self.parsed_type, sites=self.parsed_interface)
for site in agent:
site.agent = agent
return agent
[docs]
@dataclass
class PatternBuilder(Visitor):
"""Builds Pattern objects from Lark parse trees.
Attributes:
parsed_agents: List of agents in the pattern.
"""
parsed_agents: list[Agent]
def __init__(self, tree: ParseTree):
super().__init__()
self.parsed_agents: list[Agent] = []
assert tree.data == "pattern"
self.visit(tree)
# Visitor method for Lark
[docs]
def agent(self, tree: ParseTree) -> None:
self.parsed_agents.append(AgentBuilder(tree).object)
@property
def object(self) -> Pattern:
return Pattern(agents=self.parsed_agents)
[docs]
@dataclass
class RuleBuilder(Visitor):
"""Builds Rule objects from Lark parse trees.
Attributes:
parsed_label: Optional label for the rule.
left_agents: Agents on the left side of the rule.
right_agents: Agents on the right side of the rule.
parsed_rates: Rate expressions for the rule.
tree_data: Type of rule being built.
"""
parsed_label: Optional[str]
left_agents: list[Optional[Agent]]
right_agents: list[Optional[Agent]]
parsed_rates: list[Expression]
tree_data: str
def __init__(self, tree: ParseTree):
super().__init__()
self.parsed_label = None
self.left_agents = []
self.right_agents = []
self.parsed_rates = []
assert tree.data in ["f_rule", "fr_rule", "ambi_rule", "ambi_fr_rule"]
self.tree_data = tree.data
self.visit(tree)
# Visitor method for Lark
[docs]
def rate(self, tree: ParseTree) -> None:
assert tree.data == "rate"
expr = tree.children[0]
assert expr.data == "algebraic_expression"
rate = parse_tree_to_expression(expr)
self.parsed_rates.append(rate)
# Visitor method for Lark
[docs]
def rule_expression(self, tree: ParseTree) -> None:
assert tree.data in ["rule_expression", "rev_rule_expression"]
mid_idx = next(
(i for i, child in enumerate(tree.children) if child in ["->", "<->"])
) # Locate the arrow in the expression
for i, child in enumerate(tree.children):
if i == mid_idx:
continue
if child == ".":
agent = None
elif child.data == "agent":
agent = AgentBuilder(child).object
if i < mid_idx:
self.left_agents.append(agent)
else:
self.right_agents.append(agent)
# Visitor method for Lark
[docs]
def rev_rule_expression(self, tree: ParseTree) -> None:
self.rule_expression(tree)
@property
def objects(self) -> list[Rule]:
rules = []
left = Pattern(self.left_agents)
right = Pattern(self.right_agents)
rates = self.parsed_rates
match self.tree_data:
case "f_rule":
assert len(rates) == 1
rules.append(KappaRule(left, right, rates[0]))
case "fr_rule":
assert len(rates) == 2
rules.append(KappaRule(left, right, rates[0]))
rules.append(KappaRule(right, left, rates[1]))
case "ambi_rule":
# TODO: check that the order of the rates is right
assert len(rates) == 2
try:
assert rates[0].evaluate() == 0
except:
rules.append(KappaRuleBimolecular(left, right, rates[0]))
try:
assert rates[1].evaluate() == 0
except:
rules.append(KappaRuleUnimolecular(left, right, rates[1]))
case "ambi_fr_rule":
assert len(rates) == 3
try:
assert rates[0].evaluate() == 0
except:
rules.append(KappaRuleBimolecular(left, right, rates[0]))
try:
assert rates[1].evaluate() == 0
except:
rules.append(KappaRuleUnimolecular(left, right, rates[1]))
rules.append(KappaRule(right, left, rates[2]))
return [r for r in rules if r is not None]
[docs]
class LarkTreetoExpression(Transformer_NonRecursive):
"""Transforms a Lark ParseTree into an Expression object.
Note:
Uses a Transformer to preserve the tree structure of the original
ParseTree. This doesn't need to use Transformer_NonRecursive anymore
due to grammar changes, but methods explicitly call transform on children.
"""
[docs]
def algebraic_expression(self, children):
children = [self.transform(c) for c in children]
if len(children) == 1:
return children[0]
elif len(children) == 3 and children[0] == "(" and children[2] == ")":
return children[1]
else:
raise Exception(f"Invalid algebraic expression: {children}")
# --- Literals ---
[docs]
def SIGNED_FLOAT(self, token):
return Expression("literal", value=float(token.value))
[docs]
def SIGNED_INT(self, token):
return Expression("literal", value=int(token.value))
# --- Variables/Constants ---
[docs]
def declared_variable_name(self, children):
child = self.transform(children[0])
return Expression("variable", name=child.value.strip("'\""))
[docs]
def reserved_variable_name(self, children):
child = self.transform(children[0])
return Expression("reserved_variable", value=child)
[docs]
def pattern(self, children):
tree = Tree("pattern", children)
pattern = PatternBuilder(tree).object
assert (
len(pattern.components) == 1
), "The pattern {pattern} must consist of a single component, since it is part of an Expression."
component = pattern.components[0]
return Expression("component_pattern", value=component)
[docs]
def defined_constant(self, children):
child = self.transform(children[0])
return Expression("defined_constant", name=child.value)
# --- Operations ---
[docs]
def binary_op_expression(self, children):
children = [self.transform(c) for c in children]
left, op, right = children
return Expression("binary_op", operator=op, left=left, right=right)
[docs]
def binary_op(self, children):
return children[0]
[docs]
def unary_op_expression(self, children):
children = [self.transform(c) for c in children]
op, child = children
return Expression("unary_op", operator=op, child=child)
[docs]
def unary_op(self, children):
return children[0]
[docs]
def list_op_expression(self, children):
children = [self.transform(c) for c in children]
op_token, *args = children
return Expression("list_op", operator=op_token.children[0], children=args)
# --- Parentheses ---
[docs]
def parentheses(self, children):
children = [self.transform(c) for c in children]
return Expression("parentheses", child=children[0])
# --- Ternary Conditional ---
[docs]
def conditional_expression(self, children):
children = [self.transform(c) for c in children]
cond, true_expr, false_expr = children
cond = cond.children[0]
return Expression(
"conditional", condition=cond, true_expr=true_expr, false_expr=false_expr
)
# --- Boolean Logic ---
[docs]
def comparison(self, children):
children = [self.transform(c) for c in children]
left, op, right = children
return Expression("comparison", operator=op.value, left=left, right=right)
[docs]
def logical_or(self, children):
children = [self.transform(c) for c in children]
left, right = children
return Expression("logical_or", left=left, right=right)
[docs]
def logical_and(self, children):
children = [self.transform(c) for c in children]
left, right = children
return Expression("logical_and", left=left, right=right)
[docs]
def logical_not(self, children):
children = [self.transform(c) for c in children]
return Expression("logical_not", child=children[0])
# --- Boolean Literals ---
[docs]
def TRUE(self, token):
return Expression("boolean_literal", value=True)
[docs]
def FALSE(self, token):
return Expression("boolean_literal", value=False)
# --- Default Fallthrough ---
def __default__(self, data, children, meta):
return Tree(data, children, meta)
[docs]
def parse_tree_to_expression(tree: Tree) -> Expression:
"""Convert a Lark ParseTree to an Expression object.
Note:
Since there isn't extra logic when converting algebraic expressions,
we can convert from the Lark representation in-place, without creating
a new object, hence a Transformer instead of Visitor.
Args:
tree: Lark ParseTree rooted at algebraic_expression.
Returns:
Expression object representing the parsed expression.
"""
return LarkTreetoExpression().transform(tree)