Action Language for EMF
The AleInterpreter class provides a public API to interpret ALE scripts.
It can be used as follows:
// Retrieve:
// - the .dsl file defining the metamodel and its semantics
// - the .xmi file defining the model to execute
IResource dslFile = ...
String dslFileLocation = dslFile.getLocationURI().getPath();
IResource xmiModelFile = ...
String xmiModelFileLocation = xmiModelFile.getLocation().toString();
// Initialize the ALE interpreter
List<Object> args = List.of();
Set<String> plugins = Set.of();
Set<String> projects = Set.of(dslFile.getProject().getName(), xmiModelFile.getProject().getName());
ALEInterpreter interpreter = new ALEInterpreter();
interpreter.javaExtensions.updateScope(plugins,projects);
interpreter.javaExtensions.reloadIfNeeded();
// Run the ALE interpreter
IEvaluationResult result = interpreter.eval(xmiModelLocation, args, new WorkbenchDsl(dslFileLocation));
Hint: This class is defined in the
org.eclipse.emf.ecoretools.ale
bundle.
This chapter presents all the files used to configure ALE.
This file describes a Domain Specific Language (DSL) made executable with ALE.
It defines a scope containing all declared elements of a DSL, which are the abstract syntax files (.ecore files) and the semantic files (.ale files)
It is mainly used for the execution and the validation (type checking, name conflicts…).
It uses the file extension dsl.
It is a standard Java properties file (i.e a textual file where each line follows the syntax <key>=<value>)
We use the special keys:
Examples:
logo-standalone.dsl
syntax=../logo.model/model/ASMLogo.ecore,../logo.model/model/VMLogo.ecore
behavior=../logo.example/data/LogoProgram.ale
We also support Eclipse Platform URL scheme that eases the location of files within a workspace. It follows the syntax: platform:/resource/<project>/<path to file.ale>
logo.dsl
syntax=platform:/resource/logo.model/model/ASMLogo.ecore,platform:/resource/logo.model/model/VMLogo.ecore
behavior=platform:/resource/logo.example/data/LogoProgram.ale
This file defines a DSL implementation (i.e. its semantics).
It allows to re-open EClass from the abstract syntax to mainly implements existing EOperations but also to declare new ones, new features and even new classes specific to the runtime.
It uses the file extension ale.
The file must start with behavior <unique identifier>
.
Example:
fsm.ale
behavior fsm.implementation;
open class FSM {
override void exec() {
// Do stuff
}
}
class Variables {
// Store values here
}
The following table lists ALE’s reserved keywords. They cannot be use to name attributes, classes or operations.
Keyword | Purpose |
---|---|
behavior | Defines the semantics’ namespace |
class | Declares a new class |
def | Defines an operation |
else | Declares a condition statement |
extends | Declares a class’ super class |
false | Represents a false expression |
for | Iterates on a collection of items |
if | Declares a conditional statement |
null | Represents non-existing value |
open | Enhances an existing class with new features |
opposite | Indicates that a reference is bidirectional |
override | Specifies the semantics of an existing operation |
self | Represents the current instance |
true | Represents a true expression |
unique | Indicates that a collection should not contain duplicates(#declaring-a-set)) |
use | Imports a Java service |
Note: The word
result
is also reserved inside an operation. See defining operations for further details.
Type | Description | Instanciation |
---|---|---|
Integer | A number without decimal part | int a := 12 |
Real | A number with decimal part | double r := 23.5 |
String | A sequence of characters | String str := ‘Hello World!’ |
Boolean | Either true of false | boolean b := true |
Sequence | An ordered collection of elements | Sequence(Integer) numbers = Sequence{1, 2, 3} |
OrderedSet | An ordered collection of unique elements | OrderedSet(String) users = OrderedSet{‘Foo’, ‘Bar’} |
Hint: Looking for types’ default value? See variable declaration.
Operators on primitive types:
Priority | Operator | Operand Types | Syntax | Semantic |
---|---|---|---|---|
1 | + | Numeric | <op1> + <op2> |
Add two numeric values |
1 | - | Numeric | <op1> - <op2> |
Substract two numeric values |
2 | * | Numeric | <op1> * <op2> |
Multiply two numeric values |
2 | / | Numeric | <op1> / <op2> |
Divide <op1> by <op2> |
5 | += | Numeric (<op1> must be a l-value) |
<op1> += <op2> |
Set <op1> to <op1> + <op2> |
5 | -= | Numeric (<op1> must be a l-value) |
<op1> -= <op2> |
Set <op1> to <op1> - <op2> |
5 | *= | Numeric (<op1> must be a l-value) |
<op1> *= <op2> |
Set <op1> to <op1> * <op2> |
5 | /= | Numeric (<op1> must be a l-value) |
<op1> /= <op2> |
Set <op1> to <op1> / <op2> |
3 | < | Numeric | <op1> < <op2> |
True if <op1> is lower than <op2> |
3 | > | Numeric | <op1> > <op2> |
True if <op1> is greater than <op2> |
3 | <= | Numeric | <op1> <= <op2> |
True if <op1> is strictly lower than <op2> |
3 | >= | Numeric | <op1> >= <op2> |
True if <op1> is strictly greater than <op2> |
1 | + | String | <op1> + <op2> |
Concatenate two strings |
2 | += | String (<op1> must be a l-value) |
<op1> += <op2> |
Set <op1> to <op1> + <op2> |
4 | not | Boolean | not <op> |
True if <op> is false. False if <op> operand is true |
4 | and | Boolean | <op1> and <op2> |
True if both operands are evaluated to true |
4 | or | Boolean | <op1> or <op2> |
True if at least one of its operands is evaluated to true |
4 | xor | Boolean | <op1> xor <op2> |
True if either <op1> or <op2> is true, but not both |
4 | implies | Boolean | <op1> implies <op2> |
True if <op1> is false, equals to <op2> otherwise |
Operators on collections:
Priority | Operator | Syntax | Operand Types | Semantic |
---|---|---|---|---|
? | in | <item> in <collection> |
All, Collection | Provide an iterator in for loops |
? | contains | <collection> contains <item> |
? | True if <item> can be found in <collection> |
5 | += | <collection> += item |
Collection(T), T | Add <item> to <collection> |
5 | -= | <collection> -= item |
Collection(T), T | Remove <item> from <collection> |
Global operators:
Priority | Operator | Operand Types | Syntax | Semantic |
---|---|---|---|---|
3 | = | All | <op1> = <op2> |
True if <op1> is equal to <op2> (using Java’s equals ) |
3 | != | All | <op1> != <op2> |
True if <op1> is not equal to <op2> (using Java’s equals ) |
3 | <> | All | <op1> <> <op2> |
True if <op1> is not equal to <op2> (using Java’s equals ) |
5 | := | All (<op1> must be a l-value) |
<op1> := <op2> |
Set <op2> as the value of <op1> |
Note: As stated in the Expressions section ALE relies on the AQL interpreter. As a result, most of these keywords are actually AQL keywords.
Parenthesis ()
can be used to change the priority of evaluation:
2 + 3 * 5 = 17; // true
(2 + 3) * 5 = 25; // true
Expressions are anything that returns a value. For instance:
true // the boolean true
1 + 5 // 6
isVerbose or isVerbose // conditional boolean
Expressions are evaluated thanks to the AQL interpreter and must hence be valid AQL expressions. See AQL for an overview of the language.
Simple line comments start with //
:
// This is a comment
String name := "Foo"; // This is another comment at the end of a line
Multi-ligne comments are written between /*
and */
:
/*
* This comment
* is on
* several lines.
*/
Documentation for classes, attributes and operations can be written between /**
and */
:
/**
* The time before droping the request.
*/
int timeoutInMs := 10;
Note: At the moment this documentation is not leveraged at all.
A statement is an instruction understandable by the ALE interpreter. A statement always ends with a semicolon (;
):
self.greeting().log();
int length := 12;
A variable can be declared either within a class or an operation body. It follows the following syntax:
<type> <var_name> [:= <initial_value>];
<type>
is the type of the variable.
<var_name>
is a unique string identifying the variable. Two different variables with the same name cannot be defined in the same scope.
<initial_value>
is an expression specifying the value of the variable. It must match <type>
. It is optional; the following table list the default value given to the variable according to its type:
Type | Default value |
---|---|
Integer | 0 |
Boolean | false |
String | ’’ |
Any other | null |
Example:
int localVar; // set to 0
int otherVar := 2; // set to 2
The :=
operators allows to assign a value to a variable. It follows the following syntax:
<var_name> := <expression>;
<var_name>
is the identifier used to declare the variable.
<expression>
is the value that must take the variable. It must match the type of the variable.
Example:
aString := 'hello';
anInteger := 42;
Caution: The assignment operator := must not be confused with the comparison operator = used in expressions.
In ALE, the scope of a variable is determined by the block within which it has been declared. A block is defined between {
and }
.
The if
keyword allows to specify a condition before executing a block:
if (<guard>) {
// execute if the guard is true
}
<guard>
is any expression that can be evaluated to true. Example:
Boolean isRaining := true;
if (isRaining) {
self.getFluffyUmbrella();
}
The else
keyword allows to execute a block if the guard evaluates to false. else
can only be written after the }
closing if
’s block:
String weather := 'sunny';
String raining := 'raining';
if (weather = raining) {
self.getFluffyUmbrella();
}
else {
self.enjoySunshine();
}
The for
keyword allows to execute a block for each element of a collection:
for (<item> in <collection>) {
// executed for each element of the collection
}
<item>
is the name of a new variable that will take successively the value of each element of the collection. Only accessible within for
’s block.
<collection>
is an existing variable which multiplicity is greater than 1, such as a sequence or a set.
For example:
Sequence(String) names := Sequence{'Foo', 'Bar'};
for (name in names) {
name.log(); // Print "Foo" on the first iteration then "Bar"
}
It is also possible to iterate over a sequence of integers with a range comprehension ([<min>..<max>]
):
for (i in [1..5]) {
('' + i).log(); // Print "1", then "2", etc.
}
The while
keyword allows to execute a block until a condition is evaluated to false. It follows the following syntax:
while (<condition>) {
}
<condition>
is any expression that can be evaluated as a boolean.
Example:
while (isRaining) {
self.keepUmbrellaOpened();
}
The class
keyword allows to define a new class. It follows the following syntax:
class <class_name> {
}
<class_name>
is a string identifying the class.
Example:
/**
* A class that only exists during the runtime.
*/
class State {
}
Unlike open classes, these classes only exist at runtime, when the ALE interpreter is running. Semantically, that means that the classes are not part of the DSL’s abstract syntax but are still useful during the development.
The def
keyword allows to define new operations. It follows the following syntax:
def <return_type> <op_name>([<param_type> <param_name>]*) {
}
<return_type>
defines the type of the value returned by the operation.
<op_name>
is a string uniquely identifying the operation.
<param_type> <param_name>
define the type and the name of a parameter. Parameters are optional and separated by commas (,
).
An operation must be defined within a class body.
Example:
open class Vector {
def int add(int a, int b) {
result := self.a + self.b;
}
}
result
is a special variable which is only accessible within an operation body. It stores the result of the operation and is automatically returned at the end of the operation body. As such, it replaces the return
keyword used by a lot of programming languages.
Within a method, one can refer to the current instance with the self
keyword. It allows to access instance’s attributes and call instance’s operations. Example:
open class User {
def String fullname() {
result := self.firstname + ' ' + self.lastname;
}
}
Unlike overridden methods, these methods only exist at runtime, when the ALE interpreter is running. Semantically, that means that their are not part of DSL’s abstract syntax but are still useful during the development.
create()
is a Java service (see Call Java code) that is automatically added by ALE to every EClass and which allows to create a new instance. It follows the following syntax:
<class_name> <var_name> := <package_name>::<class_name>.create();
<class_name>
is the Java name of the class to instantiate.
<var_name>
is the name of the variable that stores the new instance.
<package_name>
is the Java package in which the class lies.
Example:
HelloWorld world := helloworld::HelloWorld.create();
The import
keyword allows to use classes defined in another file. It follows the following syntax:
import <behavior> as <alias>
<behavior>
is the ID of the behavior to which the class belongs.
<alias>
is the name under which the behavior is known in the current file.
Example:
behavior fsm.composite.executable;
import fsm.executable as fsm;
open class Start {
@main
def void run()
fsm.State initial = fsm::State.create();
}
}
This chapter binds ALE’s syntax with EMF concepts and explains how the ALE script can affect a metamodel at runtime.
The open
keyword allows to re-open an existing EClass to add new elements and implement existing EOperations. We call such classes: “open classes”. It follows the following syntax:
behavior <EClass_package>
open class <EClass> {
}
Example:
behavior helloworld;
open class HelloWorld {
}
Note: An .ale file can declare only one
open class
per EClass.
Creating a variable in the body of an opened in fact adds a new attribute to the weaved EClass. If the feature is a primitive (boolean, int, string, etc.) it is inferred as an EAttribute. Otherwise it is inferred as an EReference.
Example:
open class FSM {
State currentState;
override void on(String event) {
if (event = 'right-click') {
self.currentState := fsm::MoveState.create();
}
}
}
Tip: the new feature should be runtime specific, such as adding a currentState inside a Finite State Machine.
A multiplicity can be specified to indicate that the attribute represents several values. It follows the following syntax:
<min>..<max> <type> <name>;
<min>
is the minimum number of values in the attribute.
<max>
is the maximum number of values. *
indicates any number of values.
<type>
is the type of the values acceptable in the attribute.
<name>
is the name of the attribute.
Example:
open class FSM {
0..* State states;
override def addState(State newState) {
self.states += newState;
}
def void display() {
for (State state in self.states) {
state.display();
}
}
}
The opposite
keyword allows to declare a bidirectional reference. It is equivalent to the EMF opposite relation. It follows the following syntax:
opposite <var_name_in_opposite_class> <opposite_class> <var_name_in_class>;
<opposite_class>
is the name of the class with which the bidirectional must be set.
<var_name_in_opposite_class>
is the name of the variable holding the reference to the current class in the opposite class.
<var_name_in_class>
is the name of the variable holding the reference in the current EClass.
Important: an opposite reference must be declared in both classes.
Example:
class ParentNode {
opposite parent ChildNode child; // can be accessed with self.child
}
class ChildNode {
opposite child ParentNode parent; // can be accessed with self.parent
}
The unique
keyword allows to declare a multiplicity-many attribute that should only store unique values. Values are compared thanks to their Java equals
method. It is equivalent to the EMF unique feature. It follows the following syntax:
unique <multiplicity> <type> <var_name>
<multiplicity>
is the multiplicity of the attribute (see declaring a new attribute). It must be a many multiplicity.
<type>
is the type of the elements contained in the collection.
<var_name>
is the name of the variable holding the reference.
Example:
class Parent {
unique 0..* Child children;
}
The override
keyword allows to implement the behavior of an EOperation declared in the ECore metamodel of the weaved EClass. We call such operations: “overridden operations”. It follows the following syntax:
override <type> <name>([<param_type> <param_name>]*) {
<statement>*
}
<type>
is the type of the value returned by the operation.
<name>
is the name of the operation.
<param_type> <param_name>
define the type and the name of a parameter. Parameters are optional and separated by commas (,
).
<stamement>
is an arbitrary number of statements.
Important: an overridden operation can only be declared within an open class an must have the exact same signature as an existing EOperation from the weaved EClass or one of its super types.
Example:
open class Circle {
override int circumference() {
double PI := 3.14159;
result := self.radius * PI * 2;
}
}
Tip: see also defining operations for further details about operation syntax.
The extends
keyword allows to extend an open class with another open class.
When writing an Open Class you may want to specialize one (or more) existing open class. The new Open Class reuses all the inherited content, declares new features/operations and refines body of inherited operations.
The extended open class have to be weaved on the same EClass (or super type) of the current open class.
It follows the following syntax:
open class <name> extends <open_class_1>[, <open_class_n>]* {
}
<name>
is the name of the extending class.
<open_class_1>
is the name of the open class being extended. An open class may inherit from several open classes; in such a case the name of the extended classes must be separated by commas (,
).
Any open class from the same file can be extended.
Example:
open class State {
override String execute() {
result := 'State ' + self.name;
}
}
open class Initial extends State {
override String execute() {
result := 'Initial State ' + self.name;
}
}
Classes from other files can be extended too but must be imported first (see importing an existing class).
Example:
behavior fsm.composite.executable;
import fsm.executable as simplefsm;
open class CompositeState extends simplefsm.State {
override void exec() {
// Redefine the behavior here
}
}
Some cheatsheets on AQL expressions
See AQL syntax reference for more details.
anEPackage.oclIsKindOf(ecore::ENamedElement) //true
anEPackage.oclIsTypeOf(ecore::ENamedElement) //false
OrderedSet{'a', 'b', 'c'} + OrderedSet{'c', 'b', 'f'}
Sequence{'a', 'b', 'c'}->any(str | str.size() = 1)
Sequence{'a', 'b', 'c'}->asOrderedSet()
OrderedSet{'a', 'b', 'c'}->asSequence()
Sequence{'a', 'b', 'c', 'c', 'a'}->asSet()
Sequence{'a', 'b', 'c'}->at(1)
Sequence{'a', 'b', 'c'}->collect(str | str.toUpper())
OrderedSet{'a', 'b', 'c'}->concat(Sequence{'d', 'e'})
OrderedSet{'a', 'b', 'c'}->count('d')
Sequence{'a', 'b', 'c'}->excludes('a')
Sequence{'a', 'b'}->excludesAll(OrderedSet{'a','f'})
OrderedSet{'a', 'b', 'c'}->excluding('c')
Sequence{'a', 'b', 'c'}->exists(str | str.size() > 5)
Sequence{anEClass, anEAttribute}->filter(ecore::EStructuralFeature)
Sequence{'a', 'b', 'c'}->first()
Sequence{'a', 'b', 'c'}->forAll(str | str.size() = 1)
Sequence{'a', 'b', 'c'}->includes('d')
Sequence{'a', 'b', 'c'}->includesAll(OrderedSet{'a', 'f'})
OrderedSet{1, 2, 3, 4}->indexOf(3)
OrderedSet{'a', 'b', 'c'}->insertAt(2, 'f')
OrderedSet{'a', 'b', 'c'}->intersection(OrderedSet{'a', 'f'})
OrderedSet{'a', 'b', 'c'}->isEmpty()
Sequence{'a', 'b', 'c'}->isUnique(str | str.size())
Sequence{'a', 'b', 'c'}->last()
OrderedSet{'a', 'b', 'c'}->notEmpty()
Sequence{'a', 'b', 'c'}->one(str | str.equals('a'))
OrderedSet{'a', 'b', 'c'}->prepend('f')
OrderedSet{'a', 'b', 'c'}->reject(str | str.equals('a'))
OrderedSet{'a', 'b', 'c'}->reverse()
Sequence{'a', 'b', 'c'}->select(str | str.equals('a'))
Sequence{'a', 'b', 'c'}->sep('[', '-', ']')
Sequence{'a', 'b', 'c'}->sep('-')
Sequence{'a', 'b', 'c'}->size()
Sequence{'aa', 'bbb', 'c'}->sortedBy(str | str.size())
Sequence{'a', 'b', 'c'} - Sequence{'c', 'b', 'f'}
OrderedSet{'a', 'b', 'c'}->subOrderedSet(1, 2)
Sequence{'a', 'b', 'c'}->subSequence(1, 2)
Sequence{1, 2, 3, 4}->sum()
Sequence{'a', 'b', 'c'}->union(Sequence{'d', 'c'})
ALE offers the possibility to call static methods written in Java from the body of an EOperation.
For example let’s assume that you have a Java class MyService
providing the method foo()
:
package some.packagename;
public class MyService {
// By convention the caller object is the first argument
public static void foo(EObject caller) {
System.out.println("Foo: "+ caller.eClass().getName());
}
}
You can call foo()
on any EObject just by importing the class MyService
with the keyword use
at the beginning of your .ale
file.
The only requirement is that MyService
has to be in the classpath of your project.
use some.packagename.MyService; //Import external Java services
open class FSM {
def void callJavaFoo() {
self.states.forAll(state | state.foo());
}
}
ALE provides the log()
method that can be used on String instances to print themselves to the console.
Example:
'Hello World!'.log();
('' + 42).log();