Integración Jlex con Cup

Ya que sabemos como hacer archivos de entrada para Jlex y Cup ahora es hora de hacer que funcionen en conjunto. Para hacer de mas ilustrativo el ejemplo usaremos una clase externa al scanner y al parser que nos servirá para almacenar información de cada token que se esta leyendo. Llamaremos a esta clase token

class token(){
int posicionX;
int posicionY;
String valor;
public token(String val,int x,int y){
   this.valor=val;
   this.posicionX=x;
   this.posicionY=y;
}
    public int getX(){return this.posicionX;}
    public int getY(){ return this.posicionY;}
    public String getValor(){return this.valor;}
}

Seguiremos con el ejemplo de la expresión condicional , para esto debemos escribir el archivo jlex para que reconozca las palabras reservadas ó terminales del lenguaje. El archivo quedaría de la siguiente forma:

import java_cup.runtime.Symbol;
%%
%{
  public void imprime(String str){
    System.out.println(str+"-"+yychar+"-"+yyline);
  }
%}
%class lexc
%public
%char
%line
%ignorecase
%cup
%full
%type java_cup.runtime.Symbol
%implements java_cup.runtime.Scanner
%eofval{
System.out.println("FIN DEL ARCHIVO");
return null;
%eofval}
letra=[a-zA-Z]
entero=[0-9]
id=[a-zA-Z][A-Za-z0-9]*
%% 
"(" {imprime("Abre Parentesis");
      return new Symbol(csym.open_par,new token(yytext(),yychar,yyline));
      }
