Bug #20970
open`it /1/i` raises undefined method 'it' for main (NoMethodError) even if binding.local_variables includes `it`
Added by tompng (tomoya ishida) 3 days ago. Updated about 4 hours ago.
Description
it
parameter became a local variable with #20965, but it does not behave like local variable with --parser=prism
i=2
42.tap do
p it # 42
p local_variables # [:it, :i]
p it /1/i # should be 21, got NoMethodError
end
It prints 42
, [:it, :i],
21with
--parser=parse.y`
Updated by k0kubun (Takashi Kokubun) 3 days ago
- Related to Feature #18980: `it` as a default block parameter added
Updated by zverok (Victor Shepelev) 3 days ago
Interesting. Before the change (on 3.4.0dev (2024-12-15T13:36:38Z master 366fd9642f)
) it was an error both with Prism and parse.y:
$ ruby -e "i = 2; [1, 2, 3].each { it /1/i }"
-e:1:in 'block in <main>': undefined method 'it' for main (NoMethodError)
$ ruby --parser=parse.y -e "i = 2; [1, 2, 3].each { it /1/i }"
-e:1:in 'block in <main>': undefined method 'it' for main (NoMethodError)
As far as I can guess (by IRB syntax highlighting), it /1/i
parses at once as it(/1/i)
(with /1/i
treated as a regexp literal). Reproduced in other ambiguous cases (that are resolved by local name presence, for all I can understand), say:
ruby -e "[1, 2, 3].each { it + 1 }"
#=> OK
$ ruby -e "[1, 2, 3].each { it +1 }"
-e:1:in 'block in <main>': undefined method 'it' for main (NoMethodError)
$ ruby -e "[1, 2, 3].each { |it| it +1 }"
#=> OK
I.e., ambiguous operators parsing decisions are made based on whether it
is known to be a local var (then it is it.+(1)
), or not known (then it is tentatively considered it(+1)
).
Updated by zverok (Victor Shepelev) 3 days ago
- Backport changed from 3.1: UNKNOWN, 3.2: UNKNOWN, 3.3: UNKNOWN to 3.1: DONTNEED, 3.2: DONTNEED, 3.3: DONTNEED
Updated by alanwu (Alan Wu) 2 days ago
- Assignee set to prism
Prism outputs undesired parse tree with the example.
Updated by tompng (tomoya ishida) 2 days ago
The behavior of nested it
discussed in #20930 has changed with #20969
[1].each { p it; [5].each { p it } }
# 1, 5 # --parser=prism, (confirmed by matz at dev meeting?)
# 1, 1 # --parser=parse.y (result was 1, 5 before)
So I think there is a room for discussion to fix prism or to revert the change made to parse.y.
Updated by alanwu (Alan Wu) 1 day ago
- Assignee deleted (
prism)
Sorry, I was too quick to blame Prism. I agree that having it
as a local brings hidden semantic complexity that warrent discussion. Whether an identifier is a local influences parsing, so it's important to know exactly when it
becomes a local to the parser. Usually, assignments make locals, but the way parse.y has it currently, it
becomes a local on read, so in the OP example the first it
essentially does it => it
(not it = it
because that always sets nil) implicitly to influence lines below it. It's a sort of promotion-on-first-read, if you will, a brand new way to add a local in the grammar. It can be hard to parse code mentally and know that whether an it
is reading a variable and promoting. To illustrate:
i=2
42.tap do
p it /1/i rescue 0 # `it` treated as method call
p it /1/i # NoMethodError under parse.y(e23a60b92) surprising if you expect first `it` to refer to a variable
end
Essentially, with the promote-to-local-on-first-use behavior, the ambiguity inherent to the design of it
infects subsequent lines in the same scope. (Maybe this is the same point raised in [ruby-core:120326])
@k0kubun (Takashi Kokubun) @nobu (Nobuyoshi Nakada) I feel 46fec0f62a1803d44edb8b06e39ac0f358e56670 makes a semantic change maybe too close to the release date. Do we really want this right now? How about a narrower fix for #20955? It's fair enough to say "just don't write code that trigger parse warnings", but it's worth thinking through the details of the design, I think.
Updated by k0kubun (Takashi Kokubun) 1 day ago ยท Edited
changed with #20969
I think you meant #20965 (https://github.com/ruby/ruby/pull/12398).
Before the change (on 3.4.0dev (2024-12-15T13:36:38Z master 366fd9642f)) it was an error both with Prism and parse.y
I feel 46fec0f62a1803d44edb8b06e39ac0f358e56670 makes a semantic change maybe too close to the release date. Do we really want this right now?
It's too close to the release date to discuss and change how it /1/i
in the ticket description's code is parsed. Since Matz said "the current master's behavior is good" before https://github.com/ruby/ruby/pull/12398, for the 3.4.0 release, it /1/i
should be parsed like before the PR. As @zverok (Victor Shepelev) said, both parsers used to raise NoMethodError
. Prism is good; it didn't change the behavior on the PR. parse.y
now parses it
in it /1/i
as a local variable ((it / 1) / i
), but parse.y
should raise a NoMethodError
(it( /1/i )
).
The same applies to [1].each { p it; [5].each { p it } }
. Prism didn't change the behavior, but parse.y
should be changed back to print 1
and 5
.
@nobu (Nobuyoshi Nakada) said he's looking into the [1].each { p it; [5].each { p it } }
issue last night, so I'd like to wait for his parse.y
fix for now, hoping that the it /1/i
case is fixed as well. We could revert https://github.com/ruby/ruby/pull/12398 as a last resort if we don't come up with a fix.
Updated by kddnewton (Kevin Newton) about 7 hours ago
I'm not sure if I'm reading this correctly or not. Is there any action required on Prism's side as of right now? Happy to make changes, though we are getting close to the release.
Updated by k0kubun (Takashi Kokubun) about 4 hours ago
No, Prism is all set from my perspective. Thank you for checking on it.