Ran Wei/MDE Series
中文
Model-Driven Engineering with Eclipse Epsilon — Ran Wei

MDE meets LLMs

Let a language model draft metamodels and programs, check the drafts with Epsilon, and generate prompts from models.

3 hours3 sessions8 runnable examplesEpsilon Playground

By the end you can

  • Identify where an LLM draft needs formal checking.
  • Run Epsilon checks on illustrative metamodel and model drafts.
  • Generate a prompt from a model without making a live LLM call.

Before you start

Modules 1–7 are recommended. Examples run in the Playground unless noted.

Download all examples of this module (zip)

Contents

Study plan

3 hours

Times are estimates and include running the examples. Progress is saved in this browser and shared by both language editions.

Session 160 min
Draft and review a metamodel
Session 260 min
Check model drafts and claims
Session 360 min
Apply and practise
1

LLMs as drafting assistants

A language model can suggest a metamodel, an example model or an Epsilon program. Its answer is a draft: the text may parse yet express the wrong domain rule, and a plausible explanation may disagree with the model. Use the metamodel to define structure, EVL to state constraints, and runnable examples to test the result. An LLM can help propose alternatives; evidence from the model decides which alternative works.

Adapted from Dimitris Kolovos, Metamodelling with ChatGPT, Episode 2. In that experiment, an example of Emfatic helped the language model produce a syntactically usable draft; later feedback improved a generated EGL template. Our drafts below are illustrations written for this tutorial, not recorded outputs from that experiment or a live LLM. No example calls an LLM service.

2

Review a metamodel draft

The first illustrative draft widens Connector.source and Connector.target to the abstract Port type. It therefore accepts an input port as a source and an output port as a target. The EOL query runs successfully and prints a reversed connection. A successful parse and run show that the files are usable by Epsilon; they do not prove that the intended wiring rule holds.

ExampleAn illustrative draft admits reversed wiringOpen in Playground ↗
inspect.eol
for (c in Connector.all) {
  (c.source.eContainer().name + "." + c.source.name + " -> " + c.target.eContainer().name + "." + c.target.name).println();
}
More files: alarm.flexmi, components.emf
alarm.flexmi
<?nsuri components?>
<architecture name="Alarm">
  <component name="OrGate">
    <inPort name="door" type="DIGITAL"/>
    <inPort name="window" type="DIGITAL"/>
    <outPort name="open" type="DIGITAL"/>
  </component>
  <component name="AndGate">
    <inPort name="open" type="DIGITAL"/>
    <inPort name="armed" type="DIGITAL"/>
    <outPort name="sound" type="DIGITAL"/>
  </component>
  <component name="Siren">
    <inPort name="sound" type="DIGITAL"/>
  </component>
  <connector source="Alarm.AndGate.open" target="Alarm.OrGate.open"/>
  <connector source="Alarm.AndGate.sound" target="Alarm.Siren.sound"/>
</architecture>
components.emf
@namespace(uri="components", prefix="components")
package components;

enum PortType {
  DIGITAL;
  ANALOG;
}

class Architecture {
  attr String[1] name;
  val Component[*]#architecture components;
  val Connector[*] connectors;
}

class Component {
  attr String[1] name;
  ref Architecture#components architecture;
  val Port[*] ports;
}

abstract class Port {
  attr String[1] name;
  attr PortType type;
}
class InPort extends Port {}
class OutPort extends Port {}

class Connector {
  ref Port source;
  ref Port target;
}
Output captured from Epsilon 2.8.0 when this page was built. The Playground may word messages differently and does not show warnings about the model.
Output
AndGate.open -> OrGate.open
AndGate.sound -> Siren.sound

Ask a drafting assistant for the classes, containment, multiplicities, endpoint direction and one valid and one invalid example. Review each answer against domain requirements. In particular, check val versus ref: connectors refer to ports owned by components; they do not contain new copies of those ports.

3

Check a model draft

EVL can make the direction rule executable. The next check rejects the reversed connector even though the broad draft metamodel accepts it. In the Playground, EVL findings appear as notes on the model diagram; the captured result below records the same validation run.

