População da tabela de símbolos após análise;Construção de compilador
-
13-12-2019 - |
Pergunta
Depois de criar a árvore de análise, preciso preencher a tabela de símbolos agora.
Eu tenho que armazenar informações como
Tipo, escopo, deslocamento etc. para os identificadores.
Agora, como posso saber o tipo e o escopo dos identificadores, já que tudo que sei é o valor do lexema e o número da linha desse ID específico (após análise lexical).
Como é que eu entendi tudo isso?obrigado.
Solução
Agora, como eu conheço o tipo, escopo dos identificadores, pois tudo o que sei é o valor do lexeme e o número da linha para esse ID específico (após a análise lexical).
Como o EJP mencionou, você precisa percorrer a árvore de análise.Sua árvore deveria ter sido criada para que você possa fazer uma travessia em ordem, visitando cada nó na mesma ordem em que as instruções e expressões do código-fonte são avaliadas.Os nós da sua árvore também devem corresponder a uma construção de linguagem específica, por exemplo. WhileStmtNode
, MethodDeclNode
, etc.
Suponha que eu esteja construindo a tabela de símbolos, percorrendo recursivamente a árvore, e acabei de inserir um nó do corpo do método.Eu poderia ter algo como o seguinte:
public void doAction(MethodBodyNode methodBody) {
currScope = 2;
methodBody.getExpr().applyAction(this);
currScope = 2;
}
Mantenho uma variável global para gerenciar o escopo.Cada vez que entro em um bloco onde o escopo muda, eu incremento currScope
.Da mesma forma, eu manteria currClass
e currMethod
variáveis para armazenar com o nome do símbolo, tipo, deslocamento, etc.para fases posteriores.
Atualizar:
Agora diga, eu estou atravessando a árvore, toda vez que eu me deparo com um ID i teria que inserir o valor para a tabela de símbolos junto com o tipo, escopo e outros, digamos para escopo eu verifico se eu me deparo com '{' ou nome da função, mas como faço para saber que tipo de ID é este.
Cada nó da árvore deve conter todas as informações necessárias para toda a construção.Se estiver usando um gerador de analisador, como CUP ou Bison, você poderá especificar como construir a árvore nas ações gramaticais.Por exemplo.
variableDeclaration::= identifier:i identifier:i2 SEMICOLON {: RESULT = new VarDeclNode(i, i2, null); :};
identifier::= ID:i {: RESULT = new IdNode(i.getLineNum(), i.getCharNum(), i.getStringValue()); :};
Essas produções combinariam Foo f;
e anexe um nó de declaração de variável à árvore.Esse nó encapsula dois nós identificadores que contêm o número da linha, o número do caractere e o valor da string do lexema.O primeiro nó identificador é o tipo e o segundo é o nome da variável. ID
é um símbolo de terminal retornado pelo lexer ao combinar um identificador.Presumo que você esteja familiarizado com isso até certo ponto.
public class VarDeclNode extends StmtNode {
private IdNode id;
private IdNode type;
private ExprNode expr;
public VarDeclNode(IdNode id, IdNode type, ExprNode expr) {
super();
this.id = id;
this.type = type;
this.expr = expr;
}
}
Quando você tem uma árvore de sintaxe com nós como este, você tem todas as informações que precisa.
2ª atualização:
Não importa se você está usando um analisador personalizado ou gerado, há um ponto distinto onde você adiciona um nó à árvore ao combinar uma produção.E não importa qual idioma você está usando.Estruturas C funcionarão perfeitamente.
se for um não terminal tem a informação como nome Nonterminals, e se ele é um terminal, ou seja,um token, então as informações no token, ou seja,valor do lexema, O nome do token e o número da linha são armazenados
Você deve ter nós especializados na árvore, por ex.ClassNode, TypeNode, MethodDeclNode, IfStmtNode, ExprNode.Você não pode simplesmente armazenar um tipo de nó e colocar não-terminais e terminais nele.Um não-terminal é representado como um nó de árvore, não há nenhuma outra informação para armazenar sobre ele além das partes que o compõem, que geralmente são eles próprios não-terminais.Você não armazenaria nenhuma informação de token.Existem apenas alguns casos em que você realmente armazenaria o valor da string de um lexema:para um identificador e para um literal de string/booleano/inteiro.
Dê uma olhada em esse exemplo.Durante a primeira redução quando S
é reduzido a (S + F)
, você anexa um ParenExprNode
até a raiz da árvore.Você também anexa um AddExprNode
como filho do ParenExprNode
.Essa lógica deve ser codificada em seu analisador ao aplicar uma redução pela regra 2 da gramática.
A árvore:
ExprNode (root)
|
ParenExprNode
|
AddExprNode
/ \
ExprNode ExprNode
O código:
struct ExprNode { void* exprNode; };
struct ParenExprNode { void* exprNode; };
struct AddExprNode { void* op1, * op2; };
struct IdNode { char* val; int line; int charNum; };
struct IntLiteralNode { int val; int line; int charNum; };
void reduce_rule_2(ExprNode* expr) {
//remove stack symbols
//create nodes
struct ParenExprNode* parenExpr = malloc(sizeof(struct ParenExprNode));
struct AddExprNode* addExpr = malloc(sizeof(struct AddExprNode));
addExpr->op1 = malloc(sizeof(struct ExprNode));
addExpr->op2 = malloc(sizeof(struct ExprNode));
//link them
parenExpr->exprNode = (void*)addExpr;
expr->exprNode = (void*)parenExpr;
}
Na próxima etapa, o parêntese esquerdo é removido da entrada.Depois, S
está no topo da pilha e é reduzido a F
pela regra 1.Desde F
é o não-terminal de um identificador, é representado por IdNode
.
A árvore:
ExprNode
|
ParenExprNode
|
AddExprNode
/ \
ExprNode ExprNode
|
IdNode
O código:
reduce_rule_2(addExpr->op1);
void reduce_rule_1(ExprNode* expr) {
//reduce stack symbols
struct IdNode* id = malloc(sizeof(struct IdNode));
id->val = parser_matched_text();
id->lineNum = parser_line_num();
id->charNum = parser_char_num();
expr->exprNode = (void*)id;
}
E assim por diante...
Outras dicas
.Tudo o que sei é o valor do Lexeme e o número da linha para esse ID específico
Isso não é verdade.Você sabe onde é declarado na árvore de análise, o que lhe diz tudo que você precisa.Você faz esta etapa processamento a árvore de análise.