From 0b2d012beddc2cb0ed6f4dbdca8486a8ce396b23 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Fri, 18 Nov 2022 10:32:57 -0500 Subject: [PATCH 01/13] Handle args forwarding --- lib/syntax_tree/visitor/compiler.rb | 99 +++++++++++++++++------------ test/compiler_test.rb | 4 ++ 2 files changed, 63 insertions(+), 40 deletions(-) diff --git a/lib/syntax_tree/visitor/compiler.rb b/lib/syntax_tree/visitor/compiler.rb index 10c59a77..029d858a 100644 --- a/lib/syntax_tree/visitor/compiler.rb +++ b/lib/syntax_tree/visitor/compiler.rb @@ -1218,13 +1218,26 @@ def visit_bodystmt(node) end def visit_call(node) + if node.is_a?(CallNode) + return visit_call( + CommandCall.new( + receiver: node.receiver, + operator: node.operator, + message: node.message, + arguments: node.arguments, + block: nil, + location: node.location + ) + ) + end + arg_parts = argument_parts(node.arguments) + argc = arg_parts.length # First we're going to check if we're calling a method on an array # literal without any arguments. In that case there are some # specializations we might be able to perform. - if arg_parts.empty? && - (node.message.is_a?(Ident) || node.message.is_a?(Op)) + if argc == 0 && (node.message.is_a?(Ident) || node.message.is_a?(Op)) case node.receiver when ArrayLiteral parts = node.receiver.contents&.parts || [] @@ -1257,48 +1270,39 @@ def visit_call(node) end node.receiver ? visit(node.receiver) : builder.putself - - visit(node.arguments) - block_iseq = visit(node.block) if node.respond_to?(:block) && node.block - - if arg_parts.last.is_a?(ArgBlock) - flag = node.receiver.nil? ? VM_CALL_FCALL : 0 - flag |= VM_CALL_ARGS_BLOCKARG - - if arg_parts.any? { |part| part.is_a?(ArgStar) } + flag = 0 + + arg_parts.each do |arg_part| + case arg_part + when ArgBlock + argc -= 1 + flag |= VM_CALL_ARGS_BLOCKARG + visit(arg_part) + when ArgStar flag |= VM_CALL_ARGS_SPLAT - end + visit(arg_part) + when ArgsForward + flag |= VM_CALL_ARGS_SPLAT | VM_CALL_ARGS_BLOCKARG - if arg_parts.any? { |part| part.is_a?(BareAssocHash) } + lookup = current_iseq.local_table.find(:*, 0) + builder.getlocal(lookup.index, lookup.level) + builder.splatarray(arg_parts.length != 1) + + lookup = current_iseq.local_table.find(:&, 0) + builder.getblockparamproxy(lookup.index, lookup.level) + when BareAssocHash flag |= VM_CALL_KW_SPLAT + visit(arg_part) + else + visit(arg_part) end + end - builder.send( - node.message.value.to_sym, - arg_parts.length - 1, - flag, - block_iseq - ) - else - flag = 0 - arg_parts.each do |arg_part| - case arg_part - when ArgStar - flag |= VM_CALL_ARGS_SPLAT - when BareAssocHash - flag |= VM_CALL_KW_SPLAT - end - end + block_iseq = visit(node.block) if node.block + flag |= VM_CALL_ARGS_SIMPLE if block_iseq.nil? && flag == 0 + flag |= VM_CALL_FCALL if node.receiver.nil? - flag |= VM_CALL_ARGS_SIMPLE if block_iseq.nil? && flag == 0 - flag |= VM_CALL_FCALL if node.receiver.nil? - builder.send( - node.message.value.to_sym, - arg_parts.length, - flag, - block_iseq - ) - end + builder.send(node.message.value.to_sym, argc, flag, block_iseq) end def visit_class(node) @@ -1781,7 +1785,18 @@ def visit_params(node) checkkeywords.each { |checkkeyword| checkkeyword[1] = lookup.index } end - visit(node.keyword_rest) if node.keyword_rest + if node.keyword_rest.is_a?(ArgsForward) + current_iseq.local_table.plain(:*) + current_iseq.local_table.plain(:&) + + current_iseq.argument_options[:rest_start] = current_iseq.argument_size + current_iseq.argument_options[:block_start] = current_iseq.argument_size + 1 + + current_iseq.argument_size += 2 + elsif node.keyword_rest + visit(node.keyword_rest) + end + visit(node.block) if node.block end @@ -2122,7 +2137,11 @@ def argument_parts(node) when Args node.parts when ArgParen - node.arguments.parts + if node.arguments.is_a?(ArgsForward) + [node.arguments] + else + node.arguments.parts + end when Paren node.contents.parts end diff --git a/test/compiler_test.rb b/test/compiler_test.rb index fe0bd1f6..ec3766e2 100644 --- a/test/compiler_test.rb +++ b/test/compiler_test.rb @@ -354,6 +354,10 @@ class CompilerTest < Minitest::Test "def foo(bar: 1, baz: qux, **rest); end", "def foo(bar: qux, baz: 1, **rest); end", "def foo(bar: baz, qux: qaz, **rest); end", + "def foo(...); end", + "def foo(bar, ...); end", + "def foo(...); bar(...); end", + "def foo(bar, ...); baz(1, 2, 3, ...); end", # Class/module definitions "module Foo; end", "module ::Foo; end", From c59c58550b19186a7d8e61fd3b6bee3796760263 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Fri, 18 Nov 2022 10:35:12 -0500 Subject: [PATCH 02/13] Handle until --- lib/syntax_tree/visitor/compiler.rb | 18 ++++++++++++++++++ test/compiler_test.rb | 3 +++ 2 files changed, 21 insertions(+) diff --git a/lib/syntax_tree/visitor/compiler.rb b/lib/syntax_tree/visitor/compiler.rb index 029d858a..1fe4365f 100644 --- a/lib/syntax_tree/visitor/compiler.rb +++ b/lib/syntax_tree/visitor/compiler.rb @@ -1997,6 +1997,24 @@ def visit_undef(node) end end + def visit_until(node) + jumps = [] + + jumps << builder.jump(-1) + builder.putnil + builder.pop + jumps << builder.jump(-1) + + label = builder.label + visit(node.statements) + builder.pop + jumps.each { |jump| jump[1] = builder.label } + + visit(node.predicate) + builder.branchunless(label) + builder.putnil if last_statement? + end + def visit_var_field(node) case node.value when CVar, IVar diff --git a/test/compiler_test.rb b/test/compiler_test.rb index ec3766e2..9fd3cfe9 100644 --- a/test/compiler_test.rb +++ b/test/compiler_test.rb @@ -260,6 +260,9 @@ class CompilerTest < Minitest::Test "if foo then bar elsif baz then qux end", "foo if bar", "foo while bar", + "while foo do bar end", + "foo until bar", + "until foo do bar end", "for i in [1, 2, 3] do i end", "foo ? bar : baz", # Constructed values From 2f78d142b15a2aefae11d4dbf94961ad9c4fee28 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Fri, 18 Nov 2022 10:38:49 -0500 Subject: [PATCH 03/13] Handle sclass --- lib/syntax_tree/visitor/compiler.rb | 13 +++++++++++++ test/compiler_test.rb | 2 ++ 2 files changed, 15 insertions(+) diff --git a/lib/syntax_tree/visitor/compiler.rb b/lib/syntax_tree/visitor/compiler.rb index 1fe4365f..d93021ef 100644 --- a/lib/syntax_tree/visitor/compiler.rb +++ b/lib/syntax_tree/visitor/compiler.rb @@ -1876,6 +1876,19 @@ def visit_rest_param(node) current_iseq.argument_size += 1 end + def visit_sclass(node) + visit(node.target) + builder.putnil + + singleton_iseq = + with_instruction_sequence(:class, "singleton class", current_iseq, node) do + visit(node.bodystmt) + builder.leave + end + + builder.defineclass(:singletonclass, singleton_iseq, VM_DEFINECLASS_TYPE_SINGLETON_CLASS) + end + def visit_statements(node) statements = node.body.select do |statement| diff --git a/test/compiler_test.rb b/test/compiler_test.rb index 9fd3cfe9..aba9cff5 100644 --- a/test/compiler_test.rb +++ b/test/compiler_test.rb @@ -378,6 +378,8 @@ class CompilerTest < Minitest::Test "class ::Foo::Bar < Baz; end", "class Foo; class Bar < Baz; end; end", "class Foo < baz; end", + "class << Object; end", + "class << ::String; end", # Block "foo do end", "foo {}", From 69bf4bcf447180e5bfc5daf20a282815cbbad307 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Fri, 18 Nov 2022 10:44:00 -0500 Subject: [PATCH 04/13] Handle lambda --- lib/syntax_tree/visitor/compiler.rb | 16 ++++++++++++++++ test/compiler_test.rb | 7 ++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/lib/syntax_tree/visitor/compiler.rb b/lib/syntax_tree/visitor/compiler.rb index d93021ef..e5807c0f 100644 --- a/lib/syntax_tree/visitor/compiler.rb +++ b/lib/syntax_tree/visitor/compiler.rb @@ -1591,6 +1591,22 @@ def visit_label(node) builder.putobject(node.accept(RubyVisitor.new)) end + def visit_lambda(node) + lambda_iseq = + with_instruction_sequence(:block, "block in #{current_iseq.name}", current_iseq, node) do + visit(node.params) + visit(node.statements) + builder.leave + end + + builder.putspecialobject(VM_SPECIAL_OBJECT_VMCORE) + builder.send(:lambda, 0, VM_CALL_FCALL, lambda_iseq) + end + + def visit_lambda_var(node) + visit_block_var(node) + end + def visit_method_add_block(node) visit_call( CommandCall.new( diff --git a/test/compiler_test.rb b/test/compiler_test.rb index aba9cff5..d0fed6fd 100644 --- a/test/compiler_test.rb +++ b/test/compiler_test.rb @@ -385,7 +385,12 @@ class CompilerTest < Minitest::Test "foo {}", "foo do |bar| end", "foo { |bar| }", - "foo { |bar; baz| }" + "foo { |bar; baz| }", + "-> do end", + "-> {}", + "-> (bar) do end", + "-> (bar) {}", + "-> (bar; baz) { }" ] # These are the combinations of instructions that we're going to test. From 5fa8a6f44800a638b14c886a0a11b7a2e9bd21bb Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Fri, 18 Nov 2022 10:47:49 -0500 Subject: [PATCH 05/13] Handle unless --- lib/syntax_tree/visitor/compiler.rb | 24 ++++++++++++++++++++++++ test/compiler_test.rb | 3 +++ 2 files changed, 27 insertions(+) diff --git a/lib/syntax_tree/visitor/compiler.rb b/lib/syntax_tree/visitor/compiler.rb index e5807c0f..fab538ae 100644 --- a/lib/syntax_tree/visitor/compiler.rb +++ b/lib/syntax_tree/visitor/compiler.rb @@ -2026,6 +2026,30 @@ def visit_undef(node) end end + def visit_unless(node) + visit(node.predicate) + branchunless = builder.branchunless(-1) + node.consequent ? visit(node.consequent) : builder.putnil + + if last_statement? + builder.leave + branchunless[1] = builder.label + + visit(node.statements) + else + builder.pop + + if node.consequent + jump = builder.jump(-1) + branchunless[1] = builder.label + visit(node.consequent) + jump[1] = builder.label + else + branchunless[1] = builder.label + end + end + end + def visit_until(node) jumps = [] diff --git a/test/compiler_test.rb b/test/compiler_test.rb index d0fed6fd..c9476b81 100644 --- a/test/compiler_test.rb +++ b/test/compiler_test.rb @@ -259,6 +259,9 @@ class CompilerTest < Minitest::Test "if foo then bar else baz end", "if foo then bar elsif baz then qux end", "foo if bar", + "unless foo then bar end", + "unless foo then bar else baz end", + "foo unless bar", "foo while bar", "while foo do bar end", "foo until bar", From 8e88b0d09fe32f759339fa6fc61d1efdf2e5d1cb Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Fri, 18 Nov 2022 10:51:06 -0500 Subject: [PATCH 06/13] definesmethod --- lib/syntax_tree/visitor/compiler.rb | 16 ++++++++++++++-- test/compiler_test.rb | 2 ++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/lib/syntax_tree/visitor/compiler.rb b/lib/syntax_tree/visitor/compiler.rb index fab538ae..c8e5b3e8 100644 --- a/lib/syntax_tree/visitor/compiler.rb +++ b/lib/syntax_tree/visitor/compiler.rb @@ -418,7 +418,7 @@ def serialize(insn) [insn[0], iseq.local_table.offset(insn[1]), *insn[2..]] when :defineclass [insn[0], insn[1], insn[2].to_a, insn[3]] - when :definemethod + when :definemethod, :definesmethod [insn[0], insn[1], insn[2].to_a] when :send # For any instructions that push instruction sequences onto the @@ -511,6 +511,11 @@ def definemethod(name, method_iseq) iseq.push([:definemethod, name, method_iseq]) end + def definesmethod(name, method_iseq) + stack.change_by(-1) + iseq.push([:definesmethod, name, method_iseq]) + end + def dup stack.change_by(-1 + 2) iseq.push([:dup]) @@ -1390,7 +1395,14 @@ def visit_def(node) end name = node.name.value.to_sym - builder.definemethod(name, method_iseq) + + if node.target + visit(node.target) + builder.definesmethod(name, method_iseq) + else + builder.definemethod(name, method_iseq) + end + builder.putobject(name) end diff --git a/test/compiler_test.rb b/test/compiler_test.rb index c9476b81..af42fe0a 100644 --- a/test/compiler_test.rb +++ b/test/compiler_test.rb @@ -364,6 +364,8 @@ class CompilerTest < Minitest::Test "def foo(bar, ...); end", "def foo(...); bar(...); end", "def foo(bar, ...); baz(1, 2, 3, ...); end", + "def self.foo; end", + "def foo.bar(baz); end", # Class/module definitions "module Foo; end", "module ::Foo; end", From f8ac1227dad0fc0c54db01c38e916d4e0f47b109 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Fri, 18 Nov 2022 10:57:28 -0500 Subject: [PATCH 07/13] getblockparam --- lib/syntax_tree/visitor/compiler.rb | 38 ++++++++++++++++++++--------- test/compiler_test.rb | 1 + 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/lib/syntax_tree/visitor/compiler.rb b/lib/syntax_tree/visitor/compiler.rb index c8e5b3e8..df5e4838 100644 --- a/lib/syntax_tree/visitor/compiler.rb +++ b/lib/syntax_tree/visitor/compiler.rb @@ -209,7 +209,7 @@ def change_by(value) class LocalTable # A local representing a block passed into the current instruction # sequence. - class BlockProxyLocal + class BlockLocal attr_reader :name def initialize(name) @@ -260,9 +260,9 @@ def size locals.length end - # Add a BlockProxyLocal to the local table. - def block_proxy(name) - locals << BlockProxyLocal.new(name) unless has?(name) + # Add a BlockLocal to the local table. + def block(name) + locals << BlockLocal.new(name) unless has?(name) end # Add a PlainLocal to the local table. @@ -401,15 +401,15 @@ def to_a def serialize(insn) case insn[0] - when :checkkeyword, :getblockparamproxy, :getlocal_WC_0, - :getlocal_WC_1, :getlocal, :setlocal_WC_0, :setlocal_WC_1, - :setlocal + when :checkkeyword, :getblockparam, :getblockparamproxy, + :getlocal_WC_0, :getlocal_WC_1, :getlocal, + :setlocal_WC_0, :setlocal_WC_1, :setlocal iseq = self case insn[0] when :getlocal_WC_1, :setlocal_WC_1 iseq = iseq.parent_iseq - when :getblockparamproxy, :getlocal, :setlocal + when :getblockparam, :getblockparamproxy, :getlocal, :setlocal insn[2].times { iseq = iseq.parent_iseq } end @@ -536,6 +536,11 @@ def dupn(number) iseq.push([:dupn, number]) end + def getblockparam(index, level) + stack.change_by(+1) + iseq.push([:getblockparam, index, level]) + end + def getblockparamproxy(index, level) stack.change_by(+1) iseq.push([:getblockparamproxy, index, level]) @@ -1214,7 +1219,7 @@ def visit_block_var(node) def visit_blockarg(node) current_iseq.argument_options[:block_start] = current_iseq.argument_size - current_iseq.local_table.block_proxy(node.name.value.to_sym) + current_iseq.local_table.block(node.name.value.to_sym) current_iseq.argument_size += 1 end @@ -1274,7 +1279,16 @@ def visit_call(node) end end - node.receiver ? visit(node.receiver) : builder.putself + if node.receiver + if node.receiver.is_a?(VarRef) && (lookup = current_iseq.local_variable(node.receiver.value.value.to_sym)) && lookup.local.is_a?(LocalTable::BlockLocal) + builder.getblockparamproxy(lookup.index, lookup.level) + else + visit(node.receiver) + end + else + builder.putself + end + flag = 0 arg_parts.each do |arg_part| @@ -2105,8 +2119,8 @@ def visit_var_ref(node) lookup = current_iseq.local_variable(node.value.value.to_sym) case lookup.local - when LocalTable::BlockProxyLocal - builder.getblockparamproxy(lookup.index, lookup.level) + when LocalTable::BlockLocal + builder.getblockparam(lookup.index, lookup.level) when LocalTable::PlainLocal builder.getlocal(lookup.index, lookup.level) end diff --git a/test/compiler_test.rb b/test/compiler_test.rb index af42fe0a..5f320ef2 100644 --- a/test/compiler_test.rb +++ b/test/compiler_test.rb @@ -342,6 +342,7 @@ class CompilerTest < Minitest::Test "def foo(bar, baz, *qux, quaz); end", "def foo(bar, baz, &qux); end", "def foo(bar, *baz, &qux); end", + "def foo(&qux); qux; end", "def foo(&qux); qux.call; end", "def foo(bar:); end", "def foo(bar:, baz:); end", From db88d3309390f061c62fa2896bd8d07d9ab81f61 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Fri, 18 Nov 2022 11:17:51 -0500 Subject: [PATCH 08/13] Handle case/when --- lib/syntax_tree/visitor/compiler.rb | 47 +++++++++++++++++++++++++++++ test/compiler_test.rb | 2 ++ 2 files changed, 49 insertions(+) diff --git a/lib/syntax_tree/visitor/compiler.rb b/lib/syntax_tree/visitor/compiler.rb index df5e4838..331a937e 100644 --- a/lib/syntax_tree/visitor/compiler.rb +++ b/lib/syntax_tree/visitor/compiler.rb @@ -1324,6 +1324,49 @@ def visit_call(node) builder.send(node.message.value.to_sym, argc, flag, block_iseq) end + def visit_case(node) + visit(node.value) if node.value + + clauses = [] + else_clause = nil + + current = node.consequent + + while current + clauses << current + + if (current = current.consequent).is_a?(Else) + else_clause = current + break + end + end + + branches = + clauses.map do |clause| + visit(clause.arguments) + builder.topn(1) + builder.send(:===, 1, VM_CALL_FCALL | VM_CALL_ARGS_SIMPLE) + [clause, builder.branchif(:label_00)] + end + + builder.pop + + if else_clause + visit(else_clause) + else + builder.putnil + end + + builder.leave + + branches.each_with_index do |(clause, branchif), index| + builder.leave if index != 0 + branchif[1] = builder.label + builder.pop + visit(clause) + end + end + def visit_class(node) name = node.constant.constant.value.to_sym class_iseq = @@ -2148,6 +2191,10 @@ def visit_vcall(node) builder.send(node.value.value.to_sym, 0, flag) end + def visit_when(node) + visit(node.statements) + end + def visit_while(node) jumps = [] diff --git a/test/compiler_test.rb b/test/compiler_test.rb index 5f320ef2..8b95c07a 100644 --- a/test/compiler_test.rb +++ b/test/compiler_test.rb @@ -268,6 +268,8 @@ class CompilerTest < Minitest::Test "until foo do bar end", "for i in [1, 2, 3] do i end", "foo ? bar : baz", + "case foo when bar then 1 end", + "case foo when bar then 1 else 2 end", # Constructed values "foo..bar", "foo...bar", From a7e78259dccd31606e1e2078ae72c3a22ccf5634 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Fri, 18 Nov 2022 11:41:39 -0500 Subject: [PATCH 09/13] Handle BEGIN{} and END{} --- lib/syntax_tree/visitor/compiler.rb | 70 +++++++++++++++++++++++------ 1 file changed, 56 insertions(+), 14 deletions(-) diff --git a/lib/syntax_tree/visitor/compiler.rb b/lib/syntax_tree/visitor/compiler.rb index 331a937e..8114a9c5 100644 --- a/lib/syntax_tree/visitor/compiler.rb +++ b/lib/syntax_tree/visitor/compiler.rb @@ -336,8 +336,6 @@ def local_variable(name, level = 0) lookup elsif parent_iseq parent_iseq.local_variable(name, level + 1) - else - raise "Unknown local variable: #{name}" end end @@ -388,7 +386,7 @@ def to_a name, "", "", - 1, + location.start_line, type, local_table.names, argument_options, @@ -424,6 +422,8 @@ def serialize(insn) # For any instructions that push instruction sequences onto the # stack, we need to call #to_a on them as well. [insn[0], insn[1], (insn[2].to_a if insn[2])] + when :once + [insn[0], insn[1].to_a, insn[2]] else insn end @@ -655,6 +655,11 @@ def objtostring(method_id, argc, flag) iseq.push([:objtostring, call_data(method_id, argc, flag)]) end + def once(postexe_iseq, inline_storage) + stack.change_by(+1) + iseq.push([:once, postexe_iseq, inline_storage]) + end + def opt_getconstant_path(names) if RUBY_VERSION >= "3.2" stack.change_by(+1) @@ -1002,6 +1007,10 @@ def initialize( @last_statement = false end + def visit_BEGIN(node) + visit(node.statements) + end + def visit_CHAR(node) if frozen_string_literal builder.putobject(node.value[1..]) @@ -1010,6 +1019,27 @@ def visit_CHAR(node) end end + def visit_END(node) + name = "block in #{current_iseq.name}" + once_iseq = + with_instruction_sequence(:block, name, current_iseq, node) do + postexe_iseq = + with_instruction_sequence(:block, name, current_iseq, node) do + *statements, last_statement = node.statements.body + visit_all(statements) + with_last_statement { visit(last_statement) } + builder.leave + end + + builder.putspecialobject(VM_SPECIAL_OBJECT_VMCORE) + builder.send(:"core#set_postexe", 0, VM_CALL_FCALL, postexe_iseq) + builder.leave + end + + builder.once(once_iseq, current_iseq.inline_storage) + builder.pop + end + def visit_alias(node) builder.putspecialobject(VM_SPECIAL_OBJECT_VMCORE) builder.putspecialobject(VM_SPECIAL_OBJECT_CBASE) @@ -1898,17 +1928,23 @@ def visit_program(node) end end - statements = - node.statements.body.select do |statement| - case statement - when Comment, EmbDoc, EndContent, VoidStmt - false - else - true - end + preexes = [] + statements = [] + + node.statements.body.each do |statement| + case statement + when Comment, EmbDoc, EndContent, VoidStmt + # ignore + when BEGINBlock + preexes << statement + else + statements << statement end + end with_instruction_sequence(:top, "", nil, node) do + visit_all(preexes) + if statements.empty? builder.putnil else @@ -2144,8 +2180,13 @@ def visit_var_field(node) current_iseq.inline_storage_for(name) when Ident name = node.value.value.to_sym - current_iseq.local_table.plain(name) - current_iseq.local_variable(name) + + if (local_variable = current_iseq.local_variable(name)) + local_variable + else + current_iseq.local_table.plain(name) + current_iseq.local_variable(name) + end end end @@ -2460,12 +2501,13 @@ def with_instruction_sequence(type, name, parent_iseq, node) # last statement of a scope and allow visit methods to query that # information. def with_last_statement + previous = @last_statement @last_statement = true begin yield ensure - @last_statement = false + @last_statement = previous end end From b022d5617560f57cc23a367946a4722b5dd6a9fd Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Fri, 18 Nov 2022 11:50:43 -0500 Subject: [PATCH 10/13] Handle mrhs, mlhs, massign --- lib/syntax_tree/visitor/compiler.rb | 41 +++++++++++++++++++++++++++++ test/compiler_test.rb | 6 +++++ 2 files changed, 47 insertions(+) diff --git a/lib/syntax_tree/visitor/compiler.rb b/lib/syntax_tree/visitor/compiler.rb index 8114a9c5..c09efe84 100644 --- a/lib/syntax_tree/visitor/compiler.rb +++ b/lib/syntax_tree/visitor/compiler.rb @@ -94,6 +94,10 @@ def visit_label(node) node.value.chomp(":").to_sym end + def visit_mrhs(node) + visit_all(node.parts) + end + def visit_qsymbols(node) node.elements.map { |element| visit(element).to_sym } end @@ -536,6 +540,11 @@ def dupn(number) iseq.push([:dupn, number]) end + def expandarray(length, flag) + stack.change_by(-1 + length) + iseq.push([:expandarray, length, flag]) + end + def getblockparam(index, level) stack.change_by(+1) iseq.push([:getblockparam, index, level]) @@ -1706,6 +1715,12 @@ def visit_lambda_var(node) visit_block_var(node) end + def visit_massign(node) + visit(node.value) + builder.dup + visit(node.target) + end + def visit_method_add_block(node) visit_call( CommandCall.new( @@ -1719,6 +1734,23 @@ def visit_method_add_block(node) ) end + def visit_mlhs(node) + lookups = [] + + node.parts.each do |part| + case part + when VarField + lookups << visit(part) + end + end + + builder.expandarray(lookups.length, 0) + + lookups.each do |lookup| + builder.setlocal(lookup.index, lookup.level) + end + end + def visit_module(node) name = node.constant.constant.value.to_sym module_iseq = @@ -1749,6 +1781,15 @@ def visit_module(node) builder.defineclass(name, module_iseq, flags) end + def visit_mrhs(node) + if (compiled = RubyVisitor.compile(node)) + builder.duparray(compiled) + else + visit_all(node.parts) + builder.newarray(node.parts.length) + end + end + def visit_not(node) visit(node.statement) builder.send(:!, 0, VM_CALL_ARGS_SIMPLE) diff --git a/test/compiler_test.rb b/test/compiler_test.rb index 8b95c07a..8868b801 100644 --- a/test/compiler_test.rb +++ b/test/compiler_test.rb @@ -130,6 +130,12 @@ class CompilerTest < Minitest::Test "foo ||= 1", "foo <<= 1", "foo ^= 1", + "foo, bar = 1, 2", + "foo, bar, = 1, 2", + "foo, bar, baz = 1, 2", + "foo, bar = 1, 2, 3", + "foo = 1, 2, 3", + "foo, * = 1, 2, 3", # Instance variables "@foo", "@foo = 1", From 358d029abd6bfa9bafee78154f89a03c95999d04 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Fri, 18 Nov 2022 11:55:54 -0500 Subject: [PATCH 11/13] Better handle visit_string_parts --- lib/syntax_tree/visitor/compiler.rb | 44 ++++++++++++++--------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/lib/syntax_tree/visitor/compiler.rb b/lib/syntax_tree/visitor/compiler.rb index c09efe84..8b42613c 100644 --- a/lib/syntax_tree/visitor/compiler.rb +++ b/lib/syntax_tree/visitor/compiler.rb @@ -1636,8 +1636,8 @@ def visit_heredoc(node) elsif node.parts.length == 1 && node.parts.first.is_a?(TStringContent) visit(node.parts.first) else - visit_string_parts(node) - builder.concatstrings(node.parts.length) + length = visit_string_parts(node) + builder.concatstrings(length) end end @@ -2026,10 +2026,9 @@ def visit_rational(node) def visit_regexp_literal(node) builder.putobject(node.accept(RubyVisitor.new)) rescue RubyVisitor::CompilationError - visit_string_parts(node) - flags = RubyVisitor.new.visit_regexp_literal_flags(node) - builder.toregexp(flags, node.parts.length) + length = visit_string_parts(node) + builder.toregexp(flags, length) end def visit_rest_param(node) @@ -2086,8 +2085,8 @@ def visit_string_literal(node) if node.parts.length == 1 && node.parts.first.is_a?(TStringContent) visit(node.parts.first) else - visit_string_parts(node) - builder.concatstrings(node.parts.length) + length = visit_string_parts(node) + builder.concatstrings(length) end end @@ -2114,13 +2113,7 @@ def visit_symbols(node) element.parts.first.is_a?(TStringContent) builder.putobject(element.parts.first.value.to_sym) else - length = element.parts.length - unless element.parts.first.is_a?(TStringContent) - builder.putobject("") - length += 1 - end - - visit_string_parts(element) + length = visit_string_parts(element) builder.concatstrings(length) builder.intern end @@ -2299,13 +2292,7 @@ def visit_word(node) if node.parts.length == 1 && node.parts.first.is_a?(TStringContent) visit(node.parts.first) else - length = node.parts.length - unless node.parts.first.is_a?(TStringContent) - builder.putobject("") - length += 1 - end - - visit_string_parts(node) + length = visit_string_parts(node) builder.concatstrings(length) end end @@ -2330,8 +2317,8 @@ def visit_words(node) def visit_xstring_literal(node) builder.putself - visit_string_parts(node) - builder.concatstrings(node.parts.length) if node.parts.length > 1 + length = visit_string_parts(node) + builder.concatstrings(node.parts.length) if length > 1 builder.send(:`, 1, VM_CALL_FCALL | VM_CALL_ARGS_SIMPLE) end @@ -2493,6 +2480,13 @@ def push_interpolate # heredocs, etc. This method will visit all the parts of a string within # those containers. def visit_string_parts(node) + length = 0 + + unless node.parts.first.is_a?(TStringContent) + builder.putobject("") + length += 1 + end + node.parts.each do |part| case part when StringDVar @@ -2504,7 +2498,11 @@ def visit_string_parts(node) when TStringContent builder.putobject(part.accept(RubyVisitor.new)) end + + length += 1 end + + length end # The current instruction sequence that we're compiling is always stored From 593486dff299c01b39bf16a0ad4cd40a9147e45f Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Fri, 18 Nov 2022 11:59:56 -0500 Subject: [PATCH 12/13] Handle the &. operator --- lib/syntax_tree/visitor/compiler.rb | 82 ++++++++++++++++++++--------- test/compiler_test.rb | 4 ++ 2 files changed, 60 insertions(+), 26 deletions(-) diff --git a/lib/syntax_tree/visitor/compiler.rb b/lib/syntax_tree/visitor/compiler.rb index 8b42613c..bac8b914 100644 --- a/lib/syntax_tree/visitor/compiler.rb +++ b/lib/syntax_tree/visitor/compiler.rb @@ -404,8 +404,8 @@ def to_a def serialize(insn) case insn[0] when :checkkeyword, :getblockparam, :getblockparamproxy, - :getlocal_WC_0, :getlocal_WC_1, :getlocal, - :setlocal_WC_0, :setlocal_WC_1, :setlocal + :getlocal_WC_0, :getlocal_WC_1, :getlocal, :setlocal_WC_0, + :setlocal_WC_1, :setlocal iseq = self case insn[0] @@ -480,6 +480,11 @@ def branchif(index) iseq.push([:branchif, index]) end + def branchnil(index) + stack.change_by(-1) + iseq.push([:branchnil, index]) + end + def branchunless(index) stack.change_by(-1) iseq.push([:branchunless, index]) @@ -1268,14 +1273,16 @@ def visit_bodystmt(node) def visit_call(node) if node.is_a?(CallNode) - return visit_call( - CommandCall.new( - receiver: node.receiver, - operator: node.operator, - message: node.message, - arguments: node.arguments, - block: nil, - location: node.location + return( + visit_call( + CommandCall.new( + receiver: node.receiver, + operator: node.operator, + message: node.message, + arguments: node.arguments, + block: nil, + location: node.location + ) ) ) end @@ -1319,7 +1326,11 @@ def visit_call(node) end if node.receiver - if node.receiver.is_a?(VarRef) && (lookup = current_iseq.local_variable(node.receiver.value.value.to_sym)) && lookup.local.is_a?(LocalTable::BlockLocal) + if node.receiver.is_a?(VarRef) && + ( + lookup = + current_iseq.local_variable(node.receiver.value.value.to_sym) + ) && lookup.local.is_a?(LocalTable::BlockLocal) builder.getblockparamproxy(lookup.index, lookup.level) else visit(node.receiver) @@ -1328,6 +1339,12 @@ def visit_call(node) builder.putself end + branchnil = + if node.operator&.value == "&." + builder.dup + builder.branchnil(-1) + end + flag = 0 arg_parts.each do |arg_part| @@ -1361,6 +1378,7 @@ def visit_call(node) flag |= VM_CALL_FCALL if node.receiver.nil? builder.send(node.message.value.to_sym, argc, flag, block_iseq) + branchnil[1] = builder.label if branchnil end def visit_case(node) @@ -1390,11 +1408,7 @@ def visit_case(node) builder.pop - if else_clause - visit(else_clause) - else - builder.putnil - end + else_clause ? visit(else_clause) : builder.putnil builder.leave @@ -1701,7 +1715,12 @@ def visit_label(node) def visit_lambda(node) lambda_iseq = - with_instruction_sequence(:block, "block in #{current_iseq.name}", current_iseq, node) do + with_instruction_sequence( + :block, + "block in #{current_iseq.name}", + current_iseq, + node + ) do visit(node.params) visit(node.statements) builder.leave @@ -1746,9 +1765,7 @@ def visit_mlhs(node) builder.expandarray(lookups.length, 0) - lookups.each do |lookup| - builder.setlocal(lookup.index, lookup.level) - end + lookups.each { |lookup| builder.setlocal(lookup.index, lookup.level) } end def visit_module(node) @@ -1944,10 +1961,14 @@ def visit_params(node) if node.keyword_rest.is_a?(ArgsForward) current_iseq.local_table.plain(:*) current_iseq.local_table.plain(:&) - - current_iseq.argument_options[:rest_start] = current_iseq.argument_size - current_iseq.argument_options[:block_start] = current_iseq.argument_size + 1 - + + current_iseq.argument_options[ + :rest_start + ] = current_iseq.argument_size + current_iseq.argument_options[ + :block_start + ] = current_iseq.argument_size + 1 + current_iseq.argument_size += 2 elsif node.keyword_rest visit(node.keyword_rest) @@ -2042,12 +2063,21 @@ def visit_sclass(node) builder.putnil singleton_iseq = - with_instruction_sequence(:class, "singleton class", current_iseq, node) do + with_instruction_sequence( + :class, + "singleton class", + current_iseq, + node + ) do visit(node.bodystmt) builder.leave end - builder.defineclass(:singletonclass, singleton_iseq, VM_DEFINECLASS_TYPE_SINGLETON_CLASS) + builder.defineclass( + :singletonclass, + singleton_iseq, + VM_DEFINECLASS_TYPE_SINGLETON_CLASS + ) end def visit_statements(node) diff --git a/test/compiler_test.rb b/test/compiler_test.rb index 8868b801..7afd920e 100644 --- a/test/compiler_test.rb +++ b/test/compiler_test.rb @@ -259,6 +259,10 @@ class CompilerTest < Minitest::Test "Foo::Bar.baz = 1", "::Foo::Bar.baz = 1", # Control flow + "foo&.bar", + "foo&.bar(1)", + "foo&.bar 1, 2, 3", + "foo&.bar {}", "foo && bar", "foo || bar", "if foo then bar end", From 7c58e9204e12c84f17825175fa65bc67f3489bd0 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Fri, 18 Nov 2022 13:48:11 -0500 Subject: [PATCH 13/13] Test evaluation --- test/compiler_test.rb | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test/compiler_test.rb b/test/compiler_test.rb index 7afd920e..632b3e55 100644 --- a/test/compiler_test.rb +++ b/test/compiler_test.rb @@ -2,9 +2,17 @@ return if !defined?(RubyVM::InstructionSequence) || RUBY_VERSION < "3.1" require_relative "test_helper" +require "fiddle" module SyntaxTree class CompilerTest < Minitest::Test + ISEQ_LOAD = + Fiddle::Function.new( + Fiddle::Handle::DEFAULT["rb_iseq_load"], + [Fiddle::TYPE_VOIDP] * 3, + Fiddle::TYPE_VOIDP + ) + CASES = [ # Various literals placed on the stack "true", @@ -430,6 +438,11 @@ class CompilerTest < Minitest::Test end end + def test_evaluation + assert_evaluates 5, "2 + 3" + assert_evaluates 5, "a = 2; b = 3; a + b" + end + private def serialize_iseq(iseq) @@ -463,5 +476,17 @@ def assert_compiles(source, **options) serialize_iseq(program.accept(Visitor::Compiler.new(**options))) ) end + + def assert_evaluates(expected, source, **options) + program = SyntaxTree.parse(source) + compiled = program.accept(Visitor::Compiler.new(**options)).to_a + + # Temporary hack until we get these working. + compiled[4][:node_id] = 11 + compiled[4][:node_ids] = [1, 0, 3, 2, 6, 7, 9, -1] + + iseq = Fiddle.dlunwrap(ISEQ_LOAD.call(Fiddle.dlwrap(compiled), 0, nil)) + assert_equal expected, iseq.eval + end end end