M2T Engine User Guide
Fennec M2T is a lightweight MOFM2T v1.0 (Model to Text) engine that works as a standalone Java library — no Eclipse platform required. It follows the specification, with the deviations listed in §18, and reads the Acceleo 3.7 forms most templates use (the [comment … /] and [comment]…[/comment] comments, protected visibility, post, same-named templates); it is not Acceleo, and Acceleo's AQL is not supported.
Table of Contents
- Overview
- Quick Start
- Engine Setup
- Template Syntax
- Template Invocation
- Multiple Inputs
- Multiple Output Files
- Module Composition
- Guards and Post-Processing
- Protected Areas
- Standard Library
- Error Handling
- Calling Operations from a Template
- Engine Lifetime and Module Retention
- M2tGenerationStrategy
- Whitespace Handling
- Generating from a bnd Build
- Deviations from MOFM2T 1.0
1. Overview
The Fennec M2T Engine provides:
- MOFM2T v1.0 template parsing and execution
- Acceleo 3.7 compatible — existing
.mtltemplates work out of the box - Standalone operation — works as a plain Java library without OSGi
- OSGi-optional — full Declarative Services support when running in OSGi (see §3.6)
- ANTLR4-based parser — fast, reliable parsing with precise error locations
- Built on Fennec OCL — full OCL v2.5 expression support within templates
- Protected area merging — hash-based smart merge (D31) for user-code preservation
- Configurable whitespace — MOFM2T §8.4 strict mode and Acceleo-compatible mode
Bundles
| Bundle | Description |
|---|---|
org.eclipse.fennec.m2x.m2t.api | Public API interfaces |
org.eclipse.fennec.m2x.m2t.parser | ANTLR4 parser |
org.eclipse.fennec.m2x.m2t.engine | Evaluator implementation |
org.eclipse.fennec.m2x.m2t.model | MOFM2T EMF metamodel |
org.eclipse.fennec.m2x.ocl.* | Required OCL bundles (transitive) |
2. Quick Start
Minimal example — parse an MOFM2T template and generate a Java file from an EClass:
import org.eclipse.fennec.m2x.m2t.api.M2tConfiguration;
import org.eclipse.fennec.m2x.m2t.api.M2tContext;
import org.eclipse.fennec.m2x.m2t.api.M2tResult;
import org.eclipse.fennec.m2x.m2t.engine.M2tEngine;
import org.eclipse.fennec.m2x.model.m2t.Module;
import org.eclipse.fennec.m2x.ocl.api.OclConfiguration;
import org.eclipse.fennec.m2x.ocl.parser.OclParserSupport;
// 1. Create engine
OclConfiguration oclConfig = OclConfiguration.builder(new OclParserSupport()).build();
M2tConfiguration config = M2tConfiguration.builder(oclConfig).build();
M2tEngine engine = M2tEngines.create(config);
// 2. Parse template
Module module = engine.parse(
"[module m(ecore)/]\n" +
"[template public main(c : EClass)]\n" +
"[file (c.name.concat('.java'), false)]\n" +
"public class [c.name/] {}\n" +
"[/file]\n" +
"[/template]\n",
"MyTemplate");
// 3. Execute with an EClass as input
M2tResult result = engine.execute(module, M2tContext.of(myEClass));
// 4. Read generated output
if (result.isSuccess()) {
String javaCode = result.generatedFiles().get("Employee.java");
// → "public class Employee {}"
}Five lines of setup, one template, one generated file.
For a worked example over real files — a module import, both file open modes and a protected area whose hand-edits survive regeneration — see the org.eclipse.fennec.m2x.m2t.example project. Its main() classes are the walkthrough; for templates run as a build step instead, see the M2T bnd Generator Guide.
3. Engine Setup
In a bnd workspace, one instruction puts the M2T API on the buildpath — the OCL API comes with it, because part of M2T's API surface is OCL:
-library: enableM2TRunning templates from a build needs none of this: see the M2T bnd Generator Guide.
3.1 Three ways to give M2T its OCL engine
M2T evaluates template expressions with an OCL engine. Which one, is the caller's choice — and the first door needs no OCL knowledge at all:
// 1. don't care: M2T settings alone, the factory supplies a default OCL engine
M2tConfiguration.builder().whitespaceMode(WhitespaceMode.ACCELEO).build();
// 2. bring the engine you already have — the injected service under OSGi,
// OclEngines.create(...) in plain Java
M2tConfiguration.builder(oclEngine).build();
// 3. configure the OCL side yourself
M2tConfiguration.builder(oclConfiguration).build();A supplied engine is used as it is: its cache, its operation providers, its evaluation settings. The MOFM2T §8.3 string operations travel with each evaluation, so an engine that knows nothing about M2T needs no preparation.
M2tEngine.getOclEngine() returns the engine that actually runs — useful to warm it up, inspect its cache, or install EMF delegates on it.
Under OSGi the engine is a service and door 2 is what the component uses — see §3.6.
3.2 Minimal
import org.eclipse.fennec.m2x.m2t.api.M2tConfiguration;
import org.eclipse.fennec.m2x.m2t.engine.M2tEngine;
import org.eclipse.fennec.m2x.ocl.api.OclConfiguration;
import org.eclipse.fennec.m2x.ocl.parser.OclParserSupport;
OclConfiguration oclConfig = OclConfiguration.builder(new OclParserSupport()).build();
M2tConfiguration config = M2tConfiguration.builder(oclConfig).build();
M2tEngine engine = M2tEngines.create(config);3.3 With Builder Options
import java.nio.charset.StandardCharsets;
import org.eclipse.fennec.m2x.m2t.api.M2tConfiguration;
import org.eclipse.fennec.m2x.m2t.api.WhitespaceMode;
import org.eclipse.fennec.m2x.ocl.api.OclConfiguration;
import org.eclipse.fennec.m2x.ocl.engine.OclLruExpressionCache;
import org.eclipse.fennec.m2x.ocl.parser.OclParserSupport;
OclConfiguration oclConfig = OclConfiguration.builder(new OclParserSupport())
.expressionCache(OclLruExpressionCache.ofSize(2048))
.build();
M2tConfiguration config = M2tConfiguration.builder(oclConfig)
.defaultCharset(StandardCharsets.UTF_8)
.whitespaceMode(WhitespaceMode.ACCELEO)
.generationStrategy(myStrategy)
.build();
M2tEngine engine = M2tEngines.create(config);3.4 Configuration Options
| Method | Default | Description |
|---|---|---|
resourceSet(resourceSet) | — | Resource set whose package registry resolves metamodel type names (see §3.5) |
packageRegistry(registry) | EPackage.Registry.INSTANCE | The registry itself; wins over resourceSet when both are set |
defaultCharset(charset) | UTF-8 | Charset for file output encoding |
whitespaceMode(mode) | ACCELEO | Whitespace normalization mode (see §16) |
generationStrategy(strategy) | null (in-memory) | SPI for file output (see §15) |
maxDiagnostics(int) | 10,000 | Maximum diagnostics before truncation |
maxTemplateDepth(int) | 1,000 | Maximum template invocation depth (recursion limit) |
maxForIterations(int) | 1,000,000 | Maximum for-block iterations |
timeoutMs(long) | 30,000 | Wall-clock deadline of one generation in ms, 0 for none |
maxCrossProductSize(int) | 1,000,000 | Maximum cross-product size for set-argument invocations |
maxOutputSize(long) | 10,000,000 | Maximum total output size in characters (~10 MB), 0 for unlimited |
protectedAreaEnabled(boolean) | true | Enable/disable protected area markers and merging (see §10.5) |
unresolvedReferenceMode(mode) | FAIL | What an unresolvable extends, import, overrides or invocation does (see §5.6) |
3.5 Which Metamodels the Engine Sees
Every metamodel type name in a template is resolved when the template is parsed — the Book in [template public main(b : Book)], in [b.oclIsKindOf(Book)/], in a [for (b : Book | …)] and in an overrides declaration. The engine looks them up in its EPackage.Registry.
// the ResourceSet you already have — under OSGi this is what emf.osgi injects
M2tConfiguration config = M2tConfiguration.builder(oclConfig)
.resourceSet(resourceSet)
.build();
// or the registry itself, if that is what you hold
M2tConfiguration config = M2tConfiguration.builder(oclConfig)
.packageRegistry(registry)
.build();If both are set, the explicitly configured registry wins — the more specific setting beats the more general one. Only the resource set's package registry is used; nothing is loaded through it.
Under OSGi you do not do this yourself: the DS component binds the ResourceSet that emf.osgi publishes, as a mandatory reference — see §3.6.
The default is EPackage.Registry.INSTANCE, which is the right answer in plain Java. Supply your own when you hold the packages yourself — under OSGi, or wherever two versions of one nsURI can coexist. Nothing inside the engine reaches for the global registry on its own (D42), so what you pass in is what the parser sees. Model version identity stays yours; see the emf.osgi fingerprint guide.
This matters more than it looks. A declared type that resolves nowhere used to degrade to EObject without a word, and two things broke at once: oclIsKindOf answered for the wrong type, and MOFM2T §8.1.3 override dispatch selected no override at all, because EObject is not a supertype of a dynamic EClass.
A type name that resolves nowhere is now an error — parse fails with M2tParseException, and getErrors() lists every unresolved name in the template:
[template public main(x : NoSuchType)] → M2tParseException: Unknown type (NoSuchType)So a missing or wrong registry announces itself at parse time instead of producing quietly wrong output.
Templates are loaded with engine.parse(URI), taking an EMF URI, and read through the resource set's URIConverter. UriHelper in the shared org.eclipse.fennec.m2x bundle converts what a caller usually holds:
import org.eclipse.fennec.m2x.utils.UriHelper;
Module module = engine.parse(UriHelper.fromPath(Path.of("/srv/templates/report.mtl")));Prefer it over URI.createURI(path.toString()) — a path is not a URI, and on Windows that would produce a URI whose scheme is the drive letter.
UriHelperships in the bundleorg.eclipse.fennec.m2x(packageorg.eclipse.fennec.m2x.utils) — the shared commons of this workspace, separate from the engine bundles:xml<dependency> <groupId>org.eclipse.fennec.m2x</groupId> <artifactId>org.eclipse.fennec.m2x</artifactId> <version>0.1.1-SNAPSHOT</version> </dependency>
3.6 OSGi (Declarative Services)
DefaultM2tEngine publishes M2tEngine as a PROTOTYPE-scoped service. Every @Reference injection gets its own engine with its own caches, so one consumer's warm-up and another's memory footprint stay separate.
Its configuration policy is optional: there is a working engine without configuring anything, and configuring one later does not register a second service that consumers would have to disambiguate.
import org.eclipse.fennec.m2x.m2t.api.M2tEngine;
import org.eclipse.fennec.m2x.m2t.api.annotation.require.RequireM2T;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
@RequireM2T // resolver: a bundle providing the M2T engine must be present
@Component
public class MyGenerator {
@Reference
private M2tEngine engine; // fresh instance, own caches
public void generate(EObject model) throws M2tParseException {
Module module = engine.parse(templateSource, "myModule");
engine.link(module);
engine.execute(module, M2tContext.of(model));
}
}Components and scopes:
| Component | Scope | What's shared? |
|---|---|---|
DefaultM2tEngine | PROTOTYPE | Nothing — each consumer gets its own engine, module cache and linker state |
DefaultOclEngine | PROTOTYPE | Nothing — bound prototype_required, so each M2T engine evaluates on its own OCL engine |
DefaultOclExpressionCacheComponent | SINGLETON | The parsed-expression cache, shared by all OCL engines |
ResourceSet (from emf.osgi) | PROTOTYPE | Nothing — bound prototype_required, so each engine resolves metamodels through its own resource set |
The engine takes its OCL engine as a mandatory service reference. That is door 2 from §3.1: the OCL engine a consumer configured — with its cache and its operation providers — is the one that evaluates the template expressions, rather than a second one built beside it.
The resource set is a mandatory reference as well, and deliberately not an optional one with a fallback to the global registry. Under emf.osgi a metamodel registered for the resource set (emf.model.scope=resourceset) lives in the resource set's package registry and never in the global one — an engine built on the fallback would resolve a module's metamodel URI to the wrong package instance and match nothing, silently. And an optional reference cannot say when the service arrives, only that it was not there at activation. Mandatory means: the engine exists once the resource set does, and what §3.5 describes for plain Java happens by itself. Where several resource sets are published, pick one:
{
"DefaultM2tEngine": { "resourceSet.target": "(emf.name=myapp)" }
}The fingerprint service emf.osgi provides is bound the same way, mandatory, for the same reason.
3.6.1 Configuration via ConfigAdmin
All properties use the m2t. prefix. OCL limits are not repeated here; they are configured on DefaultOclEngine with ocl.*, so the two cannot drift apart.
{
":configurator:resource-version": 1,
"DefaultM2tEngine": {
"m2t.whitespaceMode": "SPEC",
"m2t.defaultCharset": "UTF-8",
"m2t.maxOutputSize": 5000000
},
"DefaultOclEngine": {
"ocl.maxDepth": 500
}
}Available properties:
| Property | Type | Default | Description |
|---|---|---|---|
m2t.defaultCharset | String | UTF-8 | Charset for generated files that do not name one |
m2t.whitespaceMode | String | ACCELEO | NONE, SPEC or ACCELEO — see §16 |
m2t.maxDiagnostics | int | 10,000 | Diagnostics collected before generation gives up (T-2) |
m2t.maxTemplateDepth | int | 1,000 | Nesting depth of template invocations (T-1 runaway recursion) |
m2t.maxForIterations | int | 1,000,000 | Iterations of a single for block (T-3) |
m2t.timeout | long | 30,000 | Wall-clock deadline of one generation in ms (0 = no timeout) |
m2t.maxCrossProductSize | int | 1,000,000 | Size of a for over several collections (T-3) |
m2t.maxOutputSize | long | 10,000,000 | Characters one generation may produce (T-4 output flooding) |
m2t.protectedAreaEnabled | boolean | true | Whether protected area markers in existing files are honoured (T-6) |
m2t.unitResolverEnabled | boolean | false | Whether modules may be reached beyond the ones handed to link |
m2t.discoverUnitResolvers | boolean | false | Whether resolvers registered as services are looked up by the name a template extends or imports |
m2t.allowedUnitModules | String[] | (empty) | Module names that may be resolved; empty puts no restriction on the names |
m2t.maxUnitResolvers | int | 5 | How many resolvers one generation may consult |
The same settings are available to plain Java through M2tConfiguration.Builder — see §3.4.
3.6.2 Modules the engine fetches
extends and [import/] resolve against the modules handed to link — and, if you let them, against modules fetched by name:
M2tEngines.create(M2tConfiguration.builder()
.addUnitResolver(name -> Optional.of(new M2tUnit.SourceUnit(name, uri, source)))
.unitResolverEnabled(true)
.allowedUnitModules(Set.of("base"))
.build());Under OSGi a bundle offers a module by publishing a resolver under the name it answers for, and the engine looks it up when it links:
@Component(service = M2tUnitResolver.class, property = "m2t.unit.name=base")
public class BaseTemplates implements M2tUnitResolver { … }Off by default. A resolver produces template source from a name a template chose, which is a way out of the set of modules the caller decided to generate from. m2t.unitResolverEnabled is the gate; m2t.allowedUnitModules narrows what may be reached once it is open, and an empty list narrows nothing.
A name that stays unresolved is a warning from link, as it always was — a linker asked to link a partial set is not a failure.
Running on a specific OCL engine: the reference is unfiltered by default, so the engine binds whichever OclEngine service is there. To pin it to one you configured yourself, set a target filter:
{
"DefaultM2tEngine": { "oclEngine.target": "(component.name=MyTunedOclEngine)" }
}3.7 Compiling a Module
parse() gives you the module to execute. compile() gives you the module to store:
CompiledUnit compiled = engine.compile(source, "my.module");
Module module = (Module) compiled.getUnit(); // the same module parse() would return
engine.execute(module, M2tContext.of(input)); // executes as before
Resource resource = resourceSet.createResource(URI.createURI("my.module.compiled"));
resource.getContents().add(compiled);
resource.save(null); // this is what parse() could not doThe result of parse() is not storable: the parser creates objects the module references but that nothing contains — variables, type instances — and saving fails on the first of them. compile() packs them beside the module into one self-contained document, so it saves, loads in a fresh resource set and copies without pointing back at the original. The module inside is the very object parse() would have returned; compile() adds a document around it, nothing else.
The CompiledUnit carries an id (unique across all compiled units), namespace, version and description for a store to list it by, and a manifest that records the language, the qualified name, the unit and source fingerprints, the metamodels the module was compiled against and the modules it depends on.
Dependencies. A module depends on what it extends and imports. compile() resolves every such name through the unit resolvers of the engine configuration (they have to be enabled, exactly as for link()), and binds according to the dependency mode:
CompiledUnit compiled = engine.compile(source, "my.module",
UnitCompileOptions.of(DependencyMode.EMBED));| mode | what the unit carries | binding |
|---|---|---|
embed | the dependency, compiled in turn, under getEmbedded() | done at compile time — extends, imports, overrides and invocations point into the document; the unit generates on an engine without any resolver, also after a reload |
pin (default) | the dependency's name and unit fingerprint in the manifest | later — the module stays unbound |
rebind | the dependency's name | later, against whatever the runtime then serves under the name |
Under pin and rebind the module keeps what it still has to bind on itself (m2t.link.* annotations: the extends/imports names, pending overrides and invocations — the parser leaves those beside the AST, not in it), so an engine that never parsed the module can link() it from its own resolvers after a reload. A name nobody resolves fails compile() in every mode with Cannot resolve import: <name>; a cycle of extends is reported under embed and pin. M2tParseException.getErrors() holds one diagnostic per failed extends/import, at its declaration, and under embed one per unresolved override or invocation (#264). MOFM2T has no blackboxes — Java services arrive through the OCL operation providers of the engine — so a compiled module never carries a blackbox requirement.
A compiled module can be stored and generated from somewhere else:
UnitStore store = new DefaultUnitStore(new InMemoryUnitStoreBackend());
UnitKey key = store.put(engine.compile(source, "my.module"));
PreparedContext prepared = UnitPreparer.withDefaults(store, engine.unitBinder()).prepare(key);
M2tResult result = engine.execute(prepared, "my.module", M2tContext.of(input));M2tStoreUnitResolver resolves extends and import from a store. The mechanism is described once for all languages in the Compiled Units Guide — including what happens with a pinned module that arrives at an engine which never parsed it.
4. Template Syntax
MOFM2T templates use [ and ] as delimiters. Everything outside [...] is literal text; everything inside is a directive or OCL expression.
4.1 Module Declaration
Every template file starts with a module declaration:
[module myModule(ecore)/]The parameter in parentheses is the metamodel the templates transform: a package of the engine's package registry, named by its nsURI as an escaped identifier (_'http://www.eclipse.org/emf/2002/Ecore') or by its package name (ecore — the name is case-sensitive, Ecore is not it). A metamodel the registry does not have is a parse error with its position (MOFM2T §8.1.1); before, it was ignored silently.
A module extends at most one module: [module m(ecore) extends a, b/] is a parse error, as §7 supports "only single inheritance".
4.2 Template
A template declares its visibility, public or private (MOFM2T §8.2), or protected (Acceleo). A declaration without one is taken as public and reported as a WARNING "missing visibility (MOFM2T §8.2)"; the same holds for a query.
Templates are the main code-generation constructs. A template marked main (or named main) is the entry point. The mark is the Acceleo comment [comment @main/] directly before the template; a module has one entry point, so a second @main is a parse error:
[module m(ecore)/]
[template public main(c : EClass)]
[file (c.name.concat('.java'), false)]
class [c.name/]
{
// Constructor
[c.name/]()
{
}
}
[/file]
[/template]Input: EClass(name="Employee") produces:
class Employee
{
// Constructor
Employee()
{
}
}4.3 OCL Expressions
Inline OCL expressions are enclosed in [ and /]:
[c.name/] [comment]single property[/comment]
[c.eAttributes->size()/] [comment]collection operation[/comment]
[c.name.toUpper()/] [comment]string operation[/comment]
['hello'.concat(' world')/] [comment]string literal[/comment]
[c.abstract ? 'A' : 'C'/] [comment]conditional (MOFM2T §8.2 ifExp)[/comment]The expression part is OCL v2.4 as the OCL engine parses it — escaped identifiers (_'it\'s'), qualified features (self.EClassifier::name), iterate without an iterator variable and nested /* */ comments included; a test runs the OCL expression corpus through [expr/] as well. cond ? a : b is the MOFM2T shorthand for if cond then a else b endif.
In a template, query or macro, self is the first parameter; in a post expression it is the generated text.
4.4 For Block
Iterate over collections with optional separator, before, after, and guard:
[comment]simple iteration[/comment]
[for (a : EAttribute | c.eAttributes)]
[a.eType.name/] [a.name/];
[/for]
[comment]with separator[/comment]
[for (a : EAttribute | c.eAttributes) separator (', ')]
[a.name/]
[/for]
[comment]with before/after[/comment]
[for (c : EClassifier | p.eClassifiers) before ('[') separator (', ') after (']')]
[c.name/]
[/for]
[comment]with guard — only matching elements[/comment]
[for (cl : EClassifier | p.eClassifiers) ? (cl.name.startsWith('C'))]
[cl.name/]
[/for]The i variable is available inside for blocks as a 1-based iteration counter:
[for (cl : EClassifier | p.eClassifiers)]
[i/]. [cl.name/]
[/for]Output: 1. ClasseA, 2. ClasseB, 3. AbstractClass
What a for block iterates over (MOFM2T §8.1.8):
- a collection: its elements;
- a single value: that value, once — as Acceleo and OCL's implicit
asSetdo:[for (s : String | c.name)][s/][/for]writes the name; null: nothing;invalid: nothing, and an ERROR.
An element that is no instance of the loop variable's type is skipped, with one ERROR per block: [for (a : EAttribute | c.eStructuralFeatures)] leaves out the references. A for block takes an init section after its header; it is evaluated per iteration and sees the loop variable:
[for (f : EStructuralFeature | c.eStructuralFeatures) { u : String = f.name.toUpper(); }][u/] [/for]4.5 If / ElseIf / Else
Conditional output:
[if (cl.name = 'ClasseA')]
A
[elseif (cl.name = 'ClasseB')]
B
[else]
OTHER
[/if]The spec end tags [/elseif] and [/else] are optional (MOFM2T §8.2): [elseif (…)]B[/elseif][else]C[/else][/if] is the same block.
A condition "needs to evaluate to true" (MOFM2T §8.1.21). One that is neither true nor false — null, invalid, a String — decides nothing: no branch runs, the else branch included, and it is an ERROR. The same holds for a template guard and a for guard. A lenient navigation inside a comparison is not affected: [if (c.ePackage.eSuperPackage.name = 'X')] compares null with 'X', which is false.
4.6 Let Block
A let block is a type test (MOFM2T §7, §8.1.16): its body runs when the value is an instance of the declared type, and the variable is then bound to it:
[let ac : EClass = c]
class: [ac.name/]
[/let]With elselet branches the first one whose value fits runs, otherwise the else branch:
[let r : EReference = f]reference [r.name/]
[elselet a : EAttribute = f]attribute [a.name/]
[else]other
[/let]null is no instance of any type, so it takes the else branch. An invalid value is an ERROR, and the let block produces nothing — the else branch included. The type may be left out ([let n = c.name]); the variable then takes the type of its value, and only null fails the test. A declaration with neither type nor value is a parse error.
4.7 File Block
Direct output to a named file. The second parameter controls append mode (true = append, false = overwrite):
[file (c.name.concat('.java'), false)]
public class [c.name/] {}
[/file]Append mode for log files:
[file ('generation.log', true)]
Generated: [c.name/]
[/file]The arguments follow MOFM2T §7.3 and §8.2:
- URL. A path relative to the output, or a
file:URI.file:x,file:/xandfile:\xare relative to the output directory;file:///abs/xis absolute and must lie inside it. The keywordstdout, or the string'stdout', sends the body to the standard output instead of a file:M2tResult.standardOutput(). Text the entry template writes outside any[file]block goes there too, in order with the[file (stdout)]blocks. The engine prints nothing itself; the host decides. - Append flag, default
false. Any Boolean expression is allowed ([file ('log', c.abstract)]).null,invalidor a value that is not a Boolean is an ERROR, and the block is skipped. Within one run, an overwrite block for a path replaces what earlier blocks wrote, on disk too. - Unique id (
uniqId). When nothing exists under the file's path, the protected areas of the file generated before under the same id are taken over (§7.3: "even when a model element was renamed"). This is reported as INFO, and the old file is kept.FileSystemGenerationStrategyremembers the ids in.m2t-idsin its output directory. - Charset (extension). Without one,
M2tConfiguration.defaultCharset()applies. Acceleo templates put the charset third; a charset name there is the unique id, and it is reported as a WARNING.
The let block also takes [elselet v : T = e] branches; the spec's [/elselet] end tag may be written or left out (Acceleo leaves it out).
4.8 Query
Queries are reusable OCL expressions that return a value:
[query public opCount(c : EClass) : Integer = c.eOperations->size()/]Use in templates:
[c.opCount()/]The body must conform to the declared return type. Where the parser knows the body's type, a mismatch is a parse error ([query public q(c : EClass) : Integer = 'abc'/]); otherwise the evaluator checks the value and reports a WARNING when it does not conform (MOFM2T §8.1.15).
4.9 Macro
A macro is a block that is expanded where it is called and writes to the output there, like a template (MOFM2T §7.4, §8.1.19):
[macro wrap(c : EClass)]<[c.name/]>[/macro]Result: [wrap(c)/]Output: Result: <Employee>
A macro may take a body. Its last parameter is then of the predefined type Body, and the call passes the text between the start and the end tag; [b/] inserts it. The body is expanded in the context of the call, so it sees the caller's variables:
[macro javaMethod(type : String, name : String, resultType : String, body : Body)]
public [resultType/] [name/]() {
[body/]
}
[/macro]
[javaMethod(t, 'm', 'r')]result = [c.name/];[/javaMethod]Only the last parameter may be a Body. A macro with a Body parameter is called with a body ([m(…)]…[/m]), any other one without ([m(…)/]); the wrong form is an unresolved reference. The end tag must name the macro it closes. Recursion counts towards the template depth limit, and the body of a macro is normalised like that of a template (§16).
4.10 Comment
Three forms are comments:
[comment]
the Acceleo block form — its content is discarded
[/comment]
[comment()]the MOFM2T form: the predefined comment macro with a body (§7.4)[/comment]
[comment a single-line comment, Acceleo /]The body of a block comment is literal text up to the first [/comment]. Square brackets and template code inside it are neither parsed nor evaluated, so [comment]// see [:tlsPort][/comment] or [comment][c.oldName/][/comment] are fine. Because the first [/comment] closes the comment, comments do not nest. An unterminated [comment] is a parse error.
[comment @main/] directly before a template marks it as the entry point (§4.2).
// is no comment in a template body: MOFM2T §8.2 treats text after // as a comment only in code-explicit mode, and in text-explicit mode it is output like any other text (the spec's own §7 example generates // Constructor).
4.11 Trace
[trace (c)]class [c.name/][/trace]A trace block outputs its body. The model element is evaluated, and an invalid one is an ERROR (MOFM2T §8.1.18); trace records (file, range, model element, template) are not collected — M2tResult carries none.
4.12 Text Outside Templates and Escape Modes
Text between module elements is not generated. Text other than whitespace there is usually a tag whose [ went missing, so the parser reports it as a WARNING with its position (unit:line:col: text outside a template is not generated: '…' (MOFM2T §8.2)). Parse warnings reach the caller as WARNING diagnostics of the M2tResult.
The engine reads templates in text-explicit mode. @text-explicit before the module declaration is accepted and changes nothing; @code-explicit is a parse error "code-explicit mode is not supported" with its position.
5. Template Invocation
5.1 Calling Helper Templates
A template can call other templates defined in the same module:
[module m(ecore)/]
[template public genAttribute(a : EAttribute)]
private [a.eType.name/] [a.name/];
[/template]
[template public main(c : EClass)]
[file (c.name.concat('.java'), false)]
public class [c.name/] {
[for (a : EAttribute | c.eAttributes)]
[genAttribute(a)/]
[/for]
}
[/file]
[/template]Output for EClass(Employee, attrs=[name:EString, salary:EDouble]):
public class Employee {
private EString name;
private EDouble salary;
}5.2 Multi-Layer Template Chains
Templates can call templates that call further templates:
[module m(ecore)/]
[template public attrGen(a : EAttribute)]
[a.eType.name/] [a.name/]
[/template]
[template public classGen(c : EClass)]
class [c.name/]([for (a : EAttribute | c.eAttributes) separator (', ')][attrGen(a)/][/for])
[/template]
[template public main(p : EPackage)]
[file ('out', false)]
[for (c : EClassifier | p.eClassifiers)]
[classGen(c.oclAsType(EClass))/]
[/for]
[/file]
[/template]Output: class Employee(EString name, EDouble salary)
5.3 Template Invocation with Before/After/Separator
An invocation passes its arguments as MOFM2T §8.1.10 says: a collection argument for a single parameter invokes the template once per element (several such arguments: once per combination), a collection argument for a collection parameter invokes it once, and a single argument for a collection parameter is passed as a collection of one. before goes in front of the first invocation, separator between two and after behind the last — of the invocations that took place: a template its guard declines was not invoked, and gets neither. They apply to a single invocation too: [t(c) before ('<') after ('>')/] writes <…>.
When invoking a template on a collection, use before, after, and separator:
[fmt(p.eClassifiers) before ('[') separator (', ') after (']')/]With [template public fmt(c : EClassifier)][c.name/][/template] this produces:
[ClasseA, ClasseB, AbstractClass]5.4 Calling Queries from Templates
Queries encapsulate reusable OCL logic:
[module m(ecore)/]
[query public fieldCount(c : EClass) : Integer = c.eAttributes->size()/]
[template public genSummary(c : EClass)]
[c.name/] ([c.fieldCount()/] fields)
[/template]
[template public main(c : EClass)]
[file ('out', false)]
Summary: [genSummary(c)/]
[/file]
[/template]Output: Summary: Employee (2 fields)
A query is callable wherever an OCL expression is evaluated, not only in a template body — a template guard, a for guard, a let and an if all reach it:
[module m(ecore)/]
[query public hasOps(c : EClass) : Boolean = c.eOperations->size() > 0/]
[template public main(c : EClass) ? (c.hasOps())]
[file ('out', false)]
[c.name/] has operations
[/file]
[/template]Both call forms work: c.hasOps(), where the receiver is the query's first parameter, and hasOps(c). Visibility follows the same rules as an invocation: the module's own queries whatever their visibility, the public and protected ones of transitively extended modules, and the public ones of imported modules.
5.5 Arguments, Arity and Overloading
A call passes as many arguments as the template, query or macro has parameters; another number is a link error ("'t' takes 1 argument(s), the call passes 2"), handled as in §5.6. At runtime an argument that is null or invalid, or that is no instance of its parameter's type, is an ERROR at the argument, and the template is not invoked (MOFM2T §8.1.10). Unlike a QVT-O helper, which runs with a null argument, a template is skipped — as in Acceleo.
before, separator and after belong to a template invocation; on a query call they are a link error, as a query yields a value.
A template call may stand inside a larger OCL expression (§8.2 <invocation> is an OclExpressionCS); its value is the text it generates: ['<' + t(c) + '>'/].
Same-named templates of one module are kept apart by their parameter types, and a call runs the most specific one its arguments fit:
[template public t(x : EClass)]CLASS[/template]
[template public t(p : EPackage)]PACKAGE[/template][t(c)/] writes CLASS, [t(c.ePackage)/] writes PACKAGE. This is Acceleo's overloading and a deliberate deviation from MOFM2T §8.1.2 ("A module element has a unique name within the containing module"). Two templates with the same parameter types are a link error, and so are a template, query or macro sharing a name with one of another kind, or two queries or macros of the same name.
5.6 References That Cannot Be Resolved
A reference naming something that is not there — an extends, an import, an overrides or an invocation — ends the generation with an error diagnostic and produces no files:
[module child(ecore) extends missing/]
[template public main(c : EClass)][file ('out', false)]body[/file][/template]ERROR Unresolved extends 'missing' in module 'child'
generatedFiles = {}This is the same rule QVT-O and QVT-R follow with Cannot resolve import. Generating anyway is the quieter and worse outcome: a missing extends silently changes which templates are visible, so the document is not absent but wrong.
The references are attributed to the module they are written in, so one broken module in a link set does not stop a sound one from generating.
Where an incomplete module set is deliberate, the older, lenient behaviour stays reachable:
M2tConfiguration config = M2tConfiguration.builder(oclConfig)
.unresolvedReferenceMode(UnresolvedReferenceMode.WARN)
.build();link(...) reports the names in either mode; the mode decides only whether the generation proceeds.
6. Multiple Inputs
6.1 Multi-Parameter Templates
A template can accept multiple parameters. Pass multiple input elements via M2tContext.of(List, Path):
[module m(ecore)/]
[template public main(p : EPackage, c : EClass)]
[file ('out', false)]
package [p.name/];
class [c.name/] {}
[/file]
[/template]Module module = engine.parse(mtl, "test");
M2tResult result = engine.execute(module,
M2tContext.of(List.of(myPackage, myClass), null));
// → "package company;\nclass Employee {}"6.2 Navigation to Referenced Elements
A single input element can navigate to related model elements via OCL:
[template public main(c : EClass)]
[file (c.name.concat('.java'), false)]
package [c.ePackage.name/];
class [c.name/] {
[for (a : EAttribute | c.eAttributes)]
[a.eType.name/] [a.name/];
[/for]
}
[/file]
[/template]The template receives one EClass but navigates to its ePackage and iterates over its eAttributes.
6.3 Helper Templates with Multiple Parameters
Helper templates can accept additional parameters:
[template public genField(a : EAttribute, prefix : EString)]
[prefix/]_[a.name/] : [a.eType.name/]
[/template]
[template public main(c : EClass)]
[file ('out', false)]
[for (a : EAttribute | c.eAttributes) separator ('\n')]
[genField(a, c.name)/]
[/for]
[/file]
[/template]Output: Employee_name : EString / Employee_salary : EDouble
7. Multiple Output Files
7.1 One File Per Element
Iterate over model elements and generate a separate file for each:
[module m(ecore)/]
[template public genClass(c : EClass)]
[file (c.name.concat('.java'), false)]
public class [c.name/] {
[for (a : EAttribute | c.eAttributes)]
private [a.eType.name/] [a.name/];
[/for]
}
[/file]
[/template]
[template public main(p : EPackage)]
[for (c : EClassifier | p.eClassifiers)]
[genClass(c.oclAsType(EClass))/]
[/for]
[/template]Generates Employee.java and Manager.java with their respective attributes.
7.2 Different File Types
Generate different kinds of files from the same model:
[module m(ecore)/]
[template public main(p : EPackage)]
[file ('MANIFEST.MF', false)]
Bundle-Name: [p.name/]
Bundle-SymbolicName: [p.nsURI/]
[/file]
[for (c : EClassifier | p.eClassifiers)]
[file (c.name.concat('.java'), false)]
package [p.name/];
public class [c.name/] {}
[/file]
[/for]
[/template]Generates MANIFEST.MF plus one .java file per classifier.
7.3 Append-Mode Log File
Use append mode (true) to accumulate output across iterations:
[module m(ecore)/]
[template public main(p : EPackage)]
[for (c : EClassifier | p.eClassifiers)]
[file (c.name.concat('.java'), false)]
class [c.name/] {}
[/file]
[file ('generation.log', true)]
Generated: [c.name/]
[/file]
[/for]
[/template]The generation.log file contains one line per generated class.
8. Module Composition
8.1 Extends
A module can extend another module to inherit its templates, queries, and macros:
[comment]base.mtl[/comment]
[module base(ecore)/]
[template public greet(e : EClass)]Hello[/template][comment]child.mtl[/comment]
[module child(ecore) extends base/]
[template public main(e : EClass)]
[file ('out.txt', false)]
[greet(e)/]
[/file]
[/template]Module baseModule = engine.parse(baseSource, "base");
Module childModule = engine.parse(childSource, "child");
engine.link(baseModule, childModule);
M2tResult result = engine.execute(childModule, M2tContext.of(input));
// → "Hello"Key: Call engine.link(modules...) after parsing all modules and before executing. This resolves cross-module references.
8.2 Imports
Import makes another module's public templates and queries available:
[module main(ecore)/]
[import util/]
[template public main(e : EClass)]
[file ('out.txt', false)]
[render(e)/]
[/file]
[/template]Module utilModule = engine.parse(utilSource, "util");
Module mainModule = engine.parse(mainSource, "main");
engine.link(utilModule, mainModule);8.3 Overrides and Super
A child module can override a template from its parent, optionally calling [super/] to include the original output:
[comment]base.mtl[/comment]
[module base(ecore)/]
[template public render(e : EClass)]Base[/template]
[template public main(e : EClass)]
[file ('out.txt', false)]
[render(e)/]
[/file]
[/template][comment]child.mtl[/comment]
[module child(ecore) extends base/]
[template public render(e : EClass) overrides render]Before-[super/]-After[/template]Module baseMod = engine.parse(baseSource, "base");
Module childMod = engine.parse(childSource, "child");
engine.link(baseMod, childMod);
M2tResult result = engine.execute(baseMod, M2tContext.of(input));
// → "Before-Base-After"Override chains work transitively. With modules A -> B -> C, each overriding render:
[comment]A[/comment]
[template public render(e : EClass)]A[/template]
[comment]B extends A[/comment]
[template public render(e : EClass) overrides render][super/]+B[/template]
[comment]C extends B[/comment]
[template public render(e : EClass) overrides render][super/]+C[/template]Result: A+B+C
[super/] points to the template whose invocation led to the override (MOFM2T §8.1.3): with t2 overrides t1, t3, a call of t3 that runs t2 makes [super/] run t3, a call of t1 runs t1. Where t2 is called directly, the first listed template, t1, is the one. A main template in the child module that calls its own override reaches the base the same way.
An override is checked when the modules are linked (MOFM2T §7, §8.1.3). It must name a public or protected template of a module the overriding one extends — not one of its own module, not itself, not one it only imports, not a private one — and have as many parameters, each of the same type as the overridden one's, a subtype or a supertype. Anything else is a link error, handled as in §5.6. At runtime an override applies when every argument is an instance of its parameter type and its guard holds; one parameter that decides for the overridden template decides for it.
8.4 Visibility
| Visibility | Same module | Via extends | Via import |
|---|---|---|---|
public | Yes | Yes | Yes |
protected | Yes | Yes | No |
private | Yes | No | No |
8.5 Namesake Resolution
When multiple modules define a template with the same name, resolution priority is:
- Local (defined in the current module)
- Extended (inherited from the parent module)
- Imported (from imported modules)
Of the candidates in that order, the first with as many parameters as the call has arguments is taken.
8.5.1 Entry Points
execute(module, context) starts from the public template marked [comment @main/], else the public template named main, of the module or a module it extends. MOFM2T §7 has no main template — "A transformation (template) can be started by directly invoking a public template with the correct parameters" — and that is execute(module, templateName, context):
M2tResult result = engine.execute(module, "classToJava", M2tContext.of(eClass));In both, the input elements must fit the template's parameters in number and type. A module without an entry point, a private one, or inputs that do not fit are an ERROR in the result, and nothing is generated — no exception.
8.6 Linking API
// Parse all modules
Module base = engine.parse(baseSource, "base");
Module child = engine.parse(childSource, "child");
Module util = engine.parse(utilSource, "util");
// Link — returns warnings for unresolved references
List<String> warnings = engine.link(base, child, util);
// Execute the entry-point module
M2tResult result = engine.execute(child, M2tContext.of(input));For single-module scenarios, linking happens automatically during execute().
9. Guards and Post-Processing
9.1 Template Guards
A guard is an OCL boolean expression that controls whether a template produces output:
[template public concreteOnly(c : EClassifier)
? (c.oclIsKindOf(EClass) and not c.oclAsType(EClass).abstract)]
[c.name/]
[/template]When the guard evaluates to false, the template produces no output. A guard that is neither true nor false is an ERROR, and the template is not invoked (§4.5).
A guard is an ordinary OCL expression and may call the module's queries, like any other expression in the module — see §5.4.
9.2 Override Guards
Override templates can have guards. When the guard is false, the engine falls back to the overridden template:
[comment]base[/comment]
[template public render(e : EClass)]Original[/template]
[comment]child — only overrides for abstract classes[/comment]
[template public render(e : EClass) overrides render ? (e.abstract)]Abstract: [e.name/][/template]- Abstract class input:
Abstract: AbstractClass - Concrete class input:
Original(fallback to base)
The fallback does not depend on where the call is written (#149). A template and the templates that override it are one invocation target: the guard decides which of them runs, not whether anything runs. So [e.render()/] in the base module and the same call in the child module answer the same — the engine collects the family from the root of the override chain, asks the most derived candidate first, and runs the first whose parameter types fit and whose guard holds. Where no candidate applies, nothing is generated. That is the behaviour of the MOFM2T-conformant reference: Acceleo 3.7 builds the candidate list from a call's overriding templates and namesakes, evaluates the guards over the whole list, and takes the most specific of what remains — its candidate list does not depend on the calling module either.
9.3 Parameter Type Narrowing
An override can narrow the parameter type. The override applies only when the actual argument matches the narrower type:
[comment]base — accepts EClassifier[/comment]
[template public render(e : EClassifier)]Base[/template]
[comment]child — narrowed to EDataType[/comment]
[template public render(e : EDataType) overrides render]Narrowed[/template]EDataTypeargument:NarrowedEClassargument:Base(fallback — EClass is not an EDataType)
9.4 Post-Processing (Acceleo Extension)
The post() clause applies a String operation to the entire template output:
[template public trimmed(p : EPackage) post(trim())]
[p.name/]
[/template][template public uppered(p : EPackage) post(toUpper())]
[p.name/]
[/template]trimmed(p)withp.name = "target"producestarget(whitespace trimmed)uppered(p)producesTARGET
10. Protected Areas
Protected areas mark regions in generated files that are preserved across regenerations. User modifications inside protected areas survive re-generation.
10.1 Syntax
[protected ('unique_id')]
// default content — replaced by user code after first generation
[/protected]The generated output contains markers with a content hash (D31). Both markers stand on lines of their own, indented like the [protected] tag (MOFM2T §7.2 example):
// [protected 'unique_id' a1b2c3d4]
// default content
// [/protected]The comment syntax of the markers depends on the file's extension: // for .java and any extension not configured, <!-- … --> for .xml and .html, # for .py, -- for .sql. More can be configured:
M2tConfiguration config = M2tConfiguration.builder(oclConfig)
.protectedAreaDelimiter("yaml", ProtectedAreaDelimiter.HASH)
.protectedAreaDelimiter("vue", new ProtectedAreaDelimiter("<!--", "-->"))
.build();An id is unique per file (§7.2: "a unique identification"). Several cases are an ERROR:
- an id used twice in one file: the second area is not written;
- an id held twice by the existing file: that id is not merged;
- an empty or
invalidid: the area is not written.
An id containing ' is written escaped ('it\'s') and survives the round trip.
10.2 Dynamic IDs
Use OCL expressions for unique IDs per model element:
[for (c : EClassifier | p.eClassifiers)]
[protected (c.name)]
code for [c.name/]
[/protected]
[/for]Produces separate protected areas for ClasseA, ClasseB, etc.
10.3 Hash-Based Smart Merge (D31)
The protected area merger uses SHA-256 hashes to detect changes:
- First generation: Default content is written with its hash in the marker
- Regeneration (no user changes): The existing content hash matches the marker hash — content is regenerated fresh
- Regeneration (user edited): The existing content hash differs from the marker — user content is preserved
This approach is more robust than marker-only matching because it detects when the default content was changed by the user.
An area the existing file has but the templates no longer generate keeps its user code (§7.2: "preserved and not overwritten"). If the user changed it, it goes to M2tGenerationStrategy.lostContent(path, areas, charset) and is reported as a WARNING. FileSystemGenerationStrategy appends it to <file>.lost, as Acceleo does. An area left at its default goes without a word.
10.4 M2tGenerationStrategy for Protected Areas
Protected area merging requires reading existing file content. Implement readExistingContent() in your M2tGenerationStrategy:
@Override
public String readExistingContent(String filePath, Charset charset) {
Path path = outputDir.resolve(filePath);
if (Files.exists(path)) {
return Files.readString(path, charset);
}
return null; // no existing content — first generation
}When no M2tGenerationStrategy is configured (in-memory mode), protected areas use default content on every execution.
10.5 Disabling Protected Areas
Protected areas can be disabled entirely via protectedAreaEnabled(false). When disabled, [protected] blocks emit their body content without markers, and no merge with existing content is performed:
M2tConfiguration config = M2tConfiguration.builder(oclConfig)
.protectedAreaEnabled(false)
.build();[template public main(c : EClass)]
[file ('out.java', false)]
public class [c.name/] {
[protected ('fields')]
// custom fields
[/protected]
}
[/file]
[/template]With protectedAreaEnabled(false), the output contains no markers:
public class MyClass {
// custom fields
}With protectedAreaEnabled(true) (default), the output includes markers for merge:
public class MyClass {
// [protected 'fields' a1b2c3d4]
// custom fields
// [/protected]
}Use protectedAreaEnabled(false) when processing untrusted templates or when protected area merging is not needed.
11. Standard Library
MOFM2T defines 13 string operations (§8.3.1) in addition to the full OCL standard library. These are automatically available in all templates:
| Operation | Signature | Description | Example |
|---|---|---|---|
substitute | substitute(r, t) : String | Replace the first occurrence of r with t | 'aXbXc'.substitute('X', '-') = a-bXc |
index | index(r) : Integer | 1-based position of the first occurrence, -1 if none | 'test'.index('es') = 2 |
first | first(n) : String | First n characters | 'hello'.first(3) = hel |
last | last(n) : String | Last n characters | 'hello'.last(3) = llo |
strstr | strstr(r) : Boolean | Contains check | 'test'.strstr('es') = true |
strtok | strtok(s1, flag) : String | Next token; flag 0 starts, 1 continues, '' at the end | 'a,b'.strtok(',', 0) = a |
strcmp | strcmp(s1) : Integer | Lexicographic comparison, the sign of C's strcmp(self, s1) | 'b'.strcmp('a') > 0 |
isAlpha | isAlpha() : Boolean | All characters are letters ('' included) | 'abc'.isAlpha() = true |
isAlphanum | isAlphanum() : Boolean | All characters are letters or digits ('' included) | 'abc1'.isAlphanum() = true |
toUpper | toUpper() : String | Convert to uppercase | 'Hello'.toUpper() = HELLO |
toLower | toLower() : String | Convert to lowercase | 'Hello'.toLower() = hello |
toUpperFirst | toUpperFirst() : String | Capitalize first character | 'test'.toUpperFirst() = Test |
toLowerFirst | toLowerFirst() : String | Lowercase first character | 'TEST'.toLowerFirst() = tEST |
§8.3.2 and §8.3.3 add Integer::toString() and Real::toString(), which the OCL library provides ([42.toString()/]).
Notes on the deviations and decisions (#373):
substitute. It replaces the first occurrence, as the spec's singular "substring r" and Acceleo 3.7 read it. Every occurrence is replaced bysubstituteAll, an extension the OCL library provides, as it doestrim(also an extension, not MOFM2T). An empty search string makessubstituteinvalid.strcmp. It keeps the sign of C and Acceleo: positive whenselfis greater. The spec's sentence ("depending on whether s1 is lexicographically less than … self") reads the other way round; it is taken as an erratum.strtok. Flag1without a preceding0starts the tokenization, as in Acceleo. Any flag other than0or1is invalid.- Invalid calls. A
nullorinvalidsource or argument, and a negative length (first(-1)), make the callinvalidwith an ERROR such as "Null argument for operation 'first'". They no longer yield''or the text of a Java exception — the line of #306 for the predefined operations. - Case mapping does not depend on the platform's default locale: on a Turkish system
'i'.toUpper()isIas well.
All OCL String and Collection operations are also available:
[comment]OCL String operations[/comment]
['hello'.concat(' world')/] [comment]→ hello world[/comment]
['hello'.size()/] [comment]→ 5[/comment]
['hello'.substring(2, 4)/] [comment]→ ell[/comment]
[comment]OCL Collection operations[/comment]
[p.eClassifiers->size()/] [comment]→ 3[/comment]
[p.eClassifiers->first().name/] [comment]→ ClasseA[/comment]
[p.eClassifiers->exists(c | c.name = 'ClasseA')/] [comment]→ true[/comment]
[p.eClassifiers->select(c | c.name.startsWith('C'))->collect(c | c.name)/]12. Error Handling
12.1 Parse Errors
M2tParseException is a checked exception carrying one diagnostic per problem:
try {
engine.parse(source, "MyTemplate");
} catch (M2tParseException e) {
System.err.println(e.getMessage());
for (Resource.Diagnostic error : e.getErrors()) {
System.err.printf(" Line %d, Col %d: %s%n",
error.getLine(), error.getColumn(), error.getMessage());
}
}Positions are real for both kinds now (#110). A syntax error carries what ANTLR reported; an unresolved name, unknown type or unknown metamodel carries the place it stands. 0:0 still means "position unknown", which is what a diagnostic reported without any parse context gets.
12.2 Execution Diagnostics
M2tResult contains diagnostics collected during template execution:
M2tResult result = engine.execute(module, context);
if (!result.isSuccess()) {
for (Diagnostic d : result.diagnostics()) {
System.err.printf("[%s] %s%n",
d.getSeverity() == Diagnostic.ERROR ? "ERROR" : "WARN",
d.getMessage());
}
}12.3 Severity Levels
| Level | Value | Meaning |
|---|---|---|
Diagnostic.OK | 0 | Success |
Diagnostic.WARNING | 1 | Warning (generation completed) |
Diagnostic.ERROR | 2 | Error (generation may have partial output) |
12.4 Linking Warnings
The link() method returns warnings for unresolved cross-module references:
List<String> warnings = engine.link(baseMod, childMod);
if (!warnings.isEmpty()) {
warnings.forEach(w -> System.err.println("Link warning: " + w));
}Common causes: private templates accessed from another module, typos in template names, missing module in link() call.
13. Calling Operations from a Template
[book.displayLabel()/] resolves in this order: an EOperation of the model first, then the OCL standard library, then registered custom operations.
The first of those has a catch worth knowing before you design around it. A generated EMF model implements its operations in the generated Impl, so the call lands in your Java method. A dynamic model — an .ecore loaded at runtime — has no such code, and the call only works if the operation carries an OCL body annotation and the package declares the delegate; see OCL User Guide §9.4. Without that, generation reports:
Operation 'displayLabel' has no implementation. Either the model is generated and
implements it, or the operation carries an OCL body annotation and the package
declares the delegate (see installDelegates())For generator concerns — output paths, cross-references, diagrams — an EOperation is the wrong home anyway. Those belong to the generator, not to the metamodel, and are better registered as OCL custom operations (see OCL User Guide §11): the receiver is the real EObject, the implementation is an ordinary Java lambda, and the model stays free of documentation concerns.
13.1 Java from a template
A generator usually needs things a template language cannot express: an output path derived from the model, a link relative to the document that carries it, a diagram written to disk. There is no M2T-specific extension point for that — a Java method becomes callable by being registered as an OCL operation, and M2T picks it up:
OclConfiguration ocl = OclConfiguration.builder(new OclParserSupport())
.customOperationsEnabled(true) // required — see below
.addOperationProvider(new DocOperations(outputDirectory))
.build();
M2tEngine engine = M2tEngines.create(M2tConfiguration.builder(ocl)
.packageRegistry(registry)
.generationStrategy(new FileSystemGenerationStrategy(outputDirectory))
.build());[file (b.docPath(), false)]
# [b.title/]
- Slug: [b.title.kebab()/]
- Author: [b.author.mdLinkFrom(b)/]
[/file]customOperationsEnabled(true) on the OCL configuration is not optional. M2T sets the flag on the evaluation options it uses, but a provider registered on the configuration is active only when the flag is set in both places (D29 — custom operations are off by default, deliberately). Leave it out and the calls resolve to nothing: [file (b.docPath(), false)] evaluates to invalid, the block is skipped, and the run fails and says why — Unknown operation: docPath — because M2T forwards what the OCL engine reports. Before #114 it discarded those diagnostics and reported success over a broken document; before #270 the block went on and wrote a file literally named OclInvalid. See OCL User Guide §11 for how to write a provider, and register as many as you need.
The whole configuration above runs as M2tDocGenerationExampleTest in org.eclipse.fennec.m2x.m2t.tests — the model, the three operations, the template, and the files it writes. It is the executable version of this section, so the two cannot drift apart.
What the receiver is decides where an operation can be called, and that is the ownerType:
ownerType | callable on |
|---|---|
an EClass | instances of that class and its subclasses — [b.docPath()/] |
PrimitiveType("String") | attribute values — [b.title.kebab()/] |
PrimitiveType("Integer") / ("Real") / ("Boolean") | the matching Java type; an Integer widens to Real |
AnyType or PrimitiveType("OclAny") | anything — the catch-all |
EcorePackage.Literals.EOBJECT | every model element |
One method that makes sense for two receiver types is registered twice, once per type. The implementation is an ordinary lambda, so anything Java can reach is reachable — a static utility, a file written to disk, an SVG rendered by hand.
Four things that will bite:
A literal [ cannot be written | The lexer opens code mode on [ with no escape, so neither [text](url) nor  can be typed in template text. Let a Java operation return the finished link or image. A lone bracket: ['['/]. |
| Types must resolve | Without packageRegistry(…), a type name in the template degrades and oclIsKindOf stops discriminating — see §3.5. |
| An unset reference is your operation's problem | M2T evaluates leniently, so a call on null reaches your lambda with a null receiver instead of failing — guard there, as mdLinkFrom does. Navigating from an unset reference (b.author.name) answers null, which renders as nothing and is reported as a warning. Neither writes OclInvalid into the document — nothing does. A value that comes out invalid ('by ' + b.author.name with the name unset, say) leaves its spot empty and fails the run with an error at the expression's line, Null argument for operation '+' (#270). |
| An exception becomes a diagnostic | The evaluator catches RuntimeException, reports it and yields null. An empty document usually means an operation threw — check result.isSuccess() and result.diagnostics(). |
What a generation diagnostic looks like. A problem found while generating names the place it happened, so a log is enough to find it:
librarydoc:7:11 [template main → books/anonymous-work.md] Null source for property 'name' — evaluated as null (lenient)The unit comes first, then line and column of the expression inside it, then the template that was running and the document it was writing. The unit matters as soon as a module extends or imports another: several units contribute to one generation, and a bare line reads as the main file's. The template named is the one that was executing when the problem occurred, not the one the generation started from — so a problem inside an imported template names that template. The position also travels as SourcePosition in the diagnostic's data, for a consumer that would rather not read the text. Two cases have no position and say so by leaving it out: an expression the engine took from its cache — it belongs to a parse this run did not do — and an OCL expression that came from an EAnnotation, which has no template at all.
Operations of the same name are told apart by how many arguments the call passes, and then by receiver type. Among equally applicable candidates the first registered one answers, so registration order is what decides — not which provider it came from.
13.2 allInstances() and what it sees
allInstances() works in a template. What it answers with is the model the generation runs on:
| Situation | The extent |
|---|---|
self is a model element in a resource | that resource — every root in it and everything they contain |
self is a model element outside any resource | the containment tree of its root container |
self is not a model element (a template called with a String, for instance) | the input elements the generation was started with |
| no input elements and no model context | none — allInstances() reports a missing extent instead of answering emptily |
The first two rows are what Acceleo does, because both sit on Eclipse OCL's extent map. The third goes beyond it deliberately: Eclipse OCL hands out an empty extent there, so allInstances() would quietly answer 0 inside a template with a string parameter — the kind of wrong answer nobody notices. Subtypes count in every row, so Book.allInstances() includes the instances of Novel.
[template public toc(s : Shelf)]
[Book.allInstances()->size()/] books in this catalogue
[/template]Two things worth knowing: a model in another resource of the same ResourceSet is not in the extent — a generator generates from a model, not from everything that happens to be loaded — and the answer per class is collected once per model root and kept for the run, so asking inside a [for] loop costs one pass, not one per iteration. maxCollectionSize still caps the result.
14. Engine Lifetime and Module Retention
The engine remembers what it parsed: a module's parse result, whether it has been linked and normalized, and the indentation of its template invocations. That is what makes repeated executions cheap.
Those caches are keyed weakly, so a module is let go once you drop it. Garbage collection is a safety net, not a schedule — an engine that lives as long as the application and parses modules in a loop should say when it is done with one:
Module module = engine.parse(source, "report");
engine.link(module);
engine.execute(module, context);
engine.release(module); // this module is done
// engine.clearCaches(); // or: forget everythingAfter release, the engine no longer knows the module. Linking it again reports it as unknown until it is parsed again — link returns a warning per module it has no parse result for, instead of quietly reporting success.
15. M2tGenerationStrategy
M2tGenerationStrategy is the SPI for controlling where generated text goes. When no strategy is configured, the engine collects all output in memory (M2tResult.generatedFiles()).
15.1 Interface
public interface M2tGenerationStrategy {
// Create a writer for a file block
Writer createWriter(String filePath, OpenModeKind mode, Charset charset);
// Close a writer when the file block ends (default: writer.close())
default void closeWriter(String filePath, Writer writer) { ... }
// Read existing content for protected area merging (default: null)
default String readExistingContent(String filePath, Charset charset) { ... }
// User code of protected areas no longer generated (default: dropped; file system: <file>.lost)
default void lostContent(String filePath, Map<String, String> areas, Charset charset) { ... }
// The path a [file] unique id was written to before, and remembering the paths of a run
// (default: none; file system: .m2t-ids)
default String pathOfUniqueId(String uniqueId) { ... }
default void recordUniqueIds(Map<String, String> pathsByUniqueId) { ... }
}15.2 Writing to the File System
FileSystemGenerationStrategy ships with the engine — there is no need to write one:
import org.eclipse.fennec.m2x.m2t.engine.FileSystemGenerationStrategy;
M2tConfiguration config = M2tConfiguration.builder(oclConfig)
.generationStrategy(new FileSystemGenerationStrategy(Path.of("/tmp/output")))
.build();Every [file (...)] block of the run is written below that directory after generation:
- missing parent directories are created;
[file (path, false)]truncates an existing file,[file (path, true)]appends to it;- existing content is read back for protected area merging (§10), except for append blocks — an appended file is extended, not regenerated, so merging its own content back in would duplicate it;
- a path that would escape the output directory is rejected with a
SecurityException, including..segments, absolute paths and symlinks pointing outside (T-4); - if one file cannot be written, the failure becomes an error diagnostic and the remaining files are still written. Check
result.isSuccess().
Files are handed to the strategy after protected area merging, not streamed during evaluation: the merger needs the complete generated and the complete existing content before it can decide anything.
15.3 In-Memory Mode (Default)
When no strategy is configured, the engine uses an internal StringWriter for each file block. All generated content is available in the result:
M2tResult result = engine.execute(module, context);
Map<String, String> files = result.generatedFiles();
for (Map.Entry<String, String> entry : files.entrySet()) {
System.out.printf("File: %s (%d chars)%n", entry.getKey(), entry.getValue().length());
}15.4 Registration
Pass the strategy via M2tConfiguration:
M2tConfiguration config = M2tConfiguration.builder(oclConfig)
.generationStrategy(new FileSystemStrategy(Path.of("/tmp/output")))
.build();16. Whitespace Handling
MOFM2T §8.4 defines precise whitespace normalization rules. The engine supports three modes via WhitespaceMode:
16.1 Modes
| Mode | Description |
|---|---|
NONE | No normalization — raw template output as written |
SPEC | MOFM2T §8.4 strict mode with all spec rules including BOL ^ indicator |
ACCELEO | Acceleo 3.7 compatible mode (default). Like SPEC but without BOL ^ support |
16.2 §8.4 Rules (SPEC and ACCELEO modes)
The whitespace normalizer applies these rules:
- Body-trimming — The first and last newlines of template and multi-line block bodies are stripped
- Standalone block stripping — When a block tag (
[for],[if],[/for],[/if], etc.) is alone on a line, the whitespace in front of the tag is removed from the output. What happens to the newline that ends the tag line follows the shape of the block — see 16.2.2. A block that shares its head or tail line with other body elements is embedded and is output as is — see 16.2.3 - Default separator injection — Standalone multi-line
[for]blocks without an explicitseparator()get a"\n"separator. A single-line[for …]…[/for]alone on its line runs its iterations together ([for (c : EClass | p.eClassifiers)][i/][/for]produces123), exactly as Acceleo 3.7 does - BOL indicator (SPEC mode only) — The
^character at the beginning of a line resets indentation to column 0. It removes only the whitespace the template writes itself: an invoked template's^l2under a caller's propagated indentation keeps that indentation, as propagation belongs to the call site. In ACCELEO mode,^is passed through as literal text (Acceleo 3.7 does not implement this feature) - Indent propagation — When a template invocation stands on a line by itself and is indented, that indent is put in front of every line of the invoked template's output, the first line included
- Iterative invocation separator — A standalone template invocation whose argument is a collection separates the outputs of the iterations with
"\n"unless it names aseparator(), the same way a multi-line[for]does
16.2.2 The Newline After a Standalone Block
§8.4 defines the body of the two block shapes differently, and that decides what becomes of the newline ending the tail line:
| Shape | Body per §8.4 | Newline after the tail |
|---|---|---|
multi-line ([for] … [/for] on their own lines) | starts on the line after the head, ends excluding the newline in front of the tail | kept — the construct already spent a newline in front of its tail |
single-line ([if (…)]text[/if] on one line) | starts after the head's closing bracket, ends before the tail's opening bracket | kept when the block wrote text, removed when it wrote nothing |
The newline after a single-line block is a whitespace element of the enclosing body, which §8.4 outputs "as is". A\n [if (true)]X[/if]\nB\n [if (false)]Y[/if]\nC therefore produces A\nX\nB\nC: the X keeps its own line, and the line of the block that wrote nothing disappears instead of leaving an empty one, as in Acceleo's testingIf. This holds in both modes (#374).
A multi-line block leaves the line after it on its own line:
A
[for (a : EAttribute | c.eAttributes)]
[a.name/];
[/for]
Zproduces A\n name;\n salary;\nZ — the [for] and [/for] tag lines are gone, every line the body wrote keeps its newline. Every branch of an [if] behaves the same way, whether the if, an elseif or the else is taken.
16.2.3 Embedded Blocks
A block is standalone only when nothing else stands on the lines of its head and its tail. Blocks nest to any depth, and an inner block whose head follows the enclosing block's head on the same line is embedded — §8.4 says its text is "output as is", so it gets neither the stripping nor the default separator:
<[if (p.name = 'company')][for (c : EClass | p.eClassifiers)][c.name/] [/for][/if]>produces <Employee Manager Marker >. Being the first or last element of the enclosing body does not make a block standalone; what counts is whether the enclosing block is multi-line, so that the inner block's line is its own (#272).
16.2.1 SPEC vs ACCELEO Differences
| Feature | SPEC | ACCELEO |
|---|---|---|
| Body-trimming | Yes | Yes |
| Standalone block stripping | Yes | Yes |
| Default separator injection | Yes | Yes |
BOL indicator ^ | Yes — strips leading whitespace | No — literal passthrough |
| Indent propagation | Yes | Yes |
A template saved with CRLF line ends is read as if it had \n ones (line and column numbers unchanged), so no stray \r remains where a rule takes a newline away; the output uses \n.
The §8.2 keywords guard, init, each and mode have no tokens: a guard is written ? (…), an init section { … }, the separator separator (…), and only text-explicit mode exists (§4.12).
16.3 Configuration
import org.eclipse.fennec.m2x.m2t.api.WhitespaceMode;
M2tConfiguration config = M2tConfiguration.builder(oclConfig)
.whitespaceMode(WhitespaceMode.SPEC)
.build();16.4 Example: Standalone Block Stripping
Given this template:
[template public main(c : EClass)]
[file ('out', false)]
class [c.name/] {
[for (a : EAttribute | c.eAttributes)]
[a.eType.name/] [a.name/];
[/for]
}
[/file]
[/template]The [for ...] and [/for] lines are standalone (only whitespace + block tag). They are stripped entirely, producing clean output without extra blank lines:
class Employee {
EString name;
EDouble salary;
}17. Generating from a bnd Build
The engine is also available as a bnd external plugin, fennecM2T, so a bnd project can run templates as a build step instead of from Java — configured entirely through a -generate instruction, with no Java in the generating project.
That has its own guide: M2T bnd Generator Guide — dependencies, the instruction and its attributes, how metamodels are registered under their namespace URI, and how the models are bound to main.
18. Deviations from MOFM2T 1.0
Where the engine does something other than, or more than, the specification says — each a decision, made in the spec audit of 2026-10 (#363–#376):
| Topic | Specification | Fennec M2T |
|---|---|---|
| Escape direction (§7.1, §8.2) | text-explicit and code-explicit mode | text-explicit only; @code-explicit is a parse error (§4.12) |
| Trace block (§7.2, §8.1.18) | text is traced to model elements and templates | the body is written and the model element evaluated; no trace records (§4.11) |
| Unique names (§8.1.2) | a module element has a unique name | same-named templates with different parameter types are kept and dispatched by argument type, as in Acceleo (§5.5) |
| Visibility (§8.2) | public or private, required | also protected (Acceleo, §7 prose); a missing one is public with a WARNING (§4.2) |
| Entry point (§7) | no main template; any public template is started by name | execute(module, name, context), and execute(module, context) for @main/main (§8.5.1) |
| Comments (§7.4, §8.2) | [comment()]…[/comment] | also [comment]…[/comment] and [comment … /] (Acceleo); // is text in text-explicit mode (§4.10) |
| Keywords (§8.2) | guard, init, each, mode | ? (…), { … }, separator (…); no mode (§16) |
post | — | Acceleo extension (§9.4) |
[file] charset | — | optional fourth argument (§4.7) |
| Protected-area delimiters (§7.2) | left to the tool | per file extension, configurable (§10.1) |
substitute (§8.3.1) | "substring r" | the first occurrence; substituteAll for all (§11) |
strcmp sign (§8.3.1) | the sentence reads s1 < self ⇒ negative | the sign of C's strcmp(self, s1), as Acceleo (§11) |
| Whitespace (§8.4) | the newline after a single-line stand-alone block is output | kept when the block wrote text, dropped when it wrote nothing (§16.2.2); ACCELEO mode passes ^ through |