")" {imprime("Cierra Parentesis");
      return new Symbol(csym.close_par,new token(yytext(),yychar,yyline));
"true" {
	  imprime("true");
	  return new Symbol(csym.true_,new token(yytext(),yychar,yyline));
      }
"false" {
	  imprime("false");
	  return new Symbol(csym.false_,new token(yytext(),yychar,yyline));
	}
"<=" {imprime("menor igual");
      return new Symbol(csym.menor_igual,new token(yytext(),yychar,yyline));
    }
">=" {imprime("mayor igual");
      return new Symbol(csym.mayor_igual,new token(yytext(),yychar,yyline));
      }
"||" {imprime("or");
	return new Symbol(csym.or_,new token(yytext(),yychar,yyline));
      }
"&&" {imprime("and");
	return new Symbol(csym.and_,new token(yytext(),yychar,yyline));
      }
"==" {imprime("igual_igual");
	return new Symbol(csym.igual_igual,new token(yytext(),yychar,yyline));
      }
"!=" {imprime("no igual");
	return new Symbol(csym.no_igual,new token(yytext(),yychar,yyline));
      }
({id})+("_")*({id})* {imprime("id");
	return new Symbol(csym.id,new token(yytext(),yychar,yyline));
	}
{entero}+ {imprime("entero");
	  return new Symbol(csym.entero,new token(yytext(),yychar,yyline));
	}
[\t\r\f]  {}
[\n] {yychar=0;}
" " {}
 . {imprime("error: "+yytext());
    }

Explicaré el fragmento de código que se utilizó al lado derecho:

")" {imprime("Cierra Parentesis");
      return new Symbol(csym.close_par,new token(yytext(),yychar,yyline));}

Se llama la función imprime que se definió al inicio del archivo. Ahora bien, el scanner es construido de tal manera que dentro de el existe una funcion con una sentencia de control donde se decide que tipo de token se esta leyendo y que valor retornar. Allí reside el hecho de escribir el return. Como podemos ver se retorna un tipo de dato Symbol, este en su constructor recibe dos parámetros :

  • El primero que es un entero que es declarado en la clase sym generada por cup (Explicaré mas adelante)
  • El segundo que recibe es de tipo Object, esto nos facilita poder enviar cualquier tipo de dato que deseemos., en este caso fue un tipo token, el que hemos definido al inicio de esta sección

.Una vez terminado nuestro archivo jlex es hora de escribir el archivo cup:

action code{:
 public void ImprimeValor(String str){
  System.out.println("el valor del token"+str) ;
   }
:}
parser code{:
public void syntax_error(Symbol st){
    token t=(token)st.value;
    report_error("Error Sintactico:"+ t.getValue()+"- "+t.getX()+"-"+t.getY(),null);
:}

/*Declaracion de variables no terminales*/
non terminal token COND, OREXP,ANDEXP,IGEXP,CMP,SIMBOLOSCOMPARAR,TIPO_DATO;
/*DECLARACION DE VARIABLES TERMINALES */
terminal token or_,and_,igual_igual,no_igual,mayor, menor, mayor_igual,menor_igual,
open_par,close_par,id,numero,true,false;
Start with COND; // start with sirve para indicarle al parser con que produccion empezar
COND::=OREXP;
OREXP::=OREXP or_ ANDEXP
    |ANDEXP;

ANDEXP::=ANDEXP and_ IGEXP
      |IGEXP;

IGEXP::= IGEXP igual_igual CMP
      |IGEXP no_igual CMP
      |CMP;

SIMBOLOS_COMPARAR::=mayor:m{:RESULT=m:}
		|menor:m{:RESULT=m:}
		|mayor_igual:m{:RESULT=m:}
		|menor_igual:m{:RESULT=m:};

CMP::= CMP:c SIMBOLOS_COMPARAR:sc TIPO_DATO:t{:
                String val1=c.getValor();
                String val2=t.getValor(); 
           if(sc.getValor().equals(">")){
                ImprimeValor(val1+"mayor"+val2);
            }
            if(sc.getValor().equals("<")){
                ImprimeValor(val1+"menor"+val2);
            }
            if(sc.getValor().equals("<=")){
                ImprimeValor(val1+"menor igual"+val2);
             }
            if(sc.getValor().equals(">=")){
                ImprimeValor(val1+">="+val2);
            }
      :}
      |TIPO_DATO:T{:RESULT=T;:}
      |open_par COND:c close_par{:RESULT=c;:} ;
TIPO_DATO::= id:i{:RESULT=i; :}
          |numero:n{:RESULT=n;:}
          |true:t {:RESULT=t;:}
          |false:f{:RESULT=t;:};

Llego la hora de explicar cada parte de el archivo cup:

  • En la sección action code como expliqué anteriormente se definen las funciones que que utilizaran cuando se este recorriendo la gramática, en este caso definí la función ImprimeValor que recibe de parámetro una cadena, lo único que hace es imprimir el valor de la cadena que recibe de parámetro.
  • En la sección parser code se encuentran los métodos propios del parser, aqui hice un override de la función syntax_error. Esta función nos permite ejecutar una acción cuando el parser encuentra un error sintáctico.
  • En la declaración de terminales y no terminales se definen como tipo token para poder manejar usarlos de una forma mas cómoda en las acciones de la gramática.
  • Ahora vamos con las acciones en la gramatica, Cup permite agregar acciones como las que podemos ver en el ejemplo anterior estas pueden ir en cualquier lugar del lado derecho de la producción en este tipo de herramientas se sugiere ponerlas al final para evitar  ambigüedades ya que Cup toma la producción como un símbolo las en la gramática y al estar en el medio pueden haber errores de reducción o de movimiento del parser. La sintaxis para para las acciones es: {: /*acciones*/ :} donde acciones puede ser cualquier sentencia de código.
  • La variable RESULT es usada por CUP para devolver el valor al padre de la producción, básicamente devuelve el valor asignado  al no terminal del lado derecho. Este valor debe ser el mismo tipo de dato que el no terminal obviamente.
En el ejemplo se imprimirán los operadores relacionales en forma de texto junto con los valores de cada miembro de la expresión.

11 comentarios to “Integración Jlex con Cup”

  1. Que excelente tu tutorial, te felicito, esta bien explicado, me ha servido mucho.

  2. Muchísimas Gracias, en la UC3M de madrid , tardan un mes en explicar esto, y lo peor que que no te enteras de nada!

  3. donde mandas llamar a syntax_error(Symbol st)

  4. gracias me fue de ayuda

  5. Josue: soy algo nuevo en esto, pero como enlazas la clase token con el archivo jlex y cup???

  6. Hola, mira soy nuevo en esto y me gustaría que me enviaras el código completo por favor!! (Archivo .cup, .lex, clase token)
    Gracias

  7. Excelente tu ejemplo, pocos hacen referencia a la variable RESULT. Tengo una duda, como podría almacenar el valor de una variable y utilizarla en una futura operación con la complicación de que puede haber mas de una variable declarada en el texto. Gracias!

Deja un comentario