¿Por qué se permite una declaración como 1+n*= 3 en Ruby?
(4)
Las tablas de precedencia en muchas documentaciones de Ruby enumeran las operaciones aritméticas binarias con mayor prioridad que sus operadores de asignación compuesta correspondientes. Esto me lleva a creer que un código como este no debería ser un código Ruby válido, pero lo es.
1 + age *= 2
Si las reglas de precedencia fueran correctas, esperaría que el código anterior se pusiera entre paréntesis de esta manera:
((1 + age) *= 2) #ERROR: Doesn''t compile
Pero no lo hace.
Entonces, ¿qué da?
La respuesta simplificada es.
Solo puede asignar un valor a una variable, no a una expresión.
Por lo tanto, el orden es
1 + (age *= 2)
.
La prioridad solo entra en juego si son posibles varias opciones.
Por ejemplo,
age *= 2 + 1
puede verse como
(age *= 2) + 1
o
age *= (2 + 1)
, ya que son posibles varias opciones y el
+
tiene una precedencia mayor que
*=
,
age *= (2 + 1)
se utiliza.
Ruby tiene 3 fases antes de que su código se ejecute realmente.
Tokenize -> Parse -> Compilar
Veamos el AST (Árbol de sintaxis abstracta) que Ruby genera, que es la fase de análisis.
# @ NODE_SCOPE (line: 1, location: (1,0)-(1,12))
# | # new scope
# | # format: [nd_tbl]: local table, [nd_args]: arguments, [nd_body]: body
# +- nd_tbl (local table): :age
# +- nd_args (arguments):
# | (null node)
# +- nd_body (body):
# @ NODE_OPCALL (line: 1, location: (1,0)-(1,12))*
# | # method invocation
# | # format: [nd_recv] [nd_mid] [nd_args]
# | # example: foo + bar
# +- nd_mid (method id): :+
# +- nd_recv (receiver):
# | @ NODE_LIT (line: 1, location: (1,0)-(1,1))
# | | # literal
# | | # format: [nd_lit]
# | | # example: 1, /foo/
# | +- nd_lit (literal): 1
# +- nd_args (arguments):
# @ NODE_ARRAY (line: 1, location: (1,4)-(1,12))
# | # array constructor
# | # format: [ [nd_head], [nd_next].. ] (length: [nd_alen])
# | # example: [1, 2, 3]
# +- nd_alen (length): 1
# +- nd_head (element):
# | @ NODE_DASGN_CURR (line: 1, location: (1,4)-(1,12))
# | | # dynamic variable assignment (in current scope)
# | | # format: [nd_vid](current dvar) = [nd_value]
# | | # example: 1.times { x = foo }
# | +- nd_vid (local variable): :age
# | +- nd_value (rvalue):
# | @ NODE_CALL (line: 1, location: (1,4)-(1,12))
# | | # method invocation
# | | # format: [nd_recv].[nd_mid]([nd_args])
# | | # example: obj.foo(1)
# | +- nd_mid (method id): :*
# | +- nd_recv (receiver):
# | | @ NODE_DVAR (line: 1, location: (1,4)-(1,7))
# | | | # dynamic variable reference
# | | | # format: [nd_vid](dvar)
# | | | # example: 1.times { x = 1; x }
# | | +- nd_vid (local variable): :age
# | +- nd_args (arguments):
# | @ NODE_ARRAY (line: 1, location: (1,11)-(1,12))
# | | # array constructor
# | | # format: [ [nd_head], [nd_next].. ] (length: [nd_alen])
# | | # example: [1, 2, 3]
# | +- nd_alen (length): 1
# | +- nd_head (element):
# | | @ NODE_LIT (line: 1, location: (1,11)-(1,12))
# | | | # literal
# | | | # format: [nd_lit]
# | | | # example: 1, /foo/
# | | +- nd_lit (literal): 2
# | +- nd_next (next element):
# | (null node)
# +- nd_next (next element):
# (null node)
Como puede ver
# +- nd_mid (method id): :+
donde
1
se trata como el receptor y todo a la derecha como argumentos.
Ahora, va más allá y hace todo lo posible para evaluar los argumentos.
Para seguir apoyando la gran respuesta de Aleksei.
@ NODE_DASGN_CURR (line: 1, location: (1,4)-(1,12))
es la asignación de
age
como una variable local, ya que la decodifica como
age = age * 2
, razón por la cual
+- nd_mid (method id): :*
se trata como la operación en la
age
como el receptor y
2
como su argumento.
Ahora, cuando continúa compilando, intenta como operación:
age * 2
donde
age
es nulo porque ya lo analizó como una variable local sin valor preasignado, genera el
undefined method ''*'' for nil:NilClass (NoMethodError)
excepción
undefined method ''*'' for nil:NilClass (NoMethodError)
.
Funciona de la manera en que lo hizo porque cualquier operación en el receptor debe tener un argumento evaluado del RHO.
Verificando la salida
ruby -y
, puede ver exactamente lo que está sucediendo.
Dada la fuente de
1 + age *= 2
, el resultado sugiere que esto sucede (simplificado):
tINTEGER
encontrado, reconocido como
simple_numeric
, que es un número, que es un
literal
, que es un
primary
.
Sabiendo que
+
viene después,
primary
se reconoce como
arg
.
+
encontrado.
No puedo tratar aún.
tIDENTIFIER
encontrado.
Sabiendo que el siguiente token es
tOP_ASGN
(asignación de operador),
tIDENTIFIER
se reconoce como
user_variable
y luego como
var_lhs
.
tOP_ASGN
encontrado.
No puedo tratar aún.
tINTEGER
encontrado.
Igual que el último, finalmente se reconoce como
primary
.
Sabiendo que el siguiente token es
/n
,
primary
se reconoce como
arg
.
En este momento tenemos
arg + var_lhs tOP_ASGN arg
en la pila.
En este contexto, reconocemos el último
arg_rhs
como
arg_rhs
.
Ahora podemos
var_lhs tOP_ASGN arg_rhs
de la pila y reconocerlo como
arg
, con la pila terminando como
arg + arg
, que se puede reducir a
arg
.
arg
se reconoce como
expr
,
stmt
,
top_stmt
,
top_stmts
.
/n
se reconoce como
term
, luego
terms
, luego
opt_terms
.
top_stmts opt_terms
se reconocen como
top_compstmt
y, en última instancia, se
program
.
Por otro lado, dada la fuente
1 + age * 2
, esto sucede:
tINTEGER
encontrado, reconocido como
simple_numeric
, que es un número, que es un
literal
, que es un
primary
.
Sabiendo que
+
viene después,
primary
se reconoce como
arg
.
+
encontrado.
No puedo tratar aún.
tIDENTIFIER
encontrado.
Sabiendo que el siguiente token es
*
,
tIDENTIFIER
se reconoce como
user_variable
, luego
var_ref
, luego
primary
y
arg
.
*
encontrado.
No puedo tratar aún.
tINTEGER
encontrado.
Igual que el último, finalmente se reconoce como
primary
.
Sabiendo que el siguiente token es
/n
,
primary
se reconoce como
arg
.
La pila ahora es
arg + arg * arg
.
arg * arg
puede reducirse a
arg
, y el resultante
arg + arg
también puede reducirse a
arg
.
arg
se reconoce como
expr
,
stmt
,
top_stmt
,
top_stmts
.
/n
se reconoce como
term
, luego
terms
, luego
opt_terms
.
top_stmts opt_terms
se reconocen como
top_compstmt
y, en última instancia, se
program
.
¿Cuál es la diferencia crítica?
En la primera parte del código,
age
(un
tIDENTIFIER
) se reconoce como
var_lhs
(lado izquierdo de la asignación), pero en la segunda, es
var_ref
(una referencia variable).
¿Por qué?
Porque Bison es un analizador LALR (1), lo que significa que tiene un token anticipado.
Entonces la
age
es
var_lhs
porque Ruby vio venir
tOP_ASGN
;
y fue
var_ref
cuando vio
*
.
Esto ocurre porque Ruby sabe (usando la enorme
tabla de transición de estado
que genera Bison) que una producción específica es imposible.
Específicamente, en este momento, la pila es
arg + tIDENTIFIER
, y el siguiente token es
*=
.
Si
tIDENTIFIER
se reconoce como
var_ref
(que conduce a
arg
) y
arg + arg
reduce a
arg
, entonces no hay ninguna regla que comience con
arg tOP_ASGN
;
por lo tanto, no se puede permitir que
var_ref
convierta en
var_ref
, y miramos la siguiente regla coincidente (la
var_lhs
).
Entonces Aleksei tiene razón en parte en que hay algo de verdad en "cuando ve un error de sintaxis, intenta de otra manera", pero se limita a una ficha en el futuro, y el "intento" es solo una búsqueda en la tabla de estado. Ruby es incapaz de estrategias de reparación complejas que los humanos usamos para entender oraciones como "el caballo corrió más allá del granero cayó" , donde analizamos felizmente hasta la última palabra, luego reevaluamos toda la oración cuando el primer análisis resulta imposible.
tl; dr: la tabla de precedencia no es exactamente correcta. No hay lugar en la fuente de Ruby donde exista; más bien, es el resultado de la interacción de varias reglas de análisis. Muchas de las reglas de precedencia interfieren cuando se introduce el lado izquierdo de una tarea.
Nota: esta respuesta no debe marcarse como la solución del problema. Vea la respuesta de @Amadan para la explicación correcta.
No estoy seguro de qué "muchas documentaciones de Ruby" mencionaste, aquí está la oficial .
El analizador Ruby hace todo lo posible para comprender y analizar con éxito la entrada; cuando ve un error de sintaxis, intenta de otra manera. Dicho esto, los errores de sintaxis tienen mayor prioridad en comparación con todas las reglas de precedencia de operadores.
Como LHO debe ser variable, comienza con
una asignación
.
Este es el caso cuando el análisis
se
puede hacer con un orden de precedencia predeterminado y
+
se hace antes de
*=
:
age = 2
age *= age + 1
#⇒ 6