Почему в Ruby допустимо утверждение типа 1 + n * = 3?

В таблицах приоритетов во многих документах Ruby перечисляются двоичные арифметические операции как имеющие более высокий приоритет, чем их соответствующие составные операторы присваивания. Это заставляет меня поверить, что подобный код не должен быть действительным кодом Ruby, но это так.

1 + age *= 2

Если бы правила приоритетов были правильными, я ожидал бы, что приведенный выше код будет заключен в скобки следующим образом:

((1 + age) *= 2) #ERROR: Doesn't compile

Но это не так.

Так что же дает?

Ответы

Ответ 1

Проверяя вывод ruby -y, вы можете точно видеть, что происходит. Учитывая источник 1 + age *= 2, вывод предполагает, что это происходит (упрощенно):

tINTEGER найден, распознается как simple_numeric, который является numeric, который является literal, который является primary. Зная, что + будет следующим, primary распознается как arg.

+ найден. Пока не могу разобраться.

tIDENTIFIER найден. Зная, что следующим токеном является tOP_ASGN (назначение оператора), tIDENTIFIER распознается как user_variable, а затем как var_lhs.

tOP_ASGN найден. Пока не могу разобраться.

tINTEGER найден. То же, что и в прошлом, в конечном итоге признается как primary. Зная, что следующий токен - \n, primary распознается как arg.

На данный момент у нас в стеке arg + var_lhs tOP_ASGN arg. В этом контексте мы распознаем последний arg как arg_rhs. Теперь мы можем извлечь var_lhs tOP_ASGN arg_rhs из стека и распознать его как arg со стеком, заканчивающимся как arg + arg, который можно уменьшить до arg.

arg затем распознается как expr, stmt, top_stmt, top_stmts. \n распознается как term, затем terms, затем opt_terms. top_stmts opt_terms распознаются как top_compstmt и, в конечном итоге, program.


С другой стороны, учитывая источник 1 + age * 2, это происходит:

tINTEGER найден, распознается как simple_numeric, который представляет собой numeric, который представляет собой literal, который представляет собой primary. Зная, что + будет следующим, primary распознается как arg.

+ найден. Пока не могу разобраться.

tIDENTIFIER найден. Зная, что следующим токеном является *, tIDENTIFIER распознается как user_variable, затем var_ref, затем primary и arg.

* найден. Пока не могу разобраться.

tINTEGER найден. То же, что и в прошлом, в конечном итоге признается как primary. Зная, что следующий токен - \n, primary распознается как arg.

Стек теперь arg + arg * arg. arg * arg может быть уменьшено до arg, а результирующий arg + arg также может быть уменьшен до arg.

arg затем распознается как expr, stmt, top_stmt, top_stmts. \n распознается как term, затем terms, затем opt_terms. top_stmts opt_terms распознаются как top_compstmt и, в конечном итоге, program.


Какая критическая разница? В первом фрагменте кода age (a tIDENTIFIER) распознается как var_lhs (левая часть назначения), но во втором - это var_ref (ссылка на переменную). Почему? Потому что Bison - это анализатор LALR (1), что означает, что он имеет прогноз с одним токеном. Итак, age - это var_lhs, потому что Руби увидела tOP_ASGN приближающегося; и это был var_ref, когда он увидел *. Это происходит потому, что Ruby знает (используя огромную таблицу перехода состояний, которую генерирует Bison), что одно конкретное производство невозможно. В частности, в это время стек является arg + tIDENTIFIER, а следующий токен - *=. Если tIDENTIFIER распознается как var_ref (что приводит к arg), а arg + arg сокращается до arg, то не существует правила, которое начинается с arg tOP_ASGN; таким образом, tIDENTIFIER нельзя позволить стать var_ref, и мы смотрим на следующее правило соответствия (var_lhs).

Так что Алексей отчасти прав в том, что есть некоторая правда в том, что "когда он видит синтаксическую ошибку, он пробует другой путь", но он ограничен одним токеном в будущем, а "попытка" - это просто поиск в таблице состояний. Ruby не способен на сложные стратегии восстановления, которые мы, люди, используем, чтобы понимать такие предложения, как "лошадь мчалась мимо упавшего сарая", где мы с радостью анализируем до последнего слова, а затем переоцениваем целое предложение, когда получается первый анализ невозможно.

tl;dr: таблица приоритетов не совсем верна. В источнике Ruby нет места, где он существует; скорее это результат взаимодействия различных правил синтаксического анализа. Многие из правил приоритета нарушаются, когда вводится левая часть назначения.

Ответ 2

Упрощенный ответ есть. Вы можете присвоить значение только переменной, а не выражению. Следовательно, порядок 1 + (age *= 2). Приоритет вступает в игру только тогда, когда возможны несколько вариантов. Например, age *= 2 + 1 можно рассматривать как (age *= 2) + 1 или age *= (2 + 1), поскольку возможны несколько вариантов, и + имеет более высокий приоритет, чем *=, используется age *= (2 + 1).

Ответ 3

NB. Этот ответ не должен быть помечен как решение проблемы. См. ответ @Amadan для правильного объяснения.

Я не уверен, какие "многие документы Ruby" вы упомянули, вот официальная.

Ruby parser делает все возможное, чтобы понять и успешно проанализировать входные данные; когда он видит синтаксическую ошибку, он пытается по-другому. При этом синтаксические ошибки имеют больший приоритет по сравнению со всеми правилами приоритетов операторов.

Поскольку LHO должен быть переменным, он начинается с присваивания. Вот случай, когда синтаксический анализ может быть выполнен с порядком приоритета по умолчанию, а + выполняется до *=:

age = 2
age *= age + 1
#⇒ 6

Ответ 4

В Ruby есть 3 этапа, прежде чем ваш код будет фактически выполнен.

Tokenize → Parse → Compile

Давайте посмотрим на AST (абстрактное синтаксическое дерево), которое генерирует Ruby, что является фазой анализа.

# @ 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)

Как вы можете видеть # +- nd_mid (method id): :+, где 1 рассматривается как получатель, а все справа - как аргументы. Теперь он идет дальше и делает все возможное, чтобы оценить аргументы.

Для дальнейшей поддержки Алексей отличный ответ. @ NODE_DASGN_CURR (line: 1, location: (1,4)-(1,12)) является присваиванием age в качестве локальной переменной, поскольку он декодирует ее как age = age * 2, поэтому +- nd_mid (method id): :* рассматривается как операция над age в качестве получателя, а 2 в качестве аргумента.

Теперь, когда он переходит к компиляции, он пытается выполнить операцию: age * 2, где age равен nil, поскольку он уже проанализировал ее как локальную переменную без заранее заданного значения, вызывает исключение undefined method '*' for nil:NilClass (NoMethodError).

Это работает так, как это происходит, потому что любая операция на получателе должна иметь оценочный аргумент от RHO.