ExampleValidate the illustrative draftOpen in Playground ↗
rules.evl
context Connector {
  constraint OutputToInput {
    check: self.source.isTypeOf(OutPort) and self.target.isTypeOf(InPort)
    message: "Connector endpoints must run from OutPort to InPort"
  }
}
More files: alarm.flexmi, components.emf
alarm.flexmi
<?nsuri components?>
<architecture name="Alarm">
  <component name="OrGate">
    <inPort name="door" type="DIGITAL"/>
    <inPort name="window" type="DIGITAL"/>
    <outPort name="open" type="DIGITAL"/>
  </component>
  <component name="AndGate">
    <inPort name="open" type="DIGITAL"/>
    <inPort name="armed" type="DIGITAL"/>
    <outPort name="sound" type="DIGITAL"/>
  </component>
  <component name="Siren">
    <inPort name="sound" type="DIGITAL"/>
  </component>
  <connector source="Alarm.AndGate.open" target="Alarm.OrGate.open"/>
  <connector source="Alarm.AndGate.sound" target="Alarm.Siren.sound"/>
</architecture>
components.emf
@namespace(uri="components", prefix="components")
package components;

enum PortType {
  DIGITAL;
  ANALOG;
}

class Architecture {
  attr String[1] name;
  val Component[*]#architecture components;
  val Connector[*] connectors;
}

class Component {
  attr String[1] name;
  ref Architecture#components architecture;
  val Port[*] ports;
}

abstract class Port {
  attr String[1] name;
  attr PortType type;
}
class InPort extends Port {}
class OutPort extends Port {}

class Connector {
  ref Port source;
  ref Port target;
}
Output captured from Epsilon 2.8.0 when this page was built. The Playground may word messages differently and does not show warnings about the model.
Output
Error [OutputToInput]: Connector endpoints must run from OutPort to InPort

The corrected metamodel narrows the source to OutPort and target to InPort. The corrected model also points from OrGate.open to AndGate.open. The same EVL check now passes.

ExampleCorrect the draft and check againOpen in Playground ↗
rules.evl
context Connector {
  constraint OutputToInput {
    check: self.source.isTypeOf(OutPort) and self.target.isTypeOf(InPort)
    message: "Connector endpoints must run from OutPort to InPort"
  }
}
More files: alarm.flexmi, components.emf
alarm.flexmi
<?nsuri components?>
<architecture name="Alarm">
  <component name="OrGate">
    <inPort name="door" type="DIGITAL"/>
    <inPort name="window" type="DIGITAL"/>
    <outPort name="open" type="DIGITAL"/>
  </component>
  <component name="AndGate">
    <inPort name="open" type="DIGITAL"/>
    <inPort name="armed" type="DIGITAL"/>
    <outPort name="sound" type="DIGITAL"/>
  </component>
  <component name="Siren">
    <inPort name="sound" type="DIGITAL"/>
  </component>
  <connector source="Alarm.OrGate.open" target="Alarm.AndGate.open"/>
  <connector source="Alarm.AndGate.sound" target="Alarm.Siren.sound"/>
</architecture>
components.emf
@namespace(uri="components", prefix="components")
package components;

enum PortType {
  DIGITAL;
  ANALOG;
}

class Architecture {
  attr String[1] name;
  val Component[*]#architecture components;
  val Connector[*] connectors;
}

class Component {
  attr String[1] name;
  ref Architecture#components architecture;
  val Port[*] ports;
}

abstract class Port {
  attr String[1] name;
  attr PortType type;
}
class InPort extends Port {}
class OutPort extends Port {}

class Connector {
  ref OutPort source;
  ref InPort target;
}
Output captured from Epsilon 2.8.0 when this page was built. The Playground may word messages differently and does not show warnings about the model.
Output
All constraints are satisfied.

These are two kinds of evidence. The metamodel restricts reference types; EVL checks a rule that can also be applied to the broad draft. A more realistic review would add uniqueness, signal-type compatibility and connection completeness checks from Module 5.

4

Validate generated claims

An assistant might describe the alarm as having three connectors. The model has two. This illustrative claim becomes a constraint so the disagreement is observable rather than a matter of confidence in prose.

