programming-languages - portero - que es un fuera de lugar en el futbol yahoo
¿Cómo implementarías la regla fuera de juego? (3)
Tokenizer en ruby por diversión:
def tokenize(input)
result, prev_indent, curr_indent, line = [""], 0, 0, ""
line_started = false
input.each_char do |char|
case char
when '' ''
if line_started
# Content already started, add it.
line << char
else
# No content yet, just count.
curr_indent += 1
end
when "/n"
result.last << line + "/n"
curr_indent, line = 0, ""
line_started = false
else
# Check if we are at the first non-space character.
unless line_started
# Insert indent and dedent tokens if indentation changed.
if prev_indent > curr_indent
# 2 spaces dedentation
((prev_indent - curr_indent) / 2).times do
result << :DEDENT
end
result << ""
elsif prev_indent < curr_indent
result << :INDENT
result << ""
end
prev_indent = curr_indent
end
# Mark line as started and add char to line.
line_started = true; line << char
end
end
result
end
Solo funciona para sangría de dos espacios. El resultado es algo así como ["Hello there from level 0/n", :INDENT, "This/nis level/ntwo/n", :DEDENT, "This is level0 again/n"]
.
Ya he escrito un generador que hace el truco, pero me gustaría saber la mejor manera posible de implementar la regla de fuera de juego.
Brevemente: regla de fuera de juego significa en este contexto que la sangría se está reconociendo como un elemento sintáctico.
Aquí está la regla de fuera de juego en pseudocódigo para hacer tokenizadores que capturan sangrías en forma utilizable, no quiero limitar las respuestas por idioma:
token NEWLINE
matches r"/n/ *"
increase line count
pick up and store the indentation level
remember to also record the current level of parenthesis
procedure layout tokens
level = stack of indentation levels
push 0 to level
last_newline = none
per each token
if it is NEWLINE put it to last_newline and get next token
if last_newline contains something
extract new_level and parenthesis_count from last_newline
- if newline was inside parentheses, do nothing
- if new_level > level.top
push new_level to level
emit last_newline as INDENT token and clear last_newline
- if new_level == level.top
emit last_newline and clear last_newline
- otherwise
while new_level < level.top
pop from level
if new_level > level.top
freak out, indentation is broken.
emit last_newline as DEDENT token
clear last_newline
emit token
while level.top != 0
emit token as DEDENT token
pop from level
comments are ignored before they are getting into the layouter
layouter lies between a lexer and a parser
Este creador de listas no genera más de una NEWLINE a la vez, y no genera NEWLINE cuando está surgiendo una sangría. Por lo tanto, las reglas de análisis siguen siendo bastante simples. Es bastante bueno, creo, pero informar si hay mejor manera de lograrlo.
Mientras uso esto por un tiempo, he notado que después de los DEDENTES puede ser bueno emitir nueva línea de todos modos, de esta forma puedes separar las expresiones con NEWLINE mientras mantienes el INDENT DEDENT como un avance de la expresión.
He escrito tokenizers y analizadores sintácticos para un par de pequeños lenguajes específicos de dominio centrados en sangría en los últimos años, y lo que tienes allí parece bastante razonable para mí, para lo que sea que valga la pena. Si no me equivoco, su método es bastante similar al de Python, por ejemplo, que parece que debería tener algo de peso.
La conversión de NEWLINE NEWLINE INDENT a simplemente INDENT antes de que llegue al analizador definitivamente parece ser la forma correcta de hacer las cosas; ¡es una pena (IME) estar siempre mirando hacia adelante en el analizador sintáctico! De hecho, he hecho ese paso como una capa separada en lo que terminó siendo un proceso de tres pasos: el primero combinó lo que hacen tu lexer y layouter menos todas las cosas de NEWLINE lookahead (que lo hicieron muy simple), el segundo (también muy simple) ) layer NEWLED consecutivas dobladas y NEWLINE INDENT convertido a solo INDENT (o, en realidad, COLON NEWLINE INDENT a INDENT, ya que en este caso todos los bloques sangrados siempre fueron precedidos por dos puntos), entonces el analizador fue la tercera etapa además de eso. Pero también tiene mucho sentido para mí hacer las cosas de la forma en que las describiste, especialmente si deseas separar el Lexer del creador de planos, lo que presumiblemente querrías hacer si estuvieras usando una herramienta de generación de código. hacer tu Lexer, por ejemplo, como es práctica común.
Tenía una aplicación que necesitaba ser un poco más flexible acerca de las reglas de sangría, esencialmente dejando que el analizador las aplicara cuando fuera necesario; lo siguiente tenía que ser válido en ciertos contextos, por ejemplo:
this line introduces an indented block of literal text:
this line of the block is indented four spaces
but this line is only indented two spaces
lo cual no funciona muy bien con los tokens INDENT / DEDENT, ya que terminas necesitando generar un INDENT para cada columna de sangría y un igual número de DEDENTES en el camino de regreso, a menos que mires hacia adelante para averiguar dónde están los niveles de indentación van a terminar siendo, lo que no parece que quieras que haga un tokenizador. En ese caso probé algunas cosas diferentes y terminé simplemente almacenando un contador en cada token NEWLINE que dio el cambio en la sangría (positiva o negativa) para la siguiente línea lógica. (Cada token también almacenaba todos los espacios en blanco finales, en caso de que fuera necesario conservarlos, para NEWLINE, el espacio en blanco almacenado incluía el EOL mismo, las líneas en blanco intermedias y la sangría en la siguiente línea lógica.) No token en absoluto INDENT o DEDENT. Lograr que el analizador manejara eso fue un poco más trabajo que simplemente anidar INDENT y DEDENT, y bien podría haber sido un infierno con una gramática complicada que necesitaba un generador de analizadores sofisticado, pero no era tan malo como lo había temido, ya sea. Nuevamente, no es necesario que el analizador mire hacia adelante desde NEWLINE para ver si hay un INDENT en este esquema.
Aún así, creo que estarías de acuerdo en que permitir y preservar todo tipo de espacios en blanco de aspecto loco en el tokenizador / creador de listas y dejar que el analizador decida qué es un literal y qué es un código es un requisito poco habitual. Ciertamente no querrás que tu analizador se cargue con ese contador de sangría si solo quisieras analizar el código de Python, por ejemplo. La forma en que haces las cosas es casi seguro que es el enfoque correcto para tu aplicación y muchas otras. Aunque si alguien más tiene ideas sobre la mejor manera de hacer este tipo de cosas, obviamente me encantaría escucharlas ...
He estado experimentando con esto recientemente, y llegué a la conclusión de que, al menos para mis necesidades, quería que NEWLINES marcara el final de cada "declaración", si era la última frase en un bloque sangrado o no, es decir, necesita las nuevas líneas incluso antes de DEDENT.
Mi solución fue darle la vuelta, y en lugar de que NEWLINES marque el final de las líneas, uso un token LINE para marcar el comienzo de una línea.
Tengo un lector que contrae las líneas vacías (incluidas las líneas de solo comentario) y emite un solo token de LÍNEA con información sobre la sangría de la última línea. Entonces mi función de preprocesamiento toma esta secuencia de token y agrega INDENT o DEDENT "entre" cualquier línea donde la sangría cambie. Asi que
line1
line2
line3
line4
daría la transmisión de fichas
LINE "line1" INDENT LINE "line2" LINE "line3" DEDENT LINE "line4" EOF
Esto me permite escribir producciones gramaticales claras para las afirmaciones sin preocuparme por detectar el final de las instrucciones, incluso cuando terminan con subbloques anidados y sangrados, algo que puede ser difícil si está coincidiendo con NEWLINES (y DEDENTES).
Aquí está el núcleo del preprocesador, escrito en O''Caml:
match next_token () with
LINE indentation ->
if indentation > !current_indentation then
(
Stack.push !current_indentation indentation_stack;
current_indentation := indentation;
INDENT
)
else if indentation < !current_indentation then
(
let prev = Stack.pop indentation_stack in
if indentation > prev then
(
current_indentation := indentation;
BAD_DEDENT
)
else
(
current_indentation := prev;
DEDENT
)
)
else (* indentation = !current_indentation *)
let token = remove_next_token () in
if next_token () = EOF then
remove_next_token ()
else
token
| _ ->
remove_next_token ()
Todavía no he agregado soporte para paréntesis, pero debería ser una extensión simple. Sin embargo, evita emitir una LINEA perdida al final del archivo.