ruby

¿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