2012-01-07 10 views
17

El libro 'Modern Compiler Design' es el buen libro sobre compiladores. En su código fuente, algo que me molesta es AST o Abstract Syntax Tree. Supongamos que queremos escribir un analizador de expresiones entre paréntesis que analiza algo como: ((2+3)*4) * 2! El libro dice que tenemos un AST como:Recursive Descent Parser

 ((2+3)*4) * 2 
     / |  \ 
     (2+3) *4 * 2 
     / | \ 
    (2+3) * 4 
    /| \ 
    2 + 3 

Así que debo guardar un árbol en memoria o simplemente utilizar llamadas recursivas; Nota: si no lo guardo en la memoria, ¿cómo puedo convertirlo a código de máquina?

código Analizador:

int parse(Expression &expr) 
{ 
    if(token.class=='D') 
    { 
    expr.type='D'; 
    expr.value=token.val-'0'; 
    get_next_token(); 
    return 1; 
    } 
    if(token.class=='(') 
    { 
    expr.type='P'; 
    get_next_token(); 
    parse(&expr->left); 
    parse_operator(&expr->op); 
    parse(&expr->right); 
    if(token.class!=')') 
     Error("missing)"); 
    get_next_token(); 
    return 1; 
    } 
    return 0; 
} 

gramática es:

expr -> expr | (expr op expr) 
digit -> 0|1|2....|9 
op -> +|* 

Respuesta

18

Puede almacenar el árbol en la memoria o se puede producir directamente el código de salida requerida. El almacenamiento de la forma intermedia normalmente se realiza para poder realizar algún procesamiento en el código en un nivel superior antes de generar la salida.

En su caso, por ejemplo, sería fácil descubrir que su expresión no contiene variables y, por lo tanto, el resultado es un número fijo. Sin embargo, mirando solo un nodo a la vez, esto no es posible. Para ser más explícito, si después de mirar "2 *" genera código de máquina para calcular el doble de algo, este código se desperdicia cuando la otra parte es, por ejemplo, "3" porque su programa calculará "3" y luego calculará el código el doble de ese cada vez mientras que solo cargar "6" sería equivalente pero más corto y más rápido.

Si desea generar el código de máquina, primero necesita saber para qué tipo de máquina se va a generar el código ... el modelo más simple es un enfoque basado en la pila. En este caso, no necesita lógica de asignación de registro y es fácil compilar directamente en el código de máquina sin la representación intermedia. Considere este pequeño ejemplo que solo maneja números enteros, cuatro operaciones, negación unaria y variables ... notará que no se usa ninguna estructura de datos: los caracteres del código fuente se leen y las instrucciones de la máquina se escriben en la salida ...

#include <stdio.h> 
#include <stdlib.h> 

void error(const char *what) 
{ 
    fprintf(stderr, "ERROR: %s\n", what); 
    exit(1); 
} 

void compileLiteral(const char *& s) 
{ 
    int v = 0; 
    while (*s >= '0' && *s <= '9') 
    { 
     v = v*10 + *s++ - '0'; 
    } 
    printf(" mov eax, %i\n", v); 
} 

void compileSymbol(const char *& s) 
{ 
    printf(" mov eax, dword ptr "); 
    while ((*s >= 'a' && *s <= 'z') || 
      (*s >= 'A' && *s <= 'Z') || 
      (*s >= '0' && *s <= '9') || 
      (*s == '_')) 
    { 
     putchar(*s++); 
    } 
    printf("\n"); 
} 

void compileExpression(const char *&); 

void compileTerm(const char *& s) 
{ 
    if (*s >= '0' && *s <= '9') { 
     // Number 
     compileLiteral(s); 
    } else if ((*s >= 'a' && *s <= 'z') || 
       (*s >= 'A' && *s <= 'Z') || 
       (*s == '_')) { 
     // Variable 
     compileSymbol(s); 
    } else if (*s == '-') { 
     // Unary negation 
     s++; 
     compileTerm(s); 
     printf(" neg eax\n"); 
    } else if (*s == '(') { 
     // Parenthesized sub-expression 
     s++; 
     compileExpression(s); 
     if (*s != ')') 
      error("')' expected"); 
     s++; 
    } else { 
     error("Syntax error"); 
    } 
} 

