sábado, 21 de febrero de 2009

Parseando en Scala (3): Modelo

Retomando, decíamos que el resultado de parsear es generar un abstract syntax tree (AST). Vamos a ser un poco más gráficos, para que no queden dudas de que se trata de algo bien facilongo.

Si el código PL/SQL a parsear fuera éste:

PROCEDURE myproc IS
BEGIN
s1;
s2;
IF cond1 THEN
s3;
ELSE
s4;
WHILE cond2 LOOP
s5;
s6;
IF cond 3 THEN
s7;
END IF;
END LOOP;
s8;
END IF;
END;
...nuestro objetivo lo vemos plasmado en este garabato:

Se ve bien clarita en el árbol la estructura sintáctica del código parseado. El árbol no es más que un grafo, compuesto por nodos y relaciones entre ellos.

Podemos identificar distintos tipos de nodos. En el dibujito, cada tipo de nodo está pintado de un color diferente (las sentencias en amarillo, etc.) Las relaciones (líneas) indican los subnodos. Las sentencias son nodos terminales, nunca tienen subnodos.

Para armar el AST necesitamos modelar los tipos de nodo, cada uno con sus características. Verán cómo la orientación a objetos convive en armonía con la programación funcional: el modelo es una jerarquía de clases. Todos los tipos de nodo son subclase de un nodo abstracto, pero también identifiqué una abstracción en el medio a la que llamé Code, ya veremos por qué. Este sería un pseudo-UML del modelo:



Y en Scala:

abstract class Node
abstract case class Code extends Node
case class Stmt(s: String) extends Code
case class Cond(c: String, cThen: List[Code], cElse: List[Code]) extends Code
case class Loop(head:String, body: List[Code]) extends Code
case class WhenExcep(e: String, body: List[Code]) extends Node
case class ExcepSection(ee: List[WhenExcep]) extends Node
case class Block(content: List[Code], exceps:Option[ExcepSection]) extends Code
case class Module(typ: ModuleType, name: String, pars: Option[Stmt],
returns: Option[String], vars: List[Stmt], body: Block) extends Node

Como se puede ver, ninguna de estas clases define métodos; sólo atributos, que son seteados como parámetros del constructor. Además, son inmutables. El contenido relevante de cada tipo de nodo está en los parámetros: esto determina qué tipo de relaciones con subnodos puede tener en el grafo.

Para las colecciones de elementos, usamos List[T], la lista inmutable de la librería estándar de Scala. Para los elementos opcionales, usamos Option[T], cuyos posibles valores son Some[T] (con un valor de T específico) o None. Es mejor que manejar la opcionalidad chequeando por la referencia en null, pero no nos vayamos por las ramas. Vamos a comentar un poco el modelo (los que no conozcan PL/SQL, podrán imaginárselo más o menos).

El tipo de nodo "sentencia" (Stmt) sólo contiene un String, que es la sentencia misma en PL/SQL (no nos interesa la estructura interna, nuestro modelo es simplificado). No puede haber subnodos, como decíamos recién.

Las condiciones (Cond) son más complejas: tienen un String (que es la condición en sí), una sección para el "then" y otra para el "else". Cada una de estas secciones podría ser, en principio, una colección de sentencias, pero lo cierto es que puede haber cosas más diversas... ¡por eso fue necesaria la abstracción Code! Estos "elementos de código" pueden ser varias cosas (las cuatro subclases), pero no cualquier Node. El mismo Cond es un Code, y claro, porque puede haber condiciones anidadas.

Podría decirse que la sección "then" debe ser modelada diferente a la sección "else", porque en un IF de PL/SQL, la primera es obligatoria y la segunda opcional. Pero el modelo no necesariamente debe reflejar todas las reglas de sintaxis que aplican al parseo. Mi parser va a dar error si falta el "then", pero no si falta el "else"; eso está contemplado. Pero con una List vacía (Nil) puedo representar un "else" ausente. Asimismo el modelo tampoco refleja que el "then" tendrá por lo menos un elemento.

Con la clase Loop modelo los distintos tipos de loop, no me interesa diferenciarlos. Cada uno contendrá un encabezado (tipo de loop y condición), y un contenido que es lista de Code.

WhenExcep es el "catch" de excepciones de PL/SQL: contiene el nombre de la excepción y la lista de Code asociada.

ExcepSection es la sección final (dentro de un bloque) que contiene una sucesión de WhenExcep.

Block es un bloque BEGIN / END. Consiste en una lista de Code, y opcionalmente una sección de excepciones.

Finalmente, Module representa, obviamente, un módulo, que puede ser un procedimiento, una función o un bloque anónimo de PL/SQL. Tiene un tipo de módulo, un nombre, opcionalmente parámetros (aquí usamos Stmt, no porque los parámetros sean una sentencia, sino por simplificar), opcionalmente un tipo de retorno (aplicable a módulos función), declaraciones / variables (una lista de Stmt, también por simplificar), y un Block, que es el bloque de código de mayor nivel en el módulo.

Para los tipos de módulo uso esta definición:

abstract class ModuleType
case object MTProcedure extends ModuleType { override def toString = "procedure" }
case object MTFunction extends ModuleType { override def toString = "function" }
case object MTAnonymousBlock extends ModuleType { override def toString = "anonymous block" }

En la próxima: vamos a parsear de una vez.

No hay comentarios:

Publicar un comentario