Jump to: navigation, search

Difference between revisions of "ATL/EMFTVM"

< ATL
(Added section on lazy collections)
(22 intermediate revisions by 2 users not shown)
Line 3: Line 3:
 
Since 2011, the ATL tools include a research VM (EMFTVM), which allows for experimentation with advanced language features. Currently, these features include:
 
Since 2011, the ATL tools include a research VM (EMFTVM), which allows for experimentation with advanced language features. Currently, these features include:
  
* New bytecode format with explicit representation of rules [http://dev.eclipse.org/viewcvs/viewvc.cgi/org.eclipse.m2m/org.eclipse.m2m.atl/plugins/org.eclipse.m2m.atl.emftvm/model/emftvm.ecore?root=Modeling_Project&view=markup]
+
* [http://git.eclipse.org/c/mmt/org.eclipse.atl.git/plain/plugins/org.eclipse.m2m.atl.emftvm/doc/EMFTVM.html New bytecode format with explicit representation of rules] ([http://git.eclipse.org/c/mmt/org.eclipse.atl.git/tree/plugins/org.eclipse.m2m.atl.emftvm/model/emftvm.ecore emftvm.ecore])
* Compiler defined as higher-order ATL transformation [http://dev.eclipse.org/viewcvs/viewvc.cgi/org.eclipse.m2m/org.eclipse.m2m.atl/plugins/org.eclipse.m2m.atl.emftvm.compiler/transformations/ATLtoEMFTVM.atl?root=Modeling_Project&view=markup]
+
* [http://git.eclipse.org/c/mmt/org.eclipse.atl.git/tree/plugins/org.eclipse.m2m.atl.emftvm.compiler/transformations/ATLtoEMFTVM.atl Compiler defined as higher-order ATL transformation]
 
* [[#Rule_Inheritance|Multiple rule inheritance]]
 
* [[#Rule_Inheritance|Multiple rule inheritance]]
* Module import that works with rule inheritance
+
* [[#Module import|Module import that works with rule inheritance]]
* Closures
+
 
* [[#Multiple dispatch|Multiple dispatch for helper methods]]
 
* [[#Multiple dispatch|Multiple dispatch for helper methods]]
 
* [[#Lazy collections|Lazy implementation of OCL collections]]
 
* [[#Lazy collections|Lazy implementation of OCL collections]]
 
* [[#Advanced tracing|Advanced tracing]]
 
* [[#Advanced tracing|Advanced tracing]]
 +
* [[#In-place transformation|In-place transformation]]
 +
* [http://soft.vub.ac.be/soft/research/mdd/simpleocl Closures (or Lambda expressions)]
 +
* [http://soft.vub.ac.be/soft/research/mdd/simplegt Recursive in-place transformation rules]
 +
* [http://git.eclipse.org/c/mmt/org.eclipse.atl.git/tree/plugins/org.eclipse.m2m.atl.emftvm/src/org/eclipse/m2m/atl/emftvm/jit/CodeBlockJIT.java JIT compiler from EMFTVM bytecode to Java bytecode]
  
 
== Architecture ==
 
== Architecture ==
Line 33: Line 36:
 
=== Installation ===
 
=== Installation ===
  
EMFTVM is currently only available from ATL CVS, but will be included in the next ATL release (3.3). As a temporary measure, EMFTVM can be downloaded as an ATL add-on for ATL 3.1 or up from [http://soft.vub.ac.be/soft/research/mdd/emftvm]
+
EMFTVM is included in the latest ATL release (3.3): http://eclipse.org/mmt/downloads/?project=atl. Snapshot releases of EMFTVM can be downloaded as an ATL add-on from [http://soft.vub.ac.be/soft/research/mdd/emftvm]
  
 
=== Compiling to EMFTVM ===
 
=== Compiling to EMFTVM ===
  
To use EMFTVM for your ATL transformations, just add "<code>-- @atlcompiler emftvm</code>" on the first line of your ATL module to compile them to EMFTVM. An example ATL file can be found at [http://dev.eclipse.org/viewcvs/viewvc.cgi/org.eclipse.m2m/org.eclipse.m2m.atl/plugins/org.eclipse.m2m.atl.emftvm.tests/test-data/TestQuery.atl?root=Modeling_Project&view=markup].
+
To use EMFTVM for your ATL transformations, just add "<code>-- @atlcompiler emftvm</code>" on the first line of your ATL module to compile them to EMFTVM. An example ATL file can be found at [http://git.eclipse.org/c/mmt/org.eclipse.atl.git/tree/tests/org.eclipse.m2m.atl.emftvm.tests/test-data/TestQuery.atl].
  
 
=== Running EMFTVM modules ===
 
=== Running EMFTVM modules ===
Line 46: Line 49:
 
<br>''Fig. 2: EMFTVM Launch Configuration Dialog''
 
<br>''Fig. 2: EMFTVM Launch Configuration Dialog''
  
EMFTVM also includes its own Ant tasks. See [http://dev.eclipse.org/viewcvs/viewvc.cgi/org.eclipse.m2m/org.eclipse.m2m.atl/plugins/org.eclipse.m2m.atl.emftvm.tests/anttest/build.xml?root=Modeling_Project&view=markup] to find out how to use the Ant tasks.
+
EMFTVM also includes its own Ant tasks. See [http://git.eclipse.org/c/mmt/org.eclipse.atl.git/tree/tests/org.eclipse.m2m.atl.emftvm.tests/anttest/build.xml] to find out how to use the Ant tasks.
 +
 
 +
=== API ===
 +
 
 +
The API pattern for using EMFTVM from Java looks like this:
 +
 
 +
ExecEnv env = EmftvmFactory.eINSTANCE.createExecEnv();
 +
ResourceSet rs = new ResourceSetImpl();
 +
 +
// Load metamodels
 +
Metamodel metaModel = EmftvmFactory.eINSTANCE.createMetamodel();
 +
metaModel.setResource(rs.getResource(URI.createURI("http://www.eclipse.org/m2m/atl/2011/EMFTVM"), true));
 +
env.registerMetaModel("METAMODEL", metaModel);
 +
 +
// Load models
 +
Model inModel = EmftvmFactory.eINSTANCE.createModel();
 +
inModel.setResource(rs.getResource(URI.createURI("input.xmi", true), true));
 +
env.registerInputModel("IN", inModel);
 +
 +
Model inOutModel = EmftvmFactory.eINSTANCE.createModel();
 +
inOutModel.setResource(rs.getResource(URI.createURI("inout.xmi", true), true));
 +
env.registerInOutModel("INOUT", inOutModel);
 +
 +
Model outModel = EmftvmFactory.eINSTANCE.createModel();
 +
outModel.setResource(rs.createResource(URI.createFileURI("out.xmi")));
 +
env.registerOutputModel("OUT", outModel);
 +
 +
// Load and run module
 +
ModuleResolver mr = new DefaultModuleResolver("platform:/plugin/my.plugin.id/transformations/", new ResourceSetImpl());
 +
TimingData td = new TimingData();
 +
env.loadModule(mr, "Module");
 +
td.finishLoading();
 +
env.run(td);
 +
td.finish();
 +
 +
// Save models
 +
inOutModel.getResource().save(Collections.emptyMap());
 +
outModel.getResource().save(Collections.emptyMap());
 +
 
 +
Note that it is possible to reuse an ExecEnv instance. While you cannot load other metamodels or transformation modules into an existing ExecEnv after it has already run, you can change the input/inout/output models. The advantage of reusing an ExecEnv is that you save on loading time, and the JIT has already warmed up. Please also note that ExecEnv instances are '''not''' thread-safe, and should only be used by a single thread. You should create new ExecEnv instances for use in other threads.
 +
 
 +
=== Standalone use ===
 +
 
 +
EMFTVM can also run outside Eclipse. To do this, add the following plug-in jars to your classpath (based on Juno):
 +
 
 +
org.objectweb.asm_3.3.1.v201105211655.jar
 +
org.eclipse.emf.common_2.8.0-v20120911-0500.jar
 +
org.eclipse.emf.ecore_2.8.0-v20120911-0500.jar
 +
org.eclipse.emf.ecore.xmi_2.8.0-v20120911-0500.jar
 +
org.eclipse.m2m.atl.common_3.3.1.v201209061455.jar
 +
org.eclipse.m2m.atl.emftvm_3.4.0.201301142231.jar
 +
org.eclipse.m2m.atl.emftvm.trace_3.4.0.201301142231.jar
 +
 
 +
Instead of the DefaultModuleResolver, which uses EMF URI prefixes, you can also define your own ModuleResolver that loads modules using Java class resource streams, like so:
 +
 
 +
ModuleResolver mr = new ModuleResolver() {
 +
    @Override
 +
    public Module resolveModule(String module) throws ModuleNotFoundException {
 +
        Resource moduleRes = new EMFTVMResourceImpl();
 +
        try {
 +
            InputStream inputStream = MyClass.class.getResourceAsStream(module + ".emftvm");
 +
            try {
 +
                moduleRes.load(inputStream, Collections.emptyMap());
 +
            } finally {
 +
                inputStream.close();
 +
            }
 +
            return (Module) moduleRes.getContents().get(0);
 +
        } catch (IOException e) {
 +
            throw new RuntimeException(e);
 +
        }
 +
    }
 +
};
 +
 
 +
== Features ==
  
 
=== Rule Inheritance ===
 
=== Rule Inheritance ===
Line 58: Line 134:
  
 
To use this annotation, the regular ATL '''extends''' keyword must not be used: the regular ATL '''extends''' keyword will override the <code>@extends</code> annotation.
 
To use this annotation, the regular ATL '''extends''' keyword must not be used: the regular ATL '''extends''' keyword will override the <code>@extends</code> annotation.
 +
 +
Detailed information on the rule inheritance semantics can be found in the paper "[http://soft.vub.ac.be/Publications/2011/vub-soft-tr-11-07.pdf Towards a general composition semantics for rule-based model transformation.]"
 +
 +
=== Module import ===
 +
 +
Module import is the new [[ATL_Superimposition|module superimposition]]. Instead of providing a list of superimposed modules in your launch configuration, EMFTVM automatically loads any modules that are mentioned in an ATL '''uses''' clause. It does this on the basis of a "module path", which is the EMFTVM equivalent of Java's classpath: modules are looked up within certain base URIs (EMF resources always have a URI). A module path is a comma-separated list of base URIs, to which the module file name can be appended. Module paths are specified in the ATL EMFTVM launch configuration under "Path" (see Fig. 3).
 +
 +
[[Image:ATL_EMFTVM_Modulepath.png|EMFTVM launch configuration with module path]]
 +
<br>''Fig. 3: EMFTVM launch configuration with module path''
 +
 +
The loading order of modules is specified by the order of the '''uses''' clauses of each module; in addition, modules are loaded depth-first. As in module superimposition, rules and helpers in imported modules can be re-defined in the importing module. Detailed information on the module import semantics can be found in the paper "[http://soft.vub.ac.be/Publications/2011/vub-soft-tr-11-07.pdf Towards a general composition semantics for rule-based model transformation.]"
  
 
=== Multiple dispatch ===
 
=== Multiple dispatch ===
  
EMFTVM support multiple dispatch of ATL helpers. This means you can greatly reduce the amount of "oclIsKindOf()" checks in your code, because you can simply write a new helper with the required context/parameter type (not only the context is virtual, but also all parameters). To illustrate how this works, look at the following ATL code:
+
EMFTVM supports multiple dispatch of ATL helpers. This means you can greatly reduce the amount of "oclIsKindOf()" checks in your code, because you can simply write a new helper with the required context/parameter type (not only the context is virtual, but also all parameters). To illustrate how this works, look at the following ATL code:
  
 
  ''-- These helpers check for value equality (as opposed to object identity equality).''
 
  ''-- These helpers check for value equality (as opposed to object identity equality).''
Line 116: Line 203:
 
The above query will first invoke "run" on 100, which creates a Sequence of all numbers from 0 to 100. Then, the query invokes "collect" on that Sequence, and replaces each value in the Sequence by its squared value (the "expensive" operation). Finally, we're only interested in the last value of the changed Sequence.
 
The above query will first invoke "run" on 100, which creates a Sequence of all numbers from 0 to 100. Then, the query invokes "collect" on that Sequence, and replaces each value in the Sequence by its squared value (the "expensive" operation). Finally, we're only interested in the last value of the changed Sequence.
  
In regular ATL, everything is evaluated left-to-right, and the whole sequence is converted to a Sequence of squared values before the last value is returned. In EMFTVM, "collect" returns a lazy Sequence, which is just waiting to be evaluated. Only when "last" is invoked on the lazy Sequence will the Sequence invoke the "expensive" operation on the last element of the input Sequence. As a result, "expensive" is only invoked once by EMFTVM.
+
In regular ATL, everything is evaluated left-to-right, and the whole {0..100} Sequence is converted to a Sequence of squared values before the last value is returned. In EMFTVM, "collect" returns a lazy Sequence, which is just waiting to be evaluated. Only when "last" is invoked on the lazy Sequence will the Sequence invoke the "expensive" operation on the last element of the input Sequence. As a result, "expensive" is only invoked once by EMFTVM.
  
 
Of course, the above example does not seem a very practical one. However, the following OCL patterns probably do ring a bell for you:
 
Of course, the above example does not seem a very practical one. However, the following OCL patterns probably do ring a bell for you:
Line 127: Line 214:
  
 
On a sidenote: because EMFTVM performs lazy evaluation of collections, it also performs lazy evaluation of boolean operators ('''and''', '''or''', etc.). Regular ATL does not do this, and requires you to use nested '''if''' expressions to optimise your code. It is important to be '''aware''' of EMFTVM's lazy evaluation, because it may not invoke all of your code! If you're invoking a lazy rule from inside a lazy collection iterator body, you '''must''' evaluate the collection to trigger the lazy rule invocation!
 
On a sidenote: because EMFTVM performs lazy evaluation of collections, it also performs lazy evaluation of boolean operators ('''and''', '''or''', etc.). Regular ATL does not do this, and requires you to use nested '''if''' expressions to optimise your code. It is important to be '''aware''' of EMFTVM's lazy evaluation, because it may not invoke all of your code! If you're invoking a lazy rule from inside a lazy collection iterator body, you '''must''' evaluate the collection to trigger the lazy rule invocation!
 +
 +
=== Advanced tracing ===
 +
 +
In 2009, Andrés Yie did some work on ATL's tracing mechanism, which involved advanced reflection on the traces generated by ATL, as well as storing the traces to an EMF model. EMFTVM takes this work a step further, and provides an extended tracing metamodel (see Fig. 4). This metamodel allows efficient lookup of "default" traces, as well as "unique" traces. The "default" traces are used by ATL's implicit tracing mechanism to translate source values to target values in rule bindings. The "unique" traces are an addition that allows reflective lookup of target values for source values transformed by lazy unique rules or "nodefault" rules (a hidden feature of ATL that switches off implicit tracing for a matched rule). Note that the TraceElement metaclass has one more reference, which is not shown in the diagram: "object : EObject [0..1]". This is the reference to the external model element that is being traced.
 +
 +
[[Image:ATL_EMFTVM_Trace.png|Structure of the EMFTVM trace metamodel]]
 +
<br>''Fig. 4: Structure of the EMFTVM trace metamodel''
 +
 +
At runtime, the EMFTVM execution environment provides a "traces" field, which contains a instance of the TraceLinkSet metaclass shown above. You can navigate this TraceLinkSet to find the tracing information you need. The following example code is part of the "UML2Copy.atl" transformation module, and reflectively copies all stereotype applications:
 +
 +
'''lazy rule''' ApplyStereotypes {
 +
'''from''' s : UML2!"uml::Element" '''in''' IN
 +
'''using''' {
 +
t : UML2!"uml::Element" = s.resolve();
 +
}
 +
'''do''' {
 +
'''for''' (st '''in''' s.getAppliedStereotypes()) {
 +
t.applyStereotype(st);
 +
'''for''' (a '''in''' st.getAllAttributes()) {
 +
'''if''' ('''not''' a.name.startsWith('base_') '''and''' s.hasValue(st, a.name)) {
 +
t.setValue(st, a.name, s.getValue(st, a.name));
 +
}
 +
}
 +
}
 +
}
 +
}
 +
 +
'''endpoint rule''' ApplyAllStereotypes() {
 +
'''do''' {
 +
'''for''' (element '''in thisModule'''.traces.defaultSourceElements
 +
->collect(e|e.object)
 +
->select(o|o.oclIsKindOf(UML2!"uml::Element"))) {
 +
'''thisModule'''.ApplyStereotypes(element);
 +
}
 +
}
 +
}
 +
 +
The "'''thisModule'''.traces" field is used to iterate over all transformed elements (represented by the "defaultSourceElements"). Then, for each source UML2!Element, the "ApplyStereotypes" rule is invoked. The "ApplyStereotypes" rule then just invokes the reflective operations provided by the UML metamodel.
 +
 +
In order to store the trace model to a file for later processing, you can just add an output or in/out model named "trace" to the launch configuration. EMFTVM does not require you to specify the metamodel for a model, because EMF will work this out by itself.
 +
 +
Apart from navigating the trace model yourself, you can use one of these built-in helper operations as well:
 +
 +
'''helper def''' : resolveTemp(var : OclAny, target_pattern_name : String) : OclAny
 +
'''helper def''' : resolveTemp(var : OclAny, rule_name : String, target_pattern_name : String) : OclAny
 +
'''helper context''' OclAny '''def''' : resolve() : OclAny
 +
'''helper context''' OclAny '''def''' : resolve(rule : String) : OclAny
 +
'''helper context''' Collection(OclAny) '''def''' : resolve() : Sequence(OclAny)
 +
'''helper context''' Collection(OclAny) '''def''' : resolve(rule : String) : Sequence(OclAny)
 +
 +
The first "resolveTemp" operation is well-known, and resolves a named target element for a given source element, using the default traces. The second "resolveTemp" resolves a "unique" trace, and also requires the name of the rule for which the requested trace is unique.
 +
 +
The first "resolve" operation implements the implicit tracing mechanism: it is invoked on a source element, and returns its default target element, if any. If there is no default target element, it just returns the source element. The second "resolve" operation also takes a rule name as parameter, and uses the unique traces for that rule to resolve a target element. If there is no unique target element for that rule and source element, it returns the source element.
 +
 +
The last two "resolve" operations are defined on Collections, and resolve each element in the collection to a target element. The elements are returned in a Sequence: the order in which the input Collection returns the source elements is maintained in the output.
 +
 +
Fun fact:
 +
 +
target_property <- s.source_property
 +
 +
is equivalent to
 +
 +
target_property <:= s.source_property.resolve()
 +
 +
=== In-place transformation ===
 +
 +
ATL/EMFTVM supports in-place transformation through '''refining mode''' since January 4th, 2013. Refining mode allows one to write an in-place transformation as if it were a copy transformation, where all unmatched elements are copied by default. This is different from the [http://soft.vub.ac.be/soft/research/mdd/simplegt recursive in-place transformation supported by SimpleGT/EMFTVM], as no recursive matching takes place in ATL.
 +
 +
In order to use refining mode, use the '''refining''' keyword instead of the '''from''' keyword in the '''create''' clause:
 +
 +
'''create''' OUT : EMFTVM '''refining''' IN : EMFTVM;
 +
 +
You can now adapt the "copying" behaviour by adding rules. The following rule adapts the name of all EMFTVM!Rule elements:
 +
 +
'''rule''' RuleAppendedName {
 +
  '''from''' s : EMFTVM!Rule
 +
  '''to''' t : EMFTVM!Rule (
 +
    name <- s.name + 'Appended')
 +
}
 +
 +
In addition to the standard situation, where properties of an existing model element are changed, you can also delete elements. This is done by leaving out the '''to''' part. The following rule will match and delete instances of EMFTVM!Delete:
 +
 +
'''rule''' Delete {
 +
  '''from''' s : EMFTVM!Delete
 +
}
 +
 +
Finally, it is possible to replace an existing element by another element. This feature is new to ATL, and allows you to implement migration transformations. To replace an input element by another element, just specify a different metaclass for the output element. The following rule replaces all instances of EMFTVM!Set with instances of EMFTVM!Add:
 +
 +
'''rule''' SetToAdd {
 +
  '''from''' s : EMFTVM!"Set"
 +
  '''to''' t : EMFTVM!Add (
 +
    fieldname <- s.fieldname)
 +
}
 +
 +
The above rule also takes care of any existing references to "s" in the loaded models. Each reference to "s" is remapped to "t" instead. As a result, everything that used to point to an EMFTVM!Set instance now points to an EMFTVM!Add instance instead. This is in line with the copy transformation metaphor, where "s" resolves to "t" using the implicit tracing mechanism.
  
 
== Publications ==
 
== Publications ==
  
* Dennis Wagelaar, Massimo Tisi, Jordi Cabot, Frédéric Jouault. [http://soft.vub.ac.be/Publications/2011/vub-soft-tr-11-07.pdf Towards a general composition semantics for rule-based model transformation.] Proceedings of the ACM/IEEE 14th International Conference on Model Driven Engineering Languages and Systems (MoDELS 2011), LNCS 6981, pp.623 - 637. [[http://youtu.be/-P9vgTtPVzg Presentation on Youtube]]
+
* Dennis Wagelaar, Massimo Tisi, Jordi Cabot, Frédéric Jouault. [http://soft.vub.ac.be/Publications/2011/vub-soft-tr-11-07.pdf Towards a general composition semantics for rule-based model transformation.] Proceedings of the ACM/IEEE 14th International Conference on Model Driven Engineering Languages and Systems ([http://ecs.victoria.ac.nz/Events/MODELS2011/ MoDELS 2011]), LNCS 6981, pp.623 - 637. [[http://youtu.be/-P9vgTtPVzg Presentation on Youtube]]
 +
 
 +
* Dennis Wagelaar. [http://soft.vub.ac.be/Publications/2011/vub-soft-tr-11-04.pdf A Revised Semantics for Rule Inheritance and Module Superimposition in ATL.] Proceedings of the 3rd International Workshop on Model Transformation with ATL ([http://www.emn.fr/z-info/atlanmod/index.php/MtATL2011 MtATL 2011]), pp.63 - 74.
 +
 
 +
* Andrés Yie, Dennis Wagelaar. [http://soft.vub.ac.be/Publications/2009/vub-soft-tr-09-08.pdf Advanced Traceability for ATL.] Proceedings of the 1st International Workshop on Model Transformation with ATL ([http://www.emn.fr/z-info/atlanmod/index.php/MtATL2009 MtATL 2009]), pp.78 - 87.

Revision as of 16:18, 22 January 2013

ATL EMF Transformation Virtual Machine (research VM)

Since 2011, the ATL tools include a research VM (EMFTVM), which allows for experimentation with advanced language features. Currently, these features include:

Architecture

The EMF Transformation Virtual Machine (EMFTVM) is derived from the current ATL VMs and bytecode format. However, instead of using a proprietary XML format, it stores its bytecode as EMF models, such that they may be manipulated by model transformations. A special EMF resource implementation allows EMFTVM models to be stored in binary format, which is faster to load and save, and results in smaller files.

Apart from the standard ATL bytecode primitives, such as modules, fields, and operations, EMFTVM bytecode includes rules and code blocks. Fig. 1 shows the structure of rules and code blocks. Code blocks are executable lists of instructions, and have a number of local variables and a local stack space. Operation bodies and field initialisers are represented as code blocks in EMFTVM. Code blocks may also have nested code blocks, which can be manipulated and invoked from its containing block. These nested code blocks therefore effectively represent closures, which are nameless functions that can be passed as parameters to other functions. Closures are helpful for the implementation of OCL's higher-order operations, such as select and collect, which are parametrised by nested OCL expressions.

Structure of EMFTVM rules and code blocks
Fig. 1: Structure of EMFTVM rules and code blocks

Rules consist of input and output rule elements, a matcher code block, applier code block, and post-apply code block. The matcher code block takes potential input element candidates as parameters, and returns a boolean value, representing a match. The applier code block takes the input and (newly created) output elements as parameters, and assigns the bindings of the output elements. The post-apply code block also takes the input and output elements as parameters, and performs any (imperative) post-processing specified in the rule. Execution of rules is therefore done in three phases: (1) matching; only input elements are guaranteed to be present, (2) applying; all output elements and traces are guaranteed to exist, but no bindings may have been applied, (3) post-apply; all input and output elements, traces, and bindings are guaranteed to be present.

Rules can be invoked manually, automatically, and recursively automatically. Manual rules correspond to ATL lazy rules (and called rules). Automatic rules correspond to ATL matched rules. Recursively automatic rules do not apply to ATL, but can be used when compiling other transformation languages to EMFTVM. Rules can also be marked as default, which causes that rule to create default traces. Default traces can be looked up using ATL's implicit tracing mechanism, and only one default trace may exist for any given source pattern. Non-default traces are just stored in the trace model, and are not used by the EMFTVM transformation engine.

Rules can have a number of super-rules, which are stored by name. This decision allows EMFTVM to resolve and link the super-rules of each rule at load-time, whereas storing a super-rule reference would have hardcoded the super-rule in the bytecode. This is comparable to how the Java VM does super-class lookup. Finally, rules can be marked as abstract, which means that they are only applied as part of a non-abstract sub-rule, but never by themselves.

To summarise: by explicitly representing rules in the bytecode, rule inheritance can be resolved at load-time. As a consequence, rules stored in imported modules can be taken into account, and super-rules can be redefined by module superimposition before the reference to the super-rule is resolved in the sub-rules. This solves the historic mismatch between ATL's rule inheritance and module superimposition.

How to use EMFTVM?

Installation

EMFTVM is included in the latest ATL release (3.3): http://eclipse.org/mmt/downloads/?project=atl. Snapshot releases of EMFTVM can be downloaded as an ATL add-on from [1]

Compiling to EMFTVM

To use EMFTVM for your ATL transformations, just add "-- @atlcompiler emftvm" on the first line of your ATL module to compile them to EMFTVM. An example ATL file can be found at [2].

Running EMFTVM modules

EMFTVM includes a separate launch configuration dialog that looks very much like ATL's launch configuration dialog (see Fig. 2). It can be found via "Run -> Run Configurations...".

EMFTVM Launch Configuration Dialog
Fig. 2: EMFTVM Launch Configuration Dialog

EMFTVM also includes its own Ant tasks. See [3] to find out how to use the Ant tasks.

API

The API pattern for using EMFTVM from Java looks like this:

ExecEnv env = EmftvmFactory.eINSTANCE.createExecEnv();
ResourceSet rs = new ResourceSetImpl();

// Load metamodels
Metamodel metaModel = EmftvmFactory.eINSTANCE.createMetamodel();
metaModel.setResource(rs.getResource(URI.createURI("http://www.eclipse.org/m2m/atl/2011/EMFTVM"), true));
env.registerMetaModel("METAMODEL", metaModel);

// Load models
Model inModel = EmftvmFactory.eINSTANCE.createModel();
inModel.setResource(rs.getResource(URI.createURI("input.xmi", true), true));
env.registerInputModel("IN", inModel);

Model inOutModel = EmftvmFactory.eINSTANCE.createModel();
inOutModel.setResource(rs.getResource(URI.createURI("inout.xmi", true), true));
env.registerInOutModel("INOUT", inOutModel);

Model outModel = EmftvmFactory.eINSTANCE.createModel();
outModel.setResource(rs.createResource(URI.createFileURI("out.xmi")));
env.registerOutputModel("OUT", outModel);

// Load and run module
ModuleResolver mr = new DefaultModuleResolver("platform:/plugin/my.plugin.id/transformations/", new ResourceSetImpl());
TimingData td = new TimingData();
env.loadModule(mr, "Module");
td.finishLoading();
env.run(td);
td.finish();

// Save models
inOutModel.getResource().save(Collections.emptyMap());
outModel.getResource().save(Collections.emptyMap());

Note that it is possible to reuse an ExecEnv instance. While you cannot load other metamodels or transformation modules into an existing ExecEnv after it has already run, you can change the input/inout/output models. The advantage of reusing an ExecEnv is that you save on loading time, and the JIT has already warmed up. Please also note that ExecEnv instances are not thread-safe, and should only be used by a single thread. You should create new ExecEnv instances for use in other threads.

Standalone use

EMFTVM can also run outside Eclipse. To do this, add the following plug-in jars to your classpath (based on Juno):

org.objectweb.asm_3.3.1.v201105211655.jar
org.eclipse.emf.common_2.8.0-v20120911-0500.jar
org.eclipse.emf.ecore_2.8.0-v20120911-0500.jar
org.eclipse.emf.ecore.xmi_2.8.0-v20120911-0500.jar
org.eclipse.m2m.atl.common_3.3.1.v201209061455.jar
org.eclipse.m2m.atl.emftvm_3.4.0.201301142231.jar
org.eclipse.m2m.atl.emftvm.trace_3.4.0.201301142231.jar

Instead of the DefaultModuleResolver, which uses EMF URI prefixes, you can also define your own ModuleResolver that loads modules using Java class resource streams, like so:

ModuleResolver mr = new ModuleResolver() {
    @Override
    public Module resolveModule(String module) throws ModuleNotFoundException {
        Resource moduleRes = new EMFTVMResourceImpl();
        try {
            InputStream inputStream = MyClass.class.getResourceAsStream(module + ".emftvm");
            try {
                moduleRes.load(inputStream, Collections.emptyMap());
            } finally {
                inputStream.close();
            }
            return (Module) moduleRes.getContents().get(0);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
};

Features

Rule Inheritance

ATL rule inheritance is limited to single inheritance between rules within the same module. This limitation is hardwired in the ATL grammar, and cannot easily be changed. Therefore, the EMFTVM compiler introduces support for a rule inheritance @extends annotation, which supports multiple inheritance between rules situated in any module:

-- @extends SuperRule1, SuperRule2
rule SubRule {
    ....
}

To use this annotation, the regular ATL extends keyword must not be used: the regular ATL extends keyword will override the @extends annotation.

Detailed information on the rule inheritance semantics can be found in the paper "Towards a general composition semantics for rule-based model transformation."

Module import

Module import is the new module superimposition. Instead of providing a list of superimposed modules in your launch configuration, EMFTVM automatically loads any modules that are mentioned in an ATL uses clause. It does this on the basis of a "module path", which is the EMFTVM equivalent of Java's classpath: modules are looked up within certain base URIs (EMF resources always have a URI). A module path is a comma-separated list of base URIs, to which the module file name can be appended. Module paths are specified in the ATL EMFTVM launch configuration under "Path" (see Fig. 3).

EMFTVM launch configuration with module path
Fig. 3: EMFTVM launch configuration with module path

The loading order of modules is specified by the order of the uses clauses of each module; in addition, modules are loaded depth-first. As in module superimposition, rules and helpers in imported modules can be re-defined in the importing module. Detailed information on the module import semantics can be found in the paper "Towards a general composition semantics for rule-based model transformation."

Multiple dispatch

EMFTVM supports multiple dispatch of ATL helpers. This means you can greatly reduce the amount of "oclIsKindOf()" checks in your code, because you can simply write a new helper with the required context/parameter type (not only the context is virtual, but also all parameters). To illustrate how this works, look at the following ATL code:

-- These helpers check for value equality (as opposed to object identity equality).
-- The base case checks meta-type equality, and covers
-- comparison of different element types, including OclUndefined.
helper context OclAny def : sameAs(other : OclAny) : Boolean =
	self.oclType() = other.oclType();

helper context OCL!"OclType" def : sameAs(other : OCL!"OclType") : Boolean =
	super.sameAs(other) and
	self.name = other.name;

helper context OCL!CollectionType def : sameAs(other : OCL!CollectionType) : Boolean =
	super.sameAs(other) and
	self.elementType.sameAs(other.elementType);

In regular ATL, you cannot assume the parameter types to be correct: you must make sure you invoke the helpers with the correct parameter types. You certainly cannot expect ATL to invoke a different helper, based on the types of the parameters! In the case of the above code, regular ATL will invoke one of the three "sameAs" helpers based only on the context type (and the number of parameters). That means that it is pointless to change the type of the "other" parameter to "OCL!OclType" in the second "sameAs", because ATL will not use it.

EMFTVM does use the parameter types, however. EMFTVM will only invoke the second "sameAs" as soon as both the context and the "other" parameter are of type "OCL!OclType". Otherwise, it will invoke the first "sameAs". As a comparison, the second "sameAs" would have to look like this in regular ATL:

helper context OCL!"OclType" def : sameAs(other : OclAny) : Boolean =
	if other.oclIsKindOf(OCL!"OclType") then
		super.sameAs(other) and
		self.name = other.name
	else
		super.sameAs(other)
	endif;

Secondly, OclUndefined is an instance of OclAny in EMFTVM, which means that you can invoke helpers on it. That's why the first "sameAs" in our example works! To compare, this is what you would have to write in regular ATL:

helper context OclAny def : sameAs(other : OclAny) : Boolean =
	if other.oclIsUndefined() then
		false
	else
		self.oclType() = other.oclType()
	endif;

Lazy collections

EMFTVM provides a lazy implementation of the OCL collection types. That means you can invoke operations on the collections, but those operations will not be executed until you actually evaluate the collection. Also, collection operations will only be evaluated partially, depending on how much of the collection you evaluate. To illustrate how this works, look at the following example code:

query lazytest = (100).run()->collect(x | x.expensive())->last();

helper context Integer def : run() : Sequence(Integer) =
	if self <= 0 then
		Sequence{0}
	else
		(self - 1).run()->append(self)
	endif;

helper context Integer def : expensive() : Integer =
	(self * self).debug('expensive');

The above query will first invoke "run" on 100, which creates a Sequence of all numbers from 0 to 100. Then, the query invokes "collect" on that Sequence, and replaces each value in the Sequence by its squared value (the "expensive" operation). Finally, we're only interested in the last value of the changed Sequence.

In regular ATL, everything is evaluated left-to-right, and the whole {0..100} Sequence is converted to a Sequence of squared values before the last value is returned. In EMFTVM, "collect" returns a lazy Sequence, which is just waiting to be evaluated. Only when "last" is invoked on the lazy Sequence will the Sequence invoke the "expensive" operation on the last element of the input Sequence. As a result, "expensive" is only invoked once by EMFTVM.

Of course, the above example does not seem a very practical one. However, the following OCL patterns probably do ring a bell for you:

mySequence->select(x | x.oclIsKindOf(MM!MyType))->first()
MM!MyType.allInstances()->first()

In regular ATL, all elements are computed before invoking "first". In EMFTVM, only the first element of each sequence is evaluated. In fact, the "select(...)->first()" code was common enough for the OCL committee to come up with the "any(...)" iterator. In EMFTVM, "any(...)" is actually implemented by calling "select(...)->first()".

On a sidenote: because EMFTVM performs lazy evaluation of collections, it also performs lazy evaluation of boolean operators (and, or, etc.). Regular ATL does not do this, and requires you to use nested if expressions to optimise your code. It is important to be aware of EMFTVM's lazy evaluation, because it may not invoke all of your code! If you're invoking a lazy rule from inside a lazy collection iterator body, you must evaluate the collection to trigger the lazy rule invocation!

Advanced tracing

In 2009, Andrés Yie did some work on ATL's tracing mechanism, which involved advanced reflection on the traces generated by ATL, as well as storing the traces to an EMF model. EMFTVM takes this work a step further, and provides an extended tracing metamodel (see Fig. 4). This metamodel allows efficient lookup of "default" traces, as well as "unique" traces. The "default" traces are used by ATL's implicit tracing mechanism to translate source values to target values in rule bindings. The "unique" traces are an addition that allows reflective lookup of target values for source values transformed by lazy unique rules or "nodefault" rules (a hidden feature of ATL that switches off implicit tracing for a matched rule). Note that the TraceElement metaclass has one more reference, which is not shown in the diagram: "object : EObject [0..1]". This is the reference to the external model element that is being traced.

Structure of the EMFTVM trace metamodel
Fig. 4: Structure of the EMFTVM trace metamodel

At runtime, the EMFTVM execution environment provides a "traces" field, which contains a instance of the TraceLinkSet metaclass shown above. You can navigate this TraceLinkSet to find the tracing information you need. The following example code is part of the "UML2Copy.atl" transformation module, and reflectively copies all stereotype applications:

lazy rule ApplyStereotypes {
	from s : UML2!"uml::Element" in IN
	using {
		t : UML2!"uml::Element" = s.resolve();
	}
	do {
		for (st in s.getAppliedStereotypes()) {
			t.applyStereotype(st);
			for (a in st.getAllAttributes()) {
				if (not a.name.startsWith('base_') and s.hasValue(st, a.name)) {
					t.setValue(st, a.name, s.getValue(st, a.name));
				}
			}
		}
	}
}

endpoint rule ApplyAllStereotypes() {
	do {
		for (element in thisModule.traces.defaultSourceElements
				->collect(e|e.object)
				->select(o|o.oclIsKindOf(UML2!"uml::Element"))) {
			thisModule.ApplyStereotypes(element);
		}
	}
}

The "thisModule.traces" field is used to iterate over all transformed elements (represented by the "defaultSourceElements"). Then, for each source UML2!Element, the "ApplyStereotypes" rule is invoked. The "ApplyStereotypes" rule then just invokes the reflective operations provided by the UML metamodel.

In order to store the trace model to a file for later processing, you can just add an output or in/out model named "trace" to the launch configuration. EMFTVM does not require you to specify the metamodel for a model, because EMF will work this out by itself.

Apart from navigating the trace model yourself, you can use one of these built-in helper operations as well:

helper def : resolveTemp(var : OclAny, target_pattern_name : String) : OclAny
helper def : resolveTemp(var : OclAny, rule_name : String, target_pattern_name : String) : OclAny
helper context OclAny def : resolve() : OclAny
helper context OclAny def : resolve(rule : String) : OclAny
helper context Collection(OclAny) def : resolve() : Sequence(OclAny)
helper context Collection(OclAny) def : resolve(rule : String) : Sequence(OclAny)

The first "resolveTemp" operation is well-known, and resolves a named target element for a given source element, using the default traces. The second "resolveTemp" resolves a "unique" trace, and also requires the name of the rule for which the requested trace is unique.

The first "resolve" operation implements the implicit tracing mechanism: it is invoked on a source element, and returns its default target element, if any. If there is no default target element, it just returns the source element. The second "resolve" operation also takes a rule name as parameter, and uses the unique traces for that rule to resolve a target element. If there is no unique target element for that rule and source element, it returns the source element.

The last two "resolve" operations are defined on Collections, and resolve each element in the collection to a target element. The elements are returned in a Sequence: the order in which the input Collection returns the source elements is maintained in the output.

Fun fact:

target_property <- s.source_property

is equivalent to

target_property <:= s.source_property.resolve()

In-place transformation

ATL/EMFTVM supports in-place transformation through refining mode since January 4th, 2013. Refining mode allows one to write an in-place transformation as if it were a copy transformation, where all unmatched elements are copied by default. This is different from the recursive in-place transformation supported by SimpleGT/EMFTVM, as no recursive matching takes place in ATL.

In order to use refining mode, use the refining keyword instead of the from keyword in the create clause:

create OUT : EMFTVM refining IN : EMFTVM;

You can now adapt the "copying" behaviour by adding rules. The following rule adapts the name of all EMFTVM!Rule elements:

rule RuleAppendedName {
  from s : EMFTVM!Rule
  to t : EMFTVM!Rule (
    name <- s.name + 'Appended')
}

In addition to the standard situation, where properties of an existing model element are changed, you can also delete elements. This is done by leaving out the to part. The following rule will match and delete instances of EMFTVM!Delete:

rule Delete {
  from s : EMFTVM!Delete
}

Finally, it is possible to replace an existing element by another element. This feature is new to ATL, and allows you to implement migration transformations. To replace an input element by another element, just specify a different metaclass for the output element. The following rule replaces all instances of EMFTVM!Set with instances of EMFTVM!Add:

rule SetToAdd {
  from s : EMFTVM!"Set"
  to t : EMFTVM!Add (
    fieldname <- s.fieldname)
}

The above rule also takes care of any existing references to "s" in the loaded models. Each reference to "s" is remapped to "t" instead. As a result, everything that used to point to an EMFTVM!Set instance now points to an EMFTVM!Add instance instead. This is in line with the copy transformation metaphor, where "s" resolves to "t" using the implicit tracing mechanism.

Publications