Статьи

Создание внешних DSL с использованием ANTLR и Java

В моем предыдущем посте довольно давно я писал о внутренних DSL с использованием Java. В книге Мартина Фаулера « Специфичные для предметной области языки » он обсуждает другой тип DSL, называемый внешними DSL, в котором DSL написан на другом языке, который затем анализируется основным языком для заполнения семантической модели.

В предыдущем примере я обсуждал создание DSL для определения графа. Преимущество использования внешнего dsl состоит в том, что любое изменение данных графа не требует перекомпиляции программы, вместо этого программа может просто загрузить внешний dsl, создать дерево разбора и затем заполнить семантическую модель. Семантическая модель останется прежней, и преимущество использования семантической модели состоит в том, что можно вносить изменения в DSL, не внося значительных изменений в семантическую модель. В примере между внутренними DSL и внешними DSL я не модифицировал семантическую модель. Для создания внешнего DSL я использую ANTLR .

Что такое ANTLR?

Определение, данное на официальном сайте :

ANTLR (ANother Tool for Language Recognition) — мощный генератор синтаксического анализа для чтения, обработки, выполнения или перевода структурированного текста или двоичных файлов. Он широко используется для создания языков, инструментов и сред. Из грамматики ANTLR генерирует синтаксический анализатор, который может создавать и анализировать деревья разбора.

Примечательные особенности ANTLR из приведенного выше определения:

  • генератор синтаксических анализаторов для структурированного текста или двоичных файлов
  • умеет строить и гулять разбирать деревья

Семантическая модель

В этом примере я буду использовать вышеупомянутые функции ANTLR для разбора DSL, а затем пройдусь по дереву разбора, чтобы заполнить семантическую модель. Напомним, что семантическая модель состоит из классов Graph , Edge и Vertex которые представляют собой граф и Edge и вершину графа соответственно. В приведенном ниже коде показаны определения классов:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
public class Graph {
 
  private List<Edge> edges;
  private Set<Vertex> vertices;
 
  public Graph() {
    edges = new ArrayList<>();
    vertices = new TreeSet<>();
  }
  public void addEdge(Edge edge){
    getEdges().add(edge);
    getVertices().add(edge.getFromVertex());
    getVertices().add(edge.getToVertex());
  }
 
  public void addVertice(Vertex v){
    getVertices().add(v);
  }
 
  public List<Edge> getEdges() {
    return edges;
  }
 
  public Set<Vertex> getVertices() {
    return vertices;
  }
 
  public static void printGraph(Graph g){
    System.out.println("Vertices...");
    for (Vertex v : g.getVertices()) {
      System.out.print(v.getLabel() + " ");
    }
    System.out.println("");
    System.out.println("Edges...");
    for (Edge e : g.getEdges()) {
      System.out.println(e);
    }
  }
 
}
 
public class Edge {
 
  private Vertex fromVertex;
  private Vertex toVertex;
  private Double weight;
 
  public Edge() {
  }
 
  public Edge(Vertex fromVertex,
          Vertex toVertex,
          Double weight) {
    this.fromVertex = fromVertex;
    this.toVertex = toVertex;
    this.weight = weight;
  }
 
  @Override
  public String toString() {
    return fromVertex.getLabel() +
            " to " + toVertex.getLabel() +
            " with weight " + getWeight();
  }
 
  public Vertex getFromVertex() {
    return fromVertex;
  }
 
  public void setFromVertex(Vertex fromVertex) {
    this.fromVertex = fromVertex;
  }
 
  public Vertex getToVertex() {
    return toVertex;
  }
 
  public void setToVertex(Vertex toVertex) {
    this.toVertex = toVertex;
  }
 
  public Double getWeight() {
    return weight;
  }
 
  public void setWeight(Double weight) {
    this.weight = weight;
  }
}
 
public class Vertex implements Comparable<Vertex> {
  private String label;
 
  public Vertex(String label) {
    this.label = label.toUpperCase();
  }
 
  @Override
  public int compareTo(Vertex o) {
    return (this.getLabel().compareTo(o.getLabel()));
  }
 
  public String getLabel() {
    return label;
  }
 
  public void setLabel(String label) {
    this.label = label;
  }
}

Создание DSL

Давайте разберемся со структурой языка, прежде чем приступить к созданию правил грамматики. Структура, которую я планирую придумать, выглядит примерно так:

1
2
3
4
5
Graph {
  A -> B (10)
  B -> C (20)
  D -> E (30)
}

Каждая строка в блоке График представляет ребро, а вершины, участвующие в ребре, а значение в фигурных скобках представляет вес ребра. Одно ограничение, которое я навязываю, состоит в том, что граф не может иметь висячие вершины, то есть вершины, которые не являются частью какого-либо ребра. Это ограничение можно снять, немного изменив грамматику, но я бы оставил это в качестве упражнения для читателей этого поста.

Первая задача при создании DSL — определить правила грамматики. Это правила, которые ваш лексер и парсер будут использовать для преобразования DSL в дерево абстрактного синтаксиса / дерево разбора .

Затем ANTLR использует эту грамматику для генерации Parser, Lexer и Listener, которые являются ничем иным, как Java-классами, расширяющими / реализующими некоторые классы из библиотеки ANTLR. Создатели DSL должны использовать эти Java-классы для загрузки внешнего DSL, его синтаксического анализа и последующего использования слушателя для заполнения семантической модели, когда парсер встречает определенные узлы (представьте, что это вариант SAX-парсера для XML ).

