Skip to content

Commit

Permalink
Merge pull request #2273 from natalie-lang/rb/block-forwarding
Browse files Browse the repository at this point in the history
Add support for anonymous block forwarding
  • Loading branch information
seven1m authored Oct 26, 2024
2 parents 015516e + 65b3619 commit e66e7f6
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 66 deletions.
2 changes: 2 additions & 0 deletions lib/natalie/compiler/instructions.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
require_relative './instructions/alias_global_instruction'
require_relative './instructions/alias_method_instruction'
require_relative './instructions/anonymous_block_get_instruction'
require_relative './instructions/anonymous_block_set_instruction'
require_relative './instructions/array_concat_instruction'
require_relative './instructions/array_pop_instruction'
require_relative './instructions/array_pop_with_default_instruction'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
require_relative './base_instruction'

module Natalie
class Compiler
class AnonymousBlockGetInstruction < BaseInstruction
def to_s
'anonymous_block_get'
end

def generate(transform)
transform.push('anon_block')
end

def execute(vm)
if (var = vm.find_var(:anon_block))
vm.push(var.fetch(:value))
else
raise "anonymous block was not defined #{@name}"
end
end

def serialize(_)
[instruction_number].pack('C')
end

def self.deserialize(_, _)
new
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
require_relative './base_instruction'

module Natalie
class Compiler
class AnonymousBlockSetInstruction < BaseInstruction
def to_s
'anonymous_block_set'
end

def generate(transform)
value = transform.pop
transform.exec("Value anon_block = #{value}")
end

def execute(vm)
vm.scope[:vars][:anon_block] = { value: vm.pop }
end

def serialize(_)
[instruction_number].pack('C')
end

