Saltar la navegación

Ejemplo práctico: crear un lenguaje

Como ejemplo, implementaremos un pequeño lenguaje específico de dominio para definir entidades de base de datos, de forma similar a un ORM como el de Spring Roo o Rails. El lenguaje tendría este aspecto:

datatype String
datatype Integer

entity Curso {
    titulo: String
    many contenidos: Contenido
}
 
entity Contenido {
    titulo: String
    author: String
}
 
entity Encuesta extends Contenido {
    many preguntas: Pregunta
}
 
entity Pregunta {
    contenido: String
    many respuestas: String
    respuestaCorrecta: Integer
}

Crear un nuevo proyecto

En la interfaz de Eclipse, ir a File/New/Project y seleccionar Xtext/Xtext project.

En el cuadro de diálogo que aparece hay tres propiedades importantes:


Cuadro de diálogo "Nuevo proyecto Xtext"

Project name: Nombre del proyecto de Eclipse.
Language/Name: Ruta donde se colocará el fichero de definición del nuevo lenguaje.
Language/Extensions: Extensión que tendrán los ficheros del nuevo lenguaje.

Al hacer clic en Finalizar se crearán cuatro proyectos:

  • es.usal.modelado.Entidades: Contiene la definición de la gramática y el compilador.
  • es.usal.modelado.Entidades.sdk: Es un paquete virtual que contiene metadatos del lenguaje (por ejemplo, versión) e incluye los demás como dependencias.
  • es.usal.modelado.Entidades.tests: Contiene tests unitarios opcionales.
  • es.usal.modelado.Entidades.ui: Clases generadas con las extensiones del editor Eclipse.

Etapas de compilación

La compilación de código en un lenguaje de programación se puede dividir en tres etapas:

  1. Lexing: Se decodifican las unidades atómicas del lenguaje, como números, palabras clave o cadenas. A cada una de estas unidades se les denomina terminales o tokens.
  2. Parsing: Se construye un árbol de sintaxis abstracta (AST) aplicando reglas de grámática sobre los terminales.
  3. Traducción: el árbol de sintaxis se decodifica a un lenguaje de destino, habitualmente de más bajo nivel (ej. DSL a Java o C a ensamblador de x86).
Ilustración de las tres etapas

Escribir tu propia gramática

El asistente de proyectos habrá creado un fichero con extensión .xtext. Es este fichero donde definiremos nuestra gramática, expuesta a continuación:

grammar es.usal.modelado.EntidadesDSL with org.eclipse.xtext.common.Terminals

generate entidadesDSL "http://www.usal.es/modelado/EntidadesDSL"

ModeloEntidades:
    (elementos += Tipo)*
;
Tipo:
    TipoBasico | Entidad
;
TipoBasico:
    'datatype' name = ID
;
Entidad:
    'entity' name = ID ('extends' tipoAntecesor = [Entidad])? '{'
        (propiedades += Propiedad)*
    '}'
;
Propiedad:
    name = ID ':' (many ?= 'many')? tipo = [Tipo]
;

En las dos primeras líneas se define el paquete Java donde se generará el código para compilar el lenguaje, así como los terminales que se utilizarán. Por defecto Xtext soporta los terminales básicos de la mayoría de lenguajes de programacion, como números e identificadores.

A continuación se definen las clases del lenguaje. Cada nodo del árbol de sintaxis será una instancia de alguna de ellas. Cada una de ellas se define con un nombre, seguido de dos puntos y una descripción del código a partir del que representa. La descripción de la clase se termina con punto y coma.

Dentro de la definición de la clase se puede hacer uso de varias construcciones, explicadas a continuación. Cuando varias construcciones se separen con un espacio significará que en el lenguaje deben aparecer en ese orden, separadas por uno o más caracteres de espaciado (espacio, salto de línea, tabulación...).

Texto literal

Cuando en la definición aparece un texto entre comillas simples significa que se requiere ese texto exacto, sin las comillas, en el código. Habitualmente se utiliza este elemento para representar palabras clave o símbolos de agrupación.

Por ejemplo:

'datatype'

Asignación simple