Теперь, когда мы очень кратко знаем, что может делать ANTLR и как использовать ANTLR, нам нужно будет настроить ANTLR, то есть загрузить jar API ANTLR и настроить некоторые сценарии для генерации синтаксического анализатора и лексера, а затем опробовать язык через командную строку. инструмент. Для этого, пожалуйста, посетите этот официальный учебник ANTLR, который показывает, как настроить ANTLR, и простой пример Hello World.

Грамматика для DSL

Теперь, когда у вас есть настройка ANTLR, позвольте мне погрузиться в грамматику моего DSL:

1
2
3
4
5
6
7
grammar Graph;
graph: 'Graph {' edge+ '}';
vertex: ID;
edge: vertex '->' vertex '(' NUM ')' ;
ID: [a-zA-Z]+;
NUM: [0-9]+;
WS: [ \t\r\n]+ -> skip;

Давайте пройдемся по этим правилам:

1
graph: 'Graph {' edge+ '}';

Вышеупомянутое правило грамматики, которое является правилом начала, говорит, что язык должен начинаться с ‘Graph {‘ и заканчиваться ‘}’ и должен содержать как минимум один край или более одного края.

1
2
3
4
vertex: ID;
edge: vertex '->' vertex '(' NUM ')' ;
ID: [a-zA-Z]+;
NUM: [0-9]+;

Приведенные выше четыре правила говорят, что вершина должна иметь как минимум один символ или несколько символов. И ребро определяется как набор из двух вершин, разделенных ‘->’ и с некоторыми цифрами в ‘()’.

Я назвал язык грамматики как «Graph», и, следовательно, как только мы будем использовать ANTLR для генерации java-классов, то есть парсера и лексера, мы увидим следующие классы: GraphParser, GraphLexer, GraphListener и GraphBaseListener. Первые два класса имеют дело с генерацией дерева разбора, а последние два класса имеют дело с обходом дерева разбора. GraphListener — это интерфейс, который содержит все методы для работы с деревом разбора, то есть для обработки событий, таких как ввод правила, выход из правила, посещение конечного узла, и в дополнение к ним он содержит методы для обработки событий, связанных с входом в график. Правило, введя правило ребра и введя правило вершины. Мы будем использовать эти методы для перехвата данных, присутствующих в dsl, а затем заполним семантическую модель.

Заполнение семантической модели

Я создал файл graph.gr в пакете ресурсов, который содержит DSL для заполнения графа. Поскольку файлы в пакете ресурсов доступны ClassLoader во время выполнения, мы можем использовать ClassLoader, чтобы прочитать сценарий DSL и затем передать его классам Lexer и анализатора. Используемый сценарий DSL:

1
2
3
4
5
6
7
Graph {
  A -> B (10)
  B -> C (20)
  D -> E (30)
  A -> E (12)
  B -> D (8)
}

И код, который загружает DSL и заполняет семантическую модель:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
//Please resolve the imports for the classes used.
public class GraphDslAntlrSample {
  public static void main(String[] args) throws IOException {
    //Reading the DSL script
    InputStream is =
            ClassLoader.getSystemResourceAsStream("resources/graph.gr");
 
    //Loading the DSL script into the ANTLR stream.
    CharStream cs = new ANTLRInputStream(is);
 
    //Passing the input to the lexer to create tokens
    GraphLexer lexer = new GraphLexer(cs);
 
    CommonTokenStream tokens = new CommonTokenStream(lexer);
 
    //Passing the tokens to the parser to create the parse trea.
    GraphParser parser = new GraphParser(tokens);
 
    //Semantic model to be populated
    Graph g = new Graph();
 
    //Adding the listener to facilitate walking through parse tree.
    parser.addParseListener(new MyGraphBaseListener(g));
 
    //invoking the parser.
    parser.graph();
 
    Graph.printGraph(g);
  }
}
 
/**
 * Listener used for walking through the parse tree.
 */
class MyGraphBaseListener extends GraphBaseListener {
 
  Graph g;
 
  public MyGraphBaseListener(Graph g) {
    this.g = g;
  }
 
  @Override
  public void exitEdge(GraphParser.EdgeContext ctx) {
    //Once the edge rule is exited the data required for the edge i.e
    //vertices and the weight would be available in the EdgeContext
    //and the same can be used to populate the semantic model
    Vertex fromVertex = new Vertex(ctx.vertex(0).ID().getText());
    Vertex toVertex = new Vertex(ctx.vertex(1).ID().getText());
    double weight = Double.parseDouble(ctx.NUM().getText());
    Edge e = new Edge(fromVertex, toVertex, weight);
    g.addEdge(e);
  }
}

И вывод, когда выше будет выполнено, будет:

1
2
3
4
5
6
7
8
Vertices...
A B C D E
Edges...
A to B with weight 10.0
B to C with weight 20.0
D to E with weight 30.0
A to E with weight 12.0
B to D with weight 8.0

Подводя итог, этот пост создает внешний DSL для заполнения данных для графиков с использованием ANTLR. Я улучшу этот простой DSL и представлю его как утилиту, которая может использоваться программистами, работающими над графиками.

В этом посте очень много идей и кода, не стесняйтесь добавлять любые ваши вопросы, чтобы я мог попытаться ответить на них и в интересах других.

Ссылка: Создание внешних DSL с использованием ANTLR и Java от нашего партнера JCG Мохамеда Санауллы в блоге Experiences Unlimited .