Skip to content

Make it = it assign nil to match parse.y behavior [Bug #21139]#3604

Merged
tenderlove merged 5 commits into
ruby:mainfrom
S-H-GAMELINKS:fix/bug-21139
Aug 1, 2025
Merged

Make it = it assign nil to match parse.y behavior [Bug #21139]#3604
tenderlove merged 5 commits into
ruby:mainfrom
S-H-GAMELINKS:fix/bug-21139

Conversation

@S-H-GAMELINKS

Copy link
Copy Markdown
Contributor

Currently Prism returns 42 for code like this:

42.tap { it = it; p it } # => 42

But parse.y returns nil:

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

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
Comment thread test/prism/ruby/parser_test.rb Outdated
assert_equal(it_block_parameter_sexp, actual_ast.to_sexp)
end

def test_it_assignment_syntax

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for pointing this out.
It appears I misunderstood and added the test incorrectly. I've removed this test case.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.txt

Those 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe also test 42.tap { it; it = it; p it }? This should still generate a it read for the first one.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@vinistock vinistock left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread src/prism.c Outdated
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;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
end

The block still needs to have an implicit it parameter as the first print is reading from it. And this matches parse.y.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
end

The 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.

@tenderlove

Copy link
Copy Markdown
Member

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.

@vinistock do you mind commenting on the original Redmine ticket? It seems like matz wants the behavior specified in this PR.

@kddnewton kddnewton left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@S-H-GAMELINKS

Copy link
Copy Markdown
Contributor Author

Thank you for your comment.
I've moved the logic to convert it to a local variable to the beginning of parse_expression_infix as you suggested.
All tests including memcheck pass locally, but CI shows memcheck failures. I'm investigating what might be different in the CI environment.

@Earlopain

Copy link
Copy Markdown
Collaborator

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.

@tenderlove tenderlove left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good to me.

@tenderlove
tenderlove merged commit b35e37b into ruby:main Aug 1, 2025
59 of 60 checks passed
@S-H-GAMELINKS
S-H-GAMELINKS deleted the fix/bug-21139 branch August 3, 2025 06:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants