Part 2: Model definition
From textX we can generate an AST (textX calls it a model). Using rule attributes, we will add to the grammar an explicit description to decide what to include in the generated AST. The resulting AST will consist of a tree structure made up of Python objects, with each object corresponding to a specific grammar rule. By judiciously selecting rule attributes and customizing class behavior, information from the rules can be passed to the objects. The following sections will delve into the intricacies of attributes and classes in more detail.
Attributes
Attributes are specified by the grammar rules, and the matched values are passed to the python objects that makes up the model constructed from the input. To specify a basic attribute, you must designate an identifier for the attribute, followed by an equals sign and then a rule. This rule can either be a built-in rule like ID
or a custom rule like WFF
. This process results in an instance of an object from the specified class being connected to that attribute, facilitating the creation of a hierarchy of Python objects in a tree structure.
The Atom rule will include an attribute called id
of type ID
. This attribute will store the unique identifier of the atom, allowing us to access it easily using the id
attribute. Here’s the result:
Model: wff=WFF;
WFF: Atom | Negation | Conjunction | Disjunction | Implication | Equivalence;
Atom: id=ID;
To track sub-formulas within the input, we must add attributes to the rules representing the connectives. Therefore, we assign a WFF
object to an attribute labeled wff
for the unary connective Negation
. Binary connectives will instead have lhs
(left-hand side) and rhs
(right-hand side) attributes.
When we add attributes to the relevant rules, the grammar looks like this:
Model: wff=WFF;
WFF: Atom | Negation | Conjunction | Disjunction | Implication | Equivalence;
Atom: id=ID;
Negation: '~' wff=WFF;
Equivalence: '(' lhs=WFF '<=>' rhs=WFF ')';
Implication: '(' lhs=WFF '=>' rhs=WFF ')';
Disjunction: '(' lhs=WFF '|' rhs=WFF ')';
Conjunction: '(' lhs=WFF '&' rhs=WFF ')';
A metamodel, as called by textX, is the parser built from the grammar. More precisely, textX metamodel is a Python object that knows about all classes that can be instantiated while parsing the input. Due to its recursive nature, the metamodel has several loops as can be seen in the following diagram.
Classes
The classes corresponding to the rules are created automatically from the grammar rules. So, by defining the class attributes, we can generate an AST. However, to control the behavior of the classes, we need to create them explicitly and modify them as needed.
Unless the class is abstract, it needs to receive initial parameters. The parameters include the parent
which is mandatory and the user-defined attributes. The Atom
and Negation
classes only take one parameter id
and wff
respectively. The binary connective classes will need a left-hand side (lhs
) and a right-hand side (rhs
) parameter which represent the operands. We present below a possible implementation for the custom classes.
from abc import ABC
from typing import Any, List
type Variable = str
class WFF(ABC):
pass
class Atom(WFF):
def __init__(self, parent: Any, id: Variable):
self.parent = parent
self.id = id
class Negation(WFF):
def __init__(self, parent: Any, wff: WFF):
self.parent = parent
self.wff = wff
class Conjunction(WFF):
def __init__(self, parent: Any, lhs: WFF, rhs: WFF):
self.parent = parent
self.lhs = lhs
self.rhs = rhs
class Disjunction(WFF):
def __init__(self, parent: Any, lhs: WFF, rhs: WFF):
self.parent = parent
self.lhs = lhs
self.rhs = rhs
class Implication(WFF):
def __init__(self, parent: Any, lhs: WFF, rhs: WFF):
self.parent = parent
self.lhs = lhs
self.rhs = rhs
class Equivalence(WFF):
def __init__(self, parent: Any, lhs: WFF, rhs: WFF):
self.parent = parent
self.lhs = lhs
self.rhs = rhs
In part 3 we will instantiate a model from the input WFF
and the grammar.