Download Transformaciones de modelos

Document related concepts
no text concepts found
Transcript
Máster MNTI
Desarrollo Dirigido por Modelos
Seminario 3 - Transformaciones de modelos
Grupo Modelum
Universidad de Murcia
15 de diciembre de 2010
Resumen
En este seminario se presentarán los conceptos relativos a la transformación de modelos. El
objetivo final es aprender a utilizar el lenguaje de transformación RubyTL, para poder transformar
modelos de sintaxis abstracta del DSL.
Primero se explicará RubyTL a través de una serie de ejemplos sencillos. También, utilizando
RubyTL, se introducirá una técnica para comprobar la semántica estática (restricciones) asociada
los metamodelos.
1
1.
Introducción
En este seminario se presentarán los conceptos relativos a la transformación de modelos. Se utilizará el lenguaje de transformación RubyTL para practicar con los conceptos. Se introducirá RubyTL
utilizando un ejemplo simple de transformación de un modelo de clases a un modelo Java y se describirán los ejercicios del caso práctico.
Antes de seguir completando el DSL desarrollado hasta ahora se realizará un pequeño ejercicio
práctico de transformación de modelos para aprender algunos conceptos básicos con un ejemplo más
sencillo. El enunciado del ejercicio se encuentra en el Anexo D.
2.
Breve introducción a RubyTL
RubyTL es un lenguaje de transformación modelo-modelo que ha sido implementado como un
lenguaje embebido dentro de Ruby. Esto significa que no se ha utilizado la técnica habitual de implementar un parser y un interprete o un compilador, sino que la sintaxis de RubyTL está basado
en las construcciones ofrecidas por el propio Ruby, en este caso, llamadas a métodos. La implicación
fundamental de esta técnica de implementación es que es posible utilizar código Ruby imperativo en
cualquier parte de una definición de transformación1 .
RubyTL es un lenguaje de transformación basado en reglas hı́brido. La parte declarativa está basada en reglas y bindings, mientras que la parte imperativa viene dada por el propio Ruby. En RubyTL
una definición de transformación está compuesta de reglas de transformación. Como se verá, hay varios
tipos de reglas de transformación, pero todas ellas comparten la misma estructura. Están compuestas
de cuatro partes from, to, filter and mapping:
from. Especifica una metaclase origen, que pertenece a un metamodelo origen. La regla se aplicará sobre elementos del modelo origen que sean de ese tipo o de alguna de sus subclases.
to. Especifica una o más metaclases destino, que pertenecen a un metamodelo destino. La regla
creará elementos de ese tipo (excepto si es una regla de refinamiento).
filter. Una expresión sobre el elemento origen. La regla solo se ejecuta si esta expresión (un
bloque de código, en realidad) se evalúa a true. Esta parte es opcional.
mapping. Un bloque de código de Ruby, que se ejecuta cuando se aplica la regla. El bloque
recibe un elemento origen y el elemento destino creado por la regla. Dentro del bloque es posible
escribir cualquier código Ruby, aunque se recomienda un estilo declarativo basado en bindings.
Un binding es un tipo especial de asignación que tiene la siguiente estructura expr izq.propiedad =
Si el tipo de la expresión de la parte derecha no es compatible con el tipo de la propiedad
especificada, el motor de RubyTL automáticamente busca una regla capaz de transformar la parte
derecha en la parte izquierda.
Hay tres tipos básicos de reglas:
expr der.
Regla “top”. Sirve como “regla main” para iniciar la ejecución. Se aplica sobre todos los elementos
del modelo origen del tipo que indique su parte from.
Regla “normal”. Se ejecuta solo como resultado de evaluar un binding. Una caracterı́stica peculiar
(que comparte con las reglas “top”) es que nunca transforma dos veces el mismo elemento. Si
esto sucede, se devuelve el elemento destino creado previamente.
Regla “copy”. Se ejecuta solo como resultado de evaluar un binding, pero en este caso un elemento
puede ser transformado más de una vez.
La ejecución de una transformación empieza con la ejecución de las reglas “top”. Para cada regla
top (normalmente habrá una por cada transformación) se obtienen todos elementos del modelo origen
cuyo tipo es la clase especificada en la parte from o una de sus subclases. Para cada elemento origen
se crea el correspondiente elemento destino. Si no existen reglas top se considera la primera regla de
la transformación como tal.
A continuación se ejecuta la parte mapping, ejecutando los bindings que se hayan especificado. La
ejecución de los bindings implicará la ejecución de ciertas reglas, para las cuales se ejecutará también
los bindings correspondientes.
1 La
primera edición del libro Programming Ruby está disponible en la web: http://www.rubycentral.com/book/.
2
2.1.
Ejemplo: UML2Java
El código RubyTL que se muestra en esta sección corresponde a una definición transformación que
transforma modelos conformes al metamodelo de la Figura 1a, en modelos conformes al metamodelo
de la Figura 1b.
(a) Metamodelo de clases
(b) Metamodel Java
Figura 1: (a) Metamodelo de clases simple. (b) Metamodelo Java.
Las correspondencias que se establecen son las siguientes:
UML
Clase
Atributo
Atributo
Operación
Parámetro
Tipo primitivo
Java
Clase, Interface
Método get, Método set
Variable de instancia
Método
Parámetro
Tipo primitivo
Comentario
La clase implementa la interfaz
Solo atributos públicos
Todos
Para clases e interfaces
Además se desea que las clases e interfaces Java que se generen queden englobadas en un paquete
Java, que no tiene ninguna correspondencia con ningún elemento del modelo UML. Igual ocurre con
los tipos primitivos, que deben quedar contenidos en un paquete llamado java.lang.
El código de transformación es el siguiente.
decorator UML::Class do
def attributes
self.features.select { |f| f.kind_of?(UML::Attribute) }
end
def operations
self.features.select { |f| f.kind_of?(UML::Operation) }
end
end
lang_pkg = Java::Package.new(:name => ’java.lang’)
app_pkg = Java::Package.new(:name => ’mnti2010’)
top_rule ’class2class’ do
from UML::Class
to Java::Class, Java::Interface
mapping do |uml_class, java_class, java_interface|
java_class.name
= uml_class.name
java_class.features = uml_class.attributes
java_class.implements = java_interface
java_class.features = uml_class.operations
java_interface.name = ’I’ + uml_class.name
java_interface.abstractMethods = uml_class.operations.
select { |o| o.visibility == UML::Visibility::PUBLIC }
app_pkg.classifiers << java_class
app_pkg.classifiers << java_interface
end
end
rule ’attribute2get_set’ do
from UML::Attribute
to
Java::Field
mapping do |attr, field|
field.name = attr.name
field.type = attr.type
field.visibility = ’private’
end
3
end
rule ’attribute2get_set’ do
from UML::Attribute
to
Java::Method, Java::Method
filter { |attr| attr.visibility == UML::Visibility::PUBLIC }
mapping do |attr, get, set|
get.name = ’get’ + attr.name.capitalize
get.type = attr.type
get.visibility = ’public’
set.name = ’set’ + attr.name.capitalize
set.visibility = ’public’
set.params = Java::Parameter.new do |param|
param.name = attr.name
param.type = attr.type
end
end
end
copy_rule ’operation2method’ do
from UML::Operation
to Java::Method
mapping do |operation, method|
method.name = operation.name
method.type = operation.type
method.params = operation.params
end
end
copy_rule ’type2parameter’ do
from UML::Parameter
to
Java::Parameter
mapping do |uml_param, java_param|
java_param.name = uml_param.name
java_param.type = uml_param.type
end
end
rule ’datatype2primitive’ do
from UML::PrimitiveType
to Java::PrimitiveType
mapping do |src, target|
target.name = src.name
target.owner = lang_pkg
end
end
2.2.
Ejecución de transformaciones en RubyTL
Para ejecutar una transformación hay que especificar cuáles son los modelos y metamodelos de
entrada concreto que usa la definición de transformación.
En el caso de RubyTL, se definen ficheros Rakefile (un sistema parecido a Make, pero embebido en
Ruby) en los que se especifican tareas de construcción. Ası́, hay tareas especı́ficas para transformaciones
modelo-modelo, modelo-código, etc.
A continuación se muestra un ejemplo de definición de una tarea para ejecutar la transformación
de la sección anterior.
model_to_mo del : uml2java do | t |
t . sources : package
= > ’ UML ’ ,
: metamodel = > ’ metamodels / SUML . ecore ’ ,
: model
= > ’ models / test . class . xmi ’
t . targets : package
= > ’ Java ’ ,
: metamodel = > ’ metamodels / JavaM . ecore ’ ,
: model
= > ’ models / result . xmi ’
t . transfor mation ’ tr an s fo rm at i on s / uml2java . rb ’
end
Una tarea model to model está compuesta de los siguiente elementos.
El nombre de la tarea. Por convención se prefija con “:”, aunque puede especificarse como una
cadena. En este caso :uml2java.
Un conjunto de elementos sources, que representan modelos y metamodelos de entrada. Se especifica el espacio de nombres que espera la definición transformación (ej., UML), el fichero .ecore
que contiene el metamodelo, y un modelo que conforma al metamodelo.
4
Un conjunto de elementos targets, que representan modelos y metamodelos de salida. Se especifica el espacio de nombres que espera la definición transformación (ej., Java), el fichero .ecore
que contiene el metamodelo, y un modelo que conforma al metamodelo. Si el modelo no existe,
al finalizar la transformación se creará nuevo, si ya existe será reemplazado por el nuevo.
La ruta de la transformación que se ejecutará.
2.2.1.
Resumen de comandos
Click der. (carpeta) → New → Other ... → AGE → New RubyTL
Transformation
Crear nueva transformación RubyTL
Click der. (proyecto) → New File → Nombrar acabado en .rake
Crear nuevo fichero Rakefile
Escribir mo → CTRL + Espacio
Autocompletado de tareas Rake
2.3.
Librerı́a colecciones
Una parte importante de la tarea de escribir una transformación de modelos se suele dedicar a escribir expresiones de navegación sobre el modelo origen. La navegación sobre referencias multivaluadas
se reduce a un problema de manipulación de colecciones.
Ruby tiene una potente librerı́a para manipular colecciones (arrays según su terminologı́a), que
está basada en el uso de closures o bloques de código para implementar iteradores. Estos iteradores
tienen un estilo “funcional”, parecido a las operaciones sobre colecciones de OCL.
Se puede encontrar documentación sobre la librerı́a de colecciones en la documentación de las
clases Enumerable y Array, que se puede consultar utilizando el comando ri de la distribución Ruby
o en http://www.ruby-doc.org/.
A continuación se explica a través de ejemplos, los métodos más importantes. Estos métodos no
tienen efectos laterales, es decir, siempre devuelven un nuevo array.
En Ruby las colecciones está implementadas en la clase Array. Son arrays dinámicos que no tiene
un tamaño prefijado. Hay varias formas de crear arrays:
# Crear un array con tres elementos
[1, 2, 3]
# Crear un array con tres elementos, otra forma
mi_array = []
mi_array << 1
mi_array << 2
mi_array << 3
Para filtrar elementos de una lista, se utiliza select y reject.
[1, 2, 3, 4].select { |i| i % 2 == 0 } # => [2, 4]
[1, 2, 3, 4].reject { |i| i % 2 == 0 } # => [1, 3]
Transformaciones “in-place” con el método map. Cada elemento del array se sustituye por el resultado
del bloque.
list = [1, 2, 3, 4]
list.map { |i| i.to_s } # => ["1", "2", "3", "4"]
list.map { |i| i * 2 } # => [2, 4, 6, 8]
Para aplanar una colección, se utiliza flatten
[[1, 2], [3, 4], [5, 6]].flatten
# => [1, 2, 3, 4, 5, 6]
[1, 3, 5].map { |i| [i, i + 1] }.flatten # => [1, 2, 3, 4, 5, 6]
Para buscar el primer elemento que cumple una condición, se utiliza find
5
[1, 2, 3, 4].find { |i| i > 2 } # => 3
[1, 2, 3, 4].find { |i| i > 4 } # => nil
Para comprobar si un elemento está incluido en un array, se utiliza include?.
[1, 2, 3, 4].include?(3) # => true
[1, 2, 3, 4].include?(5) # => false
Para eliminar duplicados, se utiliza uniq
[1, 2, 2, 4, 1].uniq # => [1, 2, 4]
Para iterar sobre una colección y realizar una acción en cada paso de la iteración se utiliza each.
Por ejemplo,
[1, 2, 3, 4, 5].each do |value|
puts "El numero actual es #{value}"
end
2.4.
new
Crear elementos imperativamente
RubyTL reutiliza la manera de crear instancias que se utiliza en Ruby: se aplica el método de clase
a la clase que se quiere instanciar. Por ejemplo,
my_pkg = Java::Package.new
my_pkg.name = ’java.lang’
RubyTL proporciona dos atajos para establecer los valores de las propiedades de un objeto creado
imperativamente directamente:
my_pkg2 = Java::Package.new(:name => ’java.lang’)
my_pkg3 = Java::Package.new do |p|
p.name = ’java.lang’
end
3.
Validación de la semántica estática
Esta sección está basada en el artı́culo “Using ATL for checking models” 2 en el cual se presenta
una aproximación para la comprobación de restricciones en modelos utilizando el lenguaje ATL. Para
información más detallada consultar el artı́culo.
Existen actualmente pocas herramientas que permitan asociar restricciones a un metamodelo y
comprobar automáticamente que tales restricciones se cumplen para los modelos conformes al metamodelo. En ocasiones es necesario asegurar que se cumplen restricciones adicionales “dependientes
del contexto”. Estas restricciones adicionales no estarı́an asociadas al metamodelo, sino que deben
escribirse en algún fichero separado.
Otra motivación para esta aproximación, en lugar de un procesador OCL, es la necesidad de
asociar grados de “severidad” a las restricciones. En el artı́culo citado anteriormente se consideran
tres niveles: error, advertencia y sugerencia (error, warning y critic). Además, también permite asociar
descripciones a las restricciones.
Por ejemplo, a continuación se muestran dos restricciones en OCL. La primera comprueba un
error que debe ser resuelto e impide la manipulación del modelo (p.ej., aplicar una transformación de
modelos) hasta entonces. La segunda en cambio es una advertencia.
-- Error: No se permiten referencias multivaluadas para atributos
context UML::Attribute inv:
self.type.oclIsKindOf(UML::Primitive) implies self.multivalued = false
-- Warning: No haya nombres repetidos en los atributos de una clase
context UML::Class inv:
self.features->forAll(f1 |
self.features->excluding(f1)->forAll(f2 | f1.name <> f2.name))
2 Puedes
descargarse de: www.sciences.univ-nantes.fr/lina/atl/www/papers/GraMoT05.pdf
6
Utilizando una transformación de modelos es posible comprobar restricciones. El modelo origen
será aquel que se desea “chequear” mientras que el modelo destino conformará al metamodelo Problem
presentado en la Figura 2. Como se observa es un metamodelo muy simple con una sola metaclase.
El resultado de la ejecución de la transformación será un modelo que contendrá una instancia de la
metaclase Problem por cada restricción no satisfecha. Si el modelo destino está vacio, significará que
todas las restricciones son satisfechas por el modelo origen.
Figura 2: Metamodelo Problem
Ası́, una definición de transformación en RubyTL para la comprobación de restricciones sigue el
siguiente esquema.
Una regla “top” por cada restricción.
La metaclase origen de cada regla es el contexto de la restricción.
El filtro o guarda de la regla contiene la expresión booleana asociada, pero negada. Es decir,
seguirá el patrón not ( condition ).
La metaclase destino de la regla siempre es Problem, de manera que por cada instancia que cumpla
el filtro se creará una instancia de Problem (es decir, por cada violación del invariante. Notese la
negación en el filtro).
Las propiedades Problem se inicializan utilizando dos bindings, la “severidad” y una descripción.
La dos restricciones OCL anteriores se pueden implementar en RubyTL de la siguiente forma.
top_rule ’InvalidMultivalued’ do
from UML::Attribute
to Problem::Problem
filter { |a| ! (a.type.kind_of?(UML::PrimitiveType).implies(!a.multivalued) ) }
mapping do |attr, problem|
problem.description = "Atributo #{attr.name} es multivaluado"
problem.severity = Problem::Severity::ERROR
end
end
top_rule ’DuplicatedFeatureName’ do
from UML::Class
to
Problem::Problem
filter { |c| ! (c.features.all? {
|f1| c.features.reject { |f2| f2 == f1 }.all? { |f2| f2.name != f1.name } } ) }
mapping do |klass, problem|
problem.description = "Nombre de caracteristica duplicada"
problem.severity = Problem::Severity::WARNING
end
end
7
4.
Caso práctico
4.1.
Representación de “bundles”
En el DSL que se está desarrollando durante el curso se debe tratar el aspecto de los ficheros de
traducción y su utilización en una aplicación.
Básicamente manejar las traducciones de una aplicación consiste en utilizar nombres lógicos (claves)
para nombrar los mensajes que utiliza la aplicación. En un fichero de traducción, denominado bundle,
se asocian los nombres lógicos o claves con el texto del mensaje.
Por ejemplo, el siguiente trozo de código muestra el contenido de un bundle, denominado
MensajesEjemplo es ES.properties.
menuId . showedName = File
menuId . shortcut = ctrl + f
menuId . mnemonic = f
Un posible código Java para “internacionalizar” una aplicación es el siguiente. Ası́, en lugar de
utilizar literales de cadena se utilizan los métodos del objeto ResourceBundle que accede al fichero de
traducción adecuado para obtener la versión adecuada, según el “locale” seleccionado.
Locale currentLocale
= esLocale = new Locale ( " es " ," ES " ) ;
ResourceBund le messages = Resourc eBundle . getBundle ( " Me ns a je sE j em pl o " ,
currentLocale ) ;
// Escribir una cadena " i n t e r n a c i o n a l i z a d a " por pantalla
System . out . println ( messages . getString ( " saludo " ) ) ;
Para facilitar la generación de los bundles, es posible crear un metamodelo que los represente, de
manera que se pueden crear transformaciones modelo-modelo para reutilizar el generador de código
para diversos DSLs.
4.2.
Representación de la arquitectura
Uno de los posibles objeticos de un DSL es permitir la generación automática de código fuente
asociado a una arquitectura. Para facilitar esta labor, muchas veces se hace necesario reducir el nivel
de abstracción de los modelos con los que trabajamos con el DSL (sintaxis abstracta) para ser más
cercanos al dominio de aplicación. Para ello, se definen metamodelos de la arquitectura destino y son
necesarias transformaciones de modelos para convertir modelos de la sintaxis abstracta en modelos de
la arquitectura. Finalmente, los modelos de la arquitectura, por ser cercanos al dominio de aplicación,
facilitan la generación de código por medio de transformaciones modelo a código.
En el caso del DSL que se está desarrollando durante el curso, queremos permitir la generación
automática del código asociado a los menús y los perfiles. Nuestro objetivo será generar el código Java
para el framework Swing que permite generar los menús y perfiles de una aplicación. De esta forma,
hemos definido el metamodelo de la arquitectura mostrado en el Anexo B.
4.3.
Ejercicios
Para realizar estos ejercicios, descarga el proyecto esqueleto que hay en la web de la asignatura.
Ejercicio 1. Identificar las correspondencias entre el metamodelo de la sintaxis abstracta del
DSL y el metamodelo de “bundles” (diagrama del metamodelo en A. Completar la transformación menuprofile2bundle.rb.
Ejercicio 2. Para comprobar la semántica estática del metamodelo de la sintaxis abstracta
se escribirá la definición de transformación dsl2problem.rb y deberá seguir la aproximación
presentada en la Sección 3. Se deberán comprobar las siguientes restricciones para el DSL de los
menús (entre corchetes se indica el grado de problema a indicar):
• Dos menús no pueden compartir nombre (atributo id [ERROR].
• MenuDefinition contiene al menos un TreeMenu [WARNING]
• MergeMenu debe unir al menos dos menús [ERROR]
• Todos los menús deben estar internacionalizados para cada idioma [ERROR].
8
Ejercicio 3. Se creará una transformación de la sintaxis abstracta del DSL a un metamodelo que
representa la arquitectura de los menús. Ambos metamodelos se pueden consultar en el anexo B.
En particular, el metamodelo de la arquitectura trata de representar un “gestor de menús” con
la estructura que se muestra en el trozo de código Java incluido en el anexo C. Implementar la
transformación menuprofile2arch.rb sin considerar la herencia de perfiles ni los enlaces de menús.
Se enviará el proyecto Eclipse con los ejercicios resueltos a [email protected]. Opcionalmente se
puede implementar la transformación considerando la herencia de perfiles y enlaces de menús.
9
A.
Metamodelo de internacionalización
Figura 3: Metamodelo para representar ficheros de internacionalización.
10
B.
Metamodelos para la transformación dsl2arch
11
C.
Código Java destino
package imaci09 ;
import java . awt . event . ActionEvent ;
import java . awt . event . KeyEvent ;
import
import
import
import
import
javax . swing . J C h e c k B o x M e n u I t e m ;
javax . swing . JMenu ;
javax . swing . JMenuBar ;
javax . swing . JMenuItem ;
javax . swing . KeyStroke ;
class MenuManager {
private JMenu File ;
private JMenuItem New ;
private JMenuItem Close ;
private J Ch e c k B o x M e n u I t e m Synchronize ;
private JMenuBar N o v i c e U s e r P r o f i l e M e n u ;
private JMenuBar A d v a n c e d U s e r P r o f i l e M e n u ;
public final static MenuManager INSTANCE = new MenuManager () ;
private MenuManager () {
createMenus () ;
...
}
public void createMenus () {
this . File = new JMenu ( " Fichero " ) ;
this . File . s etToolTi pText ( " Menu Fichero " ) ;
this . File . setMnemonic ( KeyEvent . VK_F ) ;
...
this . New = new JMenuItem ( " New " ) ;
...
this . Edit = new JMenuItem ( " Edit " ) ;
...
this . N o v i c e U s e r P r o f i l e M e n u = new JMenuBar () ;
this . A d v a n c e d U s e r P r o f i l e M e n u = new JMenuBar () ;
}
public JMenuBar g e t P r o f i l e N o v i c e U s e r () {
this . N o v i c e U s e r P r o f i l e M e n u . add ( this . File ) ;
this . File . add ( this . New ) ;
...
return N o v i c e U s e r P r o f i l e M e n u ;
}
public JMenuBar g e t P r o f i l e A d v a n c e d U s e r () {
this . A d v a n c e d U s e r P r o f i l e M e n u . add ( this . File ) ;
this . File . add ( this . New ) ;
...
this . File . add ( this . Edit ) ;
...
return A d v a n c e d U s e r P r o f i l e M e n u ;
}
}
12
D.
Ejercicio de transformación: árboles de sintaxis concreta
Este ejercicio consiste en transformar un árbol de sintaxis concreta en un árbol de sintaxis abstracta
de cierto lenguaje. Supongase que un parser reconoce un lenguaje conforme a cierta gramática en
BNF. El resultado de la ejecución del parser podrı́a ser un modelo de acuerdo a un metamodelo para
representar árboles de sintaxis concreta. Para crear la sintaxis abstracta del lenguaje, se debe definir
una transformación de modelos.
Ası́, dada una gramática que reconoce un lenguaje y genera un modelo del árbol de sintaxis concreta
que contiene el resultado de reconocer el lenguaje, con una transformación modelo-modelo se crea un
modelo de la sintaxis abstracta de dicho lenguaje.
Se utilizará el metamodelo mostrado en 4a para representar el árbol de sintaxis concreta de cualquier lenguaje reconocido por el parser “imaginario” de este ejercicio. Un posible lenguaje serı́a el que
se muestra a continuación, llamado “struct”, y que sirve para representar estructuras de datos:
struct Person {
fullname : String;
address : String;
age
: Integer;
}
El modelo de sintaxis concreta que se generarı́a al reconocer el código anterior serı́a el que se
muestra en la Figura 4b. Un objeto de tipo Leaf corresponde a un token (o sı́mbolo terminal) de la
gramática usada para reconocer el lenguaje. Un objeto Node corresponde a la aplicación de una regla
gramátical (un no-terminal), que probablemente tendrá uno o más hijos (bien nodos u hojas).
(a) Metamodelo de sintaxis concreta
(b) Modelo de ejemplo
(c) Metamodelo del lenguaje “struct”
Figura 4: (a) Metamodelo para representar el árbol de sintaxis concreta que puede reconocer un parser.
(b) Posible modelo para un código del lenguaje “struct”. (c) Metamodelo lenguaje “struct”
Se asume que, para el lenguaje “struct”, cualquier programa válido satisface las restricciones que
se muestran a continuación (la palabra clave context indica que los invariantes que van entre do - end
se aplican a la metaclase especificada, mientras que inv indica un invariante, y el código entre do end es la expresión que evalua el invariante).
context CST :: Node do
inv ’ struct ’ do
( self . kind == ’ struct ’) . implies (
self . children . select { | n | n . kind_of ? CST :: Leaf }. size == 2 &&
13
self . children . select { | n | n . kind_of ? CST :: Leaf }. any ? { | n | n . kind == ’ name ’ }
)
end
inv ’ body ’ do
( self . kind == ’ body ’) . implies (
self . children . all ? { | n | n . kind_of ? CST :: Node } &&
self . children . all ? { | n | n . kind == ’ attribute ’ }
)
end
inv ’ attribute ’ do
( self . kind == ’ attribute ’) . implies (
self . children . size == 2
&&
self . children . all ? { | n | n . kind_of ? CST :: Leaf } &&
self . children . any ? { | n | n . kind == ’ name ’ }
&&
self . children . any ? { | n | n . kind == ’ type ’ }
)
end
end
context CST :: Leaf do
inv ’ ptype ’ do
( self . kind == ’ type ’) . implies (
[ ’ String ’ , ’ Integer ’ , ’ Any ’ ]. include ?( self . value )
)
end
end
La Figura 4c muestra el metamodelo del lenguaje “struct”. La metaclase Data agrega una o más
definiciones de estructuras. Un struct está compuesta de atributos. Un attribute tiene un nombre y un
tipo, que obligatoriamente tiene que ser uno de los siguientes “String”, “Integer” o “Any” (se puede
ver como un puntero a otra estructura).
D.1.
Tarea
Escribir una transformación modelo-modelo con RubyTL que crea un modelo de la sintaxis abstracta para el lenguaje “struct”.
D.2.
Ayuda
Los siguientes apuntes pueden servir de ayuda para escribir la transformación requerida en el
ejercicio.
Decoradores. Los decoradores permiten añadir métodos a una metaclase en tiempo de ejecución.
Esto es útil para factorizar código de navegación. Por ejemplo, el siguien decorador añade un
método a la metaclase Node para devolver todos los sus hijos que sean de tipo Node (esto es, no
considera los objetos de tipo Leaf).
decorator CST :: Node do
def nodes
self . children . select { | n | n . kind_of ?( CST :: Node ) }
end
end
Enumerados.
Para
acceder
a
un
valor enumerado, se debe usar la sintaxis :::
Por ejemplo, en este ejercicio se necesita acceder a los
Paquete::TipoEnumerado::NombreDelLiteral.
siguientes tres literales.
Struct :: StructType :: Integer
Struct :: StructType :: String
Struct :: StructType :: Any
Observese que los literales integer, string, any comienzan por minúscula en el metamodelo.
RubyTL convierte la primera letra a mayúsculas para hacerlos compatibles con Ruby.
Colecciones. Ruby proporciona métodos con un “estilo funcional” (parecidos a los de OCL) para
iterar sobre colecciones. Estos métodos normalmente reciben un iterador en forma de bloque de
código, que hace algo con cada valor de la colección. Los siguientes métodos pueden ser útiles
para este ejemplo (en Ruby una colección se define con []).
collection = [1 , 2 ,
collection . select {
collection . reject {
collection . find
{
3, 4,
|i| i
|i| i
|i| i
5]
% 2 == 0 }
% 2 == 0 }
== 3 }
=>
=>
=>
[2 , 4]
[1 , 3 , 5]
3
14
E.
Solución del ejercicio: árboles de sintaxis concreta
transformation ’cst2struct’
decorator CST::Node do
def nodes
self.children.select { |n| n.kind_of? CST::Node }
end
def leaves
self.children.select { |n| n.kind_of? CST::Leaf }
end
end
rule ’tree2data’ do
from CST::Tree
to SimpleStruct::Data
mapping do |tree, data|
data.structs = tree.nodes
end
end
rule ’struct2struct’ do
from CST::Node
to SimpleStruct::Struct
filter { |n| n.kind == ’struct’ }
mapping do |node, struct|
struct.name = node.leaves.find { |n| n.kind == ’name’ }.value
struct.attributes = node.nodes.find { |n| n.kind == ’body’ }.children
end
end
rule ’struct2struct’ do
from CST::Node
to SimpleStruct::Attribute
filter { |n| n.kind == ’attribute’ }
mapping do |node, struct|
struct.name = node.leaves.find { |n| n.kind == ’name’ }.value
type = node.leaves.find { |n| n.kind == ’type’ }.value
struct.type =
case type.upcase
when ’STRING’ then SimpleStruct::StructType::String
when ’INTEGER’ then SimpleStruct::StructType::Integer
when ’ANY’ then SimpleStruct::StructType::Any
end
end
end
15