ExampleTest a draft claim about the modelOpen in Playground ↗
rules.evl
context Architecture {
  constraint ThreeConnectorsClaim {
    check: self.connectors.size() = 3
    message: "Draft claim said three connectors; model has " + self.connectors.size()
  }
}
More files: alarm.flexmi, components.emf
alarm.flexmi
<?nsuri components?>
<architecture name="Alarm">
  <component name="OrGate">
    <inPort name="door" type="DIGITAL"/>
    <inPort name="window" type="DIGITAL"/>
    <outPort name="open" type="DIGITAL"/>
  </component>
  <component name="AndGate">
    <inPort name="open" type="DIGITAL"/>
    <inPort name="armed" type="DIGITAL"/>
    <outPort name="sound" type="DIGITAL"/>
  </component>
  <component name="Siren">
    <inPort name="sound" type="DIGITAL"/>
  </component>
  <connector source="Alarm.OrGate.open" target="Alarm.AndGate.open"/>
  <connector source="Alarm.AndGate.sound" target="Alarm.Siren.sound"/>
</architecture>
components.emf
@namespace(uri="components", prefix="components")
package components;

enum PortType {
  DIGITAL;
  ANALOG;
}

class Architecture {
  attr String[1] name;
  val Component[*]#architecture components;
  val Connector[*] connectors;
}

class Component {
  attr String[1] name;
  ref Architecture#components architecture;
  val Port[*] ports;
}

abstract class Port {
  attr String[1] name;
  attr PortType type;
}
class InPort extends Port {}
class OutPort extends Port {}

class Connector {
  ref OutPort source;
  ref InPort target;
}
Output captured from Epsilon 2.8.0 when this page was built. The Playground may word messages differently and does not show warnings about the model.
Output
Error [ThreeConnectorsClaim]: Draft claim said three connectors; model has 2

Translate important claims into tests or queries: element counts, type membership, reachability, uniqueness, and generated-file contents. Keep the claim close to its check. A passing check supports only the property it tests; it is not a general proof that the entire design is correct.

5

Generate prompts from models

The flow can also run from a model to a prompt. EGX makes one review prompt per component, and EGL fills in the actual port names. These are generated text files for a person or a later tool to use; running this example does not send them anywhere.

ExampleGenerate review prompts from componentsOpen in Playground ↗
generate.egx
rule Component2Prompt
  transform c : Component {
  template: "template.egl"
  target: c.name + "-review.txt"
}
template.egl
[%
var prompt = "Review component " + c.name + ".\nInputs:";
for (p in c.ports.select(p | p.isTypeOf(InPort))) { prompt += " " + p.name; }
prompt += "\nOutputs:";
for (p in c.ports.select(p | p.isTypeOf(OutPort))) { prompt += " " + p.name; }
prompt += "\nCheck missing inputs and type mismatches against the model; do not invent ports.\n";
%][%=prompt%]
More files: alarm.flexmi, components.emf
alarm.flexmi
<?nsuri components?>
<architecture name="Alarm">
  <component name="OrGate">
    <inPort name="door" type="DIGITAL"/>
    <inPort name="window" type="DIGITAL"/>
    <outPort name="open" type="DIGITAL"/>
  </component>
  <component name="AndGate">
    <inPort name="open" type="DIGITAL"/>
    <inPort name="armed" type="DIGITAL"/>
    <outPort name="sound" type="DIGITAL"/>
  </component>
  <component name="Siren">
    <inPort name="sound" type="DIGITAL"/>
  </component>
  <connector source="Alarm.OrGate.open" target="Alarm.AndGate.open"/>
  <connector source="Alarm.AndGate.sound" target="Alarm.Siren.sound"/>
</architecture>
components.emf
@namespace(uri="components", prefix="components")
package components;

enum PortType {
  DIGITAL;
  ANALOG;
}

class Architecture {
  attr String[1] name;
  val Component[*]#architecture components;
  val Connector[*] connectors;
}

class Component {
  attr String[1] name;
  ref Architecture#components architecture;
  val Port[*] ports;
}

abstract class Port {
  attr String[1] name;
  attr PortType type;
}
class InPort extends Port {}
class OutPort extends Port {}

class Connector {
  ref OutPort source;
  ref InPort target;
}
Output captured from Epsilon 2.8.0 when this page was built. The Playground may word messages differently and does not show warnings about the model.
Output
--- AndGate-review.txt ---
Review component AndGate.
Inputs: open armed
Outputs: sound
Check missing inputs and type mismatches against the model; do not invent ports.
--- OrGate-review.txt ---
Review component OrGate.
Inputs: door window
Outputs: open
Check missing inputs and type mismatches against the model; do not invent ports.
--- Siren-review.txt ---
Review component Siren.
Inputs: sound
Outputs:
Check missing inputs and type mismatches against the model; do not invent ports.

