mirror of
https://github.com/ruby/ruby.git
synced 2025-08-15 13:39:04 +02:00
241 lines
7.9 KiB
Ruby
241 lines
7.9 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
# This test is going to use the RubyVM::InstructionSequence class to compile
|
|
# local tables and compare against them to ensure we have the same locals in the
|
|
# same order. This is important to guarantee that we compile indices correctly
|
|
# on CRuby (in terms of compatibility).
|
|
#
|
|
# There have also been changes made in other versions of Ruby, so we only want
|
|
# to test on the most recent versions.
|
|
return if !defined?(RubyVM::InstructionSequence) || RUBY_VERSION < "3.4.0"
|
|
|
|
# If we're on Ruby 3.4.0 and the default parser is Prism, then there is no point
|
|
# in comparing the locals because they will be the same.
|
|
return if RubyVM::InstructionSequence.compile("").to_a[4][:parser] == :prism
|
|
|
|
# In Ruby 3.4.0, the local table for method forwarding changed. But 3.4.0 can
|
|
# refer to the dev version, so while 3.4.0 still isn't released, we need to
|
|
# check if we have a high enough revision.
|
|
return if RubyVM::InstructionSequence.compile("def foo(...); end").to_a[13][2][2][10].length != 1
|
|
|
|
# Omit tests if running on a 32-bit machine because there is a bug with how
|
|
# Ruby is handling large ISeqs on 32-bit machines
|
|
return if RUBY_PLATFORM =~ /i686/
|
|
|
|
require_relative "test_helper"
|
|
|
|
module Prism
|
|
class LocalsTest < TestCase
|
|
except = [
|
|
# Skip this fixture because it has a different number of locals because
|
|
# CRuby is eliminating dead code.
|
|
"whitequark/ruby_bug_10653.txt"
|
|
]
|
|
|
|
Fixture.each(except: except) do |fixture|
|
|
define_method(fixture.test_name) { assert_locals(fixture) }
|
|
end
|
|
|
|
def setup
|
|
@previous_default_external = Encoding.default_external
|
|
ignore_warnings { Encoding.default_external = Encoding::UTF_8 }
|
|
end
|
|
|
|
def teardown
|
|
ignore_warnings { Encoding.default_external = @previous_default_external }
|
|
end
|
|
|
|
private
|
|
|
|
def assert_locals(fixture)
|
|
source = fixture.read
|
|
|
|
expected = cruby_locals(source)
|
|
actual = prism_locals(source)
|
|
|
|
assert_equal(expected, actual)
|
|
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)
|
|
locals = [] #: Array[Array[Symbol | Integer]]
|
|
stack = [ISeq.new(ignore_warnings { 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
|
|
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 = nil
|
|
|
|
if node.is_a?(DefNode)
|
|
params = node.parameters
|
|
elsif node.parameters.is_a?(NumberedParametersNode)
|
|
# nothing
|
|
elsif node.parameters.is_a?(ItParametersNode)
|
|
names.unshift(AnonymousLocal)
|
|
else
|
|
params = 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)
|
|
if sorted.length == 0
|
|
sorted.push(:"...")
|
|
else
|
|
sorted.push(:*, :**, :&, :"...")
|
|
end
|
|
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
|