The objective of this step is the following:

  • Use a classical meta-compiler (ANTLER) to define a grammar and implement a lexer/parser for our language;
  • Leverage the Listener pattern to build a model from a given word

Guess what? The LED example is back!

For this step, we model the LED example using a textual syntax for the FSM

application led {

actuator led: 13

-> off {
led is LOW
goto on
}

on {
led is HIGH
goto off
}
}

The language is self-explainable: 

  • We declare a LED on pin #13
  • We declare two states, on and off.
  • Off is an initial state (incoming arrow)
  • the goto keyword indicates which state is the next one.

Modelling the grammar

ANTLR expects grammars to be defined in eBNF-like format, in a g4 file

app         :   'application' name=IDENTIFIER '{' actuator+ state+ '}';
actuator : 'actuator' location ;
location : id=IDENTIFIER ':' port=PORT_NUMBER;
state : initial? name=IDENTIFIER '{' action+ next '}';
action : receiver=IDENTIFIER 'is' value=SIGNAL;
next : 'goto' target=IDENTIFIER ;
initial : '->';

Instantiating models

When using maven to build the code, the antlr compiler is called on this grammar file, and generate the associated lexer and parser in target/generated-sources/antlr4. If you are using an IDE, do not forget to include this directory in your classpath.

The ANTLR tool supports two patterns to interact with the DSL code: a classical visitor or the listener pattern. As the visitor was previously described, we use here the listener one. By extending ArduinoMLBaseListener, one can implement a listener for a word conform to the ArduinoML grammar. For each rule R declared in the grammar, the listener provides two methods:

  • enterR: triggered each time the system enters in the R rule;
  • exitR: triggered each time the system enters in the R rule.

Using this pattern, we override several methods to implement a listener named ModelBuilder that build instances of the previously defined meta-model (step 4). We rely on shared variables (e.g., a symbol table for states, the current application to build, the current state) to share information between method calls.

private App theApp = null;
private Map<String, State> states = new HashMap<>();
private State currentState = null;

// …

@Override public void enterState(ArduinoMLParser.StateContext ctx) {
    State local = new State();
    local.setName(ctx.name.getText());
    this.currentState = local;
    this.states.put(local.getName(), local); // Symbol table for states
}

@Override public void exitState(ArduinoMLParser.StateContext ctx) {
    this.theApp.getStates().add(this.currentState);
    this.currentState = null;
}

//…

Calling the compiler

To call the compiler (see the Main class), we bind the lexer and parser together and associate our ModelBuilder as a parse tree walker.

private static App buildModel(CharStream stream) {
    ArduinoMLLexer lexer = new ArduinoMLLexer(stream);
    lexer.removeErrorListeners();
    lexer.addErrorListener(new StopErrorListener());

    ArduinoMLParser parser = new ArduinoMLParser(new CommonTokenStream(lexer));
    parser.removeErrorListeners();
    parser.addErrorListener(new StopErrorListener());

    ParseTreeWalker walker = new ParseTreeWalker();
    ModelBuilder builder = new ModelBuilder();

    // parser.app() is the entry point of the grammar
    walker.walk(builder, parser.app());
return builder.retrieve();
}


Expected Work

  • Adapt the language to support sensors and transitions associated to sensors;
  • Identify the abstractions needed in the language to support the 7-segment display;
  • Adapt the language to support it.

Stepback Questions

  • Who is the intended user ? What about the tooling associated with the language?
  • More generally, what is the cost of such an approach?
  • To what extent is the language fragile to the introduction of new features?
  • What is the relationship between the meta-model and the grammar?
  • How to validate that the defined syntax is the right one?