diff options
-rw-r--r-- | lib/prism.rb | 2 | ||||
-rw-r--r-- | lib/prism/debug.rb | 206 | ||||
-rw-r--r-- | lib/prism/ffi.rb | 2 | ||||
-rw-r--r-- | lib/prism/prism.gemspec | 1 | ||||
-rw-r--r-- | test/prism/comments_test.rb | 2 | ||||
-rw-r--r-- | test/prism/locals_test.rb | 185 | ||||
-rw-r--r-- | test/prism/newline_test.rb | 1 | ||||
-rw-r--r-- | test/prism/parse_test.rb | 2 |
8 files changed, 187 insertions, 214 deletions
diff --git a/lib/prism.rb b/lib/prism.rb index 2bb7f79bf6..66a64e7fd0 100644 --- a/lib/prism.rb +++ b/lib/prism.rb @@ -13,7 +13,6 @@ module Prism autoload :BasicVisitor, "prism/visitor" autoload :Compiler, "prism/compiler" - autoload :Debug, "prism/debug" autoload :DesugarCompiler, "prism/desugar_compiler" autoload :Dispatcher, "prism/dispatcher" autoload :DotVisitor, "prism/dot_visitor" @@ -32,7 +31,6 @@ module Prism # Some of these constants are not meant to be exposed, so marking them as # private here. - private_constant :Debug private_constant :LexCompat private_constant :LexRipper diff --git a/lib/prism/debug.rb b/lib/prism/debug.rb deleted file mode 100644 index 6380e628be..0000000000 --- a/lib/prism/debug.rb +++ /dev/null @@ -1,206 +0,0 @@ -# frozen_string_literal: true - -module Prism - # This module is used for testing and debugging and is not meant to be used by - # consumers of this library. - module Debug - # A wrapper around a RubyVM::InstructionSequence that provides a more - # convenient interface for accessing parts of the iseq. - class ISeq # :nodoc: - attr_reader :parts - - def initialize(parts) - @parts = parts - end - - def type - parts[0] - end - - def local_table - parts[10] - end - - def instructions - parts[13] - end - - def each_child - instructions.each do |instruction| - # Only look at arrays. Other instructions are line numbers or - # tracepoint events. - next unless instruction.is_a?(Array) - - instruction.each do |opnd| - # Only look at arrays. Other operands are literals. - next unless opnd.is_a?(Array) - - # Only look at instruction sequences. Other operands are literals. - next unless opnd[0] == "YARVInstructionSequence/SimpleDataFormat" - - yield ISeq.new(opnd) - end - end - end - end - - private_constant :ISeq - - # :call-seq: - # Debug::cruby_locals(source) -> Array - # - # For the given source, compiles with CRuby and returns a list of all of the - # sets of local variables that were encountered. - def self.cruby_locals(source) - verbose, $VERBOSE = $VERBOSE, nil - - begin - locals = [] #: Array[Array[Symbol | Integer]] - stack = [ISeq.new(RubyVM::InstructionSequence.compile(source).to_a)] - - while (iseq = stack.pop) - names = [*iseq.local_table] - names.map!.with_index do |name, index| - # When an anonymous local variable is present in the iseq's local - # table, it is represented as the stack offset from the top. - # However, when these are dumped to binary and read back in, they - # are replaced with the symbol :#arg_rest. To consistently handle - # this, we replace them here with their index. - if name == :"#arg_rest" - names.length - index + 1 - else - name - end - end - - locals << names - iseq.each_child { |child| stack << child } - end - - locals - ensure - $VERBOSE = verbose - end - end - - # Used to hold the place of a local that will be in the local table but - # cannot be accessed directly from the source code. For example, the - # iteration variable in a for loop or the positional parameter on a method - # definition that is destructured. - AnonymousLocal = Object.new - private_constant :AnonymousLocal - - # :call-seq: - # Debug::prism_locals(source) -> Array - # - # For the given source, parses with prism and returns a list of all of the - # sets of local variables that were encountered. - def self.prism_locals(source) - locals = [] #: Array[Array[Symbol | Integer]] - stack = [Prism.parse(source).value] #: Array[Prism::node] - - while (node = stack.pop) - case node - when BlockNode, DefNode, LambdaNode - names = node.locals - params = - if node.is_a?(DefNode) - node.parameters - elsif node.parameters.is_a?(NumberedParametersNode) - nil - else - node.parameters&.parameters - end - - # prism places parameters in the same order that they appear in the - # source. CRuby places them in the order that they need to appear - # according to their own internal calling convention. We mimic that - # order here so that we can compare properly. - if params - sorted = [ - *params.requireds.map do |required| - if required.is_a?(RequiredParameterNode) - required.name - else - AnonymousLocal - end - end, - *params.optionals.map(&:name), - *((params.rest.name || :*) if params.rest && !params.rest.is_a?(ImplicitRestNode)), - *params.posts.map do |post| - if post.is_a?(RequiredParameterNode) - post.name - else - AnonymousLocal - end - end, - *params.keywords.grep(RequiredKeywordParameterNode).map(&:name), - *params.keywords.grep(OptionalKeywordParameterNode).map(&:name), - ] - - sorted << AnonymousLocal if params.keywords.any? - - if params.keyword_rest.is_a?(ForwardingParameterNode) - sorted.push(:*, :**, :&, :"...") - elsif params.keyword_rest.is_a?(KeywordRestParameterNode) - sorted << (params.keyword_rest.name || :**) - end - - # Recurse down the parameter tree to find any destructured - # parameters and add them after the other parameters. - param_stack = params.requireds.concat(params.posts).grep(MultiTargetNode).reverse - while (param = param_stack.pop) - case param - when MultiTargetNode - param_stack.concat(param.rights.reverse) - param_stack << param.rest if param.rest&.expression && !sorted.include?(param.rest.expression.name) - param_stack.concat(param.lefts.reverse) - when RequiredParameterNode - sorted << param.name - when SplatNode - sorted << param.expression.name - end - end - - if params.block - sorted << (params.block.name || :&) - end - - names = sorted.concat(names - sorted) - end - - names.map!.with_index do |name, index| - if name == AnonymousLocal - names.length - index + 1 - else - name - end - end - - locals << names - when ClassNode, ModuleNode, ProgramNode, SingletonClassNode - locals << node.locals - when ForNode - locals << [2] - when PostExecutionNode - locals.push([], []) - when InterpolatedRegularExpressionNode - locals << [] if node.once? - end - - stack.concat(node.compact_child_nodes) - end - - locals - end - - # :call-seq: - # Debug::newlines(source) -> Array - # - # For the given source string, return the byte offsets of every newline in - # the source. - def self.newlines(source) - Prism.parse(source).source.offsets - end - end -end diff --git a/lib/prism/ffi.rb b/lib/prism/ffi.rb index b62a59d037..df7e183310 100644 --- a/lib/prism/ffi.rb +++ b/lib/prism/ffi.rb @@ -394,7 +394,7 @@ module Prism template << "L" if (encoding = options[:encoding]) - name = encoding.name + name = encoding.is_a?(Encoding) ? encoding.name : encoding values.push(name.bytesize, name.b) template << "A*" else diff --git a/lib/prism/prism.gemspec b/lib/prism/prism.gemspec index 374591bb70..b4504dbf4b 100644 --- a/lib/prism/prism.gemspec +++ b/lib/prism/prism.gemspec @@ -70,7 +70,6 @@ Gem::Specification.new do |spec| "include/prism/version.h", "lib/prism.rb", "lib/prism/compiler.rb", - "lib/prism/debug.rb", "lib/prism/desugar_compiler.rb", "lib/prism/dispatcher.rb", "lib/prism/dot_visitor.rb", diff --git a/test/prism/comments_test.rb b/test/prism/comments_test.rb index b99c00268c..952d03239c 100644 --- a/test/prism/comments_test.rb +++ b/test/prism/comments_test.rb @@ -6,7 +6,7 @@ module Prism class CommentsTest < TestCase def test_comment_inline source = "# comment" - assert_equal [0], Debug.newlines(source) + assert_equal [0], Prism.parse(source).source.offsets assert_comment( source, diff --git a/test/prism/locals_test.rb b/test/prism/locals_test.rb index 0eb73f1b9c..0e57a9a80c 100644 --- a/test/prism/locals_test.rb +++ b/test/prism/locals_test.rb @@ -41,8 +41,8 @@ module Prism def assert_locals(filepath) source = File.read(filepath) - expected = Debug.cruby_locals(source) - actual = Debug.prism_locals(source) + expected = cruby_locals(source) + actual = prism_locals(source) assert_equal(expected, actual) end @@ -54,5 +54,186 @@ module Prism ensure $VERBOSE = previous_verbosity end + + # A wrapper around a RubyVM::InstructionSequence that provides a more + # convenient interface for accessing parts of the iseq. + class ISeq + attr_reader :parts + + def initialize(parts) + @parts = parts + end + + def type + parts[0] + end + + def local_table + parts[10] + end + + def instructions + parts[13] + end + + def each_child + instructions.each do |instruction| + # Only look at arrays. Other instructions are line numbers or + # tracepoint events. + next unless instruction.is_a?(Array) + + instruction.each do |opnd| + # Only look at arrays. Other operands are literals. + next unless opnd.is_a?(Array) + + # Only look at instruction sequences. Other operands are literals. + next unless opnd[0] == "YARVInstructionSequence/SimpleDataFormat" + + yield ISeq.new(opnd) + end + end + end + end + + # Used to hold the place of a local that will be in the local table but + # cannot be accessed directly from the source code. For example, the + # iteration variable in a for loop or the positional parameter on a method + # definition that is destructured. + AnonymousLocal = Object.new + + # For the given source, compiles with CRuby and returns a list of all of the + # sets of local variables that were encountered. + def cruby_locals(source) + verbose, $VERBOSE = $VERBOSE, nil + + begin + locals = [] #: Array[Array[Symbol | Integer]] + stack = [ISeq.new(RubyVM::InstructionSequence.compile(source).to_a)] + + while (iseq = stack.pop) + names = [*iseq.local_table] + names.map!.with_index do |name, index| + # When an anonymous local variable is present in the iseq's local + # table, it is represented as the stack offset from the top. + # However, when these are dumped to binary and read back in, they + # are replaced with the symbol :#arg_rest. To consistently handle + # this, we replace them here with their index. + if name == :"#arg_rest" + names.length - index + 1 + else + name + end + end + + locals << names + iseq.each_child { |child| stack << child } + end + + locals + ensure + $VERBOSE = verbose + end + end + + # For the given source, parses with prism and returns a list of all of the + # sets of local variables that were encountered. + def prism_locals(source) + locals = [] #: Array[Array[Symbol | Integer]] + stack = [Prism.parse(source).value] #: Array[Prism::node] + + while (node = stack.pop) + case node + when BlockNode, DefNode, LambdaNode + names = node.locals + params = + if node.is_a?(DefNode) + node.parameters + elsif node.parameters.is_a?(NumberedParametersNode) + nil + else + node.parameters&.parameters + end + + # prism places parameters in the same order that they appear in the + # source. CRuby places them in the order that they need to appear + # according to their own internal calling convention. We mimic that + # order here so that we can compare properly. + if params + sorted = [ + *params.requireds.map do |required| + if required.is_a?(RequiredParameterNode) + required.name + else + AnonymousLocal + end + end, + *params.optionals.map(&:name), + *((params.rest.name || :*) if params.rest && !params.rest.is_a?(ImplicitRestNode)), + *params.posts.map do |post| + if post.is_a?(RequiredParameterNode) + post.name + else + AnonymousLocal + end + end, + *params.keywords.grep(RequiredKeywordParameterNode).map(&:name), + *params.keywords.grep(OptionalKeywordParameterNode).map(&:name), + ] + + sorted << AnonymousLocal if params.keywords.any? + + if params.keyword_rest.is_a?(ForwardingParameterNode) + sorted.push(:*, :**, :&, :"...") + elsif params.keyword_rest.is_a?(KeywordRestParameterNode) + sorted << (params.keyword_rest.name || :**) + end + + # Recurse down the parameter tree to find any destructured + # parameters and add them after the other parameters. + param_stack = params.requireds.concat(params.posts).grep(MultiTargetNode).reverse + while (param = param_stack.pop) + case param + when MultiTargetNode + param_stack.concat(param.rights.reverse) + param_stack << param.rest if param.rest&.expression && !sorted.include?(param.rest.expression.name) + param_stack.concat(param.lefts.reverse) + when RequiredParameterNode + sorted << param.name + when SplatNode + sorted << param.expression.name + end + end + + if params.block + sorted << (params.block.name || :&) + end + + names = sorted.concat(names - sorted) + end + + names.map!.with_index do |name, index| + if name == AnonymousLocal + names.length - index + 1 + else + name + end + end + + locals << names + when ClassNode, ModuleNode, ProgramNode, SingletonClassNode + locals << node.locals + when ForNode + locals << [2] + when PostExecutionNode + locals.push([], []) + when InterpolatedRegularExpressionNode + locals << [] if node.once? + end + + stack.concat(node.compact_child_nodes) + end + + locals + end end end diff --git a/test/prism/newline_test.rb b/test/prism/newline_test.rb index cc7f21aec7..75593d34bf 100644 --- a/test/prism/newline_test.rb +++ b/test/prism/newline_test.rb @@ -10,6 +10,7 @@ module Prism filepaths = Dir["*.rb", base: base] - %w[ encoding_test.rb errors_test.rb + locals_test.rb parser_test.rb regexp_test.rb static_literals_test.rb diff --git a/test/prism/parse_test.rb b/test/prism/parse_test.rb index afb53e0668..5c66caebb9 100644 --- a/test/prism/parse_test.rb +++ b/test/prism/parse_test.rb @@ -265,7 +265,7 @@ module Prism # Next, assert that the newlines are in the expected places. expected_newlines = [0] source.b.scan("\n") { expected_newlines << $~.offset(0)[0] + 1 } - assert_equal expected_newlines, Debug.newlines(source) + assert_equal expected_newlines, Prism.parse(source).source.offsets if ripper_should_match # Finally, assert that we can lex the source and get the same tokens as |