En la definición aparece un nombre de variable, el signo igual y el nombre de una clase, que puede ser otra clase de la gramática o un terminal.

En la clase que se está definiendo se creará una propiedad con el nombre de variable escogido, del tipo de la clase colocada a la derecha del igual.

Por ejemplo:

TipoBasico:
'datatype' name = ID
;

ID es el terminal de identificadores de variable predefinido en Xtext (importado automáticamente al usar with org.eclipse.xtext.common.Terminals). Un identificador de variable se considera como un conjunto de letras del alfabeto inglés, números y el guión bajo, con la excepción habitual de que no pueden empezar por un número. Los terminales tienen tipos básicos, como cadenas o enteros.

En este caso se está definiendo una clase en el AST llamada TipoBasico. Esta clase tendrá una propiedad name, de tipo cadena (String), cuyo valor tendrá que adecuarse a la definición de identificadores de variable.

Un ejemplo de código que sería convertido en un nodo de este tipo sería el siguiente:

datatype String

Definiciones anidadas

Incluir el nombre de otra clase sin comillas tiene el efecto de requerir que en ese punto del código se cumpla la especificación de esa clase. Habitualmente se utilizará como parte de una operación de disyunción, explicada a continuación.

Disyunción

El operador barra vertical o | puede utilizarse para colocar una disyunción en la definición de una clase. Por ejemplo:

Tipo:
    TipoBasico | Entidad
;

En el ejemplo, la clase Tipo se define como la definición de la clase TipoBasico o la definición de la clase Entidad.

Opcionalidad

Si uno o más elementos se definen entre paréntesis seguidos por una interrogación, serán considerados opcionales.

Por ejemplo, la definición de una función en un lenguaje estilo Bash podría tener este aspecto:

Funcion:
    ('function')? name = ID '()' '{'
        Codigo
    '}'
;

En este caso la palabra clave function es opcional y el usuario podría colocarla u obviarla.

Asignación booleana

El operador ?= recibe a la izquierda un nombre de variable y a la derecha una definición. Crea una propiedad con el nombre de variable en la clase de ese nodo de tipo boolean y le da un valor true si se cumple la definición de la derecha.

Se utiliza para definir palabras clave opcionales cuya aparición tiene un significado. Casi siempre se envolverá con una asignación opcional, por ejemplo:

(many ?= 'many')?

En este caso, se creará una variable many que será true si y solo si aparece la palabra literal many en esa parte del código.

Repetición

Los operadores + y * permiten que la definición anterior, habitualmente rodeada por paréntesis, se repita más de una vez.

Si se usa el operador +, la definición deberá aparecer al menos una vez. Si se usa el operador *, la definición podrá no aparecer, convirtiéndose en opcional.

Estos operadores a menudo se combinan con el operador de adición a lista, explicado a continuación.

Adición a lista

El operador += define una propiedad de tipo array con el nombre de variable especificado a la izquierda. Los elementos del array serán del tipo del elemento colocado a la derecha. Por cada aparición de esta definición se añade un elemento al array.

Por ejemplo:

(propiedades += Propiedad)*

La clase incorporaría una propiedad de tipo Array<Propiedad>, con referencias a los objetos Propiedad definidos, separados por espacio en blanco.

Enlaces

Una propiedad de un nodo puede hacer referencia a otro nodo, sin que el segundo nodo sea necesariamente hijo del primero, incluso aunque en el momento en el que se procesa ese nodo no se conozca su nodo destino. Por ejemplo, un fragmento de código puede invocar a una función, para lo cual especifica su nombre seguido de paréntesis; o una propiedad especifica un tipo que se ha declarado en otra parte del código.

Xtext permite implementar este tipo de relaciones con facilidad a través de la sintaxis de tipos entre corchetes. Por ejemplo:

tipoAntecesor = [Entidad]

En la clase se creará una variable tipoAntecesor de tipo Entidad. El usuario debe especificar en esa parte de código el nombre de una instancia de Entidad. Este nombre viene dado por la propiedad name, que debe estar definida en Entidad, por ejemplo:

Entidad:
    'entity' name = ID '{'
         ... resto de la definición de Entidad ...
    '}'