Advent of Prism: Part 15 - Call arguments

This blog series is about how the prism Ruby parser works. If you’re new to the series, I recommend starting from the beginning. This post is about call arguments.

Today we’re going to talk about the nodes we use to represent the arguments to a method call. Let’s get into it.

ArgumentsNode

The general list of methods that we pass to a method call is represented by an ArgumentsNode. This node is effectively a wrapper around the list of arguments. It is present when there is one or more arguments, and nil if there are no arguments. Here’s an example:

foo(1, 2, 3)

This is represented by the following AST:

arguments node

You’ll notice there’s an explicit field for flags. This is only used for a single flag, which indicates the presence of a splat operator within the list of arguments. If there is a splat operator, then compilers cannot statically determine the number of arguments, and must instead determine it at runtime. So if compilers want to take a different path through the code depending on whether or not there is a splat operator, they can check this flag.

Argument lists also appear on a couple of keywords: super, break, next, yield, and return. We’ve covered super already, but will get to the others soon.

BlockArgumentNode

When you pass a block to a method with the & operator, we create a BlockArgumentNode. This syntax implies that #to_proc should be called on the argument and passed to the method. Here’s an example:

foo(&bar)

The above snippet is represented by the following AST:

block argument node

The expression is actually optional if you’re within a method definition that has an anonymous block. For example:

def foo(&)
  bar(&)
end

This syntax means to forward the block from the foo method call down to the bar method call. This is also represented by a BlockArgumentNode, but with a nil expression. For example, the bar(&) in the above example:

block argument node

ForwardingArgumentsNode

You can use the ... operator to forward all arguments types (positional, keyword, and block) to a method call. This is represented by a ForwardingArgumentsNode. Here’s an example:

def foo(...)
  bar(...)
end

This will be represented by the following AST:

forwarding arguments node

In terms of actually parsing this, it’s relatively simple. The ... operator can only appear in an argument list if it has been declared in the current method’s parameter list. Internally we take a shortcut by adding ... to the local table and then checking it as we would any other identifier.

KeywordHashNode

When you use internal hash syntax within an argument list, we create a KeywordHashNode. Here are a couple of examples:

foo(bar: 1)
foo(:bar => 1)
foo(**bar)

In all of these cases we create a KeywordHashNode. The last two lines will be passed as a hash argument. The first line it depends on how the method was declared; it could end up being a hash in the first position or a keyword. Here’s what the AST looks like for the first example:

keyword hash node

SplatNode

When you use the * unary operator, we create a SplatNode. This can appear in a couple of different places. It can either imply to spread out a list of values or to group them. Here are a couple of examples:

foo(*bar)

foo, * = bar
foo, = *baz

begin
rescue *Foo
end

foo in [*bar]

Today we’ll only be looking at the first example. The others have either already been covered or will be covered in their own posts. Here’s what the AST looks like for foo(*bar):

splat node

Note that the expression field is optional. Just like the & operator, it can be used to forward arguments, as in:

def foo(*)
  bar(*)
end

The AST for the method call in this example looks like this:

splat node

ImplicitNode

The last type of node we’ll look at today is ImplicitNode. This node is used to represent an implicit hash key. While not exclusively used in method calls (it can also be used in plain hashes) it is most commonly used in keyword arguments. Here is an example:

foo(bar:)

This is represented by the following AST:

implicit node

Note that an implicit node effectively wraps the node that would have been present if it were explicit. We wrap it so that we don’t have to have an ImplicitCallNode, ImplicitLocalVariableReadNode, and ImplicitConstantReadNode. Instead it’s a marker that the following subtree is implicit.

It can also be used to represent local variables, as in:

bar = 1
foo(bar:)
implicit node

Finally, it can be used to look up constants, as in:

foo(Bar:)

That results in the following AST:

implicit node

Wrapping up

Today we looked at the many different kinds of arguments to method calls. Here are a couple of things to remember from today’s post:

Tomorrow we’ll wrap up our discussion of method calls by looking at the most complicated form: control-flow calls.

← Back to home