Make it = it assign nil to match parse.y behavior [Bug #21139]#3604
Conversation
Currently Prism returns `42` for code like this:
```ruby
42.tap { it = it; p it } # => 42
```
But parse.y returns `nil`:
```ruby
42.tap { it = it; p it } # => nil
```
In parse.y, it on the right-hand side is parsed as a local variable.
In Prism, it was parsed as the implicit block parameter it, which caused this inconsistent behavior.
This change makes the right-hand side it to be parsed as a local variable, aligning with parse.y's behavior.
Bug ticket: https://bugs.ruby-lang.org/issues/21139
| assert_equal(it_block_parameter_sexp, actual_ast.to_sexp) | ||
| end | ||
|
|
||
| def test_it_assignment_syntax |
There was a problem hiding this comment.
This is for the parser gem translation and is not necessary to add as a test. The fixture you added should generate a snapshot instead that you should commit to test the prism behaviour.
There was a problem hiding this comment.
Thank you for pointing this out.
It appears I misunderstood and added the test incorrectly. I've removed this test case.
There was a problem hiding this comment.
If I run bundle exec rake on your branch, I am getting two files that need to be commited:
Started
|Created snapshot at /home/user/code/ruby-prism/snapshots/it_assignment.txt.
-Created snapshot at /home/user/code/ruby-prism/snapshots/it_read_and_assignment.txtThose basically contain the test I commented on here, just with the native prism ast format. This is automatically tested against and when the output changes, a test would fail.
There was a problem hiding this comment.
Thank you for the comment.
I forgot to add the snapshot, and I've pushed it now.
| @@ -0,0 +1 @@ | |||
| 42.tap { it = it; p it } | |||
There was a problem hiding this comment.
Maybe also test 42.tap { it; it = it; p it }? This should still generate a it read for the first one.
There was a problem hiding this comment.
Thank you for the comment.
You're right, that test case would be good to have as well.
I've added the test case in the following commit: 659d769
There was a problem hiding this comment.
While I understand the discrepancy, I wonder if parse.y's behaviour could be counter intuitive for developers. In this example:
42.tap { it = it; p it }The proposed parse.y behaviour would treat it = it as if it were it = nil (reading from a non-intiailized local variable, to a that same local variable). But we think it's more intuitive if it prints 42, because it = it is creating a new local variable from the result of reading the it parameter (before it gets "shadowed" by a local variable of the same name), which is 42. If developers want the the nil behaviour, they can explicitly write it = nil instead.
Maybe someone with more experience on the parser can pitch in, but I'd argue that Prism's current behaviour is more intuitive than having it end up as nil.
Reviewed with @amomchilov
| pm_constant_id_t name_id = pm_parser_local_add_location(parser, parser->previous.start, parser->previous.end, 0); | ||
| pm_node_t *node = (pm_node_t *) pm_local_variable_read_node_create_constant_id(parser, &parser->previous, name_id, 0, false); | ||
|
|
||
| return node; |
There was a problem hiding this comment.
Do we not need to append to the list of implicit parameters still? For example, in this scenario:
42.tap do
p it
it = it
p it
endThe block still needs to have an implicit it parameter as the first print is reading from it. And this matches parse.y.
There was a problem hiding this comment.
Thank you for your feedback.
I applied this change to Ruby and tested the behavior you mentioned, and the result is:
42.tap do
p it # => 42
it = it
p it # => nil
endThe implicit parameter works correctly for the first it read, and the assignment properly creates a local variable it. So, no additional changes are needed for the implicit parameters list.
@vinistock do you mind commenting on the original Redmine ticket? It seems like matz wants the behavior specified in this PR. |
kddnewton
left a comment
There was a problem hiding this comment.
I would prefer that we handle this in the place where we handle the = operator. That's where we usually handle these kinds of transformations. (The very top of the parse_expression_infix function.) You'll see we have to handle call nodes in a similar way.
…n_infix` function
|
Thank you for your comment. |
|
memcheck runs with ruby-dev from 2025-07-25 and also recently started failing on main. I wouldn't worry about it too much unless you want to report it upstream, very unlikely to be related to your PR. |
Currently Prism returns
42for code like this:But parse.y returns
nil:In parse.y, it on the right-hand side is parsed as a local variable. In Prism, it was parsed as the implicit block parameter it, which caused this inconsistent behavior.
This change makes the right-hand side it to be parsed as a local variable, aligning with parse.y's behavior.
Bug ticket: https://bugs.ruby-lang.org/issues/21139