domingo, 8 de marzo de 2009

Parseando en Scala (5): El código

A continuación, el módulo para parsear PL/SQL en Scala. Insisto en que no es un parseador completo, pero por ahora me sirve así nomás. Disculpen si ven alguna desprolijidad... más abajo explico algunas cosas.

import scala.util.parsing.combinator.ImplicitConversions
import scala.util.parsing.combinator.syntactical.StdTokenParsers

class PLParser extends StdTokenParsers with ImplicitConversions {
type Tokens = PLLexical
val lexical = new PLLexical
lexical.delimiters ++= Set(";")
lexical.reserved ++= Set("procedure","begin","end","if","then","else","is","as","elsif",
                        "when","exception","loop","for","while", "function", "declare")

def regularStr: Parser[String] = rep1(ident | stringLit | "is") ^^ {s => s.mkString(" ")}

def stmt: Parser[Stmt] = regularStr <~ ";" ^^ {s => Stmt(s.mkString(""))}

def varDecl:Parser[Stmt] = (regularStr ~ opt("exception")) <~ ";" ^^      {(s:String,e:Option[String]) => Stmt(s.mkString("") + " " + e.getOrElse(""))}

def params: Parser[Stmt] = rep1(ident) ^^ {s => Stmt(s.mkString(" "))}

def loop: Parser[Loop] = "loop" ~> rep1(code) <~ ("end" ~ "loop" ~ ";") ^^ { c => Loop("loop", c) }

def forWhileLoop: Parser[Loop] = ("while" | "for") ~ (regularStr <~ "loop") ~ (rep1(code) <~ ("end" ~ "loop" ~ ";")) ^^ { (t:String, h:String, c:List[Code]) => Loop(t + " " + h, c) }

def cond: Parser[Cond] = (
 ("if" ~> regularStr <~ "then") ~ rep1(code) ~ rep(("elsif" ~> regularStr <~ "then") ~ rep1(code)) ~ opt("else" ~> rep1(code)) <~ ("end" ~ "if" ~ ";")) ^^ { (sif:String, ifthen:List[Code], elsifs:List[String ~ List[Code]], ifelse:Option[List[Code]]) =>
   val startCond = Cond(sif, ifthen, Nil)
   buildCond(startCond, elsifs, ifelse)
 }

def block: Parser[Block] = ("begin" ~> rep1(code)) ~
 opt("exception" ~> rep1(whenExcep)) <~ ("end" ~ opt(ident) ~ ";") ^^ {(s:List[Code], es:Option[List[WhenExcep]]) =>
  Block(s, if(es.isDefined) Some(ExcepSection(es.get)) else None) }

def code = stmt | block | cond | loop | forWhileLoop

def whenExcep: Parser[WhenExcep] = ("when" ~> ident <~ "then") ~ rep1(code) ^^ { (e:String,s:List[Code]) => WhenExcep(e,s) }

def excepSection: Parser[ExcepSection] = "exception" ~> rep1(whenExcep) ^^ { ee => ExcepSection(ee) }

def procedure: Parser[Module] = (
   ("procedure" ~> ident) ~
   opt(params) ~
   (("is" | "as") ~> rep(stmt)) ~
   block) ^^
   {(s:String, p:Option[Stmt], v:List[Stmt], b:Block) => Module(MTProcedure, s, p, None, v, b)}

def function: Parser[Module] = (
   ("function" ~> ident) ~
   opt(params) ~
   ("return" ~> regularStr) ~
   (("is" | "as") ~> rep(stmt)) ~
   block) ^^
   {(s:String, p:Option[Stmt], rt:String, v:List[Stmt], b:Block) => Module(MTFunction, s, p, Some(rt), v, b)}

def anonModule: Parser[Module] = (
   ("declare" ~> rep(varDecl)) ~ (opt("is" | "as")  ~> block)) ^^
   {(v:List[Stmt], b:Block) => Module(MTAnonymousBlock, "", None, None, v, b)}

def module: Parser[Module] = anonModule | procedure | function

private def buildCond(c:Cond, elsifs:List[String ~ List[Code]], ifelse:Option[List[Code]]):Cond = elsifs match {
 case Nil => Cond(c.c, c.cThen, ifelse getOrElse Nil)
 case x::xs => x match {
   case i ~ ss => Cond(c.c, c.cThen, List(buildCond(Cond(i, ss, Nil), xs, ifelse)))
 }
}

}


Extendemos StdTokenParsers porque allí se definen algunos Parsers básicos que aprovecharemos. ImplicitConversions incluye conversiones útiles para usar el operador ^^.

El trait StdTokenParsers requiere la definición de Tokens como subtipo de StdTokens:

