summaryrefslogtreecommitdiff
path: root/lib/prism/debug.rb
blob: 74f824faa70d4fdb169afd3f041735d05f55fcd2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# 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

    # A wrapping around prism's internal encoding data structures. This is used
    # for reflection and debugging purposes.
    class Encoding
      # The name of the encoding, that can be passed to Encoding.find.
      attr_reader :name

      # Initialize a new encoding with the given name and whether or not it is
      # a multibyte encoding.
      def initialize(name, multibyte)
        @name = name
        @multibyte = multibyte
      end

      # Whether or not the encoding is a multibyte encoding.
      def multibyte?
        @multibyte
      end

      # Returns the number of bytes of the first character in the source string,
      # if it is valid for the encoding. Otherwise, returns 0.
      def width(source)
        Encoding._width(name, source)
      end

      # Returns true if the first character in the source string is a valid
      # alphanumeric character for the encoding.
      def alnum?(source)
        Encoding._alnum?(name, source)
      end

      # Returns true if the first character in the source string is a valid
      # alphabetic character for the encoding.
      def alpha?(source)
        Encoding._alpha?(name, source)
      end

      # Returns true if the first character in the source string is a valid
      # uppercase character for the encoding.
      def upper?(source)
        Encoding._upper?(name, source)
      end
    end
  end
end