Gom is a generator of tree implementations in Java, allowing the
definition of a preferred canonical form, using hooks. The tree
implementations Gom generates are characterized by strong typing,
immutability (there is no way to manipulate them with side-effects), maximal
subterm sharing and the ability to be used with strategies.
6.1 Gom syntax
The basic functionality of Gom is to provide a tree implementation in
Java corresponding to an algebraic specification.
Gom provides a syntax to concisely define abstract syntax
tree, which is inspired from the algebraic type definition of ML languages.
Each Gom file consists in the definition of one or more modules. In each
module we define sorts, and operators for the sorts. Additionally, it allows to
define hooks modifying the default behavior of an operator.
GomGrammar |
::= |
Module |
Module |
::= |
`module' ModuleName
[Imports] Grammar |
Imports |
::= |
`imports' (ModuleName)* |
Grammar |
::= |
`abstract syntax'
(TypeDefinition ∣ HookDefinition)* |
TypeDefinition |
::= |
SortName `=' [`|'] OperatorDefinition
(`|' OperatorDefinition)* |
OperatorDefinition |
::= |
OperatorName
`('[SlotDefinition(`,'SlotDefinition)*]`)' |
|
∣ |
OperatorName `(' SortName`*' `)' |
SlotDefinition |
::= |
SlotName `:' SortName |
HookDefinition |
::= |
HookType `:' HookOperation |
HookType |
::= |
OperatorName |
|
∣ |
`module' ModuleName |
|
∣ |
`sort' SortName |
HookOperation |
::= |
`make' `(' [Identifier (`,' Identifier)*] `)' |
|
∣ |
`make_insert' `(' Identifier `,' Identifier)* `)' |
|
∣ |
`interface' `{' Identifier (`,' Identifier )* `}' |
|
∣ |
`import' `{' TomCode `}' |
|
∣ |
`block' `{' TomCode `}' |
ModuleName |
::= |
Identifier |
SortName |
::= |
Identifier |
OperatorName |
::= |
Identifier |
SlotName |
::= |
Identifier |
6.1.1 Builtin sorts and operators
Gom supports several builtin sorts, that may be used as sort for new
operators slots. To each of these builtin sorts corresponds a Java type.
Native data types from Java can be used as builtin fields, as well as
ATerm and ATermList structures. To use one of these builtin types
in a Gom specification, it is required to add an import for the
corresponding builtin.
Name |
Java type |
int |
int |
String |
String |
char |
char |
double |
double |
long |
long |
float |
float |
ATerm |
aterm.ATerm |
ATermList |
aterm.ATermList |
It is not possible to define a new operator whose
domain is one of the builtin sorts, since those sorts are defined in another
module.
Note:
to be able to use a builtin sort, it is necessary
to declare the import of the corresponding module (int or String) in the Imports section.
External Gom modules may be imported in a Gom specification, by adding
the name of the module to import in the `imports' section. Once a module
imported, it is possible to use any of the sorts this module declares or
imports itself as type for a new operator slot. Adding new operators to an
imported sort is however not allowed.
6.1.2 Example of signature
The syntax of Gom is quite simple and can be used to define many-sorted
abstract-datatypes.
The module section defines the name of the signature.
The imports section defines the name of the imported signatures.
The abstract syntax part defines the operators with their signature. For
each argument, a sort and a name (called slot-name) has to be given.
module Expressions
imports String int
abstract syntax
Bool = True()
| False()
| Eq(lhs:Expr, rhs:Expr)
Expr = Id(stringValue:String)
| Nat(intValue:int)
| Add(lhs:Expr, rhs:Expr)
| Mul(lhs:Expr, rhs:Expr)
The definition of a signature in Gom has several restrictions:
-
there is no overloading: two operators cannot have the same name
- for a given operator, all slot-names must be different
- if two slots have the same slot-name, they must belong to the same sort.
In the previous example, Eq, Add, and Mul can have a slot
called lhs of sort Expr. But, Id and Nat cannot
have a same slot named value, since their sort are not identical (the
slots are respectively of sorts String and int for Id
and Nat).
6.2 Generated API
For each Module, the Gom compiler generates an API specific of this
module, possibly using the API of other modules (declared in the Imports
section). This API is located in a Java package named using the
ModuleName lowercased, and contains the tree implementation itself, with
some additional utilities:
-
an abstract class named ModuleNameAbstractType is generated.
This class is the generic type for all nodes whose type is declared in this module. It declares
generic functions for all tree nodes: a toATerm() method returning a
aterm.ATerm representation of the tree; a symbolName() method
returning a String representation for the function symbol of the tree
root; the toString() method, which returns a string representation. It
declares also all accept(Visitor v) to implement the visitor
combinators pattern used by the strategy language.
- the Visitor, BasicStrategy and Forward classes
for a module named Module are respectively named
ModuleNameVisitor, ModuleNameBasicStrategy,
ModuleNameForward. Those classes implement
the visitor combinator design pattern for the module, by providing a typed
Visitor interface (one visit method per sort), a Forward visitor
providing a simple forwarding visitor respecting types, and a BasicStrategy,
which is a visitable visitor built on top of the Forward class.
- in a subpackage types, Gom generates one class for each sort
defined in the module, whose name corresponds to the sort name. Each sort class
extends AbstractType for the module, and declares methods boolean
isOperatorName() for each operator in the module (false
by default). It declares getters (methods named
getSlotName()) for
each slot used in any operator of the sort (throwing an exception by default).
A method SortName fromTerm(aterm.ATerm trm) is generated, allowing to
use ATerm as an exchange format.
- for each sort, generate classes for the operators in a package
types.SortName. All classes for the operator are named following
the operator name, and extend the class generated for the corresponding sort.
Thus they provide getters for the slots of the operator, and the
isOperatorName() method is overriden to return
true. Those classes implement the jjtraveler.Visitable
interface. It is worth noting that builtin fields are not accessible from the
jjtraveler.Visitable, and thus will not be visited by strategies.
The operator class also implements a static make method, building a
new instance of the operator. This make method is the only way to
obtain a new instance.
- for each module, one file ModuleName.tom providing a Tom
mapping for the sorts and operators defined or imported by the module.
See section 6.2.5 for an example.
6.2.1 Hooks to alter the creation operations
Hooks may be used to specify how operators should be created. `make' and
`make_insert' hooks are altering the creation operations for respectively
algebraic and variadic operators. `make_insert' is simply a derivative
case of `make' for variadic operators (see 6.2.4).
The hook operation type is followed by a list of arguments name between
`()'. The creation operation takes those arguments in order to build a new
instance of the operator. Thus, the arguments number has to match the slot
number of the operator definition, and types are inferred from this definition.
Then the body of the hook definition is composed of Java and Tom code.
The Tom code is compiled using the mapping definition for the current
module, and thus allows to build and match terms from the current module.
This code can also use the realMake function, which consists in the
"inner" default allocation function. This function takes the same number of
arguments as the hook. In any case, if the hooks code does not perform a
return itself, this realMake function is called at the end of the
hook execution, with the corresponding hooks arguments
6.2.2 Hook example
Using the expression example introduced in 6.1.2, we can add
hooks to implement the computation of Add and Mul when
both arguments are known integers (i.e. when they are Nat(x))
module Expressions
imports String int
abstract syntax
Bool = True()
| False()
| Eq(lhs:Expr, rhs:Expr)
Expr = Id(stringValue:String)
| Nat(intValue:int)
| Add(lhs:Expr, rhs:Expr)
| Mul(lhs:Expr, rhs:Expr)
Add:make(l,r) {
%match(Expr l, Expr r) {
Nat(lvalue), Nat(rvalue) -> {
return `Nat(lvalue + rvalue);
}
}
}
Mul:make(l,r) {
%match(Expr l, Expr r) {
Nat(lvalue), Nat(rvalue) -> {
return `Nat(lvalue * rvalue);
}
}
}
Using this definition, it is impossible to have an expression containing
unevaluated expressions where a value can be calculated. Thus, a procedure
doing constant propagations for Id whose value is known could simply
replace the Id by the corresponding Nat, and rely on this
mechanism to evaluate the expression. Note that the arguments of the
make hook are themselves elements built on this signature, and thus the
hooks have been applied for them. In the case of hooks encoding a rewrite
system, this corresponds to using an innermost strategy.
6.2.3 Hooks to alter the generated API
There exist three other hooks `import', `interface' and `block'
that offer possibilities to enrich the generated API. Contrary to `make'
and `make_insert', these hooks have no parameters. Moreover, they can be
associated not only to an operator but also to a module or a sort. The code
given in the hook is just added at the correct position in the corresponding
Java class:
-
for a module ModuleName, in the abstract class named
ModuleNameAbstractType (for now, you can only use this hook with
the current module),
- for a sort SortName, in the abstract class named SortName in
the package types,
- for an operator OperatorName of sort SortName, in the class
named SortName in
the package types/SortName.
There are few constraints on the form of the code in these hooks:
-
for `import', the code is a well-formed block of Java imports. For example:
sort term:import {
import termgraph.term.*;
import termgraph.term.types.*;
import tom.library.strategy.mutraveler.*;
import java.util.HashMap;
}
- for `interface', the code is a well-formed list of Java interfaces. For example:
sort term:interface { LabelCollectable,Expandable }
- for `block', the code is a well-formed Java block which can contain Tom code. For example:
sort term:block {
%include{ java/util/HashMap.tom }
%include{ java/mustrategy.tom }
%strategy CollectTerms(table:HashMap) extends `Identity() {
visit Term {
labTerm(label,term) -> {
table.put(`label,getPosition());
return `term;
}
}
}
public static HashMap collect(Term t) {
HashMap table = new HashMap();
`TopDown(CollectTerms(table)).apply(t);
return table;
}
}
6.2.4 Variadic operators
Variadic operators are operators that have no fixed arity. They can have any
number of arguments, and are usually used to represent lists.
Variadic operators are declared using the syntax:
OperatorDefinition |
::= |
OperatorName `(' SortName`*' `)' |
The generated code contains two operator classes: one name
EmptyOperator which is used to represent the empty list of arity
0, and the other named ConsOperator having two fields: one with the codomain
sort, and one with the domain sort of the variadic operator, respectively named
HeadOperator and TailOperator, leading to getter
functions getHeadOperator and getTailOperator. This
allows to define lists as the composition of many Cons and one
Empty objects.
As the interface is different than the one for standard algebraic operators,
the make_insert hook operation is used to modify the creation
operation for variadic operators.
6.2.5 Generated API example
We show elements of the generated API for a very simple example, featuring
variadic operator. It defines natural numbers as Zero() and
Suc(n), and lists of natural numbers.
module Mod
abstract syntax
Nat = Zero()
| Suc(pred:Nat)
| Plus(lhs:Nat,rhs:Nat)
| List(Nat*)
Using the command gom Mod.gom, the list of generated files is:
mod/Mod.tom (the Tom mapping)
mod/ModAbstractType.java
mod/ModBasicStrategy.java
mod/ModForward.java
mod/ModVisitor.java
mod/types/Nat.java (abstract class for the "Nat" sort)
mod/types/nat/List.java \
mod/types/nat/ConsList.java \
mod/types/nat/EmptyList.java / Implementation for the operator "List"
mod/types/nat/Plus.java (Implementation for "Plus")
mod/types/nat/Suc.java (Implementation for "Suc")
mod/types/nat/Zero.java (Implementation for "Zero")
The ModAbstractType class declares generic methods shared by all operators
in the Mod module:
public aterm.ATerm toATerm()
public String symbolName()
public String toString()
public ModAbstractType accept(mod.ModVisitor v) throws jjtraveler.VisitFailure
The classes ModVisitor, ModForward, and ModBasicStrategy
implement the visitor combinator design pattern for the data structure of the
Mod module. The mod/types/Nat.java class provides an
abstract class for all operators in the Nat sort, implementing the
ModAbstractType and contains the following methods. First, the methods
for checking the root operator, returning false by default:
public boolean isConsList()
public boolean isEmptyList()
public boolean isPlus()
public boolean isSuc()
public boolean isZero()
Then getter methods, throwing an UnsupportedOperationException by
default, as the slot may not be present in all operators. This is convenient
since at the user level, we usually manipulate objects of sort Nat,
without casting them to more specific types.
public mod.types.Nat getpred()
public mod.types.Nat getlhs()
public mod.types.Nat getHeadList()
public mod.types.Nat getrhs()
public mod.types.Nat getTailList()
The fromTerm static method allows Gom data structure to be
interoperable with ATerm
public static mod.types.Nat fromTerm(aterm.ATerm trm)
The operator implementations redefine all or some getters for the operator to
return its subterms. It also provides a static make method to build a
new tree rooted by this operator, and implements the
jjtraveler.Visitable interface. For instance, in the case of the
Plus operator, the interface is:
public static Plus make(mod.types.Nat lhs, mod.types.Nat rhs)
public int getChildCount()
public jjtraveler.Visitable getChildAt(int index)
public jjtraveler.Visitable setChildAt(int index, jjtraveler.Visitable v)
completed with the methods from the Nat class and the
ModAbstractType.
The operators implementing the variadic operator both extend the List
class, which provides list related methods, such as length,
toArray and reverse. The toArray method produces an array
of object of the codomain type corresponding to the list elements, while the
reverse method returns the list with all elements in reverse order.
The List class for our example then contains:
public int length()
public mod.types.Nat[] toArray()
public mod.types.Nat reverse()
For the ConsList class, we obtain:
/* the constructor */
public static ConsList make(mod.types.Nat _HeadList, mod.types.Nat _TailList) { ... }
public String symbolName() { ... }
/* From the "Nat" class */
public boolean isConsList() { ... }
public mod.types.Nat getHeadList() { ... }
public mod.types.Nat getTailList() { ... }
/* From the "ModAbstractType" class */
public aterm.ATerm toATerm() { ... }
public static mod.types.Nat fromTerm(aterm.ATerm trm) { ... }
/* The jjtraveler.Visitable interface */
public int getChildCount() { ... }
public jjtraveler.Visitable getChildAt(int index) { ... }
public jjtraveler.Visitable setChildAt(int index, jjtraveler.Visitable v) { ... }
/* The MuVisitable interface */
public jjtraveler.Visitable setChilds(jjtraveler.Visitable[] childs) { ... }
6.3 Combining Gom with Tom
A first solution to combine Tom with Gom is to use Gom as a standalone
tool, using the command line tool or the ant task.
In that case, the module name of the Gom specification and the
package option determine where the files are generated.
To make things correct, it is sufficient to import the generated
Java classes, as well as the generated Tom file. In the case of a Gom
module called Module, all files are generated in a directory named
module and the Tom program should do the following:
import module.*;
import module.types.*;
class MyClass {
...
%include { module/Module.tom }
...
}
A second possibility to combine Tom with Gom is to use the %gom
construct offered by Tom.
In that case, the Gom module can be directly included into the Tom file,
using the %gom instruction:
package myPackage;
import myPackage.myclass.expressions.*;
import myPackage.myclass.expressions.types.*;
class MyClass {
%gom{
module Expressions
abstract syntax
Bool = True()
| False()
...
Expr = Mul(lhs:Expr, rhs:Expr)
}
...
}
Note that the Java code is generated in a package that corresponds to the
current package, followed by the class-name and the module-name.
This allows to define the same module in several classes, and avoid name
clashes.
6.4 Strategies support
The data structures generated by Gom do provide support for the strategy
language of Tom. We assume in this section the reader is familiar with the
strategies support of Tom as described in chapter 7, and
illustrated in chapter 3 of the tutorial.
6.4.1 Basic strategy support
The data structure generated by the Gom compiler provide support for the
MuTraveler library implementing strategies for Tom.
It is thus possible without any further manipulation to use the strategy
language with Gom data structures.
This strategy support is extended by the Gom generator by providing
congruence and construction elementary strategies for all operators of the data
structure. Those strategies are made available through a Tom mapping
_<module>.tom generated during Gom compilation.
6.4.2 Congruence strategies
The congruence strategies are generated for each operator in the Gom module.
For a module containing a sort
Term = | a()
| f(lt:Term,rt:Term)
congruence strategies are generated for both a and f operators,
respectively called _a and _f. The semantics of those
elementary strategies is as follows:
(_a)[t] |
⇒ |
t if t equals a,
failure otherwise |
(_f(s1,s2))[t] |
⇒ |
f(t1',t2') if t = f(t1,t2), with
(s1)[t1] ⇒t1' and (s2)[t2] ⇒t2', |
|
|
failure otherwise |
Thus, congruence strategies allows to discriminate terms based on how they are
built, and to develop strategies adopting different behaviors depending on the
shape of the term they are applied to.
Congruence strategies are commonly used to implement a specific behavior
depending on the context (thus, it behaves like a complement to pattern
matching). For instance, to print all first childs of an operator f, it
is possible to use a generic Print() strategy under a congruence
operator.
MuStrategy specPrint = `TopDown(_f(Print(),Identity()));
Also, congruence strategies are used to implement map like strategies on
tree structures:
Consider a signature with
List = Cons(e:Element,t:List) | Empty(), then
we can define a
map strategy as:
MuStrategy map(MuStrategy arg) {
return `mu(MuVar("x"),
Choice(_Empty(),_Cons(arg,MuVar("x")))
);
}
The congruence strategy generated for variadic operators is similar to the
map strategy, and will apply its argument to all subterms of a variadic
operator.
6.4.3 Construction strategies
Gom generated strategies' purpose is to allow to build new terms for the signature at the
strategy level. Those strategies do not use the terms they are applied to, and simply create a new
term. Their semantics is as follows:
(Make_a)[t] |
⇒ |
a |
(Make_f(s1,s2))[t] |
⇒ |
f(t1,t2)
if (s1)[null] ⇒t1 and (s2)[null] ⇒t2, |
|
|
failure otherwise |
We can note that as the sub-strategies for Make_f are applied to the null term, it
is required that those strategies are themselves construction strategies and do not examine their
argument.
These construction strategies, combined with congruence strategies can be used to implement rewrite
rules as strategies. For instance, a rule f(a,b) → g(a,b) can be implemented by the
strategy:
MuStrategy rule = `Sequence(
_f(_a(),_b()),
_Make_g(_Make_a(),_Make_b())
);