trait StdTokenParsers extends TokenParsers {
 type Tokens <: StdTokens         // ...   }  


Podríamos haber declarado que el type Tokens fuese StdLexical, un parser léxico provisto por la librería estándar de Scala. En cambio usamos PLLexical, que es lo mismo pero sin diferenciar entre mayúsculas y minúsculas, porque así es PL/SQL. Ese módulo lo agregué yo al proyecto pero no lo vamos a copiar acá, sólo tengan en cuenta que StdLexical asume código case-sensistive. Si el código a parsear no lo es, hay que meter mano por ahí. 
Declaramos un objeto lexical de tipo PLLexical. 
Como se muestra, agregamos los delimitadores que corresponda (el punto y coma finaliza sentencias en PL/SQL), y una colección de palabras reservadas. Todas las palabras que yo quiera reconocer al definir los parser combinators deben estar en esta lista para que todo funcione. 
Y listo, el resto son todas las funciones para parsear el lenguaje en cuestión como se explicó en la entrada anterior
La definición de regularStr como "cadena regular" es auxiliar, me sirve como base para otros parsers para los que no necesito reconocer la estructura interna: sentencias, declaraciones de variables, y expresiones. Se define como una repetición arbitraria de ident (identificadores, definido en StdTokenParsers), stringLit (literales de cadena, idem) y la palabra reservada "is". 
Un sentencia (stmt) se define como un regularStr seguido del delimitador ";". Una declaración de variable también puede ser aceptada como una sentencia con la salvedad de que si define una excepción termina en "exception"... al ser ésta una palabra reservada según indicamos arriba, no entrará como parte de un stringLit. Obsérvese cómo, a pesar de que varDecl es una definición diferente a stmt, en ambos casos generamos un nodo Stmt, porque no necesitamos mayor detalle en el modelo
Si me vinieron siguiendo en esta serie, las piezas deberían estar encajando. Voy a detenerme un poco en la definición de cond. Al principio comentaba los problemas que tenía para parsear el ELSIF en previos intentos. El ELSIF de PL/SQL permite un aspecto "aplanado" a lo que en el fondo son IFs anidados. En un diagrama desearíamos ver ese anidamiento correctamente, por lo tanto debemos reconstruirlo. Así es cómo lo logré: 

def cond: Parser[Cond] = (
 ("if" ~> regularStr <~ "then") ~ rep1(code) ~    rep(("elsif" ~> regularStr <~ "then") ~ rep1(code)) ~    opt("else" ~> rep1(code)) <~    ("end" ~ "if" ~ ";")) ^^    { (sif:String, ifthen:List[Code], elsifs:List[String ~ List[Code]],       ifelse:Option[List[Code]]) =>
   val startCond = Cond(sif, ifthen, Nil)
   buildCond(startCond, elsifs, ifelse)
}

Lo que vemos a la izquierda del ^^ es simplemente el cuasi-EBNF que describe la estructura de los IF: el bloque THEN es obligatorio (rep1), los ELSIF pueden no existir o ser varios (rep), y el ELSE final es opcional (opt). Para construir el nodo, me valgo de esta función auxiliar recursiva:
 private def buildCond(c:Cond, elsifs:List[String ~ List[Code]],
             ifelse:Option[List[Code]]):Cond = elsifs match {
   case Nil => Cond(c.c, c.cThen, ifelse getOrElse Nil)
   case x::xs => x match {
     case i ~ ss => Cond(c.c, c.cThen, List(buildCond(Cond(i, ss, Nil), xs, ifelse)))
   }
 }

Finalmente, les muestro cómo podemos crear una aplicación que invoque el parser:
object PLParserMain extends Application with PLParser {
 val theCode = // ... obtener el código de un archivo o la fuente que corresponda
 val result = module(new lexical.Scanner(theCode.toString))
 result match {
  case Success(e, _) => Console.println("OK!\n" )
  case Failure(msg, _) => Console.println("[Failure] " + msg)
  case Error(msg, _) => Console.println("[Error] " + msg)
 }
}

Al extender PLParser, estamos integrando todo lo que ese trait define a nuestro objeto aplicación. Vemos cómo pasar el código al parser de módulo (module, que definimos en PLParser), porque un módulo es lo que esperamos encontrar como primer nivel en el código a parsear, y resultará en un nodo Module como raíz del AST.

Claro que hasta acá no hicimos más que parsear y reportar un resultado: OK, falla o error. En el case Success tenemos disponible el AST completo (en este caso bajo el nombre e) con el que podemos hacer lo que se nos ocurra.

Sobre eso, más en la próxima...

Actualización 16/5/09:  La versión actual de PLParser tuvo varias mejoras respecto a la publicada en este post. Ahora el modelo diferencia a las sentencias de las declaraciones de variables y los parámetros. Para eso tuve que, entre otras cosas, definir más símbolos como delimiters aparte del punto y coma. Ya no utilizo esa bolsa de gatos del "regularStr".