Representar un Árbol de Sintaxis Abstracta en C


Estoy implementando un compilador para un lenguaje de juguete simple en C. Tengo un escáner y un analizador de trabajo, y un fondo razonable sobre la función conceptual/construcción de un AST. Mi pregunta está relacionada con la forma específica de representar un AST en C. He encontrado tres estilos con bastante frecuencia en diferentes textos / recursos en línea:

Una estructura por tipo de nodo.

Este tiene un nodo base "class"(struct) que es el primer campo en todas las estructuras secundarias. El el nodo base contiene una enumeración que almacena el tipo de nodo (constante, operador binario, asignación, etc.). Se accede a los miembros de la estructura mediante un conjunto de macros, con un conjunto por estructura. Se ve algo como esto:

struct ast_node_base {
    enum {CONSTANT, ADD, SUB, ASSIGNMENT} class;
};

struct ast_node_constant {
    struct ast_node_base *base;
    int value;
};

struct ast_node_add {
    struct ast_node_base *base;
    struct ast_node_base *left;
    struct ast_node_base *right;
};

struct ast_node_assign {
    struct ast_node_base *base;
    struct ast_node_base *left;
    struct ast_node_base *right;
};

#define CLASS(node) ((ast_node_base*)node)->class;

#define ADD_LEFT(node) ((ast_node_add*)node)->left;
#define ADD_RIGHT(node) ((ast_node_add*)node)->right;

#define ASSIGN_LEFT(node) ((ast_node_assign*)node)->left;
#define ASSIGN_RIGHT(node) ((ast_node_assign*)node)->right;

Una estructura por diseño de nodo.

Esto parece ser principalmente el mismo que el diseño anterior, excepto que en lugar de tener ast_node_add y ast_node_assign tendría un ast_node_binary para representar ambos, porque el diseño de las dos estructuras es el igual y solo difieren por el contenido de base- > clase. La ventaja de esto parece ser un conjunto más uniforme de macros(IZQUIERDA(nodo) para todos los nodos con una izquierda y derecha en lugar de un par de macros por), pero la desventaja parece que la comprobación de tipo C no será tan útil(no habría forma de detectar un ast_node_assign donde solo debería haber un ast_node_add, por ejemplo).

Una estructura total, con una unión para contener diferentes tipos de datos de nodo.

Un mejor la explicación de esto que puedo dar se puede encontrar aquí . Usando los tipos del ejemplo anterior se vería como:

struct ast_node {
  enum { CONSTANT, ADD, SUB, ASSIGNMENT } class;
  union { int                                 value;
          struct { struct ast_node* left;    
                   struct ast_node* right;  } op;
};

Me inclino a que la tercera opción sea la que más me guste porque hace que el recorrido recursivo sea mucho más fácil(ya que se evita mucho casting de puntero a favor de la unión), pero tampoco aprovecha la comprobación de tipo C. La primera opción parece la más peligrosa, ya que se basa en punteros a estructuras que se lanzan para acceder al miembro de cualquier nodo (incluso diferentes miembros del mismo nodo que requieren diferentes casos para acceder (base vs. izquierda)), pero estos casts se comprueban de tipo por lo que podría ser discutible. La segunda opción me parece la peor de ambos mundos, aunque tal vez me estoy perdiendo algo.

¿Cuáles de estos tres esquemas son los mejores, y por qué? ¿Hay una cuarta opción mejor que no haya encontrado todavía? Asumo que ninguno de ellos es una solución" única para todos", por lo que si importa el lenguaje que estoy implementando es un lenguaje imperativo de tipo estático, casi un pequeño subconjunto de C.

Una pregunta específica que tengo sobre el tercer diseño(unión). Si utilizo solo el campo valor, ¿habrá espacio vacío después del valor para acomodar la posibilidad de que se escriba op?

Author: user1547129, 2014-01-16

2 answers

Puede hacer que cualquiera de estos funcione.

Prefiero el diseño de unión, porque entonces todos los nodos tienen "el mismo" diseño.

[Puede resultarle útil tener una opción de "sublista hija", por ejemplo, y una matriz dinámica de hijos arbitrariamente grande, en lugar de tener listas inclinadas a la izquierda o a la derecha.]

Usted va a encontrar que este problema no es el que hace que la construcción de su compilador difícil. Más bien, es tener tablas de símbolos, realizar varios tipos de análisis, elegir un nivel de máquina IR, construyendo un generador de código, y haciendo optimizaciones de código. Entonces vas a encontrar usuarios reales y descubrirás lo que realmente hiciste mal: -}

Elegiría uno y correría con él, para que tengas la oportunidad de acercarte a los otros problemas.

 16
Author: Ira Baxter,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-01-16 05:59:11

Ira Baxter te dio una buena respuesta simple y con visión de futuro , especialmente de nota son los problemas que uno encontrará en el futuro, así que me centraré en esta pregunta:

¿Hay una cuarta opción mejor que no haya encontrado todavía?

Está utilizando el lenguaje imperativo para escribir un compilador y tiene problemas para diseñar la estructura de datos para el concepto de nodo en el AST. En el mundo de los lenguajes funcionales como ML, OCaml, Haskell, F # one usaría un Tagged union para mantener todos los diferentes tipos de nodos en una estructura de datos, que es básicamente lo que ha creado.

No espero que el OP cambie a un lenguaje funcional para este problema, pero si otros tratan regularmente con árboles, entonces podrían encontrar de valor aprender un lenguaje funcional y usarlo para problemas relacionados con los árboles.

 1
Author: Guy Coder,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:24:34