void compileMulDiv(const char *& s) 
{ 
    compileTerm(s); 
    for (;;) 
    { 
     if (*s == '*') { 
      s++; 
      printf(" push eax\n"); 
      compileTerm(s); 
      printf(" mov ebx, eax\n"); 
      printf(" pop eax\n"); 
      printf(" imul ebx\n"); 
     } else if (*s == '/') { 
      s++; 
      printf(" push eax\n"); 
      compileTerm(s); 
      printf(" mov ebx, eax\n"); 
      printf(" pop eax\n"); 
      printf(" idiv ebx\n"); 
     } else break; 
    } 
} 

void compileAddSub(const char *& s) 
{ 
    compileMulDiv(s); 
    for (;;) 
    { 
     if (*s == '+') { 
      s++; 
      printf(" push eax\n"); 
      compileMulDiv(s); 
      printf(" mov ebx, eax\n"); 
      printf(" pop eax\n"); 
      printf(" add eax, ebx\n"); 
     } else if (*s == '-') { 
      s++; 
      printf(" push eax\n"); 
      compileMulDiv(s); 
      printf(" mov ebx, eax\n"); 
      printf(" pop eax\n"); 
      printf(" sub eax, ebx\n"); 
     } else break; 
    } 
} 

void compileExpression(const char *& s) 
{ 
    compileAddSub(s); 
} 

int main(int argc, const char *argv[]) 
{ 
    if (argc != 2) error("Syntax: simple-compiler <expr>\n"); 
    compileExpression(argv[1]); 
    return 0; 
} 

Por ejemplo ejecuta el compilador con 1+y*(-3+x) como entrada que se obtiene como salida

mov eax, 1 
push eax 
mov eax, dword ptr y 
push eax 
mov eax, 3 
neg eax 
push eax 
mov eax, dword ptr x 
mov ebx, eax 
pop eax 
add eax, ebx 
mov ebx, eax 
pop eax 
imul ebx 
mov ebx, eax 
pop eax 
add eax, ebx 

Sin embargo este enfoque de los compiladores de escritura no se adapta bien a un compilador de optimización.

Si bien es posible obtener algo de optimización agregando un optimizador "mirilla" en la etapa de salida, muchas optimizaciones útiles son posibles solo mirando el código desde un punto de vista más alto.

También la generación de código de máquina simple podría beneficiarse al ver más código, por ejemplo, decidir qué registro asignar a qué o decidir cuál de las posibles implementaciones del ensamblador sería conveniente para un patrón de código específico.

Por ejemplo, la misma expresión podría ser compilado por un compilador de optimización a

mov eax, dword ptr x 
sub eax, 3 
imul dword ptr y 
inc eax 
2

Puede crear un AST con Dijkstra Shunting-yard algorithm.

Sin embargo, en algún momento tendrá la expresión completa o AST en memoria, a menos que calcule resultados inmediatos durante el análisis. Esto funciona con (sub) expresiones que contienen solo literales o constantes de tiempo de compilación, pero no con ninguna variable calculada en tiempo de ejecución.

4

Nueve de cada diez veces se guardará el AST en la memoria para lo que sea que esté haciendo después de realizar el léxico y el análisis.

Una vez que tenga un AST se puede hacer una serie de cosas:

  • evaluarlo directamente (tal vez utilizando la recursividad, tal vez usando su propia pila de encargo)
  • transformarla en alguna otra salida, tal como se código en otro idioma o algún otro tipo de traducción.
  • Compilarlo a la instrucción preferida que se
  • etc.
0

Así que debo guardar un árbol en memoria o simplemente utilizar llamadas recursivas;

Utilizará llamadas recursivas en su analizador sintáctico para construir el árbol en la memoria.

Y, por supuesto, desea mantener el árbol en la memoria para procesarlo.

Un compilador de optimización guarda varias representaciones del código en la memoria (y las transforma).

0

La respuesta a la pregunta depende de si desea un compilador, un intérprete o algo intermedio (un intérprete encerrado en un idioma intermedio). Si desea un intérprete, un analizador de descenso recursivo evaluará al mismo tiempo la expresión, por lo que no es necesario mantenerla en la memoria. Si desea un compilador, una expresión constante como el ejemplo puede y debe optimizarse, pero la mayoría de las expresiones operarán en variables, y deberá convertir a forma de árbol como un paso intermedio antes de convertirlo a una forma lineal.

Un compilador/intérprete híbrido generalmente compilará expresiones, pero no tiene que ser así. A menudo es una forma barata de escribir un programa que emite un ejecutable para simplemente envolver el intérprete con el código fuente. Matlab usa esta técnica, código que solía compilarse genuinamente, pero había problemas de consistencia con la versión interactiva. Sin embargo, no permitiría que la dificultad de generar un árbol de análisis sintáctico para las expresiones determine el problema.