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

Reusing AST nodes in macro #each #15008

Open
HertzDevil opened this issue Sep 18, 2024 · 1 comment
Open

Reusing AST nodes in macro #each #15008

HertzDevil opened this issue Sep 18, 2024 · 1 comment

Comments

@HertzDevil
Copy link
Contributor

Some yielding methods in the macro language like RangeLiteral#each allocate a new node on each iteration:

when "each"
interpret_check_args(uses_block: true) do
block_arg = block.args.first?
interpret_to_range(interpreter).each do |num|
interpreter.define_var(block_arg.name, NumberLiteral.new(num)) if block_arg
interpreter.accept block.body
end
NilLiteral.new
end

This means {% (0...10000).each { } %} allocates 10000 NumberLiteral nodes. However, since all these variables have the same node type, and since Crystal::NumberLiteral#value= exists, it is actually possible to use a single node for all iterations:

block_arg = block.args.first?
block_var = nil

interpret_to_range(interpreter).each do |num|
  if block_arg
    if block_var
      block_var.value = num.to_s
    else
      block_var = NumberLiteral.new(num)
    end
    interpreter.define_var(block_arg.name, block_var)
  end
  interpreter.accept block.body
end

Thus if we know that the iteration variable won't be used outside the block, there should be a way to opt in to this behavior:

# prints 1, 2, 3, 4
{% (1..4).each(reuse: true) { |i| p i } %}

# probably unintended behavior
{%
  arr = [] of _
  (1..4).each(reuse: true) { |i| arr << i }
  arr # => [4, 4, 4, 4]
  (1..4).each(reuse: false) { |i| arr << i }
  arr # => [4, 4, 4, 4, 1, 2, 3, 4]
%}

This only makes sense for RangeLiteral#each and technically the keys for NamedTupleLiteral#each, because other similar methods like ArrayLiteral#each are heterogeneous or do not allocate new nodes per iteration. The main benefit of this approach is being able to define StringLiteral#each_char in a similar manner, since such a method is presumbaly avoided for performance reasons; and BytesLiteral#each_byte, if a new syntax is invented for #2886.

@straight-shoota
Copy link
Member

straight-shoota commented Sep 18, 2024

Are there any relevant real use cases for RangeLiteral#each in singnificant sizes where performance/memory usage has a noticable effect? I somewhat doubt that.

I can see that something like this could be useful for iterating StringLiteral (and BytesLiteral). But maybe we should focus on those use cases specifically and not so much on RangeLiteral.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants