Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add node event dispatcher #1523

Merged
merged 2 commits into from
Sep 15, 2023
Merged

Add node event dispatcher #1523

merged 2 commits into from
Sep 15, 2023

Conversation

vinistock
Copy link
Contributor

This commit changes the node template to create a dispatcher class, which can be used to walk an AST an emit events to all registered listeners.

This is useful when performing several types of analysis on the same AST to avoid having to perform several rounds of visits. Two main examples are:

  • LSPs: each different request can be a listener, which contains logic specific only to that request
  • Linters: each linter rule can be a listener that also encapsulates logic only related to itself

For both of them, this allows the complete analysis to be completed in a single round of visits, saving numerous method dispatches and improving performance.

This commit changes the node template to create a dispatcher class,
which can be used to walk an AST an emit events to all registered
listeners

Co-authored-by: Kevin Newton <[email protected]>
@kddnewton kddnewton merged commit 1cad6b0 into ruby:main Sep 15, 2023
@@ -48,6 +48,31 @@ module YARP
}.compact.join(", ") %>]
end

# def compact_child_nodes: () -> Array[Node]
def compact_child_nodes
<%- if node.fields.any? { |field| field.is_a?(YARP::OptionalNodeField) } -%>
Copy link
Member

Choose a reason for hiding this comment

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

Another possibility would be to use [node_field, *optional_node_field, *node_list_field] because there is no #to_a on Node and *nil is [].
Not sure if faster or slower though, it would make the logic here a bit simpler.

Comment on lines +216 to +223
case node.human
<%- nodes.each do |node| -%>
when :<%= node.human %>
listeners[:<%= node.human %>_enter]&.each { |listener| listener.<%= node.human %>_enter(node) }
queue = node.compact_child_nodes.concat(queue)
listeners[:<%= node.human %>_leave]&.each { |listener| listener.<%= node.human %>_leave(node) }
<%- end -%>
end
Copy link
Member

Choose a reason for hiding this comment

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

These big case/when/end statements (dispatch is 570 lines) are pretty bad for optimizing JITs like on TruffleRuby and JRuby, it's likely too much code in a single method to compile or optimize well, somewhat similar to whitequark/parser#871.
Also this relies very heavily on case expr; when :symbol; when :symbol2; ...; end to use a Hash internally, otherwise it would be very slow as it would test each Symbol in order. I think that's the case on CRuby but on no other Ruby implementation.

I think something that would be fast for all Rubies and behave better for JIT would be to use a Hash of node.human to Procs containing the body of each when (one per node type to avoid needing listerner.send which would be slow).

<%- nodes.each do |node| -%>
when :<%= node.human %>
listeners[:<%= node.human %>_enter]&.each { |listener| listener.<%= node.human %>_enter(node) }
queue = node.compact_child_nodes.concat(queue)
Copy link
Member

Choose a reason for hiding this comment

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

Is this the correct order?
Maybe it should be queue.concat(node.compact_child_nodes)?

It looks like this adds child nodes before the current elements in the array (so on the "left side" i.e. near 0 index) and queue.shift above removes from the "left side" too.
So it looks like it behaves somewhat like a stack:

q = [1]
q.shift

q = [2, 3].concat(q)
p q # => [2, 3]
p q.shift # => 2

q = [4].concat(q)
p q # => [4, 3]
p q.shift # => 4, the last added element is the first out in this case
# Not fully LIFO though because if it was `q = [4, 5].concat(q)` above, then 4 is out before 5

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.

3 participants