The prompt asks the reviewer to avoid inventing ports and to compare the answer with the source model. If an LLM later produces an answer, save the answer as a separate draft and run the appropriate Epsilon checks again. Do not silently copy a response into a trusted model.

6

Use an evidence loop

Start with a domain requirement, request or write a draft, run a parser and checks, inspect their output, then revise the draft. Record both the input and the exact check result. For generated programs, run them on at least a normal, an edge and an invalid model. For generated text, inspect the emitted files as well as whether the generator completed.

The tools test different things: Emfatic checks metamodel syntax, Flexmi loads instances, EVL checks domain constraints, ETL checks a mapping through its target, and EGL/EGX exposes generated text. Human review remains necessary for requirements that have not yet been formalised.

7

Exercises

Exercise 1★★★conceptual5 min

Find the gap. Why can the first draft run even though its first connector is reversed?

Show solution

Both endpoint references are typed as the broad Port class, so either concrete port subtype is accepted. Successful execution does not imply that the intended output-to-input rule holds.

Exercise 2★★★conceptual5 min

Separate evidence. What does a successful Emfatic parse establish? What does the EVL direction constraint establish?

Show solution

Parsing establishes that the metamodel syntax can be processed. The EVL constraint checks the direction of each connector in the loaded model. Neither alone establishes all domain requirements.

Exercise 3★★★conceptual5 min

Review a claim. A generated paragraph says there are three components and three connectors. Which part is true for the corrected alarm?

Show solution

There are three components, OrGate, AndGate and Siren, but only two connectors. Check counts against the model, not the paragraph.

Exercise 4★★★coding10 min

Check the draft’s direction. Add an EVL constraint to reject any connector whose source is not an OutPort or target is not an InPort.

Show solution

The illustrative reversed draft fails the constraint:

SolutionCheck endpoint direction in the draftOpen in Playground ↗
rules.evl
context Connector {
  constraint Directed {
    check: self.source.isTypeOf(OutPort) and self.target.isTypeOf(InPort)
    message: "Reversed or incorrectly typed connector"
  }
}
More files: alarm.flexmi, components.emf
alarm.flexmi
<?nsuri components?>
<architecture name="Alarm">
  <component name="OrGate">
    <inPort name="door" type="DIGITAL"/>
    <inPort name="window" type="DIGITAL"/>
    <outPort name="open" type="DIGITAL"/>
  </component>
  <component name="AndGate">
    <inPort name="open" type="DIGITAL"/>
    <inPort name="armed" type="DIGITAL"/>
    <outPort name="sound" type="DIGITAL"/>
  </component>
  <component name="Siren">
    <inPort name="sound" type="DIGITAL"/>
  </component>
  <connector source="Alarm.AndGate.open" target="Alarm.OrGate.open"/>
  <connector source="Alarm.AndGate.sound" target="Alarm.Siren.sound"/>
</architecture>
components.emf
@namespace(uri="components", prefix="components")
package components;

enum PortType {
  DIGITAL;
  ANALOG;
}

class Architecture {
  attr String[1] name;
  val Component[*]#architecture components;
  val Connector[*] connectors;
}

class Component {
  attr String[1] name;
  ref Architecture#components architecture;
  val Port[*] ports;
}

abstract class Port {
  attr String[1] name;
  attr PortType type;
}
class InPort extends Port {}
class OutPort extends Port {}

class Connector {
  ref Port source;
  ref Port target;
}
Output captured from Epsilon 2.8.0 when this page was built. The Playground may word messages differently and does not show warnings about the model.
Output
Error [Directed]: Reversed or incorrectly typed connector
Exercise 5★★★coding10 min

Verify a count. Replace the false three-connector claim with a check that the corrected alarm has exactly two connectors.

Show solution

The corrected model passes:

SolutionVerify a two-connector claimOpen in Playground ↗
rules.evl
context Architecture {
  constraint TwoConnectors {
    check: self.connectors.size() = 2
    message: "Expected two connectors"
  }
}
More files: alarm.flexmi, components.emf
alarm.flexmi
<?nsuri components?>
<architecture name="Alarm">
  <component name="OrGate">
    <inPort name="door" type="DIGITAL"/>
    <inPort name="window" type="DIGITAL"/>
    <outPort name="open" type="DIGITAL"/>
  </component>
  <component name="AndGate">
    <inPort name="open" type="DIGITAL"/>
    <inPort name="armed" type="DIGITAL"/>
    <outPort name="sound" type="DIGITAL"/>
  </component>
  <component name="Siren">
    <inPort name="sound" type="DIGITAL"/>
  </component>
  <connector source="Alarm.OrGate.open" target="Alarm.AndGate.open"/>
  <connector source="Alarm.AndGate.sound" target="Alarm.Siren.sound"/>
</architecture>
components.emf
@namespace(uri="components", prefix="components")
package components;

enum PortType {
  DIGITAL;
  ANALOG;
}

class Architecture {
  attr String[1] name;
  val Component[*]#architecture components;
  val Connector[*] connectors;
}

class Component {
  attr String[1] name;
  ref Architecture#components architecture;
  val Port[*] ports;
}

abstract class Port {
  attr String[1] name;
  attr PortType type;
}
class InPort extends Port {}
class OutPort extends Port {}

class Connector {
  ref OutPort source;
  ref InPort target;
}
Output captured from Epsilon 2.8.0 when this page was built. The Playground may word messages differently and does not show warnings about the model.
Output
All constraints are satisfied.
Exercise 6★★★coding10 min

Improve a prompt. Add each port’s signal type and concrete port class to the generated review prompt.

Show solution

The template iterates the model’s ports and emits their declared values:

SolutionGenerate a type-aware review promptOpen in Playground ↗
generate.egx
rule Component2Prompt
  transform c : Component {
  template: "template.egl"
  target: c.name + "-typed-review.txt"
}
template.egl
Component [%=c.name%]
[% for (p in c.ports) { %][%=p.name%]: [%=p.type%] ([%=p.eClass().name%])
[% } %]Verify each connection against these declared ports. Report uncertainty explicitly.
More files: alarm.flexmi, components.emf
alarm.flexmi
<?nsuri components?>
<architecture name="Alarm">
  <component name="OrGate">
    <inPort name="door" type="DIGITAL"/>
    <inPort name="window" type="DIGITAL"/>
    <outPort name="open" type="DIGITAL"/>
  </component>
  <component name="AndGate">
    <inPort name="open" type="DIGITAL"/>
    <inPort name="armed" type="DIGITAL"/>
    <outPort name="sound" type="DIGITAL"/>
  </component>
  <component name="Siren">
    <inPort name="sound" type="DIGITAL"/>
  </component>
  <connector source="Alarm.OrGate.open" target="Alarm.AndGate.open"/>
  <connector source="Alarm.AndGate.sound" target="Alarm.Siren.sound"/>
</architecture>
components.emf
@namespace(uri="components", prefix="components")
package components;

enum PortType {
  DIGITAL;
  ANALOG;
}

class Architecture {
  attr String[1] name;
  val Component[*]#architecture components;
  val Connector[*] connectors;
}

class Component {
  attr String[1] name;
  ref Architecture#components architecture;
  val Port[*] ports;
}

abstract class Port {
  attr String[1] name;
  attr PortType type;
}
class InPort extends Port {}
class OutPort extends Port {}

class Connector {
  ref OutPort source;
  ref InPort target;
}
Output captured from Epsilon 2.8.0 when this page was built. The Playground may word messages differently and does not show warnings about the model.
Output
--- AndGate-typed-review.txt ---
Component AndGate
open: DIGITAL (InPort)
armed: DIGITAL (InPort)
sound: DIGITAL (OutPort)
Verify each connection against these declared ports. Report uncertainty explicitly.
--- OrGate-typed-review.txt ---
Component OrGate
door: DIGITAL (InPort)
window: DIGITAL (InPort)
open: DIGITAL (OutPort)
Verify each connection against these declared ports. Report uncertainty explicitly.
--- Siren-typed-review.txt ---
Component Siren
sound: DIGITAL (InPort)
Verify each connection against these declared ports. Report uncertainty explicitly.
8

Self-check quiz

1
How should an LLM-suggested metamodel first be treated?
2
What does the broad Port endpoint type allow?
3
Which example exposes the reversed connection as a rule violation?
4
How many connectors are in the corrected alarm model?
5
What does the EGX example do?
6
What should happen after receiving a later LLM response?
7
What does a passing constraint prove?
9

Further reading