ANTLR ¿Cuál es la forma más sencilla de realizar python como la gramática dependiente de sangría?
lexer indentation (4)
Estoy tratando de realizar python como la gramática dependiente de sangría.
Ejemplo de fuente:
ABC QWE
CDE EFG
EFG CDE
ABC
QWE ZXC
Como veo, lo que necesito es realizar dos tokens INDENT y DEDENT, para poder escribir algo como:
grammar mygrammar;
text: (ID | block)+;
block: INDENT (ID|block)+ DEDENT;
INDENT: ????;
DEDENT: ????;
¿Hay alguna forma simple de darse cuenta de esto usando ANTLR?
(Preferiría, si es posible, usar el lexer ANTLR estándar).
¿Has mirado la gramática ANTLR de Python ?
Edición: Se agregó el código Python de psuedo para crear tokens INDENT / DEDENT
UNKNOWN_TOKEN = 0
INDENT_TOKEN = 1
DEDENT_TOKEN = 2
# filestream has already been processed so that each character is a newline and
# every tab outside of quotations is converted to 8 spaces.
def GetIndentationTokens(filestream):
# Stores (indentation_token, line, character_index)
indentation_record = list()
line = 0
character_index = 0
column = 0
counting_whitespace = true
indentations = list()
for c in filestream:
if IsNewLine(c):
character_index = 0
column = 0
line += 1
counting_whitespace = true
elif c != '' '' and counting_whitespace:
counting_whitespace = false
if(len(indentations) == 0):
indentation_record.append((token, line, character_index))
else:
while(len(indentations) > 0 and indentations[-1] != column:
if(column < indentations[-1]):
indentations.pop()
indentation_record.append((
DEDENT, line, character_index))
elif(column > indentations[-1]):
indentations.append(column)
indentation_record.append((
INDENT, line, character_index))
if not IsNewLine(c):
column += 1
character_index += 1
while(len(indentations) > 0):
indentations.pop()
indentation_record.append((DEDENT_TOKEN, line, character_index))
return indentation_record
Hay una biblioteca de código abierto antlr-denter para ANTLR v4 que ayuda a analizar sangrías y deducciones para usted. Echa un vistazo a su README para saber cómo usarlo.
Ya que es una biblioteca, en lugar de fragmentos de código para copiar y pegar en su gramática, su manejo de sangría se puede actualizar por separado del resto de su gramática.
Hay una forma relativamente simple de hacer este ANTLR, que escribí como un experimento: Dent.g4 . Esta solución es diferente de las otras mencionadas en esta página que fueron escritas por Kiers y Shavit. Se integra con el tiempo de ejecución únicamente a través de una anulación del método nextToken()
de nextToken()
. Hace su trabajo al examinar los tokens: (1) un token NEWLINE
desencadena el inicio de una fase de "seguimiento de sangrado"; (2) los espacios en blanco y los comentarios, ambos establecidos en el canal HIDDEN
, se cuentan y se ignoran, respectivamente, durante esa fase; y, (3) cualquier token no HIDDEN
termina la fase. Por lo tanto, controlar la lógica de sangría es una simple cuestión de configurar el canal de un token.
Ambas soluciones mencionadas en esta página requieren un token NEWLINE
para capturar también todos los espacios en blanco posteriores, pero al hacerlo no pueden manejar los comentarios de varias líneas que interrumpen ese espacio en blanco. Dent, en cambio, mantiene los tokens de espacios en blanco y NEWLINE
separados y puede manejar comentarios de varias líneas.
Tu gramática se configuraría algo como abajo. Tenga en cuenta que las reglas NEWLINE y WS lexer tienen acciones que controlan el estado pendingDent
de sangrado y pendingDent
un seguimiento del nivel de sangría con la variable indentCount
.
grammar MyGrammar;
tokens { INDENT, DEDENT }
@lexer::members {
// override of nextToken(), see Dent.g4 grammar on github
// https://github.com/wevrem/wry/blob/master/grammars/Dent.g4
}
script : ( NEWLINE | statement )* EOF ;
statement
: simpleStatement
| blockStatements
;
simpleStatement : LEGIT+ NEWLINE ;
blockStatements : LEGIT+ NEWLINE INDENT statement+ DEDENT ;
NEWLINE : ( ''/r''? ''/n'' | ''/r'' ) {
if (pendingDent) { setChannel(HIDDEN); }
pendingDent = true;
indentCount = 0;
initialIndentToken = null;
} ;
WS : [ /t]+ {
setChannel(HIDDEN);
if (pendingDent) { indentCount += getText().length(); }
} ;
BlockComment : ''/*'' ( BlockComment | . )*? ''*/'' -> channel(HIDDEN) ; // allow nesting comments
LineComment : ''//'' ~[/r/n]* -> channel(HIDDEN) ;
LEGIT : ~[ /t/r/n]+ ~[/r/n]*; // Replace with your language-specific rules...
No sé cuál es la forma más fácil de manejarlo, pero la siguiente es una forma relativamente fácil. Siempre que coincida con un salto de línea en su lexer, opcionalmente haga coincidir uno o más espacios. Si hay espacios después del salto de línea, compare la longitud de estos espacios con el tamaño de sangría actual. Si es más que el tamaño de sangría actual, emita un token de Indent
, si es menor que el tamaño de sangría actual, emita un token de Dedent
y si es el mismo, no haga nada.
También querrá emitir una cantidad de tokens de Dedent
al final del archivo para permitir que cada Indent
tenga un token de Dedent
coincida.
Para que esto funcione correctamente, debe agregar un salto de línea anterior y posterior a su archivo de fuente de entrada.
ANTRL3
Una demostración rápida:
grammar PyEsque;
options {
output=AST;
}
tokens {
BLOCK;
}
@lexer::members {
private int previousIndents = -1;
private int indentLevel = 0;
java.util.Queue<Token> tokens = new java.util.LinkedList<Token>();
@Override
public void emit(Token t) {
state.token = t;
tokens.offer(t);
}
@Override
public Token nextToken() {
super.nextToken();
return tokens.isEmpty() ? Token.EOF_TOKEN : tokens.poll();
}
private void jump(int ttype) {
indentLevel += (ttype == Dedent ? -1 : 1);
emit(new CommonToken(ttype, "level=" + indentLevel));
}
}
parse
: block EOF -> block
;
block
: Indent block_atoms Dedent -> ^(BLOCK block_atoms)
;
block_atoms
: (Id | block)+
;
NewLine
: NL SP?
{
int n = $SP.text == null ? 0 : $SP.text.length();
if(n > previousIndents) {
jump(Indent);
previousIndents = n;
}
else if(n < previousIndents) {
jump(Dedent);
previousIndents = n;
}
else if(input.LA(1) == EOF) {
while(indentLevel > 0) {
jump(Dedent);
}
}
else {
skip();
}
}
;
Id
: (''a''..''z'' | ''A''..''Z'')+
;
SpaceChars
: SP {skip();}
;
fragment NL : ''/r''? ''/n'' | ''/r'';
fragment SP : ('' '' | ''/t'')+;
fragment Indent : ;
fragment Dedent : ;
Puedes probar el analizador con la clase:
import org.antlr.runtime.*;
import org.antlr.runtime.tree.*;
import org.antlr.stringtemplate.*;
public class Main {
public static void main(String[] args) throws Exception {
PyEsqueLexer lexer = new PyEsqueLexer(new ANTLRFileStream("in.txt"));
PyEsqueParser parser = new PyEsqueParser(new CommonTokenStream(lexer));
CommonTree tree = (CommonTree)parser.parse().getTree();
DOTTreeGenerator gen = new DOTTreeGenerator();
StringTemplate st = gen.toDOT(tree);
System.out.println(st);
}
}
Si ahora pone lo siguiente en un archivo llamado in.txt
:
AAA AAAAA BBB BB B BB BBBBB BB CCCCCC C CC BB BBBBBB C CCC DDD DD D DDD D DDD
(Tenga en cuenta los saltos de línea iniciales y finales!)
entonces verás una salida que corresponde al siguiente AST:
Tenga en cuenta que mi demostración no producirá suficientes deducciones sucesivas, como la deducción de ccc
a aaa
(se necesitan 2 tokens de dedent):
aaa
bbb
ccc
aaa
Necesitaría ajustar el código en else if(n < previousIndents) { ... }
para emitir más de 1 token de dedent en función de la diferencia entre n
y previousIndents
. En la parte superior de mi cabeza, podría verse así:
else if(n < previousIndents) {
// Note: assuming indent-size is 2. Jumping from previousIndents=6
// to n=2 will result in emitting 2 `Dedent` tokens
int numDedents = (previousIndents - n) / 2;
while(numDedents-- > 0) {
jump(Dedent);
}
previousIndents = n;
}
ANTLR4
Para ANTLR4, haga algo como esto:
grammar Python3;
tokens { INDENT, DEDENT }
@lexer::members {
// A queue where extra tokens are pushed on (see the NEWLINE lexer rule).
private java.util.LinkedList<Token> tokens = new java.util.LinkedList<>();
// The stack that keeps track of the indentation level.
private java.util.Stack<Integer> indents = new java.util.Stack<>();
// The amount of opened braces, brackets and parenthesis.
private int opened = 0;
// The most recently produced token.
private Token lastToken = null;
@Override
public void emit(Token t) {
super.setToken(t);
tokens.offer(t);
}
@Override
public Token nextToken() {
// Check if the end-of-file is ahead and there are still some DEDENTS expected.
if (_input.LA(1) == EOF && !this.indents.isEmpty()) {
// Remove any trailing EOF tokens from our buffer.
for (int i = tokens.size() - 1; i >= 0; i--) {
if (tokens.get(i).getType() == EOF) {
tokens.remove(i);
}
}
// First emit an extra line break that serves as the end of the statement.
this.emit(commonToken(Python3Parser.NEWLINE, "/n"));
// Now emit as much DEDENT tokens as needed.
while (!indents.isEmpty()) {
this.emit(createDedent());
indents.pop();
}
// Put the EOF back on the token stream.
this.emit(commonToken(Python3Parser.EOF, "<EOF>"));
}
Token next = super.nextToken();
if (next.getChannel() == Token.DEFAULT_CHANNEL) {
// Keep track of the last token on the default channel.
this.lastToken = next;
}
return tokens.isEmpty() ? next : tokens.poll();
}
private Token createDedent() {
CommonToken dedent = commonToken(Python3Parser.DEDENT, "");
dedent.setLine(this.lastToken.getLine());
return dedent;
}
private CommonToken commonToken(int type, String text) {
int stop = this.getCharIndex() - 1;
int start = text.isEmpty() ? stop : stop - text.length() + 1;
return new CommonToken(this._tokenFactorySourcePair, type, DEFAULT_TOKEN_CHANNEL, start, stop);
}
// Calculates the indentation of the provided spaces, taking the
// following rules into account:
//
// "Tabs are replaced (from left to right) by one to eight spaces
// such that the total number of characters up to and including
// the replacement is a multiple of eight [...]"
//
// -- https://docs.python.org/3.1/reference/lexical_analysis.html#indentation
static int getIndentationCount(String spaces) {
int count = 0;
for (char ch : spaces.toCharArray()) {
switch (ch) {
case ''/t'':
count += 8 - (count % 8);
break;
default:
// A normal space char.
count++;
}
}
return count;
}
boolean atStartOfInput() {
return super.getCharPositionInLine() == 0 && super.getLine() == 1;
}
}
single_input
: NEWLINE
| simple_stmt
| compound_stmt NEWLINE
;
// more parser rules
NEWLINE
: ( {atStartOfInput()}? SPACES
| ( ''/r''? ''/n'' | ''/r'' ) SPACES?
)
{
String newLine = getText().replaceAll("[^/r/n]+", "");
String spaces = getText().replaceAll("[/r/n]+", "");
int next = _input.LA(1);
if (opened > 0 || next == ''/r'' || next == ''/n'' || next == ''#'') {
// If we''re inside a list or on a blank line, ignore all indents,
// dedents and line breaks.
skip();
}
else {
emit(commonToken(NEWLINE, newLine));
int indent = getIndentationCount(spaces);
int previous = indents.isEmpty() ? 0 : indents.peek();
if (indent == previous) {
// skip indents of the same size as the present indent-size
skip();
}
else if (indent > previous) {
indents.push(indent);
emit(commonToken(Python3Parser.INDENT, spaces));
}
else {
// Possibly emit more than 1 DEDENT token.
while(!indents.isEmpty() && indents.peek() > indent) {
this.emit(createDedent());
indents.pop();
}
}
}
}
;
// more lexer rules
Tomado de: https://github.com/antlr/grammars-v4/blob/master/python3/Python3.g4