def self.deserialize(_, _)
new
end
end
end
end
2 changes: 2 additions & 0 deletions lib/natalie/compiler/instructions/meta.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ class Compiler
INSTRUCTIONS = [
AliasGlobalInstruction,
AliasMethodInstruction,
AnonymousBlockGetInstruction,
AnonymousBlockSetInstruction,
ArrayConcatInstruction,
ArrayPopInstruction,
ArrayPopWithDefaultInstruction,
Expand Down
17 changes: 11 additions & 6 deletions lib/natalie/compiler/pass1.rb
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,11 @@ def transform_block_args_for_lambda(exp, used:)

def transform_block_argument_node(node, used:)
return [] unless used
[transform_expression(node.expression, used: true)]
if node.expression
[transform_expression(node.expression, used: true)]
else
[AnonymousBlockGetInstruction.new]
end
end

def transform_block_node(node, used:, is_lambda:)
Expand Down Expand Up @@ -1216,10 +1220,6 @@ def transform_defn_args(node, used:, for_block: false, check_args: true, local_o
when nil
[]
when Prism::ParametersNode
if node&.block && node.block.name.nil?
raise "Anonymous block argument forwarding not yet supported (#{file.path}:#{node.location.start_line})"
end

(
node.requireds +
[node.rest] +
Expand Down Expand Up @@ -1249,7 +1249,12 @@ def transform_defn_args(node, used:, for_block: false, check_args: true, local_o
if args.last&.type == :block_parameter_node
block_arg = args.pop
instructions << PushBlockInstruction.new
instructions << VariableSetInstruction.new(block_arg.name, local_only: local_only)
instructions <<
if block_arg.name
VariableSetInstruction.new(block_arg.name, local_only: local_only)
else
AnonymousBlockSetInstruction.new
end
end

has_complicated_args = args.any? do |arg|
Expand Down
114 changes: 54 additions & 60 deletions spec/language/block_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1015,80 +1015,74 @@ def a; 1; end

describe "Anonymous block forwarding" do
ruby_version_is "3.1" do
# NATFIXME: Compilation errors
#it "forwards blocks to other method that formally declares anonymous block" do
#eval <<-EOF
#def b(&); c(&) end
#def c(&); yield :non_null end
#EOF

#b { |c| c }.should == :non_null
#end
it "forwards blocks to other method that formally declares anonymous block" do
eval <<-EOF
def b(&); c(&) end
def c(&); yield :non_null end
EOF

b { |c| c }.should == :non_null
end

it "requires the anonymous block parameter to be declared if directly passing a block" do
-> { eval "def a; b(&); end; def b; end" }.should raise_error(SyntaxError)
end

# NATFIXME: Compilation errors
#it "works when it's the only declared parameter" do
#eval <<-EOF
#def inner; yield end
#def block_only(&); inner(&) end
#EOF
it "works when it's the only declared parameter" do
eval <<-EOF
def inner; yield end
def block_only(&); inner(&) end
EOF

#block_only { 1 }.should == 1
#end
block_only { 1 }.should == 1
end

# NATFIXME: Compilation errors
#it "works alongside positional parameters" do
#eval <<-EOF
#def inner; yield end
#def pos(arg1, &); inner(&) end
#EOF
it "works alongside positional parameters" do
eval <<-EOF
def inner; yield end
def pos(arg1, &); inner(&) end
EOF

#pos(:a) { 1 }.should == 1
#end
pos(:a) { 1 }.should == 1
end

# NATFIXME: Compilation errors
#it "works alongside positional arguments and splatted keyword arguments" do
#eval <<-EOF
#def inner; yield end
#def pos_kwrest(arg1, **kw, &); inner(&) end
#EOF
it "works alongside positional arguments and splatted keyword arguments" do
eval <<-EOF
def inner; yield end
def pos_kwrest(arg1, **kw, &); inner(&) end
EOF

#pos_kwrest(:a, arg: 3) { 1 }.should == 1
#end
pos_kwrest(:a, arg: 3) { 1 }.should == 1
end

# NATFIXME: Compilation errors
#it "works alongside positional arguments and disallowed keyword arguments" do
#eval <<-EOF
#def inner; yield end
#def no_kw(arg1, **nil, &); inner(&) end
#EOF
it "works alongside positional arguments and disallowed keyword arguments" do
eval <<-EOF
def inner; yield end
def no_kw(arg1, **nil, &); inner(&) end
EOF

#no_kw(:a) { 1 }.should == 1
#end
no_kw(:a) { 1 }.should == 1
end
end

ruby_version_is "3.2" do
# NATFIXME: Compilation errors
#it "works alongside explicit keyword arguments" do
#eval <<-EOF
#def inner; yield end
#def rest_kw(*a, kwarg: 1, &); inner(&) end
#def kw(kwarg: 1, &); inner(&) end
#def pos_kw_kwrest(arg1, kwarg: 1, **kw, &); inner(&) end
#def pos_rkw(arg1, kwarg1:, &); inner(&) end
#def all(arg1, arg2, *rest, post1, post2, kw1: 1, kw2: 2, okw1:, okw2:, &); inner(&) end
#def all_kwrest(arg1, arg2, *rest, post1, post2, kw1: 1, kw2: 2, okw1:, okw2:, **kw, &); inner(&) end
#EOF

#rest_kw { 1 }.should == 1
#kw { 1 }.should == 1
#pos_kw_kwrest(:a) { 1 }.should == 1
#pos_rkw(:a, kwarg1: 3) { 1 }.should == 1
#all(:a, :b, :c, :d, :e, okw1: 'x', okw2: 'y') { 1 }.should == 1
#all_kwrest(:a, :b, :c, :d, :e, okw1: 'x', okw2: 'y') { 1 }.should == 1
#end
it "works alongside explicit keyword arguments" do
eval <<-EOF
def inner; yield end
def rest_kw(*a, kwarg: 1, &); inner(&) end
def kw(kwarg: 1, &); inner(&) end
def pos_kw_kwrest(arg1, kwarg: 1, **kw, &); inner(&) end
def pos_rkw(arg1, kwarg1:, &); inner(&) end
def all(arg1, arg2, *rest, post1, post2, kw1: 1, kw2: 2, okw1:, okw2:, &); inner(&) end
def all_kwrest(arg1, arg2, *rest, post1, post2, kw1: 1, kw2: 2, okw1:, okw2:, **kw, &); inner(&) end
EOF

rest_kw { 1 }.should == 1
kw { 1 }.should == 1
pos_kw_kwrest(:a) { 1 }.should == 1
pos_rkw(:a, kwarg1: 3) { 1 }.should == 1
all(:a, :b, :c, :d, :e, okw1: 'x', okw2: 'y') { 1 }.should == 1
all_kwrest(:a, :b, :c, :d, :e, okw1: 'x', okw2: 'y') { 1 }.should == 1
end
end
end

0 comments on commit e66e7f6

Please sign in to comment.