Como implementar uma chamada de função com a ANTLR para que possa ser chamada antes mesmo de ser definida?
-
28-09-2019 - |
Pergunta
Depois que o AST é construído, qual é a melhor maneira de implementar o Walker de árvores para que as funções possam ser definidas e chamadas em qualquer ordem?
Por exemplo, isso é válido no PHP:
<?php
f(); // function called before it’s defined
function f() {
print 3;
}
?>
Suponho que, de alguma forma, deve haver um segundo passe ou uma transformação de árvores, mas não consigo encontrar nada interessante sobre esse assunto. O problema provavelmente não é específico da ANTLR, mas se você pudesse me indicar um exemplo de como isso é feito, melhor ainda!
Solução
Sim, você está certo: isso é feito em mais de um passe sobre o AST.
Primeiro, você cria uma gramática que constrói um AST da fonte e, em seguida, cria uma gramática de árvore usada para iterar sobre a árvore e descobre toda a função definida. Você pode avaliar o script usando outra gramática de árvore que pega as funções descobertas da gramática da árvore anterior.
Uma demonstração.
Pegue a fonte:
<?php
f(); // function called before it’s defined
function f() {
g();
}
function g() {}
?>
que é analisado no seguinte AST:
Usando a gramática (combinada):
grammar PHPMin;
options {
output=AST;
}
tokens {
SCRIPT; F_CALL; F_DECL; F_BODY;
}
parse
: script EOF -> script
;
script
: '<?php' atom* '?>' -> ^(SCRIPT atom*)
;
atom
: functionCall
| functionDecl
;
functionCall
: Identifier '(' ')' ';' -> ^(F_CALL Identifier)
;
functionDecl
: 'function' Identifier '(' ')' '{' functionBody '}' -> ^(F_DECL Identifier functionBody)
;
functionBody
: functionCall* -> ^(F_BODY functionCall*)
;
Identifier : ('a'..'z' | 'A'..'Z' | '_') ('a'..'z' | 'A'..'Z' | '_' | '0'..'9')* ;
LineComment : '//' ~('\r' | '\n')* ('\r'? '\n' | EOF){skip();} ;
Space : (' ' | '\t' | '\r' | '\n'){skip();} ;
Em seguida, descubra as funções declaradas usando um "caminhante da árvore" gerado a partir da seguinte gramática da árvore:
tree grammar PHPMinFunctionWalker;
options {
tokenVocab=PHPMin;
ASTLabelType=CommonTree;
}
@members {
java.util.Set<String> declared = new java.util.HashSet<String>();
}
discover
: script
;
script
: ^(SCRIPT atom*)
;
atom
: functionCall
| functionDecl
;
functionCall
: ^(F_CALL Identifier)
;
functionDecl
: ^(F_DECL Identifier functionBody) {declared.add($Identifier.text);}
;
functionBody
: ^(F_BODY functionCall*)
;
Para testar tudo, criar um lexer e analisador (a), gerar o "Walker" (b), compilar todos os arquivos de origem (c):
// A
java -cp antlr-3.2.jar org.antlr.Tool PHPMin.g
// B
java -cp antlr-3.2.jar org.antlr.Tool PHPMinFunctionWalker.g
// C
javac -cp antlr-3.2.jar *.java
// D
java -cp .:antlr-3.2.jar Main // *nix
java -cp .;antlr-3.2.jar Main // Windows
e execute a seguinte classe principal (d):
import org.antlr.runtime.*;
import org.antlr.runtime.tree.*;
import org.antlr.stringtemplate.*;
public class Main {
public static void main(String[] args) throws Exception {
String source = "<?php \n" +
"f(); // function called before it’s defined \n" +
"function f() { \n" +
" g(); \n" +
"} \n" +
"function g() {} \n" +
"?> \n";
// create a lexer and parser for the source
ANTLRStringStream in = new ANTLRStringStream(source);
PHPMinLexer lexer = new PHPMinLexer(in);
CommonTokenStream tokens = new CommonTokenStream(lexer);
PHPMinParser parser = new PHPMinParser(tokens);
PHPMinParser.parse_return returnValue = parser.parse();
CommonTree tree = (CommonTree)returnValue.getTree();
// create a tree walker to discover all declared functions
CommonTreeNodeStream nodes = new CommonTreeNodeStream(tree);
nodes.setTokenStream(tokens);
PHPMinFunctionWalker functions = new PHPMinFunctionWalker(nodes);
functions.discover();
System.out.println("Declared functions: "+functions.declared);
}
}
que produz a seguinte saída:
Declared functions: [f, g]
Obviamente, este é apenas um exemplo de como abordá -lo, não de como é melhor. Eu posso imaginar (ao usar o Java para interpretar o script), você não armazenaria as funções declaradas como cordas simples em um Set<String>
, mas sim como um Map<String, CommonTree>
Para obter facilmente a raiz de uma função e avaliá -la quando chamado.
Leitura adicional: http://www.antlr.org/wiki/display/antlr3/simple+tree baseado+InterPeter
Boa sorte!
EDITAR
O passe dos segundos pode verificar se todas as funções são definidas à frente dele usando o Walker anterior:
tree grammar PHPMinValidateWalker;
options {
tokenVocab=PHPMin;
ASTLabelType=CommonTree;
}
@members {
java.util.Set<String> declared = new java.util.HashSet<String>();
}
validate
: script
;
script
: ^(SCRIPT atom*)
;
atom
: functionCall
| functionDecl
;
functionCall
: ^(F_CALL Identifier)
{
if(!declared.contains($Identifier.text)) {
throw new RuntimeException("no such function: " + $Identifier.text);
}
}
;
functionDecl
: ^(F_DECL Identifier functionBody)
;
functionBody
: ^(F_BODY functionCall*)
;
Usando o teste:
import org.antlr.runtime.*;
import org.antlr.runtime.tree.*;
import org.antlr.stringtemplate.*;
public class Main {
public static void main(String[] args) throws Exception {
String source = "<?php \n" +
"f(); // function called before it’s defined \n" +
"function f() { \n" +
" g(); \n" +
" x(); \n" +
"} \n" +
"function g() {} \n" +
"?> \n";
// create a lexer and parser for the source
ANTLRStringStream in = new ANTLRStringStream(source);
PHPMinLexer lexer = new PHPMinLexer(in);
CommonTokenStream tokens = new CommonTokenStream(lexer);
PHPMinParser parser = new PHPMinParser(tokens);
PHPMinParser.parse_return returnValue = parser.parse();
CommonTree tree = (CommonTree)returnValue.getTree();
// create a tree walker to discover all declared functions
CommonTreeNodeStream nodes = new CommonTreeNodeStream(tree);
nodes.setTokenStream(tokens);
PHPMinFunctionWalker functions = new PHPMinFunctionWalker(nodes);
functions.discover();
System.out.println("Declared functions: "+functions.declared);
// PHPMinValidateWalker
nodes = new CommonTreeNodeStream(tree);
nodes.setTokenStream(tokens);
PHPMinValidateWalker validator = new PHPMinValidateWalker(nodes);
validator.declared = functions.declared;
validator.validate();
}
}
produz uma exceção desde x()
não está definido em nenhum lugar. Remover-o da fonte fará com que o Walker da Árvore não produza exceção.