From b18e05b18f5987cd5ce506af380558fd192d2d1c Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Fri, 22 Sep 2023 11:31:45 -0400 Subject: [ruby/yarp] Split up compiler versus visitor https://github.com/ruby/yarp/commit/2e6baa3f19 --- lib/yarp.rb | 25 +-- lib/yarp/desugar_compiler.rb | 204 +++++++++++++++++++++++ lib/yarp/desugar_visitor.rb | 204 ----------------------- lib/yarp/yarp.gemspec | 6 +- test/yarp/compiler_test.rb | 30 ++++ test/yarp/desugar_compiler_test.rb | 86 ++++++++++ test/yarp/desugar_visitor_test.rb | 86 ---------- yarp/templates/lib/yarp/compiler.rb.erb | 41 +++++ yarp/templates/lib/yarp/mutation_compiler.rb.erb | 19 +++ yarp/templates/lib/yarp/node.rb.erb | 11 +- yarp/templates/lib/yarp/visitor.rb.erb | 46 +++++ yarp/templates/template.rb | 4 +- 12 files changed, 440 insertions(+), 322 deletions(-) create mode 100644 lib/yarp/desugar_compiler.rb delete mode 100644 lib/yarp/desugar_visitor.rb create mode 100644 test/yarp/compiler_test.rb create mode 100644 test/yarp/desugar_compiler_test.rb delete mode 100644 test/yarp/desugar_visitor_test.rb create mode 100644 yarp/templates/lib/yarp/compiler.rb.erb create mode 100644 yarp/templates/lib/yarp/mutation_compiler.rb.erb create mode 100644 yarp/templates/lib/yarp/visitor.rb.erb diff --git a/lib/yarp.rb b/lib/yarp.rb index 9b9df081d8..4908e57b05 100644 --- a/lib/yarp.rb +++ b/lib/yarp.rb @@ -229,24 +229,6 @@ module YARP end end - # A class that knows how to walk down the tree. None of the individual visit - # methods are implemented on this visitor, so it forces the consumer to - # implement each one that they need. For a default implementation that - # continues walking the tree, see the Visitor class. - class BasicVisitor - def visit(node) - node&.accept(self) - end - - def visit_all(nodes) - nodes.map { |node| visit(node) } - end - - def visit_child_nodes(node) - visit_all(node.child_nodes) - end - end - # This represents a token from the Ruby source. class Token attr_reader :type, :value, :location @@ -539,14 +521,17 @@ module YARP # which means the files can end up being quite large. We autoload them to make # our require speed faster since consuming libraries are unlikely to use all # of these features. - autoload :DesugarVisitor, "yarp/desugar_visitor" + autoload :BasicVisitor, "yarp/visitor" + autoload :Compiler, "yarp/compiler" + autoload :DesugarCompiler, "yarp/desugar_compiler" autoload :Dispatcher, "yarp/dispatcher" autoload :DSL, "yarp/dsl" - autoload :MutationVisitor, "yarp/mutation_visitor" + autoload :MutationCompiler, "yarp/mutation_compiler" autoload :RipperCompat, "yarp/ripper_compat" autoload :Pack, "yarp/pack" autoload :Pattern, "yarp/pattern" autoload :Serialize, "yarp/serialize" + autoload :Visitor, "yarp/visitor" # Load the serialized AST using the source as a reference into a tree. def self.load(source, serialized) diff --git a/lib/yarp/desugar_compiler.rb b/lib/yarp/desugar_compiler.rb new file mode 100644 index 0000000000..2cf5fb701a --- /dev/null +++ b/lib/yarp/desugar_compiler.rb @@ -0,0 +1,204 @@ +# frozen_string_literal: true + +module YARP + class DesugarCompiler < MutationCompiler + # @@foo &&= bar + # + # becomes + # + # @@foo && @@foo = bar + def visit_class_variable_and_write_node(node) + desugar_and_write_node(node, ClassVariableReadNode, ClassVariableWriteNode, node.name) + end + + # @@foo ||= bar + # + # becomes + # + # defined?(@@foo) ? @@foo : @@foo = bar + def visit_class_variable_or_write_node(node) + desugar_or_write_defined_node(node, ClassVariableReadNode, ClassVariableWriteNode, node.name) + end + + # @@foo += bar + # + # becomes + # + # @@foo = @@foo + bar + def visit_class_variable_operator_write_node(node) + desugar_operator_write_node(node, ClassVariableReadNode, ClassVariableWriteNode, node.name) + end + + # Foo &&= bar + # + # becomes + # + # Foo && Foo = bar + def visit_constant_and_write_node(node) + desugar_and_write_node(node, ConstantReadNode, ConstantWriteNode, node.name) + end + + # Foo ||= bar + # + # becomes + # + # defined?(Foo) ? Foo : Foo = bar + def visit_constant_or_write_node(node) + desugar_or_write_defined_node(node, ConstantReadNode, ConstantWriteNode, node.name) + end + + # Foo += bar + # + # becomes + # + # Foo = Foo + bar + def visit_constant_operator_write_node(node) + desugar_operator_write_node(node, ConstantReadNode, ConstantWriteNode, node.name) + end + + # $foo &&= bar + # + # becomes + # + # $foo && $foo = bar + def visit_global_variable_and_write_node(node) + desugar_and_write_node(node, GlobalVariableReadNode, GlobalVariableWriteNode, node.name) + end + + # $foo ||= bar + # + # becomes + # + # defined?($foo) ? $foo : $foo = bar + def visit_global_variable_or_write_node(node) + desugar_or_write_defined_node(node, GlobalVariableReadNode, GlobalVariableWriteNode, node.name) + end + + # $foo += bar + # + # becomes + # + # $foo = $foo + bar + def visit_global_variable_operator_write_node(node) + desugar_operator_write_node(node, GlobalVariableReadNode, GlobalVariableWriteNode, node.name) + end + + # @foo &&= bar + # + # becomes + # + # @foo && @foo = bar + def visit_instance_variable_and_write_node(node) + desugar_and_write_node(node, InstanceVariableReadNode, InstanceVariableWriteNode, node.name) + end + + # @foo ||= bar + # + # becomes + # + # @foo || @foo = bar + def visit_instance_variable_or_write_node(node) + desugar_or_write_node(node, InstanceVariableReadNode, InstanceVariableWriteNode, node.name) + end + + # @foo += bar + # + # becomes + # + # @foo = @foo + bar + def visit_instance_variable_operator_write_node(node) + desugar_operator_write_node(node, InstanceVariableReadNode, InstanceVariableWriteNode, node.name) + end + + # foo &&= bar + # + # becomes + # + # foo && foo = bar + def visit_local_variable_and_write_node(node) + desugar_and_write_node(node, LocalVariableReadNode, LocalVariableWriteNode, node.name, node.depth) + end + + # foo ||= bar + # + # becomes + # + # foo || foo = bar + def visit_local_variable_or_write_node(node) + desugar_or_write_node(node, LocalVariableReadNode, LocalVariableWriteNode, node.name, node.depth) + end + + # foo += bar + # + # becomes + # + # foo = foo + bar + def visit_local_variable_operator_write_node(node) + desugar_operator_write_node(node, LocalVariableReadNode, LocalVariableWriteNode, node.name, node.depth) + end + + private + + # Desugar `x &&= y` to `x && x = y` + def desugar_and_write_node(node, read_class, write_class, *arguments) + AndNode.new( + read_class.new(*arguments, node.name_loc), + write_class.new(*arguments, node.name_loc, node.value, node.operator_loc, node.location), + node.operator_loc, + node.location + ) + end + + # Desugar `x += y` to `x = x + y` + def desugar_operator_write_node(node, read_class, write_class, *arguments) + write_class.new( + *arguments, + node.name_loc, + CallNode.new( + read_class.new(*arguments, node.name_loc), + nil, + node.operator_loc.copy(length: node.operator_loc.length - 1), + nil, + ArgumentsNode.new([node.value], node.value.location), + nil, + nil, + 0, + node.operator_loc.slice.chomp("="), + node.location + ), + node.operator_loc.copy(start_offset: node.operator_loc.end_offset - 1, length: 1), + node.location + ) + end + + # Desugar `x ||= y` to `x || x = y` + def desugar_or_write_node(node, read_class, write_class, *arguments) + OrNode.new( + read_class.new(*arguments, node.name_loc), + write_class.new(*arguments, node.name_loc, node.value, node.operator_loc, node.location), + node.operator_loc, + node.location + ) + end + + # Desugar `x ||= y` to `defined?(x) ? x : x = y` + def desugar_or_write_defined_node(node, read_class, write_class, *arguments) + IfNode.new( + node.operator_loc, + DefinedNode.new(nil, read_class.new(*arguments, node.name_loc), nil, node.operator_loc, node.name_loc), + StatementsNode.new([read_class.new(*arguments, node.name_loc)], node.location), + ElseNode.new( + node.operator_loc, + StatementsNode.new( + [write_class.new(*arguments, node.name_loc, node.value, node.operator_loc, node.location)], + node.location + ), + node.operator_loc, + node.location + ), + node.operator_loc, + node.location + ) + end + end +end diff --git a/lib/yarp/desugar_visitor.rb b/lib/yarp/desugar_visitor.rb deleted file mode 100644 index 6ee5861ac8..0000000000 --- a/lib/yarp/desugar_visitor.rb +++ /dev/null @@ -1,204 +0,0 @@ -# frozen_string_literal: true - -module YARP - class DesugarVisitor < MutationVisitor - # @@foo &&= bar - # - # becomes - # - # @@foo && @@foo = bar - def visit_class_variable_and_write_node(node) - desugar_and_write_node(node, ClassVariableReadNode, ClassVariableWriteNode, node.name) - end - - # @@foo ||= bar - # - # becomes - # - # defined?(@@foo) ? @@foo : @@foo = bar - def visit_class_variable_or_write_node(node) - desugar_or_write_defined_node(node, ClassVariableReadNode, ClassVariableWriteNode, node.name) - end - - # @@foo += bar - # - # becomes - # - # @@foo = @@foo + bar - def visit_class_variable_operator_write_node(node) - desugar_operator_write_node(node, ClassVariableReadNode, ClassVariableWriteNode, node.name) - end - - # Foo &&= bar - # - # becomes - # - # Foo && Foo = bar - def visit_constant_and_write_node(node) - desugar_and_write_node(node, ConstantReadNode, ConstantWriteNode, node.name) - end - - # Foo ||= bar - # - # becomes - # - # defined?(Foo) ? Foo : Foo = bar - def visit_constant_or_write_node(node) - desugar_or_write_defined_node(node, ConstantReadNode, ConstantWriteNode, node.name) - end - - # Foo += bar - # - # becomes - # - # Foo = Foo + bar - def visit_constant_operator_write_node(node) - desugar_operator_write_node(node, ConstantReadNode, ConstantWriteNode, node.name) - end - - # $foo &&= bar - # - # becomes - # - # $foo && $foo = bar - def visit_global_variable_and_write_node(node) - desugar_and_write_node(node, GlobalVariableReadNode, GlobalVariableWriteNode, node.name) - end - - # $foo ||= bar - # - # becomes - # - # defined?($foo) ? $foo : $foo = bar - def visit_global_variable_or_write_node(node) - desugar_or_write_defined_node(node, GlobalVariableReadNode, GlobalVariableWriteNode, node.name) - end - - # $foo += bar - # - # becomes - # - # $foo = $foo + bar - def visit_global_variable_operator_write_node(node) - desugar_operator_write_node(node, GlobalVariableReadNode, GlobalVariableWriteNode, node.name) - end - - # @foo &&= bar - # - # becomes - # - # @foo && @foo = bar - def visit_instance_variable_and_write_node(node) - desugar_and_write_node(node, InstanceVariableReadNode, InstanceVariableWriteNode, node.name) - end - - # @foo ||= bar - # - # becomes - # - # @foo || @foo = bar - def visit_instance_variable_or_write_node(node) - desugar_or_write_node(node, InstanceVariableReadNode, InstanceVariableWriteNode, node.name) - end - - # @foo += bar - # - # becomes - # - # @foo = @foo + bar - def visit_instance_variable_operator_write_node(node) - desugar_operator_write_node(node, InstanceVariableReadNode, InstanceVariableWriteNode, node.name) - end - - # foo &&= bar - # - # becomes - # - # foo && foo = bar - def visit_local_variable_and_write_node(node) - desugar_and_write_node(node, LocalVariableReadNode, LocalVariableWriteNode, node.name, node.depth) - end - - # foo ||= bar - # - # becomes - # - # foo || foo = bar - def visit_local_variable_or_write_node(node) - desugar_or_write_node(node, LocalVariableReadNode, LocalVariableWriteNode, node.name, node.depth) - end - - # foo += bar - # - # becomes - # - # foo = foo + bar - def visit_local_variable_operator_write_node(node) - desugar_operator_write_node(node, LocalVariableReadNode, LocalVariableWriteNode, node.name, node.depth) - end - - private - - # Desugar `x &&= y` to `x && x = y` - def desugar_and_write_node(node, read_class, write_class, *arguments) - AndNode.new( - read_class.new(*arguments, node.name_loc), - write_class.new(*arguments, node.name_loc, node.value, node.operator_loc, node.location), - node.operator_loc, - node.location - ) - end - - # Desugar `x += y` to `x = x + y` - def desugar_operator_write_node(node, read_class, write_class, *arguments) - write_class.new( - *arguments, - node.name_loc, - CallNode.new( - read_class.new(*arguments, node.name_loc), - nil, - node.operator_loc.copy(length: node.operator_loc.length - 1), - nil, - ArgumentsNode.new([node.value], node.value.location), - nil, - nil, - 0, - node.operator_loc.slice.chomp("="), - node.location - ), - node.operator_loc.copy(start_offset: node.operator_loc.end_offset - 1, length: 1), - node.location - ) - end - - # Desugar `x ||= y` to `x || x = y` - def desugar_or_write_node(node, read_class, write_class, *arguments) - OrNode.new( - read_class.new(*arguments, node.name_loc), - write_class.new(*arguments, node.name_loc, node.value, node.operator_loc, node.location), - node.operator_loc, - node.location - ) - end - - # Desugar `x ||= y` to `defined?(x) ? x : x = y` - def desugar_or_write_defined_node(node, read_class, write_class, *arguments) - IfNode.new( - node.operator_loc, - DefinedNode.new(nil, read_class.new(*arguments, node.name_loc), nil, node.operator_loc, node.name_loc), - StatementsNode.new([read_class.new(*arguments, node.name_loc)], node.location), - ElseNode.new( - node.operator_loc, - StatementsNode.new( - [write_class.new(*arguments, node.name_loc, node.value, node.operator_loc, node.location)], - node.location - ), - node.operator_loc, - node.location - ), - node.operator_loc, - node.location - ) - end - end -end diff --git a/lib/yarp/yarp.gemspec b/lib/yarp/yarp.gemspec index 16e8196140..fea48dd47f 100644 --- a/lib/yarp/yarp.gemspec +++ b/lib/yarp/yarp.gemspec @@ -59,12 +59,13 @@ Gem::Specification.new do |spec| "include/yarp/util/yp_strpbrk.h", "include/yarp/version.h", "lib/yarp.rb", - "lib/yarp/desugar_visitor.rb", + "lib/yarp/compiler.rb", + "lib/yarp/desugar_compiler.rb", "lib/yarp/dispatcher.rb", "lib/yarp/dsl.rb", "lib/yarp/ffi.rb", "lib/yarp/lex_compat.rb", - "lib/yarp/mutation_visitor.rb", + "lib/yarp/mutation_compiler.rb", "lib/yarp/node.rb", "lib/yarp/pack.rb", "lib/yarp/pattern.rb", @@ -72,6 +73,7 @@ Gem::Specification.new do |spec| "lib/yarp/serialize.rb", "lib/yarp/parse_result/comments.rb", "lib/yarp/parse_result/newlines.rb", + "lib/yarp/visitor.rb", "src/diagnostic.c", "src/enc/yp_big5.c", "src/enc/yp_euc_jp.c", diff --git a/test/yarp/compiler_test.rb b/test/yarp/compiler_test.rb new file mode 100644 index 0000000000..141e183469 --- /dev/null +++ b/test/yarp/compiler_test.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require_relative "test_helper" + +module YARP + class CompilerTest < TestCase + class SExpressions < YARP::Compiler + def visit_arguments_node(node) + [:arguments, super] + end + + def visit_call_node(node) + [:call, super] + end + + def visit_integer_node(node) + [:integer] + end + + def visit_program_node(node) + [:program, super] + end + end + + def test_compiler + expected = [:program, [[[:call, [[:integer], [:arguments, [[:integer]]]]]]]] + assert_equal expected, YARP.parse("1 + 2").value.accept(SExpressions.new) + end + end +end diff --git a/test/yarp/desugar_compiler_test.rb b/test/yarp/desugar_compiler_test.rb new file mode 100644 index 0000000000..8d2b207fed --- /dev/null +++ b/test/yarp/desugar_compiler_test.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +require_relative "test_helper" + +module YARP + class DesugarCompilerTest < TestCase + def test_and_write + assert_desugars("(AndNode (ClassVariableReadNode) (ClassVariableWriteNode (CallNode)))", "@@foo &&= bar") + assert_not_desugared("Foo::Bar &&= baz", "Desugaring would execute Foo twice or need temporary variables") + assert_desugars("(AndNode (ConstantReadNode) (ConstantWriteNode (CallNode)))", "Foo &&= bar") + assert_desugars("(AndNode (GlobalVariableReadNode) (GlobalVariableWriteNode (CallNode)))", "$foo &&= bar") + assert_desugars("(AndNode (InstanceVariableReadNode) (InstanceVariableWriteNode (CallNode)))", "@foo &&= bar") + assert_desugars("(AndNode (LocalVariableReadNode) (LocalVariableWriteNode (CallNode)))", "foo &&= bar") + assert_desugars("(AndNode (LocalVariableReadNode) (LocalVariableWriteNode (CallNode)))", "foo = 1; foo &&= bar") + end + + def test_or_write + assert_desugars("(IfNode (DefinedNode (ClassVariableReadNode)) (StatementsNode (ClassVariableReadNode)) (ElseNode (StatementsNode (ClassVariableWriteNode (CallNode)))))", "@@foo ||= bar") + assert_not_desugared("Foo::Bar ||= baz", "Desugaring would execute Foo twice or need temporary variables") + assert_desugars("(IfNode (DefinedNode (ConstantReadNode)) (StatementsNode (ConstantReadNode)) (ElseNode (StatementsNode (ConstantWriteNode (CallNode)))))", "Foo ||= bar") + assert_desugars("(IfNode (DefinedNode (GlobalVariableReadNode)) (StatementsNode (GlobalVariableReadNode)) (ElseNode (StatementsNode (GlobalVariableWriteNode (CallNode)))))", "$foo ||= bar") + assert_desugars("(OrNode (InstanceVariableReadNode) (InstanceVariableWriteNode (CallNode)))", "@foo ||= bar") + assert_desugars("(OrNode (LocalVariableReadNode) (LocalVariableWriteNode (CallNode)))", "foo ||= bar") + assert_desugars("(OrNode (LocalVariableReadNode) (LocalVariableWriteNode (CallNode)))", "foo = 1; foo ||= bar") + end + + def test_operator_write + assert_desugars("(ClassVariableWriteNode (CallNode (ClassVariableReadNode) (ArgumentsNode (CallNode))))", "@@foo += bar") + assert_not_desugared("Foo::Bar += baz", "Desugaring would execute Foo twice or need temporary variables") + assert_desugars("(ConstantWriteNode (CallNode (ConstantReadNode) (ArgumentsNode (CallNode))))", "Foo += bar") + assert_desugars("(GlobalVariableWriteNode (CallNode (GlobalVariableReadNode) (ArgumentsNode (CallNode))))", "$foo += bar") + assert_desugars("(InstanceVariableWriteNode (CallNode (InstanceVariableReadNode) (ArgumentsNode (CallNode))))", "@foo += bar") + assert_desugars("(LocalVariableWriteNode (CallNode (LocalVariableReadNode) (ArgumentsNode (CallNode))))", "foo += bar") + assert_desugars("(LocalVariableWriteNode (CallNode (LocalVariableReadNode) (ArgumentsNode (CallNode))))", "foo = 1; foo += bar") + end + + private + + def ast_inspect(node) + parts = [node.class.name.split("::").last] + + node.deconstruct_keys(nil).each do |_, value| + case value + when Node + parts << ast_inspect(value) + when Array + parts.concat(value.map { |element| ast_inspect(element) }) + end + end + + "(#{parts.join(" ")})" + end + + # Ensure every node is only present once in the AST. + # If the same node is present twice it would most likely indicate it is executed twice, which is invalid semantically. + # This also acts as a sanity check that Node#child_nodes returns only nodes or nil (which caught a couple bugs). + class EnsureEveryNodeOnceInAST < Visitor + def initialize + @all_nodes = {}.compare_by_identity + end + + def visit(node) + if node + if @all_nodes.include?(node) + raise "#{node.inspect} is present multiple times in the desugared AST and likely executed multiple times" + else + @all_nodes[node] = true + end + end + super(node) + end + end + + def assert_desugars(expected, source) + ast = YARP.parse(source).value.accept(DesugarCompiler.new) + assert_equal expected, ast_inspect(ast.statements.body.last) + + ast.accept(EnsureEveryNodeOnceInAST.new) + end + + def assert_not_desugared(source, reason) + ast = YARP.parse(source).value + assert_equal_nodes(ast, ast.accept(DesugarCompiler.new)) + end + end +end diff --git a/test/yarp/desugar_visitor_test.rb b/test/yarp/desugar_visitor_test.rb deleted file mode 100644 index 3966d7bfcb..0000000000 --- a/test/yarp/desugar_visitor_test.rb +++ /dev/null @@ -1,86 +0,0 @@ -# frozen_string_literal: true - -require_relative "test_helper" - -module YARP - class DesugarVisitorTest < TestCase - def test_and_write - assert_desugars("(AndNode (ClassVariableReadNode) (ClassVariableWriteNode (CallNode)))", "@@foo &&= bar") - assert_not_desugared("Foo::Bar &&= baz", "Desugaring would execute Foo twice or need temporary variables") - assert_desugars("(AndNode (ConstantReadNode) (ConstantWriteNode (CallNode)))", "Foo &&= bar") - assert_desugars("(AndNode (GlobalVariableReadNode) (GlobalVariableWriteNode (CallNode)))", "$foo &&= bar") - assert_desugars("(AndNode (InstanceVariableReadNode) (InstanceVariableWriteNode (CallNode)))", "@foo &&= bar") - assert_desugars("(AndNode (LocalVariableReadNode) (LocalVariableWriteNode (CallNode)))", "foo &&= bar") - assert_desugars("(AndNode (LocalVariableReadNode) (LocalVariableWriteNode (CallNode)))", "foo = 1; foo &&= bar") - end - - def test_or_write - assert_desugars("(IfNode (DefinedNode (ClassVariableReadNode)) (StatementsNode (ClassVariableReadNode)) (ElseNode (StatementsNode (ClassVariableWriteNode (CallNode)))))", "@@foo ||= bar") - assert_not_desugared("Foo::Bar ||= baz", "Desugaring would execute Foo twice or need temporary variables") - assert_desugars("(IfNode (DefinedNode (ConstantReadNode)) (StatementsNode (ConstantReadNode)) (ElseNode (StatementsNode (ConstantWriteNode (CallNode)))))", "Foo ||= bar") - assert_desugars("(IfNode (DefinedNode (GlobalVariableReadNode)) (StatementsNode (GlobalVariableReadNode)) (ElseNode (StatementsNode (GlobalVariableWriteNode (CallNode)))))", "$foo ||= bar") - assert_desugars("(OrNode (InstanceVariableReadNode) (InstanceVariableWriteNode (CallNode)))", "@foo ||= bar") - assert_desugars("(OrNode (LocalVariableReadNode) (LocalVariableWriteNode (CallNode)))", "foo ||= bar") - assert_desugars("(OrNode (LocalVariableReadNode) (LocalVariableWriteNode (CallNode)))", "foo = 1; foo ||= bar") - end - - def test_operator_write - assert_desugars("(ClassVariableWriteNode (CallNode (ClassVariableReadNode) (ArgumentsNode (CallNode))))", "@@foo += bar") - assert_not_desugared("Foo::Bar += baz", "Desugaring would execute Foo twice or need temporary variables") - assert_desugars("(ConstantWriteNode (CallNode (ConstantReadNode) (ArgumentsNode (CallNode))))", "Foo += bar") - assert_desugars("(GlobalVariableWriteNode (CallNode (GlobalVariableReadNode) (ArgumentsNode (CallNode))))", "$foo += bar") - assert_desugars("(InstanceVariableWriteNode (CallNode (InstanceVariableReadNode) (ArgumentsNode (CallNode))))", "@foo += bar") - assert_desugars("(LocalVariableWriteNode (CallNode (LocalVariableReadNode) (ArgumentsNode (CallNode))))", "foo += bar") - assert_desugars("(LocalVariableWriteNode (CallNode (LocalVariableReadNode) (ArgumentsNode (CallNode))))", "foo = 1; foo += bar") - end - - private - - def ast_inspect(node) - parts = [node.class.name.split("::").last] - - node.deconstruct_keys(nil).each do |_, value| - case value - when Node - parts << ast_inspect(value) - when Array - parts.concat(value.map { |element| ast_inspect(element) }) - end - end - - "(#{parts.join(" ")})" - end - - # Ensure every node is only present once in the AST. - # If the same node is present twice it would most likely indicate it is executed twice, which is invalid semantically. - # This also acts as a sanity check that Node#child_nodes returns only nodes or nil (which caught a couple bugs). - class EnsureEveryNodeOnceInAST < Visitor - def initialize - @all_nodes = {}.compare_by_identity - end - - def visit(node) - if node - if @all_nodes.include?(node) - raise "#{node.inspect} is present multiple times in the desugared AST and likely executed multiple times" - else - @all_nodes[node] = true - end - end - super(node) - end - end - - def assert_desugars(expected, source) - ast = YARP.parse(source).value.accept(DesugarVisitor.new) - assert_equal expected, ast_inspect(ast.statements.body.last) - - ast.accept(EnsureEveryNodeOnceInAST.new) - end - - def assert_not_desugared(source, reason) - ast = YARP.parse(source).value - assert_equal_nodes(ast, ast.accept(DesugarVisitor.new)) - end - end -end diff --git a/yarp/templates/lib/yarp/compiler.rb.erb b/yarp/templates/lib/yarp/compiler.rb.erb new file mode 100644 index 0000000000..b9ddb4daee --- /dev/null +++ b/yarp/templates/lib/yarp/compiler.rb.erb @@ -0,0 +1,41 @@ +module YARP + # A compiler is a visitor that returns the value of each node as it visits. + # This is as opposed to a visitor which will only walk the tree. This can be + # useful when you are trying to compile a tree into a different format. + # + # For example, to build a representation of the tree as s-expressions, you + # could write: + # + # class SExpressions < YARP::Compiler + # def visit_arguments_node(node) = [:arguments, super] + # def visit_call_node(node) = [:call, super] + # def visit_integer_node(node) = [:integer] + # def visit_program_node(node) = [:program, super] + # end + # + # YARP.parse("1 + 2").value.accept(SExpressions.new) + # # => [:program, [[[:call, [[:integer], [:arguments, [[:integer]]]]]]]] + # + class Compiler + # Visit an individual node. + def visit(node) + node&.accept(self) + end + + # Visit a list of nodes. + def visit_all(nodes) + nodes.map { |node| node&.accept(self) } + end + + # Visit the child nodes of the given node. + def visit_child_nodes(node) + node.compact_child_nodes.map { |node| node.accept(self) } + end + + <%- nodes.each_with_index do |node, index| -%> + <%= "\n" if index != 0 -%> + # Compile a <%= node.name %> node + alias visit_<%= node.human %> visit_child_nodes + <%- end -%> + end +end diff --git a/yarp/templates/lib/yarp/mutation_compiler.rb.erb b/yarp/templates/lib/yarp/mutation_compiler.rb.erb new file mode 100644 index 0000000000..a99721f53b --- /dev/null +++ b/yarp/templates/lib/yarp/mutation_compiler.rb.erb @@ -0,0 +1,19 @@ +module YARP + # This visitor walks through the tree and copies each node as it is being + # visited. This is useful for consumers that want to mutate the tree, as you + # can change subtrees in place without effecting the rest of the tree. + class MutationCompiler < Compiler + <%- nodes.each_with_index do |node, index| -%> +<%= "\n" if index != 0 -%> + # Copy a <%= node.name %> node + def visit_<%= node.human %>(node) + <%- fields = node.fields.select { |field| [YARP::NodeField, YARP::OptionalNodeField, YARP::NodeListField].include?(field.class) } -%> + <%- if fields.any? -%> + node.copy(<%= fields.map { |field| "#{field.name}: #{field.is_a?(YARP::NodeListField) ? "visit_all" : "visit"}(node.#{field.name})" }.join(", ") %>) + <%- else -%> + node.copy + <%- end -%> + end + <%- end -%> + end +end diff --git a/yarp/templates/lib/yarp/node.rb.erb b/yarp/templates/lib/yarp/node.rb.erb index 7d52d823da..16717de739 100644 --- a/yarp/templates/lib/yarp/node.rb.erb +++ b/yarp/templates/lib/yarp/node.rb.erb @@ -164,7 +164,8 @@ module YARP end <%- end -%> - <%- flags.each do |flag| -%> + <%- flags.each_with_index do |flag, flag_index| -%> +<%= "\n" if flag_index > 0 -%> module <%= flag.name %> <%- flag.values.each_with_index do |value, index| -%> # <%= value.comment %> @@ -172,13 +173,5 @@ module YARP <%= "\n" if value != flag.values.last -%> <%- end -%> end - <%- end -%> - class Visitor < BasicVisitor - <%- nodes.each do |node| -%> - # Visit a <%= node.name %> node - alias visit_<%= node.human %> visit_child_nodes -<%= "\n" if node != nodes.last -%> - <%- end -%> - end end diff --git a/yarp/templates/lib/yarp/visitor.rb.erb b/yarp/templates/lib/yarp/visitor.rb.erb new file mode 100644 index 0000000000..0bef47b997 --- /dev/null +++ b/yarp/templates/lib/yarp/visitor.rb.erb @@ -0,0 +1,46 @@ +module YARP + # A class that knows how to walk down the tree. None of the individual visit + # methods are implemented on this visitor, so it forces the consumer to + # implement each one that they need. For a default implementation that + # continues walking the tree, see the Visitor class. + class BasicVisitor + def visit(node) + node&.accept(self) + end + + def visit_all(nodes) + nodes.each { |node| node&.accept(self) } + end + + def visit_child_nodes(node) + node.compact_child_nodes.each { |node| node.accept(self) } + end + end + + # A visitor is a class that provides a default implementation for every accept + # method defined on the nodes. This means it can walk a tree without the + # caller needing to define any special handling. This allows you to handle a + # subset of the tree, while still walking the whole tree. + # + # For example, to find all of the method calls that call the `foo` method, you + # could write: + # + # class FooCalls < YARP::Visitor + # def visit_call_node(node) + # if node.name == "foo" + # # Do something with the node + # end + # + # # Call super so that the visitor continues walking the tree + # super + # end + # end + # + class Visitor < BasicVisitor + <%- nodes.each_with_index do |node, index| -%> +<%= "\n" if index != 0 -%> + # Visit a <%= node.name %> node + alias visit_<%= node.human %> visit_child_nodes + <%- end -%> + end +end diff --git a/yarp/templates/template.rb b/yarp/templates/template.rb index ea5946761e..f24c756438 100755 --- a/yarp/templates/template.rb +++ b/yarp/templates/template.rb @@ -366,11 +366,13 @@ module YARP "java/org/yarp/Loader.java", "java/org/yarp/Nodes.java", "java/org/yarp/AbstractNodeVisitor.java", + "lib/yarp/compiler.rb", "lib/yarp/dispatcher.rb", "lib/yarp/dsl.rb", - "lib/yarp/mutation_visitor.rb", + "lib/yarp/mutation_compiler.rb", "lib/yarp/node.rb", "lib/yarp/serialize.rb", + "lib/yarp/visitor.rb", "src/node.c", "src/prettyprint.c", "src/serialize.c", -- cgit v1.2.3