Eine eigene Programmiersprache mit Xtext modellieren
Mit Xtext, einem Sprachframework auf der JVM, ist es mit wenig Aufwand möglich, eine eigene Programmiersprache zu definieren. Dieser Artikel zeigt den Werdegang einer kleinen domänenspezifischen Sprache von der Definition der Grammatik bis hin zur lauffähigen IDE.
- Markus Amshove
- Stefan Macke
Allgemeine Programmiersprachen (General Purpose Languages, GPL) – zum Beispiel Java oder C# – bieten die Möglichkeit, Lösungen für alle nur denkbaren Probleme umzusetzen. Aber das ist meist mehr, als benötigt wird, oder der Code besteht aus vielen sich wiederholenden Bestandteilen. Dem gegenüber stehen domänenspezifische Sprachen (Domain Specific Languages, DSL) wie SQL, die einen eingeschränkten Funktionsumfang haben, die Problemdomäne aber vollständig und präzise beschreiben.
Prägnant statt Boilerplate
Als Beispiel fĂĽr Boilerplate-Code in Java dienen die sogenannten POJOs (Plain Old Java Objects). Ihre Getter und Setter sind sich wiederholende Bestandteile, die meistens keinen echten Mehrwert bieten. Das folgende Listing zeigt ein POJO zur Darstellung einer Person mit einem einzigen Attribut Name:
public class Person
{
private String name;
public String getName()
{
return name;
}
public void setName(String name)
{
this.name = name;
}
@Override
public String toString()
{
return String.format("Objekt %s { Name = %s }",
"Person",
getName()
);
}
}
In einer speziell für die Abbildung solcher Datenmodelle definierten DSL könnte obiges Beispiel wie folgt aussehen:
Eine Person hat Name vom Typ Text.
Der Artikel beschreibt im Folgenden den Werdegang dieser kompakten und trotzdem verständlichen Sprache zur einfachen Definition von POJOs mit dem Framework Eclipse Xtext. Ziel ist es, mit ihr den obigen Java-Code zu generieren. Den kompletten Sourcecode gibt es zum Download bei GitHub.
Xtext und Xtend
Xtext ist ein Open-Source-Framework zur Entwicklung von Programmiersprachen und DSLs auf der Java-Plattform. Es bietet eine nahtlose Integration in bekannte Entwicklungsumgebungen wie Eclipse oder IntelliJ IDEA und arbeitet mit etablierten Build-Werkzeugen wie Gradle oder Maven zusammen. Gestützt durch den bekannten Parsergenerator ANTLR lässt sich etwa aus einer Grammatik recht einfach ein lauffähiges Plug-in für Eclipse erzeugen, das Syntax-Hightlighting, Code-Vervollständigung, Formatierung und vieles andere mehr bietet.
Das obige Beispiel der sprechenden Definition eines Datenmodells lieĂźe sich durch eine Grammatik wie die folgende umsetzen:
grammar net.aokv.Datenmodell with org.eclipse.xtext.common.Terminals
generate datenmodell "http://www.aokv.net/Datenmodell"
Datenmodell:
elemente+=Datendefinition*;
Datendefinition:
Artikel name=ID 'hat' (attribute+=Attribut*) '.';
Attribut:
name=ID 'vom Typ' datentyp=Datentyp 'und'?;
Artikel:
'Ein' | 'Eine';
enum Datentyp:
Text | Zahl;
Eine Grammatik in Xtext, die übrigens selbst eine Xtext DSL ist, nämlich zur Beschreibung textueller Sprachen, beginnt mit der Deklaration der Sprache, wie in den ersten beiden Zeilen des Listings zu sehen ist. Die zweite Zeile weist Xtext an, aus der folgenden Grammatik eine Repräsentation in Form eines Objektgraphen mit dem Namen datenmodell zu erzeugen. Er lässt sich später nutzen, um zum Beispiel bei der Entwicklung von Code-Generatoren einfach auf die Bestandteile des konkreten Programms zuzugreifen.
Danach folgen die Regeln der DSL. Die einfachsten, Artikel und Datentyp, sind sogenannte terminale Regeln, die eine einzige atomare Ausgabe erzeugen. In diesem Fall definieren sie lediglich eine Auswahl an Alternativen (Operator |) mehrerer Schlüsselwörter (String-Literale Ein und Eine) oder Enum-Werte (Text und Zahl). Schlüsselwörter der DSL (vergleichbar mit class in Java) hebt der Editor später zum Beispiel automatisch farbig hervor und vervollständigt sie.
Die komplexeren Parser-Regeln – etwa Datenmodell oder Attribut – bestehen zusätzlich zu terminalen Elementen aus Referenzen auf andere Regeln. Sie produzieren keinen einzelnen Wert, sondern einen ganzen Parser-Baum. In Form von Objekten werden sie Teil des Abstract Syntax Tree (AST). In der baumförmigen Repräsentation des Programms lässt sich später auf objektorientierte Art navigieren, indem beispielsweise die Attribute der Objekte (Features genannt) abgefragt werden. Ein Beispiel hierfür ist die oberste Regel Datenmodell, die später als Objekt im AST ein Feature elemente haben wird, über das sich die einzelnen Datendefinitionen erreichen lassen.
Der Zuweisungsoperator += fügt die Datendefinitionen der Liste elemente hinzu. Dabei kennzeichnet die Kardinalität * aus der erweiterten Backus-Naur-Form, dass es keine oder mehrere Elemente geben kann. Die Kardinalitäten werden durch die gleichen Symbole verdeutlicht, die auch bei regulären Ausdrücken zum Einsatz kommen: ? für kein oder ein Element, + für ein oder mehrere und * für kein, ein oder mehrere Elemente.
Das Terminal ID, das Xtext selbst zur Verfügung stellt, steht für einen beliebigen gültigen Bezeichner, den der Benutzer später im Programm definieren kann. Im Beispiel kommt Person zum Einsatz, um der beschriebenen Entität einen Namen zu geben. Diese Bezeichner lassen sich etwa in Code-Generatoren auslesen und verwenden, um die Ausgabe der DSL zu programmieren (z. B. public class Person { ... }).