lib/syntax_tree/yarv/instructions.rb



# frozen_string_literal: true

module SyntaxTree
  module YARV
    # This is a base class for all YARV instructions. It provides a few
    # convenience methods for working with instructions.
    class Instruction
      # This method creates an instruction that represents the canonical
      # (non-specialized) form of this instruction. If this instruction is not
      # a specialized instruction, then this method returns `self`.
      def canonical
        self
      end

      # This returns the size of the instruction in terms of the number of slots
      # it occupies in the instruction sequence. Effectively this is 1 plus the
      # number of operands.
      def length
        1
      end

      # This returns the number of values that are pushed onto the stack.
      def pushes
        0
      end

      # This returns the number of values that are popped off the stack.
      def pops
        0
      end

      # This returns an array of labels.
      def branch_targets
        []
      end

      # Whether or not this instruction leaves the current frame.
      def leaves?
        false
      end

      # Whether or not this instruction falls through to the next instruction if
      # its branching fails.
      def falls_through?
        false
      end

      # Does the instruction have side effects? Control-flow counts as a
      # side-effect, as do some special-case instructions like Leave. By default
      # every instruction is marked as having side effects.
      def side_effects?
        true
      end
    end

    # ### Summary
    #
    # `adjuststack` accepts a single integer argument and removes that many
    # elements from the top of the stack.
    #
    # ### Usage
    #
    # ~~~ruby
    # x = [true]
    # x[0] ||= nil
    # x[0]
    # ~~~
    #
    class AdjustStack < Instruction
      attr_reader :number

      def initialize(number)
        @number = number
      end

      def disasm(fmt)
        fmt.instruction("adjuststack", [fmt.object(number)])
      end

      def to_a(_iseq)
        [:adjuststack, number]
      end

      def deconstruct_keys(_keys)
        { number: number }
      end

      def ==(other)
        other.is_a?(AdjustStack) && other.number == number
      end

      def length
        2
      end

      def pops
        number
      end

      def call(vm)
        vm.pop(number)
      end
    end

    # ### Summary
    #
    # `anytostring` ensures that the value on top of the stack is a string.
    #
    # It pops two values off the stack. If the first value is a string it
    # pushes it back on the stack. If the first value is not a string, it uses
    # Ruby's built in string coercion to coerce the second value to a string
    # and then pushes that back on the stack.
    #
    # This is used in conjunction with `objtostring` as a fallback for when an
    # object's `to_s` method does not return a string.
    #
    # ### Usage
    #
    # ~~~ruby
    # "#{5}"
    # ~~~
    #
    class AnyToString < Instruction
      def disasm(fmt)
        fmt.instruction("anytostring")
      end

      def to_a(_iseq)
        [:anytostring]
      end

      def deconstruct_keys(_keys)
        {}
      end

      def ==(other)
        other.is_a?(AnyToString)
      end

      def pops
        2
      end

      def pushes
        1
      end

      def call(vm)
        original, value = vm.pop(2)

        if value.is_a?(String)
          vm.push(value)
        else
          vm.push("#<#{original.class.name}:0000>")
        end
      end
    end

    # ### Summary
    #
    # `branchif` has one argument: the jump index. It pops one value off the
    # stack: the jump condition.
    #
    # If the value popped off the stack is true, `branchif` jumps to
    # the jump index and continues executing there.
    #
    # ### Usage
    #
    # ~~~ruby
    # x = true
    # x ||= "foo"
    # puts x
    # ~~~
    #
    class BranchIf < Instruction
      attr_reader :label

      def initialize(label)
        @label = label
      end

      def disasm(fmt)
        fmt.instruction("branchif", [fmt.label(label)])
      end

      def to_a(_iseq)
        [:branchif, label.name]
      end

      def deconstruct_keys(_keys)
        { label: label }
      end

      def ==(other)
        other.is_a?(BranchIf) && other.label == label
      end

      def length
        2
      end

      def pops
        1
      end

      def call(vm)
        vm.jump(label) if vm.pop
      end

      def branch_targets
        [label]
      end

      def falls_through?
        true
      end
    end

    # ### Summary
    #
    # `branchnil` has one argument: the jump index. It pops one value off the
    # stack: the jump condition.
    #
    # If the value popped off the stack is nil, `branchnil` jumps to
    # the jump index and continues executing there.
    #
    # ### Usage
    #
    # ~~~ruby
    # x = nil
    # if x&.to_s
    #   puts "hi"
    # end
    # ~~~
    #
    class BranchNil < Instruction
      attr_reader :label

      def initialize(label)
        @label = label
      end

      def disasm(fmt)
        fmt.instruction("branchnil", [fmt.label(label)])
      end

      def to_a(_iseq)
        [:branchnil, label.name]
      end

      def deconstruct_keys(_keys)
        { label: label }
      end

      def ==(other)
        other.is_a?(BranchNil) && other.label == label
      end

      def length
        2
      end

      def pops
        1
      end

      def call(vm)
        vm.jump(label) if vm.pop.nil?
      end

      def branch_targets
        [label]
      end

      def falls_through?
        true
      end
    end

    # ### Summary
    #
    # `branchunless` has one argument: the jump index. It pops one value off
    # the stack: the jump condition.
    #
    # If the value popped off the stack is false or nil, `branchunless` jumps
    # to the jump index and continues executing there.
    #
    # ### Usage
    #
    # ~~~ruby
    # if 2 + 3
    #   puts "foo"
    # end
    # ~~~
    #
    class BranchUnless < Instruction
      attr_reader :label

      def initialize(label)
        @label = label
      end

      def disasm(fmt)
        fmt.instruction("branchunless", [fmt.label(label)])
      end

      def to_a(_iseq)
        [:branchunless, label.name]
      end

      def deconstruct_keys(_keys)
        { label: label }
      end

      def ==(other)
        other.is_a?(BranchUnless) && other.label == label
      end

      def length
        2
      end

      def pops
        1
      end

      def call(vm)
        vm.jump(label) unless vm.pop
      end

      def branch_targets
        [label]
      end

      def falls_through?
        true
      end
    end

    # ### Summary
    #
    # `checkkeyword` checks if a keyword was passed at the callsite that
    # called into the method represented by the instruction sequence. It has
    # two arguments: the index of the local variable that stores the keywords
    # metadata and the index of the keyword within that metadata. It pushes
    # a boolean onto the stack indicating whether or not the keyword was
    # given.
    #
    # ### Usage
    #
    # ~~~ruby
    # def evaluate(value: rand)
    #   value
    # end
    #
    # evaluate(value: 3)
    # ~~~
    #
    class CheckKeyword < Instruction
      attr_reader :keyword_bits_index, :keyword_index

      def initialize(keyword_bits_index, keyword_index)
        @keyword_bits_index = keyword_bits_index
        @keyword_index = keyword_index
      end

      def disasm(fmt)
        fmt.instruction(
          "checkkeyword",
          [fmt.object(keyword_bits_index), fmt.object(keyword_index)]
        )
      end

      def to_a(iseq)
        [
          :checkkeyword,
          iseq.local_table.offset(keyword_bits_index),
          keyword_index
        ]
      end

      def deconstruct_keys(_keys)
        { keyword_bits_index: keyword_bits_index, keyword_index: keyword_index }
      end

      def ==(other)
        other.is_a?(CheckKeyword) &&
          other.keyword_bits_index == keyword_bits_index &&
          other.keyword_index == keyword_index
      end

      def length
        3
      end

      def pushes
        1
      end

      def call(vm)
        vm.push(vm.local_get(keyword_bits_index, 0)[keyword_index])
      end
    end

    # ### Summary
    #
    # `checkmatch` checks if the current pattern matches the current value. It
    # pops the target and the pattern off the stack and pushes a boolean onto
    # the stack if it matches or not.
    #
    # ### Usage
    #
    # ~~~ruby
    # foo in Foo
    # ~~~
    #
    class CheckMatch < Instruction
      VM_CHECKMATCH_TYPE_WHEN = 1
      VM_CHECKMATCH_TYPE_CASE = 2
      VM_CHECKMATCH_TYPE_RESCUE = 3
      VM_CHECKMATCH_TYPE_MASK = 0x03
      VM_CHECKMATCH_ARRAY = 0x04

      attr_reader :type

      def initialize(type)
        @type = type
      end

      def disasm(fmt)
        fmt.instruction("checkmatch", [fmt.object(type)])
      end

      def to_a(_iseq)
        [:checkmatch, type]
      end

      def deconstruct_keys(_keys)
        { type: type }
      end

      def ==(other)
        other.is_a?(CheckMatch) && other.type == type
      end

      def length
        2
      end

      def pops
        2
      end

      def pushes
        1
      end

      def call(vm)
        target, pattern = vm.pop(2)

        vm.push(
          if type & VM_CHECKMATCH_ARRAY > 0
            pattern.any? { |item| check?(item, target) }
          else
            check?(pattern, target)
          end
        )
      end

      private

      def check?(pattern, target)
        case type & VM_CHECKMATCH_TYPE_MASK
        when VM_CHECKMATCH_TYPE_WHEN
          pattern
        when VM_CHECKMATCH_TYPE_CASE
          pattern === target
        when VM_CHECKMATCH_TYPE_RESCUE
          unless pattern.is_a?(Module)
            raise TypeError, "class or module required for rescue clause"
          end

          pattern === target
        end
      end
    end

    # ### Summary
    #
    # `checktype` checks if the value on top of the stack is of a certain type.
    # The type is the only argument. It pops the value off the stack and pushes
    # a boolean onto the stack indicating whether or not the value is of the
    # given type.
    #
    # ### Usage
    #
    # ~~~ruby
    # foo in [bar]
    # ~~~
    #
    class CheckType < Instruction
      TYPE_OBJECT = 0x01
      TYPE_CLASS = 0x02
      TYPE_MODULE = 0x03
      TYPE_FLOAT = 0x04
      TYPE_STRING = 0x05
      TYPE_REGEXP = 0x06
      TYPE_ARRAY = 0x07
      TYPE_HASH = 0x08
      TYPE_STRUCT = 0x09
      TYPE_BIGNUM = 0x0a
      TYPE_FILE = 0x0b
      TYPE_DATA = 0x0c
      TYPE_MATCH = 0x0d
      TYPE_COMPLEX = 0x0e
      TYPE_RATIONAL = 0x0f
      TYPE_NIL = 0x11
      TYPE_TRUE = 0x12
      TYPE_FALSE = 0x13
      TYPE_SYMBOL = 0x14
      TYPE_FIXNUM = 0x15
      TYPE_UNDEF = 0x16

      attr_reader :type

      def initialize(type)
        @type = type
      end

      def disasm(fmt)
        name =
          case type
          when TYPE_OBJECT
            "T_OBJECT"
          when TYPE_CLASS
            "T_CLASS"
          when TYPE_MODULE
            "T_MODULE"
          when TYPE_FLOAT
            "T_FLOAT"
          when TYPE_STRING
            "T_STRING"
          when TYPE_REGEXP
            "T_REGEXP"
          when TYPE_ARRAY
            "T_ARRAY"
          when TYPE_HASH
            "T_HASH"
          when TYPE_STRUCT
            "T_STRUCT"
          when TYPE_BIGNUM
            "T_BIGNUM"
          when TYPE_FILE
            "T_FILE"
          when TYPE_DATA
            "T_DATA"
          when TYPE_MATCH
            "T_MATCH"
          when TYPE_COMPLEX
            "T_COMPLEX"
          when TYPE_RATIONAL
            "T_RATIONAL"
          when TYPE_NIL
            "T_NIL"
          when TYPE_TRUE
            "T_TRUE"
          when TYPE_FALSE
            "T_FALSE"
          when TYPE_SYMBOL
            "T_SYMBOL"
          when TYPE_FIXNUM
            "T_FIXNUM"
          when TYPE_UNDEF
            "T_UNDEF"
          end

        fmt.instruction("checktype", [name])
      end

      def to_a(_iseq)
        [:checktype, type]
      end

      def deconstruct_keys(_keys)
        { type: type }
      end

      def ==(other)
        other.is_a?(CheckType) && other.type == type
      end

      def length
        2
      end

      def pops
        1
      end

      def pushes
        # TODO: This is incorrect. The instruction only pushes a single value
        # onto the stack. However, if this is set to 1, we no longer match the
        # output of RubyVM::InstructionSequence. So leaving this here until we
        # can investigate further.
        2
      end

      def call(vm)
        object = vm.pop
        result =
          case type
          when TYPE_OBJECT
            raise NotImplementedError, "checktype TYPE_OBJECT"
          when TYPE_CLASS
            object.is_a?(Class)
          when TYPE_MODULE
            object.is_a?(Module)
          when TYPE_FLOAT
            object.is_a?(Float)
          when TYPE_STRING
            object.is_a?(String)
          when TYPE_REGEXP
            object.is_a?(Regexp)
          when TYPE_ARRAY
            object.is_a?(Array)
          when TYPE_HASH
            object.is_a?(Hash)
          when TYPE_STRUCT
            object.is_a?(Struct)
          when TYPE_BIGNUM
            raise NotImplementedError, "checktype TYPE_BIGNUM"
          when TYPE_FILE
            object.is_a?(File)
          when TYPE_DATA
            raise NotImplementedError, "checktype TYPE_DATA"
          when TYPE_MATCH
            raise NotImplementedError, "checktype TYPE_MATCH"
          when TYPE_COMPLEX
            object.is_a?(Complex)
          when TYPE_RATIONAL
            object.is_a?(Rational)
          when TYPE_NIL
            object.nil?
          when TYPE_TRUE
            object == true
          when TYPE_FALSE
            object == false
          when TYPE_SYMBOL
            object.is_a?(Symbol)
          when TYPE_FIXNUM
            object.is_a?(Integer)
          when TYPE_UNDEF
            raise NotImplementedError, "checktype TYPE_UNDEF"
          end

        vm.push(result)
      end
    end

    # ### Summary
    #
    # `concatarray` concatenates the two Arrays on top of the stack.
    #
    # It coerces the two objects at the top of the stack into Arrays by
    # calling `to_a` if necessary, and makes sure to `dup` the first Array if
    # it was already an Array, to avoid mutating it when concatenating.
    #
    # ### Usage
    #
    # ~~~ruby
    # [1, *2]
    # ~~~
    #
    class ConcatArray < Instruction
      def disasm(fmt)
        fmt.instruction("concatarray")
      end

      def to_a(_iseq)
        [:concatarray]
      end

      def deconstruct_keys(_keys)
        {}
      end

      def ==(other)
        other.is_a?(ConcatArray)
      end

      def pops
        2
      end

      def pushes
        1
      end

      def call(vm)
        left, right = vm.pop(2)
        vm.push([*left, *right])
      end
    end

    # ### Summary
    #
    # `concatstrings` pops a number of strings from the stack joins them
    # together into a single string and pushes that string back on the stack.
    #
    # This does no coercion and so is always used in conjunction with
    # `objtostring` and `anytostring` to ensure the stack contents are always
    # strings.
    #
    # ### Usage
    #
    # ~~~ruby
    # "#{5}"
    # ~~~
    #
    class ConcatStrings < Instruction
      attr_reader :number

      def initialize(number)
        @number = number
      end

      def disasm(fmt)
        fmt.instruction("concatstrings", [fmt.object(number)])
      end

      def to_a(_iseq)
        [:concatstrings, number]
      end

      def deconstruct_keys(_keys)
        { number: number }
      end

      def ==(other)
        other.is_a?(ConcatStrings) && other.number == number
      end

      def length
        2
      end

      def pops
        number
      end

      def pushes
        1
      end

      def call(vm)
        vm.push(vm.pop(number).join)
      end
    end

    # ### Summary
    #
    # `defineclass` defines a class. First it pops the superclass off the
    # stack, then it pops the object off the stack that the class should be
    # defined under. It has three arguments: the name of the constant, the
    # instruction sequence associated with the class, and various flags that
    # indicate if it is a singleton class, a module, or a regular class.
    #
    # ### Usage
    #
    # ~~~ruby
    # class Foo
    # end
    # ~~~
    #
    class DefineClass < Instruction
      TYPE_CLASS = 0
      TYPE_SINGLETON_CLASS = 1
      TYPE_MODULE = 2
      FLAG_SCOPED = 8
      FLAG_HAS_SUPERCLASS = 16

      attr_reader :name, :class_iseq, :flags

      def initialize(name, class_iseq, flags)
        @name = name
        @class_iseq = class_iseq
        @flags = flags
      end

      def disasm(fmt)
        fmt.enqueue(class_iseq)
        fmt.instruction(
          "defineclass",
          [fmt.object(name), class_iseq.name, fmt.object(flags)]
        )
      end

      def to_a(_iseq)
        [:defineclass, name, class_iseq.to_a, flags]
      end

      def deconstruct_keys(_keys)
        { name: name, class_iseq: class_iseq, flags: flags }
      end

      def ==(other)
        other.is_a?(DefineClass) && other.name == name &&
          other.class_iseq == class_iseq && other.flags == flags
      end

      def length
        4
      end

      def pops
        2
      end

      def pushes
        1
      end

      def call(vm)
        object, superclass = vm.pop(2)

        if name == :singletonclass
          vm.push(vm.run_class_frame(class_iseq, object.singleton_class))
        elsif object.const_defined?(name)
          vm.push(vm.run_class_frame(class_iseq, object.const_get(name)))
        elsif flags & TYPE_MODULE > 0
          clazz = Module.new
          object.const_set(name, clazz)
          vm.push(vm.run_class_frame(class_iseq, clazz))
        else
          clazz =
            if flags & FLAG_HAS_SUPERCLASS > 0
              Class.new(superclass)
            else
              Class.new
            end

          object.const_set(name, clazz)
          vm.push(vm.run_class_frame(class_iseq, clazz))
        end
      end
    end

    # ### Summary
    #
    # `defined` checks if the top value of the stack is defined. If it is, it
    # pushes its value onto the stack. Otherwise it pushes `nil`.
    #
    # ### Usage
    #
    # ~~~ruby
    # defined?(x)
    # ~~~
    #
    class Defined < Instruction
      TYPE_NIL = 1
      TYPE_IVAR = 2
      TYPE_LVAR = 3
      TYPE_GVAR = 4
      TYPE_CVAR = 5
      TYPE_CONST = 6
      TYPE_METHOD = 7
      TYPE_YIELD = 8
      TYPE_ZSUPER = 9
      TYPE_SELF = 10
      TYPE_TRUE = 11
      TYPE_FALSE = 12
      TYPE_ASGN = 13
      TYPE_EXPR = 14
      TYPE_REF = 15
      TYPE_FUNC = 16
      TYPE_CONST_FROM = 17

      attr_reader :type, :name, :message

      def initialize(type, name, message)
        @type = type
        @name = name
        @message = message
      end

      def disasm(fmt)
        type_name =
          case type
          when TYPE_NIL
            "nil"
          when TYPE_IVAR
            "ivar"
          when TYPE_LVAR
            "lvar"
          when TYPE_GVAR
            "gvar"
          when TYPE_CVAR
            "cvar"
          when TYPE_CONST
            "const"
          when TYPE_METHOD
            "method"
          when TYPE_YIELD
            "yield"
          when TYPE_ZSUPER
            "zsuper"
          when TYPE_SELF
            "self"
          when TYPE_TRUE
            "true"
          when TYPE_FALSE
            "false"
          when TYPE_ASGN
            "asgn"
          when TYPE_EXPR
            "expr"
          when TYPE_REF
            "ref"
          when TYPE_FUNC
            "func"
          when TYPE_CONST_FROM
            "constant-from"
          end

        fmt.instruction(
          "defined",
          [type_name, fmt.object(name), fmt.object(message)]
        )
      end

      def to_a(_iseq)
        [:defined, type, name, message]
      end

      def deconstruct_keys(_keys)
        { type: type, name: name, message: message }
      end

      def ==(other)
        other.is_a?(Defined) && other.type == type && other.name == name &&
          other.message == message
      end

      def length
        4
      end

      def pops
        1
      end

      def pushes
        1
      end

      def call(vm)
        object = vm.pop

        result =
          case type
          when TYPE_NIL, TYPE_SELF, TYPE_TRUE, TYPE_FALSE, TYPE_ASGN, TYPE_EXPR
            message
          when TYPE_IVAR
            message if vm.frame._self.instance_variable_defined?(name)
          when TYPE_LVAR
            raise NotImplementedError, "defined TYPE_LVAR"
          when TYPE_GVAR
            message if global_variables.include?(name)
          when TYPE_CVAR
            clazz = vm.frame._self
            clazz = clazz.singleton_class unless clazz.is_a?(Module)
            message if clazz.class_variable_defined?(name)
          when TYPE_CONST
            clazz = vm.frame._self
            clazz = clazz.singleton_class unless clazz.is_a?(Module)
            message if clazz.const_defined?(name)
          when TYPE_METHOD
            raise NotImplementedError, "defined TYPE_METHOD"
          when TYPE_YIELD
            raise NotImplementedError, "defined TYPE_YIELD"
          when TYPE_ZSUPER
            raise NotImplementedError, "defined TYPE_ZSUPER"
          when TYPE_REF
            raise NotImplementedError, "defined TYPE_REF"
          when TYPE_FUNC
            message if object.respond_to?(name, true)
          when TYPE_CONST_FROM
            defined =
              vm.frame.nesting.any? { |scope| scope.const_defined?(name, true) }
            message if defined
          end

        vm.push(result)
      end
    end

    # ### Summary
    #
    # `definedivar` checks if an instance variable is defined. It is a
    # specialization of the `defined` instruction. It accepts three arguments:
    # the name of the instance variable, an inline cache, and the string that
    # should be pushed onto the stack in the event that the instance variable
    # is defined.
    #
    # ### Usage
    #
    # ~~~ruby
    # defined?(@value)
    # ~~~
    #
    class DefinedIVar < Instruction
      attr_reader :name, :cache, :message

      def initialize(name, cache, message)
        @name = name
        @cache = cache
        @message = message
      end

      def disasm(fmt)
        fmt.instruction(
          "definedivar",
          [fmt.object(name), fmt.inline_storage(cache), fmt.object(message)]
        )
      end

      def to_a(_iseq)
        [:definedivar, name, cache, message]
      end

      def deconstruct_keys(_keys)
        { name: name, cache: cache, message: message }
      end

      def ==(other)
        other.is_a?(DefinedIVar) && other.name == name &&
          other.cache == cache && other.message == message
      end

      def length
        4
      end

      def pushes
        1
      end

      def call(vm)
        result = (message if vm.frame._self.instance_variable_defined?(name))

        vm.push(result)
      end
    end

    # ### Summary
    #
    # `definemethod` defines a method on the class of the current value of
    # `self`. It accepts two arguments. The first is the name of the method
    # being defined. The second is the instruction sequence representing the
    # body of the method.
    #
    # ### Usage
    #
    # ~~~ruby
    # def value = "value"
    # ~~~
    #
    class DefineMethod < Instruction
      attr_reader :method_name, :method_iseq

      def initialize(method_name, method_iseq)
        @method_name = method_name
        @method_iseq = method_iseq
      end

      def disasm(fmt)
        fmt.enqueue(method_iseq)
        fmt.instruction(
          "definemethod",
          [fmt.object(method_name), method_iseq.name]
        )
      end

      def to_a(_iseq)
        [:definemethod, method_name, method_iseq.to_a]
      end

      def deconstruct_keys(_keys)
        { method_name: method_name, method_iseq: method_iseq }
      end

      def ==(other)
        other.is_a?(DefineMethod) && other.method_name == method_name &&
          other.method_iseq == method_iseq
      end

      def length
        3
      end

      def call(vm)
        name = method_name
        nesting = vm.frame.nesting
        iseq = method_iseq

        vm
          .frame
          ._self
          .__send__(:define_method, name) do |*args, **kwargs, &block|
            vm.run_method_frame(
              name,
              nesting,
              iseq,
              self,
              *args,
              **kwargs,
              &block
            )
          end
      end
    end

    # ### Summary
    #
    # `definesmethod` defines a method on the singleton class of the current
    # value of `self`. It accepts two arguments. The first is the name of the
    # method being defined. The second is the instruction sequence representing
    # the body of the method. It pops the object off the stack that the method
    # should be defined on.
    #
    # ### Usage
    #
    # ~~~ruby
    # def self.value = "value"
    # ~~~
    #
    class DefineSMethod < Instruction
      attr_reader :method_name, :method_iseq

      def initialize(method_name, method_iseq)
        @method_name = method_name
        @method_iseq = method_iseq
      end

      def disasm(fmt)
        fmt.enqueue(method_iseq)
        fmt.instruction(
          "definesmethod",
          [fmt.object(method_name), method_iseq.name]
        )
      end

      def to_a(_iseq)
        [:definesmethod, method_name, method_iseq.to_a]
      end

      def deconstruct_keys(_keys)
        { method_name: method_name, method_iseq: method_iseq }
      end

      def ==(other)
        other.is_a?(DefineSMethod) && other.method_name == method_name &&
          other.method_iseq == method_iseq
      end

      def length
        3
      end

      def pops
        1
      end

      def call(vm)
        name = method_name
        nesting = vm.frame.nesting
        iseq = method_iseq

        vm
          .frame
          ._self
          .__send__(:define_singleton_method, name) do |*args, **kwargs, &block|
            vm.run_method_frame(
              name,
              nesting,
              iseq,
              self,
              *args,
              **kwargs,
              &block
            )
          end
      end
    end

    # ### Summary
    #
    # `dup` copies the top value of the stack and pushes it onto the stack.
    #
    # ### Usage
    #
    # ~~~ruby
    # $global = 5
    # ~~~
    #
    class Dup < Instruction
      def disasm(fmt)
        fmt.instruction("dup")
      end

      def to_a(_iseq)
        [:dup]
      end

      def deconstruct_keys(_keys)
        {}
      end

      def ==(other)
        other.is_a?(Dup)
      end

      def pops
        1
      end

      def pushes
        2
      end

      def call(vm)
        vm.push(vm.stack.last.dup)
      end

      def side_effects?
        false
      end
    end

    # ### Summary
    #
    # `duparray` dups an Array literal and pushes it onto the stack.
    #
    # ### Usage
    #
    # ~~~ruby
    # [true]
    # ~~~
    #
    class DupArray < Instruction
      attr_reader :object

      def initialize(object)
        @object = object
      end

      def disasm(fmt)
        fmt.instruction("duparray", [fmt.object(object)])
      end

      def to_a(_iseq)
        [:duparray, object]
      end

      def deconstruct_keys(_keys)
        { object: object }
      end

      def ==(other)
        other.is_a?(DupArray) && other.object == object
      end

      def length
        2
      end

      def pushes
        1
      end

      def call(vm)
        vm.push(object.dup)
      end
    end

    # ### Summary
    #
    # `duphash` dups a Hash literal and pushes it onto the stack.
    #
    # ### Usage
    #
    # ~~~ruby
    # { a: 1 }
    # ~~~
    #
    class DupHash < Instruction
      attr_reader :object

      def initialize(object)
        @object = object
      end

      def disasm(fmt)
        fmt.instruction("duphash", [fmt.object(object)])
      end

      def to_a(_iseq)
        [:duphash, object]
      end

      def deconstruct_keys(_keys)
        { object: object }
      end

      def ==(other)
        other.is_a?(DupHash) && other.object == object
      end

      def length
        2
      end

      def pushes
        1
      end

      def call(vm)
        vm.push(object.dup)
      end
    end

    # ### Summary
    #
    # `dupn` duplicates the top `n` stack elements.
    #
    # ### Usage
    #
    # ~~~ruby
    # Object::X ||= true
    # ~~~
    #
    class DupN < Instruction
      attr_reader :number

      def initialize(number)
        @number = number
      end

      def disasm(fmt)
        fmt.instruction("dupn", [fmt.object(number)])
      end

      def to_a(_iseq)
        [:dupn, number]
      end

      def deconstruct_keys(_keys)
        { number: number }
      end

      def ==(other)
        other.is_a?(DupN) && other.number == number
      end

      def length
        2
      end

      def pushes
        number
      end

      def call(vm)
        values = vm.pop(number)
        vm.push(*values)
        vm.push(*values)
      end
    end

    # ### Summary
    #
    # `expandarray` looks at the top of the stack, and if the value is an array
    # it replaces it on the stack with `number` elements of the array, or `nil`
    # if the elements are missing.
    #
    # ### Usage
    #
    # ~~~ruby
    # x, = [true, false, nil]
    # ~~~
    #
    class ExpandArray < Instruction
      attr_reader :number, :flags

      def initialize(number, flags)
        @number = number
        @flags = flags
      end

      def disasm(fmt)
        fmt.instruction("expandarray", [fmt.object(number), fmt.object(flags)])
      end

      def to_a(_iseq)
        [:expandarray, number, flags]
      end

      def deconstruct_keys(_keys)
        { number: number, flags: flags }
      end

      def ==(other)
        other.is_a?(ExpandArray) && other.number == number &&
          other.flags == flags
      end

      def length
        3
      end

      def pops
        1
      end

      def pushes
        number
      end

      def call(vm)
        object = vm.pop
        object =
          if Array === object
            object.dup
          elsif object.respond_to?(:to_ary, true)
            object.to_ary
          else
            [object]
          end

        splat_flag = flags & 0x01 > 0
        postarg_flag = flags & 0x02 > 0

        if number == 0 && splat_flag == 0
          # no space left on stack
        elsif postarg_flag
          values = []

          if number > object.size
            (number - object.size).times { values.push(nil) }
          end
          [number, object.size].min.times { values.push(object.pop) }
          values.push(object.to_a) if splat_flag

          values.each { |item| vm.push(item) }
        else
          values = []

          [number, object.size].min.times { values.push(object.shift) }
          if number > values.size
            (number - values.size).times { values.push(nil) }
          end
          values.push(object.to_a) if splat_flag

          values.reverse_each { |item| vm.push(item) }
        end
      end
    end

    # ### Summary
    #
    # `getblockparam` is a similar instruction to `getlocal` in that it looks
    # for a local variable in the current instruction sequence's local table and
    # walks recursively up the parent instruction sequences until it finds it.
    # The local it retrieves, however, is a special block local that was passed
    # to the current method. It pushes the value of the block local onto the
    # stack.
    #
    # ### Usage
    #
    # ~~~ruby
    # def foo(&block)
    #   block
    # end
    # ~~~
    #
    class GetBlockParam < Instruction
      attr_reader :index, :level

      def initialize(index, level)
        @index = index
        @level = level
      end

      def disasm(fmt)
        fmt.instruction("getblockparam", [fmt.local(index, explicit: level)])
      end

      def to_a(iseq)
        current = iseq
        level.times { current = iseq.parent_iseq }
        [:getblockparam, current.local_table.offset(index), level]
      end

      def deconstruct_keys(_keys)
        { index: index, level: level }
      end

      def ==(other)
        other.is_a?(GetBlockParam) && other.index == index &&
          other.level == level
      end

      def length
        3
      end

      def pushes
        1
      end

      def call(vm)
        vm.push(vm.local_get(index, level))
      end
    end

    # ### Summary
    #
    # `getblockparamproxy` is almost the same as `getblockparam` except that it
    # pushes a proxy object onto the stack instead of the actual value of the
    # block local. This is used when a method is being called on the block
    # local.
    #
    # ### Usage
    #
    # ~~~ruby
    # def foo(&block)
    #   block.call
    # end
    # ~~~
    #
    class GetBlockParamProxy < Instruction
      attr_reader :index, :level

      def initialize(index, level)
        @index = index
        @level = level
      end

      def disasm(fmt)
        fmt.instruction(
          "getblockparamproxy",
          [fmt.local(index, explicit: level)]
        )
      end

      def to_a(iseq)
        current = iseq
        level.times { current = iseq.parent_iseq }
        [:getblockparamproxy, current.local_table.offset(index), level]
      end

      def deconstruct_keys(_keys)
        { index: index, level: level }
      end

      def ==(other)
        other.is_a?(GetBlockParamProxy) && other.index == index &&
          other.level == level
      end

      def length
        3
      end

      def pushes
        1
      end

      def call(vm)
        vm.push(vm.local_get(index, level))
      end
    end

    # ### Summary
    #
    # `getclassvariable` looks for a class variable in the current class and
    # pushes its value onto the stack. It uses an inline cache to reduce the
    # need to lookup the class variable in the class hierarchy every time.
    #
    # ### Usage
    #
    # ~~~ruby
    # @@class_variable
    # ~~~
    #
    class GetClassVariable < Instruction
      attr_reader :name, :cache

      def initialize(name, cache)
        @name = name
        @cache = cache
      end

      def disasm(fmt)
        fmt.instruction(
          "getclassvariable",
          [fmt.object(name), fmt.inline_storage(cache)]
        )
      end

      def to_a(_iseq)
        [:getclassvariable, name, cache]
      end

      def deconstruct_keys(_keys)
        { name: name, cache: cache }
      end

      def ==(other)
        other.is_a?(GetClassVariable) && other.name == name &&
          other.cache == cache
      end

      def length
        3
      end

      def pushes
        1
      end

      def call(vm)
        clazz = vm.frame._self
        clazz = clazz.class unless clazz.is_a?(Class)
        vm.push(clazz.class_variable_get(name))
      end
    end

    # ### Summary
    #
    # `getconstant` performs a constant lookup and pushes the value of the
    # constant onto the stack. It pops both the class it should look in and
    # whether or not it should look globally as well.
    #
    # ### Usage
    #
    # ~~~ruby
    # Constant
    # ~~~
    #
    class GetConstant < Instruction
      attr_reader :name

      def initialize(name)
        @name = name
      end

      def disasm(fmt)
        fmt.instruction("getconstant", [fmt.object(name)])
      end

      def to_a(_iseq)
        [:getconstant, name]
      end

      def deconstruct_keys(_keys)
        { name: name }
      end

      def ==(other)
        other.is_a?(GetConstant) && other.name == name
      end

      def length
        2
      end

      def pops
        2
      end

      def pushes
        1
      end

      def call(vm)
        const_base, allow_nil = vm.pop(2)

        if const_base
          if const_base.const_defined?(name)
            vm.push(const_base.const_get(name))
            return
          end
        elsif const_base.nil? && allow_nil
          vm.frame.nesting.reverse_each do |clazz|
            if clazz.const_defined?(name)
              vm.push(clazz.const_get(name))
              return
            end
          end
        end

        raise NameError, "uninitialized constant #{name}"
      end
    end

    # ### Summary
    #
    # `getglobal` pushes the value of a global variables onto the stack.
    #
    # ### Usage
    #
    # ~~~ruby
    # $$
    # ~~~
    #
    class GetGlobal < Instruction
      attr_reader :name

      def initialize(name)
        @name = name
      end

      def disasm(fmt)
        fmt.instruction("getglobal", [fmt.object(name)])
      end

      def to_a(_iseq)
        [:getglobal, name]
      end

      def deconstruct_keys(_keys)
        { name: name }
      end

      def ==(other)
        other.is_a?(GetGlobal) && other.name == name
      end

      def length
        2
      end

      def pushes
        1
      end

      def call(vm)
        # Evaluating the name of the global variable because there isn't a
        # reflection API for global variables.
        vm.push(eval(name.to_s, binding, __FILE__, __LINE__))
      end
    end

    # ### Summary
    #
    # `getinstancevariable` pushes the value of an instance variable onto the
    # stack. It uses an inline cache to avoid having to look up the instance
    # variable in the class hierarchy every time.
    #
    # This instruction has two forms, but both have the same structure. Before
    # Ruby 3.2, the inline cache corresponded to both the get and set
    # instructions and could be shared. Since Ruby 3.2, it uses object shapes
    # instead so the caches are unique per instruction.
    #
    # ### Usage
    #
    # ~~~ruby
    # @instance_variable
    # ~~~
    #
    class GetInstanceVariable < Instruction
      attr_reader :name, :cache

      def initialize(name, cache)
        @name = name
        @cache = cache
      end

      def disasm(fmt)
        fmt.instruction(
          "getinstancevariable",
          [fmt.object(name), fmt.inline_storage(cache)]
        )
      end

      def to_a(_iseq)
        [:getinstancevariable, name, cache]
      end

      def deconstruct_keys(_keys)
        { name: name, cache: cache }
      end

      def ==(other)
        other.is_a?(GetInstanceVariable) && other.name == name &&
          other.cache == cache
      end

      def length
        3
      end

      def pushes
        1
      end

      def call(vm)
        method = Object.instance_method(:instance_variable_get)
        vm.push(method.bind(vm.frame._self).call(name))
      end
    end

    # ### Summary
    #
    # `getlocal` fetches the value of a local variable from a frame determined
    # by the level and index arguments. The level is the number of frames back
    # to look and the index is the index in the local table. It pushes the value
    # it finds onto the stack.
    #
    # ### Usage
    #
    # ~~~ruby
    # value = 5
    # tap { tap { value } }
    # ~~~
    #
    class GetLocal < Instruction
      attr_reader :index, :level

      def initialize(index, level)
        @index = index
        @level = level
      end

      def disasm(fmt)
        fmt.instruction("getlocal", [fmt.local(index, explicit: level)])
      end

      def to_a(iseq)
        current = iseq
        level.times { current = current.parent_iseq }
        [:getlocal, current.local_table.offset(index), level]
      end

      def deconstruct_keys(_keys)
        { index: index, level: level }
      end

      def ==(other)
        other.is_a?(GetLocal) && other.index == index && other.level == level
      end

      def length
        3
      end

      def pushes
        1
      end

      def call(vm)
        vm.push(vm.local_get(index, level))
      end
    end

    # ### Summary
    #
    # `getlocal_WC_0` is a specialized version of the `getlocal` instruction. It
    # fetches the value of a local variable from the current frame determined by
    # the index given as its only argument.
    #
    # ### Usage
    #
    # ~~~ruby
    # value = 5
    # value
    # ~~~
    #
    class GetLocalWC0 < Instruction
      attr_reader :index

      def initialize(index)
        @index = index
      end

      def disasm(fmt)
        fmt.instruction("getlocal_WC_0", [fmt.local(index, implicit: 0)])
      end

      def to_a(iseq)
        [:getlocal_WC_0, iseq.local_table.offset(index)]
      end

      def deconstruct_keys(_keys)
        { index: index }
      end

      def ==(other)
        other.is_a?(GetLocalWC0) && other.index == index
      end

      def length
        2
      end

      def pushes
        1
      end

      def canonical
        GetLocal.new(index, 0)
      end

      def call(vm)
        canonical.call(vm)
      end
    end

    # ### Summary
    #
    # `getlocal_WC_1` is a specialized version of the `getlocal` instruction. It
    # fetches the value of a local variable from the parent frame determined by
    # the index given as its only argument.
    #
    # ### Usage
    #
    # ~~~ruby
    # value = 5
    # self.then { value }
    # ~~~
    #
    class GetLocalWC1 < Instruction
      attr_reader :index

      def initialize(index)
        @index = index
      end

      def disasm(fmt)
        fmt.instruction("getlocal_WC_1", [fmt.local(index, implicit: 1)])
      end

      def to_a(iseq)
        [:getlocal_WC_1, iseq.parent_iseq.local_table.offset(index)]
      end

      def deconstruct_keys(_keys)
        { index: index }
      end

      def ==(other)
        other.is_a?(GetLocalWC1) && other.index == index
      end

      def length
        2
      end

      def pushes
        1
      end

      def canonical
        GetLocal.new(index, 1)
      end

      def call(vm)
        canonical.call(vm)
      end
    end

    # ### Summary
    #
    # `getspecial` pushes the value of a special local variable onto the stack.
    #
    # ### Usage
    #
    # ~~~ruby
    # 1 if (a == 1) .. (b == 2)
    # ~~~
    #
    class GetSpecial < Instruction
      SVAR_LASTLINE = 0 # $_
      SVAR_BACKREF = 1 # $~
      SVAR_FLIPFLOP_START = 2 # flipflop

      attr_reader :key, :type

      def initialize(key, type)
        @key = key
        @type = type
      end

      def disasm(fmt)
        fmt.instruction("getspecial", [fmt.object(key), fmt.object(type)])
      end

      def to_a(_iseq)
        [:getspecial, key, type]
      end

      def deconstruct_keys(_keys)
        { key: key, type: type }
      end

      def ==(other)
        other.is_a?(GetSpecial) && other.key == key && other.type == type
      end

      def length
        3
      end

      def pushes
        1
      end

      def call(vm)
        case key
        when SVAR_LASTLINE
          raise NotImplementedError, "getspecial SVAR_LASTLINE"
        when SVAR_BACKREF
          raise NotImplementedError, "getspecial SVAR_BACKREF"
        when SVAR_FLIPFLOP_START
          vm.frame_svar.svars[SVAR_FLIPFLOP_START]
        end
      end
    end

    # ### Summary
    #
    # `intern` converts the top element of the stack to a symbol and pushes the
    # symbol onto the stack.
    #
    # ### Usage
    #
    # ~~~ruby
    # :"#{"foo"}"
    # ~~~
    #
    class Intern < Instruction
      def disasm(fmt)
        fmt.instruction("intern")
      end

      def to_a(_iseq)
        [:intern]
      end

      def deconstruct_keys(_keys)
        {}
      end

      def ==(other)
        other.is_a?(Intern)
      end

      def pops
        1
      end

      def pushes
        1
      end

      def call(vm)
        vm.push(vm.pop.to_sym)
      end
    end

    # ### Summary
    #
    # `invokeblock` invokes the block given to the current method. It pops the
    # arguments for the block off the stack and pushes the result of running the
    # block onto the stack.
    #
    # ### Usage
    #
    # ~~~ruby
    # def foo
    #   yield
    # end
    # ~~~
    #
    class InvokeBlock < Instruction
      attr_reader :calldata

      def initialize(calldata)
        @calldata = calldata
      end

      def disasm(fmt)
        fmt.instruction("invokeblock", [fmt.calldata(calldata)])
      end

      def to_a(_iseq)
        [:invokeblock, calldata.to_h]
      end

      def deconstruct_keys(_keys)
        { calldata: calldata }
      end

      def ==(other)
        other.is_a?(InvokeBlock) && other.calldata == calldata
      end

      def length
        2
      end

      def pops
        calldata.argc
      end

      def pushes
        1
      end

      def call(vm)
        vm.push(vm.frame_yield.block.call(*vm.pop(calldata.argc)))
      end
    end

    # ### Summary
    #
    # `invokesuper` is similar to the `send` instruction, except that it calls
    # the super method. It pops the receiver and arguments off the stack and
    # pushes the return value onto the stack.
    #
    # ### Usage
    #
    # ~~~ruby
    # def foo
    #   super
    # end
    # ~~~
    #
    class InvokeSuper < Instruction
      attr_reader :calldata, :block_iseq

      def initialize(calldata, block_iseq)
        @calldata = calldata
        @block_iseq = block_iseq
      end

      def disasm(fmt)
        fmt.enqueue(block_iseq) if block_iseq
        fmt.instruction(
          "invokesuper",
          [fmt.calldata(calldata), block_iseq&.name || "nil"]
        )
      end

      def to_a(_iseq)
        [:invokesuper, calldata.to_h, block_iseq&.to_a]
      end

      def deconstruct_keys(_keys)
        { calldata: calldata, block_iseq: block_iseq }
      end

      def ==(other)
        other.is_a?(InvokeSuper) && other.calldata == calldata &&
          other.block_iseq == block_iseq
      end

      def pops
        argb = (calldata.flag?(CallData::CALL_ARGS_BLOCKARG) ? 1 : 0)
        argb + calldata.argc + 1
      end

      def pushes
        1
      end

      def call(vm)
        block =
          if (iseq = block_iseq)
            frame = vm.frame
            ->(*args, **kwargs, &blk) do
              vm.run_block_frame(iseq, frame, *args, **kwargs, &blk)
            end
          end

        keywords =
          if calldata.kw_arg
            calldata.kw_arg.zip(vm.pop(calldata.kw_arg.length)).to_h
          else
            {}
          end

        arguments = vm.pop(calldata.argc)
        receiver = vm.pop

        method = receiver.method(vm.frame.name).super_method
        vm.push(method.call(*arguments, **keywords, &block))
      end
    end

    # ### Summary
    #
    # `jump` unconditionally jumps to the label given as its only argument.
    #
    # ### Usage
    #
    # ~~~ruby
    # x = 0
    # if x == 0
    #   puts "0"
    # else
    #   puts "2"
    # end
    # ~~~
    #
    class Jump < Instruction
      attr_reader :label

      def initialize(label)
        @label = label
      end

      def disasm(fmt)
        fmt.instruction("jump", [fmt.label(label)])
      end

      def to_a(_iseq)
        [:jump, label.name]
      end

      def deconstruct_keys(_keys)
        { label: label }
      end

      def ==(other)
        other.is_a?(Jump) && other.label == label
      end

      def length
        2
      end

      def call(vm)
        vm.jump(label)
      end

      def branch_targets
        [label]
      end
    end

    # ### Summary
    #
    # `leave` exits the current frame.
    #
    # ### Usage
    #
    # ~~~ruby
    # ;;
    # ~~~
    #
    class Leave < Instruction
      def disasm(fmt)
        fmt.instruction("leave")
      end

      def to_a(_iseq)
        [:leave]
      end

      def deconstruct_keys(_keys)
        {}
      end

      def ==(other)
        other.is_a?(Leave)
      end

      def pops
        1
      end

      def pushes
        # TODO: This is wrong. It should be 1. But it's 0 for now because
        # otherwise the stack size is incorrectly calculated.
        0
      end

      def call(vm)
        vm.leave
      end

      def leaves?
        true
      end
    end

    # ### Summary
    #
    # `newarray` puts a new array initialized with `number` values from the
    # stack. It pops `number` values off the stack and pushes the array onto the
    # stack.
    #
    # ### Usage
    #
    # ~~~ruby
    # ["string"]
    # ~~~
    #
    class NewArray < Instruction
      attr_reader :number

      def initialize(number)
        @number = number
      end

      def disasm(fmt)
        fmt.instruction("newarray", [fmt.object(number)])
      end

      def to_a(_iseq)
        [:newarray, number]
      end

      def deconstruct_keys(_keys)
        { number: number }
      end

      def ==(other)
        other.is_a?(NewArray) && other.number == number
      end

      def length
        2
      end

      def pops
        number
      end

      def pushes
        1
      end

      def call(vm)
        vm.push(vm.pop(number))
      end
    end

    # ### Summary
    #
    # `newarraykwsplat` is a specialized version of `newarray` that takes a **
    # splat argument. It pops `number` values off the stack and pushes the array
    # onto the stack.
    #
    # ### Usage
    #
    # ~~~ruby
    # ["string", **{ foo: "bar" }]
    # ~~~
    #
    class NewArrayKwSplat < Instruction
      attr_reader :number

      def initialize(number)
        @number = number
      end

      def disasm(fmt)
        fmt.instruction("newarraykwsplat", [fmt.object(number)])
      end

      def to_a(_iseq)
        [:newarraykwsplat, number]
      end

      def deconstruct_keys(_keys)
        { number: number }
      end

      def ==(other)
        other.is_a?(NewArrayKwSplat) && other.number == number
      end

      def length
        2
      end

      def pops
        number
      end

      def pushes
        1
      end

      def call(vm)
        vm.push(vm.pop(number))
      end
    end

    # ### Summary
    #
    # `newhash` puts a new hash onto the stack, using `number` elements from the
    # stack. `number` needs to be even. It pops `number` elements off the stack
    # and pushes a hash onto the stack.
    #
    # ### Usage
    #
    # ~~~ruby
    # def foo(key, value)
    #   { key => value }
    # end
    # ~~~
    #
    class NewHash < Instruction
      attr_reader :number

      def initialize(number)
        @number = number
      end

      def disasm(fmt)
        fmt.instruction("newhash", [fmt.object(number)])
      end

      def to_a(_iseq)
        [:newhash, number]
      end

      def deconstruct_keys(_keys)
        { number: number }
      end

      def ==(other)
        other.is_a?(NewHash) && other.number == number
      end

      def length
        2
      end

      def pops
        number
      end

      def pushes
        1
      end

      def call(vm)
        vm.push(vm.pop(number).each_slice(2).to_h)
      end
    end

    # ### Summary
    #
    # `newrange` creates a new range object from the top two values on the
    # stack. It pops both of them off, and then pushes on the new range. It
    # takes one argument which is 0 if the end is included or 1 if the end value
    # is excluded.
    #
    # ### Usage
    #
    # ~~~ruby
    # x = 0
    # y = 1
    # p (x..y), (x...y)
    # ~~~
    #
    class NewRange < Instruction
      attr_reader :exclude_end

      def initialize(exclude_end)
        @exclude_end = exclude_end
      end

      def disasm(fmt)
        fmt.instruction("newrange", [fmt.object(exclude_end)])
      end

      def to_a(_iseq)
        [:newrange, exclude_end]
      end

      def deconstruct_keys(_keys)
        { exclude_end: exclude_end }
      end

      def ==(other)
        other.is_a?(NewRange) && other.exclude_end == exclude_end
      end

      def length
        2
      end

      def pops
        2
      end

      def pushes
        1
      end

      def call(vm)
        vm.push(Range.new(*vm.pop(2), exclude_end == 1))
      end
    end

    # ### Summary
    #
    # `nop` is a no-operation instruction. It is used to pad the instruction
    # sequence so there is a place for other instructions to jump to.
    #
    # ### Usage
    #
    # ~~~ruby
    # raise rescue true
    # ~~~
    #
    class Nop < Instruction
      def disasm(fmt)
        fmt.instruction("nop")
      end

      def to_a(_iseq)
        [:nop]
      end

      def deconstruct_keys(_keys)
        {}
      end

      def ==(other)
        other.is_a?(Nop)
      end

      def call(vm)
      end

      def side_effects?
        false
      end
    end

    # ### Summary
    #
    # `objtostring` pops a value from the stack, calls `to_s` on that value and
    # then pushes the result back to the stack.
    #
    # It has various fast paths for classes like String, Symbol, Module, Class,
    # etc. For everything else it calls `to_s`.
    #
    # ### Usage
    #
    # ~~~ruby
    # "#{5}"
    # ~~~
    #
    class ObjToString < Instruction
      attr_reader :calldata

      def initialize(calldata)
        @calldata = calldata
      end

      def disasm(fmt)
        fmt.instruction("objtostring", [fmt.calldata(calldata)])
      end

      def to_a(_iseq)
        [:objtostring, calldata.to_h]
      end

      def deconstruct_keys(_keys)
        { calldata: calldata }
      end

      def ==(other)
        other.is_a?(ObjToString) && other.calldata == calldata
      end

      def length
        2
      end

      def pops
        1
      end

      def pushes
        1
      end

      def call(vm)
        vm.push(vm.pop.to_s)
      end
    end

    # ### Summary
    #
    # `once` is an instruction that wraps an instruction sequence and ensures
    # that is it only ever executed once for the lifetime of the program. It
    # uses a cache to ensure that it is only executed once. It pushes the result
    # of running the instruction sequence onto the stack.
    #
    # ### Usage
    #
    # ~~~ruby
    # END { puts "END" }
    # ~~~
    #
    class Once < Instruction
      attr_reader :iseq, :cache

      def initialize(iseq, cache)
        @iseq = iseq
        @cache = cache
      end

      def disasm(fmt)
        fmt.enqueue(iseq)
        fmt.instruction("once", [iseq.name, fmt.inline_storage(cache)])
      end

      def to_a(_iseq)
        [:once, iseq.to_a, cache]
      end

      def deconstruct_keys(_keys)
        { iseq: iseq, cache: cache }
      end

      def ==(other)
        other.is_a?(Once) && other.iseq == iseq && other.cache == cache
      end

      def length
        3
      end

      def pushes
        1
      end

      def call(vm)
        return if @executed
        vm.push(vm.run_block_frame(iseq, vm.frame))
        @executed = true
      end
    end

    # ### Summary
    #
    # `opt_and` is a specialization of the `opt_send_without_block` instruction
    # that occurs when the `&` operator is used. There is a fast path for if
    # both operands are integers. It pops both the receiver and the argument off
    # the stack and pushes on the result.
    #
    # ### Usage
    #
    # ~~~ruby
    # 2 & 3
    # ~~~
    #
    class OptAnd < Instruction
      attr_reader :calldata

      def initialize(calldata)
        @calldata = calldata
      end

      def disasm(fmt)
        fmt.instruction("opt_and", [fmt.calldata(calldata)])
      end

      def to_a(_iseq)
        [:opt_and, calldata.to_h]
      end

      def deconstruct_keys(_keys)
        { calldata: calldata }
      end

      def ==(other)
        other.is_a?(OptAnd) && other.calldata == calldata
      end

      def length
        2
      end

      def pops
        2
      end

      def pushes
        1
      end

      def canonical
        Send.new(calldata, nil)
      end

      def call(vm)
        canonical.call(vm)
      end
    end

    # ### Summary
    #
    # `opt_aref` is a specialization of the `opt_send_without_block` instruction
    # that occurs when the `[]` operator is used. There are fast paths if the
    # receiver is an integer, array, or hash.
    #
    # ### Usage
    #
    # ~~~ruby
    # 7[2]
    # ~~~
    #
    class OptAref < Instruction
      attr_reader :calldata

      def initialize(calldata)
        @calldata = calldata
      end

      def disasm(fmt)
        fmt.instruction("opt_aref", [fmt.calldata(calldata)])
      end

      def to_a(_iseq)
        [:opt_aref, calldata.to_h]
      end

      def deconstruct_keys(_keys)
        { calldata: calldata }
      end

      def ==(other)
        other.is_a?(OptAref) && other.calldata == calldata
      end

      def length
        2
      end

      def pops
        2
      end

      def pushes
        1
      end

      def canonical
        Send.new(calldata, nil)
      end

      def call(vm)
        canonical.call(vm)
      end
    end

    # ### Summary
    #
    # `opt_aref_with` is a specialization of the `opt_aref` instruction that
    # occurs when the `[]` operator is used with a string argument known at
    # compile time. There are fast paths if the receiver is a hash. It pops the
    # receiver off the stack and pushes on the result.
    #
    # ### Usage
    #
    # ~~~ruby
    # { 'test' => true }['test']
    # ~~~
    #
    class OptArefWith < Instruction
      attr_reader :object, :calldata

      def initialize(object, calldata)
        @object = object
        @calldata = calldata
      end

      def disasm(fmt)
        fmt.instruction(
          "opt_aref_with",
          [fmt.object(object), fmt.calldata(calldata)]
        )
      end

      def to_a(_iseq)
        [:opt_aref_with, object, calldata.to_h]
      end

      def deconstruct_keys(_keys)
        { object: object, calldata: calldata }
      end

      def ==(other)
        other.is_a?(OptArefWith) && other.object == object &&
          other.calldata == calldata
      end

      def length
        3
      end

      def pops
        1
      end

      def pushes
        1
      end

      def call(vm)
        vm.push(vm.pop[object])
      end
    end

    # ### Summary
    #
    # `opt_aset` is an instruction for setting the hash value by the key in
    # the `recv[obj] = set` format. It is a specialization of the
    # `opt_send_without_block` instruction. It pops the receiver, the key, and
    # the value off the stack and pushes on the result.
    #
    # ### Usage
    #
    # ~~~ruby
    # {}[:key] = value
    # ~~~
    #
    class OptAset < Instruction
      attr_reader :calldata

      def initialize(calldata)
        @calldata = calldata
      end

      def disasm(fmt)
        fmt.instruction("opt_aset", [fmt.calldata(calldata)])
      end

      def to_a(_iseq)
        [:opt_aset, calldata.to_h]
      end

      def deconstruct_keys(_keys)
        { calldata: calldata }
      end

      def ==(other)
        other.is_a?(OptAset) && other.calldata == calldata
      end

      def length
        2
      end

      def pops
        3
      end

      def pushes
        1
      end

      def canonical
        Send.new(calldata, nil)
      end

      def call(vm)
        canonical.call(vm)
      end
    end

    # ### Summary
    #
    # `opt_aset_with` is an instruction for setting the hash value by the known
    # string key in the `recv[obj] = set` format. It pops the receiver and the
    # value off the stack and pushes on the result.
    #
    # ### Usage
    #
    # ~~~ruby
    # {}["key"] = value
    # ~~~
    #
    class OptAsetWith < Instruction
      attr_reader :object, :calldata

      def initialize(object, calldata)
        @object = object
        @calldata = calldata
      end

      def disasm(fmt)
        fmt.instruction(
          "opt_aset_with",
          [fmt.object(object), fmt.calldata(calldata)]
        )
      end

      def to_a(_iseq)
        [:opt_aset_with, object, calldata.to_h]
      end

      def deconstruct_keys(_keys)
        { object: object, calldata: calldata }
      end

      def ==(other)
        other.is_a?(OptAsetWith) && other.object == object &&
          other.calldata == calldata
      end

      def length
        3
      end

      def pops
        2
      end

      def pushes
        1
      end

      def call(vm)
        hash, value = vm.pop(2)
        vm.push(hash[object] = value)
      end
    end

    # ### Summary
    #
    # `opt_case_dispatch` is a branch instruction that moves the control flow
    # for case statements that have clauses where they can all be used as hash
    # keys for an internal hash.
    #
    # It has two arguments: the `case_dispatch_hash` and an `else_label`. It
    # pops one value off the stack: a hash key. `opt_case_dispatch` looks up the
    # key in the `case_dispatch_hash` and jumps to the corresponding label if
    # there is one. If there is no value in the `case_dispatch_hash`,
    # `opt_case_dispatch` jumps to the `else_label` index.
    #
    # ### Usage
    #
    # ~~~ruby
    # case 1
    # when 1
    #   puts "foo"
    # else
    #   puts "bar"
    # end
    # ~~~
    #
    class OptCaseDispatch < Instruction
      attr_reader :case_dispatch_hash, :else_label

      def initialize(case_dispatch_hash, else_label)
        @case_dispatch_hash = case_dispatch_hash
        @else_label = else_label
      end

      def disasm(fmt)
        fmt.instruction(
          "opt_case_dispatch",
          ["<cdhash>", fmt.label(else_label)]
        )
      end

      def to_a(_iseq)
        [
          :opt_case_dispatch,
          case_dispatch_hash.flat_map { |key, value| [key, value.name] },
          else_label.name
        ]
      end

      def deconstruct_keys(_keys)
        { case_dispatch_hash: case_dispatch_hash, else_label: else_label }
      end

      def ==(other)
        other.is_a?(OptCaseDispatch) &&
          other.case_dispatch_hash == case_dispatch_hash &&
          other.else_label == else_label
      end

      def length
        3
      end

      def pops
        1
      end

      def call(vm)
        vm.jump(case_dispatch_hash.fetch(vm.pop, else_label))
      end

      def branch_targets
        case_dispatch_hash.values.push(else_label)
      end

      def falls_through?
        true
      end
    end

    # ### Summary
    #
    # `opt_div` is a specialization of the `opt_send_without_block` instruction
    # that occurs when the `/` operator is used. There are fast paths for if
    # both operands are integers, or if both operands are floats. It pops both
    # the receiver and the argument off the stack and pushes on the result.
    #
    # ### Usage
    #
    # ~~~ruby
    # 2 / 3
    # ~~~
    #
    class OptDiv < Instruction
      attr_reader :calldata

      def initialize(calldata)
        @calldata = calldata
      end

      def disasm(fmt)
        fmt.instruction("opt_div", [fmt.calldata(calldata)])
      end

      def to_a(_iseq)
        [:opt_div, calldata.to_h]
      end

      def deconstruct_keys(_keys)
        { calldata: calldata }
      end

      def ==(other)
        other.is_a?(OptDiv) && other.calldata == calldata
      end

      def length
        2
      end

      def pops
        2
      end

      def pushes
        1
      end

      def canonical
        Send.new(calldata, nil)
      end

      def call(vm)
        canonical.call(vm)
      end
    end

    # ### Summary
    #
    # `opt_empty_p` is an optimization applied when the method `empty?` is
    # called. It pops the receiver off the stack and pushes on the result of the
    # method call.
    #
    # ### Usage
    #
    # ~~~ruby
    # "".empty?
    # ~~~
    #
    class OptEmptyP < Instruction
      attr_reader :calldata

      def initialize(calldata)
        @calldata = calldata
      end

      def disasm(fmt)
        fmt.instruction("opt_empty_p", [fmt.calldata(calldata)])
      end

      def to_a(_iseq)
        [:opt_empty_p, calldata.to_h]
      end

      def deconstruct_keys(_keys)
        { calldata: calldata }
      end

      def ==(other)
        other.is_a?(OptEmptyP) && other.calldata == calldata
      end

      def length
        2
      end

      def pops
        1
      end

      def pushes
        1
      end

      def canonical
        Send.new(calldata, nil)
      end

      def call(vm)
        canonical.call(vm)
      end
    end

    # ### Summary
    #
    # `opt_eq` is a specialization of the `opt_send_without_block` instruction
    # that occurs when the == operator is used. Fast paths exist when both
    # operands are integers, floats, symbols or strings. It pops both the
    # receiver and the argument off the stack and pushes on the result.
    #
    # ### Usage
    #
    # ~~~ruby
    # 2 == 2
    # ~~~
    #
    class OptEq < Instruction
      attr_reader :calldata

      def initialize(calldata)
        @calldata = calldata
      end

      def disasm(fmt)
        fmt.instruction("opt_eq", [fmt.calldata(calldata)])
      end

      def to_a(_iseq)
        [:opt_eq, calldata.to_h]
      end

      def deconstruct_keys(_keys)
        { calldata: calldata }
      end

      def ==(other)
        other.is_a?(OptEq) && other.calldata == calldata
      end

      def length
        2
      end

      def pops
        2
      end

      def pushes
        1
      end

      def canonical
        Send.new(calldata, nil)
      end

      def call(vm)
        canonical.call(vm)
      end
    end

    # ### Summary
    #
    # `opt_ge` is a specialization of the `opt_send_without_block` instruction
    # that occurs when the >= operator is used. Fast paths exist when both
    # operands are integers or floats. It pops both the receiver and the
    # argument off the stack and pushes on the result.
    #
    # ### Usage
    #
    # ~~~ruby
    # 4 >= 3
    # ~~~
    #
    class OptGE < Instruction
      attr_reader :calldata

      def initialize(calldata)
        @calldata = calldata
      end

      def disasm(fmt)
        fmt.instruction("opt_ge", [fmt.calldata(calldata)])
      end

      def to_a(_iseq)
        [:opt_ge, calldata.to_h]
      end

      def deconstruct_keys(_keys)
        { calldata: calldata }
      end

      def ==(other)
        other.is_a?(OptGE) && other.calldata == calldata
      end

      def length
        2
      end

      def pops
        2
      end

      def pushes
        1
      end

      def canonical
        Send.new(calldata, nil)
      end

      def call(vm)
        canonical.call(vm)
      end
    end

    # ### Summary
    #
    # `opt_getconstant_path` performs a constant lookup on a chain of constant
    # names. It accepts as its argument an array of constant names, and pushes
    # the value of the constant onto the stack.
    #
    # ### Usage
    #
    # ~~~ruby
    # ::Object
    # ~~~
    #
    class OptGetConstantPath < Instruction
      attr_reader :names

      def initialize(names)
        @names = names
      end

      def disasm(fmt)
        cache = "<ic:0 #{names.join("::")}>"
        fmt.instruction("opt_getconstant_path", [cache])
      end

      def to_a(_iseq)
        [:opt_getconstant_path, names]
      end

      def deconstruct_keys(_keys)
        { names: names }
      end

      def ==(other)
        other.is_a?(OptGetConstantPath) && other.names == names
      end

      def length
        2
      end

      def pushes
        1
      end

      def call(vm)
        current = vm.frame._self
        current = current.class unless current.is_a?(Class)

        names.each do |name|
          current = name == :"" ? Object : current.const_get(name)
        end

        vm.push(current)
      end
    end

    # ### Summary
    #
    # `opt_gt` is a specialization of the `opt_send_without_block` instruction
    # that occurs when the > operator is used. Fast paths exist when both
    # operands are integers or floats. It pops both the receiver and the
    # argument off the stack and pushes on the result.
    #
    # ### Usage
    #
    # ~~~ruby
    # 4 > 3
    # ~~~
    #
    class OptGT < Instruction
      attr_reader :calldata

      def initialize(calldata)
        @calldata = calldata
      end

      def disasm(fmt)
        fmt.instruction("opt_gt", [fmt.calldata(calldata)])
      end

      def to_a(_iseq)
        [:opt_gt, calldata.to_h]
      end

      def deconstruct_keys(_keys)
        { calldata: calldata }
      end

      def ==(other)
        other.is_a?(OptGT) && other.calldata == calldata
      end

      def length
        2
      end

      def pops
        2
      end

      def pushes
        1
      end

      def canonical
        Send.new(calldata, nil)
      end

      def call(vm)
        canonical.call(vm)
      end
    end

    # ### Summary
    #
    # `opt_le` is a specialization of the `opt_send_without_block` instruction
    # that occurs when the <= operator is used. Fast paths exist when both
    # operands are integers or floats. It pops both the receiver and the
    # argument off the stack and pushes on the result.
    #
    # ### Usage
    #
    # ~~~ruby
    # 3 <= 4
    # ~~~
    #
    class OptLE < Instruction
      attr_reader :calldata

      def initialize(calldata)
        @calldata = calldata
      end

      def disasm(fmt)
        fmt.instruction("opt_le", [fmt.calldata(calldata)])
      end

      def to_a(_iseq)
        [:opt_le, calldata.to_h]
      end

      def deconstruct_keys(_keys)
        { calldata: calldata }
      end

      def ==(other)
        other.is_a?(OptLE) && other.calldata == calldata
      end

      def length
        2
      end

      def pops
        2
      end

      def pushes
        1
      end

      def canonical
        Send.new(calldata, nil)
      end

      def call(vm)
        canonical.call(vm)
      end
    end

    # ### Summary
    #
    # `opt_length` is a specialization of `opt_send_without_block`, when the
    # `length` method is called. There are fast paths when the receiver is
    # either a string, hash, or array. It pops the receiver off the stack and
    # pushes on the result of the method call.
    #
    # ### Usage
    #
    # ~~~ruby
    # "".length
    # ~~~
    #
    class OptLength < Instruction
      attr_reader :calldata

      def initialize(calldata)
        @calldata = calldata
      end

      def disasm(fmt)
        fmt.instruction("opt_length", [fmt.calldata(calldata)])
      end

      def to_a(_iseq)
        [:opt_length, calldata.to_h]
      end

      def deconstruct_keys(_keys)
        { calldata: calldata }
      end

      def ==(other)
        other.is_a?(OptLength) && other.calldata == calldata
      end

      def length
        2
      end

      def pops
        1
      end

      def pushes
        1
      end

      def canonical
        Send.new(calldata, nil)
      end

      def call(vm)
        canonical.call(vm)
      end
    end

    # ### Summary
    #
    # `opt_lt` is a specialization of the `opt_send_without_block` instruction
    # that occurs when the < operator is used. Fast paths exist when both
    # operands are integers or floats. It pops both the receiver and the
    # argument off the stack and pushes on the result.
    #
    # ### Usage
    #
    # ~~~ruby
    # 3 < 4
    # ~~~
    #
    class OptLT < Instruction
      attr_reader :calldata

      def initialize(calldata)
        @calldata = calldata
      end

      def disasm(fmt)
        fmt.instruction("opt_lt", [fmt.calldata(calldata)])
      end

      def to_a(_iseq)
        [:opt_lt, calldata.to_h]
      end

      def deconstruct_keys(_keys)
        { calldata: calldata }
      end

      def ==(other)
        other.is_a?(OptLT) && other.calldata == calldata
      end

      def length
        2
      end

      def pops
        2
      end

      def pushes
        1
      end

      def canonical
        Send.new(calldata, nil)
      end

      def call(vm)
        canonical.call(vm)
      end
    end

    # ### Summary
    #
    # `opt_ltlt` is a specialization of the `opt_send_without_block` instruction
    # that occurs when the `<<` operator is used. Fast paths exists when the
    # receiver is either a String or an Array. It pops both the receiver and the
    # argument off the stack and pushes on the result.
    #
    # ### Usage
    #
    # ~~~ruby
    # "" << 2
    # ~~~
    #
    class OptLTLT < Instruction
      attr_reader :calldata

      def initialize(calldata)
        @calldata = calldata
      end

      def disasm(fmt)
        fmt.instruction("opt_ltlt", [fmt.calldata(calldata)])
      end

      def to_a(_iseq)
        [:opt_ltlt, calldata.to_h]
      end

      def deconstruct_keys(_keys)
        { calldata: calldata }
      end

      def ==(other)
        other.is_a?(OptLTLT) && other.calldata == calldata
      end

      def length
        2
      end

      def pops
        2
      end

      def pushes
        1
      end

      def canonical
        Send.new(calldata, nil)
      end

      def call(vm)
        canonical.call(vm)
      end
    end

    # ### Summary
    #
    # `opt_minus` is a specialization of the `opt_send_without_block`
    # instruction that occurs when the `-` operator is used. There are fast
    # paths for if both operands are integers or if both operands are floats. It
    # pops both the receiver and the argument off the stack and pushes on the
    # result.
    #
    # ### Usage
    #
    # ~~~ruby
    # 3 - 2
    # ~~~
    #
    class OptMinus < Instruction
      attr_reader :calldata

      def initialize(calldata)
        @calldata = calldata
      end

      def disasm(fmt)
        fmt.instruction("opt_minus", [fmt.calldata(calldata)])
      end

      def to_a(_iseq)
        [:opt_minus, calldata.to_h]
      end

      def deconstruct_keys(_keys)
        { calldata: calldata }
      end

      def ==(other)
        other.is_a?(OptMinus) && other.calldata == calldata
      end

      def length
        2
      end

      def pops
        2
      end

      def pushes
        1
      end

      def canonical
        Send.new(calldata, nil)
      end

      def call(vm)
        canonical.call(vm)
      end
    end

    # ### Summary
    #
    # `opt_mod` is a specialization of the `opt_send_without_block` instruction
    # that occurs when the `%` operator is used. There are fast paths for if
    # both operands are integers or if both operands are floats. It pops both
    # the receiver and the argument off the stack and pushes on the result.
    #
    # ### Usage
    #
    # ~~~ruby
    # 4 % 2
    # ~~~
    #
    class OptMod < Instruction
      attr_reader :calldata

      def initialize(calldata)
        @calldata = calldata
      end

      def disasm(fmt)
        fmt.instruction("opt_mod", [fmt.calldata(calldata)])
      end

      def to_a(_iseq)
        [:opt_mod, calldata.to_h]
      end

      def deconstruct_keys(_keys)
        { calldata: calldata }
      end

      def ==(other)
        other.is_a?(OptMod) && other.calldata == calldata
      end

      def length
        2
      end

      def pops
        2
      end

      def pushes
        1
      end

      def canonical
        Send.new(calldata, nil)
      end

      def call(vm)
        canonical.call(vm)
      end
    end

    # ### Summary
    #
    # `opt_mult` is a specialization of the `opt_send_without_block` instruction
    # that occurs when the `*` operator is used. There are fast paths for if
    # both operands are integers or floats. It pops both the receiver and the
    # argument off the stack and pushes on the result.
    #
    # ### Usage
    #
    # ~~~ruby
    # 3 * 2
    # ~~~
    #
    class OptMult < Instruction
      attr_reader :calldata

      def initialize(calldata)
        @calldata = calldata
      end

      def disasm(fmt)
        fmt.instruction("opt_mult", [fmt.calldata(calldata)])
      end

      def to_a(_iseq)
        [:opt_mult, calldata.to_h]
      end

      def deconstruct_keys(_keys)
        { calldata: calldata }
      end

      def ==(other)
        other.is_a?(OptMult) && other.calldata == calldata
      end

      def length
        2
      end

      def pops
        2
      end

      def pushes
        1
      end

      def canonical
        Send.new(calldata, nil)
      end

      def call(vm)
        canonical.call(vm)
      end
    end

    # ### Summary
    #
    # `opt_neq` is an optimization that tests whether two values at the top of
    # the stack are not equal by testing their equality and calling the `!` on
    # the result. This allows `opt_neq` to use the fast paths optimized in
    # `opt_eq` when both operands are Integers, Floats, Symbols, or Strings. It
    # pops both the receiver and the argument off the stack and pushes on the
    # result.
    #
    # ### Usage
    #
    # ~~~ruby
    # 2 != 2
    # ~~~
    #
    class OptNEq < Instruction
      attr_reader :eq_calldata, :neq_calldata

      def initialize(eq_calldata, neq_calldata)
        @eq_calldata = eq_calldata
        @neq_calldata = neq_calldata
      end

      def disasm(fmt)
        fmt.instruction(
          "opt_neq",
          [fmt.calldata(eq_calldata), fmt.calldata(neq_calldata)]
        )
      end

      def to_a(_iseq)
        [:opt_neq, eq_calldata.to_h, neq_calldata.to_h]
      end

      def deconstruct_keys(_keys)
        { eq_calldata: eq_calldata, neq_calldata: neq_calldata }
      end

      def ==(other)
        other.is_a?(OptNEq) && other.eq_calldata == eq_calldata &&
          other.neq_calldata == neq_calldata
      end

      def length
        3
      end

      def pops
        2
      end

      def pushes
        1
      end

      def call(vm)
        receiver, argument = vm.pop(2)
        vm.push(receiver != argument)
      end
    end

    # ### Summary
    #
    # `opt_newarray_send` is a specialization that occurs when a dynamic array
    # literal is created and immediately sent the `min`, `max`, or `hash`
    # methods. It pops the values of the array off the stack and pushes on the
    # result of the method call.
    #
    # ### Usage
    #
    # ~~~ruby
    # [a, b, c].max
    # ~~~
    #
    class OptNewArraySend < Instruction
      attr_reader :number, :method

      def initialize(number, method)
        @number = number
        @method = method
      end

      def disasm(fmt)
        fmt.instruction(
          "opt_newarray_send",
          [fmt.object(number), fmt.object(method)]
        )
      end

      def to_a(_iseq)
        [:opt_newarray_send, number, method]
      end

      def deconstruct_keys(_keys)
        { number: number, method: method }
      end

      def ==(other)
        other.is_a?(OptNewArraySend) && other.number == number &&
          other.method == method
      end

      def length
        3
      end

      def pops
        number
      end

      def pushes
        1
      end

      def call(vm)
        vm.push(vm.pop(number).__send__(method))
      end
    end

    # ### Summary
    #
    # `opt_nil_p` is an optimization applied when the method `nil?` is called.
    # It returns true immediately when the receiver is `nil` and defers to the
    # `nil?` method in other cases. It pops the receiver off the stack and
    # pushes on the result.
    #
    # ### Usage
    #
    # ~~~ruby
    # "".nil?
    # ~~~
    #
    class OptNilP < Instruction
      attr_reader :calldata

      def initialize(calldata)
        @calldata = calldata
      end

      def disasm(fmt)
        fmt.instruction("opt_nil_p", [fmt.calldata(calldata)])
      end

      def to_a(_iseq)
        [:opt_nil_p, calldata.to_h]
      end

      def deconstruct_keys(_keys)
        { calldata: calldata }
      end

      def ==(other)
        other.is_a?(OptNilP) && other.calldata == calldata
      end

      def length
        2
      end

      def pops
        1
      end

      def pushes
        1
      end

      def canonical
        Send.new(calldata, nil)
      end

      def call(vm)
        canonical.call(vm)
      end
    end

    # ### Summary
    #
    # `opt_not` negates the value on top of the stack by calling the `!` method
    # on it. It pops the receiver off the stack and pushes on the result.
    #
    # ### Usage
    #
    # ~~~ruby
    # !true
    # ~~~
    #
    class OptNot < Instruction
      attr_reader :calldata

      def initialize(calldata)
        @calldata = calldata
      end

      def disasm(fmt)
        fmt.instruction("opt_not", [fmt.calldata(calldata)])
      end

      def to_a(_iseq)
        [:opt_not, calldata.to_h]
      end

      def deconstruct_keys(_keys)
        { calldata: calldata }
      end

      def ==(other)
        other.is_a?(OptNot) && other.calldata == calldata
      end

      def length
        2
      end

      def pops
        1
      end

      def pushes
        1
      end

      def canonical
        Send.new(calldata, nil)
      end

      def call(vm)
        canonical.call(vm)
      end
    end

    # ### Summary
    #
    # `opt_or` is a specialization of the `opt_send_without_block` instruction
    # that occurs when the `|` operator is used. There is a fast path for if
    # both operands are integers. It pops both the receiver and the argument off
    # the stack and pushes on the result.
    #
    # ### Usage
    #
    # ~~~ruby
    # 2 | 3
    # ~~~
    #
    class OptOr < Instruction
      attr_reader :calldata

      def initialize(calldata)
        @calldata = calldata
      end

      def disasm(fmt)
        fmt.instruction("opt_or", [fmt.calldata(calldata)])
      end

      def to_a(_iseq)
        [:opt_or, calldata.to_h]
      end

      def deconstruct_keys(_keys)
        { calldata: calldata }
      end

      def ==(other)
        other.is_a?(OptOr) && other.calldata == calldata
      end

      def length
        2
      end

      def pops
        2
      end

      def pushes
        1
      end

      def canonical
        Send.new(calldata, nil)
      end

      def call(vm)
        canonical.call(vm)
      end
    end

    # ### Summary
    #
    # `opt_plus` is a specialization of the `opt_send_without_block` instruction
    # that occurs when the `+` operator is used. There are fast paths for if
    # both operands are integers, floats, strings, or arrays. It pops both the
    # receiver and the argument off the stack and pushes on the result.
    #
    # ### Usage
    #
    # ~~~ruby
    # 2 + 3
    # ~~~
    #
    class OptPlus < Instruction
      attr_reader :calldata

      def initialize(calldata)
        @calldata = calldata
      end

      def disasm(fmt)
        fmt.instruction("opt_plus", [fmt.calldata(calldata)])
      end

      def to_a(_iseq)
        [:opt_plus, calldata.to_h]
      end

      def deconstruct_keys(_keys)
        { calldata: calldata }
      end

      def ==(other)
        other.is_a?(OptPlus) && other.calldata == calldata
      end

      def length
        2
      end

      def pops
        2
      end

      def pushes
        1
      end

      def canonical
        Send.new(calldata, nil)
      end

      def call(vm)
        canonical.call(vm)
      end
    end

    # ### Summary
    #
    # `opt_regexpmatch2` is a specialization of the `opt_send_without_block`
    # instruction that occurs when the `=~` operator is used. It pops both the
    # receiver and the argument off the stack and pushes on the result.
    #
    # ### Usage
    #
    # ~~~ruby
    # /a/ =~ "a"
    # ~~~
    #
    class OptRegExpMatch2 < Instruction
      attr_reader :calldata

      def initialize(calldata)
        @calldata = calldata
      end

      def disasm(fmt)
        fmt.instruction("opt_regexpmatch2", [fmt.calldata(calldata)])
      end

      def to_a(_iseq)
        [:opt_regexpmatch2, calldata.to_h]
      end

      def deconstruct_keys(_keys)
        { calldata: calldata }
      end

      def ==(other)
        other.is_a?(OptRegExpMatch2) && other.calldata == calldata
      end

      def length
        2
      end

      def pops
        2
      end

      def pushes
        1
      end

      def canonical
        Send.new(calldata, nil)
      end

      def call(vm)
        canonical.call(vm)
      end
    end

    # ### Summary
    #
    # `opt_send_without_block` is a specialization of the send instruction that
    # occurs when a method is being called without a block. It pops the receiver
    # and the arguments off the stack and pushes on the result.
    #
    # ### Usage
    #
    # ~~~ruby
    # puts "Hello, world!"
    # ~~~
    #
    class OptSendWithoutBlock < Instruction
      attr_reader :calldata

      def initialize(calldata)
        @calldata = calldata
      end

      def disasm(fmt)
        fmt.instruction("opt_send_without_block", [fmt.calldata(calldata)])
      end

      def to_a(_iseq)
        [:opt_send_without_block, calldata.to_h]
      end

      def deconstruct_keys(_keys)
        { calldata: calldata }
      end

      def ==(other)
        other.is_a?(OptSendWithoutBlock) && other.calldata == calldata
      end

      def length
        2
      end

      def pops
        1 + calldata.argc
      end

      def pushes
        1
      end

      def canonical
        Send.new(calldata, nil)
      end

      def call(vm)
        canonical.call(vm)
      end
    end

    # ### Summary
    #
    # `opt_size` is a specialization of `opt_send_without_block`, when the
    # `size` method is called. There are fast paths when the receiver is either
    # a string, hash, or array. It pops the receiver off the stack and pushes on
    # the result.
    #
    # ### Usage
    #
    # ~~~ruby
    # "".size
    # ~~~
    #
    class OptSize < Instruction
      attr_reader :calldata

      def initialize(calldata)
        @calldata = calldata
      end

      def disasm(fmt)
        fmt.instruction("opt_size", [fmt.calldata(calldata)])
      end

      def to_a(_iseq)
        [:opt_size, calldata.to_h]
      end

      def deconstruct_keys(_keys)
        { calldata: calldata }
      end

      def ==(other)
        other.is_a?(OptSize) && other.calldata == calldata
      end

      def length
        2
      end

      def pops
        1
      end

      def pushes
        1
      end

      def canonical
        Send.new(calldata, nil)
      end

      def call(vm)
        canonical.call(vm)
      end
    end

    # ### Summary
    #
    # `opt_str_freeze` pushes a frozen known string value with no interpolation
    # onto the stack using the #freeze method. If the method gets overridden,
    # this will fall back to a send.
    #
    # ### Usage
    #
    # ~~~ruby
    # "hello".freeze
    # ~~~
    #
    class OptStrFreeze < Instruction
      attr_reader :object, :calldata

      def initialize(object, calldata)
        @object = object
        @calldata = calldata
      end

      def disasm(fmt)
        fmt.instruction(
          "opt_str_freeze",
          [fmt.object(object), fmt.calldata(calldata)]
        )
      end

      def to_a(_iseq)
        [:opt_str_freeze, object, calldata.to_h]
      end

      def deconstruct_keys(_keys)
        { object: object, calldata: calldata }
      end

      def ==(other)
        other.is_a?(OptStrFreeze) && other.object == object &&
          other.calldata == calldata
      end

      def length
        3
      end

      def pushes
        1
      end

      def call(vm)
        vm.push(object.freeze)
      end
    end

    # ### Summary
    #
    # `opt_str_uminus` pushes a frozen known string value with no interpolation
    # onto the stack. If the method gets overridden, this will fall back to a
    # send.
    #
    # ### Usage
    #
    # ~~~ruby
    # -"string"
    # ~~~
    #
    class OptStrUMinus < Instruction
      attr_reader :object, :calldata

      def initialize(object, calldata)
        @object = object
        @calldata = calldata
      end

      def disasm(fmt)
        fmt.instruction(
          "opt_str_uminus",
          [fmt.object(object), fmt.calldata(calldata)]
        )
      end

      def to_a(_iseq)
        [:opt_str_uminus, object, calldata.to_h]
      end

      def deconstruct_keys(_keys)
        { object: object, calldata: calldata }
      end

      def ==(other)
        other.is_a?(OptStrUMinus) && other.object == object &&
          other.calldata == calldata
      end

      def length
        3
      end

      def pushes
        1
      end

      def call(vm)
        vm.push(-object)
      end
    end

    # ### Summary
    #
    # `opt_succ` is a specialization of the `opt_send_without_block` instruction
    # when the method being called is `succ`. Fast paths exist when the receiver
    # is either a String or a Fixnum. It pops the receiver off the stack and
    # pushes on the result.
    #
    # ### Usage
    #
    # ~~~ruby
    # "".succ
    # ~~~
    #
    class OptSucc < Instruction
      attr_reader :calldata

      def initialize(calldata)
        @calldata = calldata
      end

      def disasm(fmt)
        fmt.instruction("opt_succ", [fmt.calldata(calldata)])
      end

      def to_a(_iseq)
        [:opt_succ, calldata.to_h]
      end

      def deconstruct_keys(_keys)
        { calldata: calldata }
      end

      def ==(other)
        other.is_a?(OptSucc) && other.calldata == calldata
      end

      def length
        2
      end

      def pops
        1
      end

      def pushes
        1
      end

      def canonical
        Send.new(calldata, nil)
      end

      def call(vm)
        canonical.call(vm)
      end
    end

    # ### Summary
    #
    # `pop` pops the top value off the stack.
    #
    # ### Usage
    #
    # ~~~ruby
    # a ||= 2
    # ~~~
    #
    class Pop < Instruction
      def disasm(fmt)
        fmt.instruction("pop")
      end

      def to_a(_iseq)
        [:pop]
      end

      def deconstruct_keys(_keys)
        {}
      end

      def ==(other)
        other.is_a?(Pop)
      end

      def pops
        1
      end

      def call(vm)
        vm.pop
      end

      def side_effects?
        false
      end
    end

    # ### Summary
    #
    # `putnil` pushes a global nil object onto the stack.
    #
    # ### Usage
    #
    # ~~~ruby
    # nil
    # ~~~
    #
    class PutNil < Instruction
      def disasm(fmt)
        fmt.instruction("putnil")
      end

      def to_a(_iseq)
        [:putnil]
      end

      def deconstruct_keys(_keys)
        {}
      end

      def ==(other)
        other.is_a?(PutNil)
      end

      def pushes
        1
      end

      def canonical
        PutObject.new(nil)
      end

      def call(vm)
        canonical.call(vm)
      end

      def side_effects?
        false
      end
    end

    # ### Summary
    #
    # `putobject` pushes a known value onto the stack.
    #
    # ### Usage
    #
    # ~~~ruby
    # 5
    # ~~~
    #
    class PutObject < Instruction
      attr_reader :object

      def initialize(object)
        @object = object
      end

      def disasm(fmt)
        fmt.instruction("putobject", [fmt.object(object)])
      end

      def to_a(_iseq)
        [:putobject, object]
      end

      def deconstruct_keys(_keys)
        { object: object }
      end

      def ==(other)
        other.is_a?(PutObject) && other.object == object
      end

      def length
        2
      end

      def pushes
        1
      end

      def call(vm)
        vm.push(object)
      end

      def side_effects?
        false
      end
    end

    # ### Summary
    #
    # `putobject_INT2FIX_0_` pushes 0 on the stack. It is a specialized
    # instruction resulting from the operand unification optimization. It is
    # equivalent to `putobject 0`.
    #
    # ### Usage
    #
    # ~~~ruby
    # 0
    # ~~~
    #
    class PutObjectInt2Fix0 < Instruction
      def disasm(fmt)
        fmt.instruction("putobject_INT2FIX_0_")
      end

      def to_a(_iseq)
        [:putobject_INT2FIX_0_]
      end

      def deconstruct_keys(_keys)
        {}
      end

      def ==(other)
        other.is_a?(PutObjectInt2Fix0)
      end

      def pushes
        1
      end

      def canonical
        PutObject.new(0)
      end

      def call(vm)
        canonical.call(vm)
      end

      def side_effects?
        false
      end
    end

    # ### Summary
    #
    # `putobject_INT2FIX_1_` pushes 1 on the stack. It is a specialized
    # instruction resulting from the operand unification optimization. It is
    # equivalent to `putobject 1`.
    #
    # ### Usage
    #
    # ~~~ruby
    # 1
    # ~~~
    #
    class PutObjectInt2Fix1 < Instruction
      def disasm(fmt)
        fmt.instruction("putobject_INT2FIX_1_")
      end

      def to_a(_iseq)
        [:putobject_INT2FIX_1_]
      end

      def deconstruct_keys(_keys)
        {}
      end

      def ==(other)
        other.is_a?(PutObjectInt2Fix1)
      end

      def pushes
        1
      end

      def canonical
        PutObject.new(1)
      end

      def call(vm)
        canonical.call(vm)
      end

      def side_effects?
        false
      end
    end

    # ### Summary
    #
    # `putself` pushes the current value of self onto the stack.
    #
    # ### Usage
    #
    # ~~~ruby
    # puts "Hello, world!"
    # ~~~
    #
    class PutSelf < Instruction
      def disasm(fmt)
        fmt.instruction("putself")
      end

      def to_a(_iseq)
        [:putself]
      end

      def deconstruct_keys(_keys)
        {}
      end

      def ==(other)
        other.is_a?(PutSelf)
      end

      def pushes
        1
      end

      def call(vm)
        vm.push(vm.frame._self)
      end

      def side_effects?
        false
      end
    end

    # ### Summary
    #
    # `putspecialobject` pushes one of three special objects onto the stack.
    # These are either the VM core special object, the class base special
    # object, or the constant base special object.
    #
    # ### Usage
    #
    # ~~~ruby
    # alias foo bar
    # ~~~
    #
    class PutSpecialObject < Instruction
      OBJECT_VMCORE = 1
      OBJECT_CBASE = 2
      OBJECT_CONST_BASE = 3

      attr_reader :object

      def initialize(object)
        @object = object
      end

      def disasm(fmt)
        fmt.instruction("putspecialobject", [fmt.object(object)])
      end

      def to_a(_iseq)
        [:putspecialobject, object]
      end

      def deconstruct_keys(_keys)
        { object: object }
      end

      def ==(other)
        other.is_a?(PutSpecialObject) && other.object == object
      end

      def length
        2
      end

      def pushes
        1
      end

      def call(vm)
        case object
        when OBJECT_VMCORE
          vm.push(vm.frozen_core)
        when OBJECT_CBASE
          value = vm.frame._self
          value = value.singleton_class unless value.is_a?(Class)
          vm.push(value)
        when OBJECT_CONST_BASE
          vm.push(vm.const_base)
        end
      end
    end

    # ### Summary
    #
    # `putstring` pushes an unfrozen string literal onto the stack.
    #
    # ### Usage
    #
    # ~~~ruby
    # "foo"
    # ~~~
    #
    class PutString < Instruction
      attr_reader :object

      def initialize(object)
        @object = object
      end

      def disasm(fmt)
        fmt.instruction("putstring", [fmt.object(object)])
      end

      def to_a(_iseq)
        [:putstring, object]
      end

      def deconstruct_keys(_keys)
        { object: object }
      end

      def ==(other)
        other.is_a?(PutString) && other.object == object
      end

      def length
        2
      end

      def pushes
        1
      end

      def call(vm)
        vm.push(object.dup)
      end
    end

    # ### Summary
    #
    # `send` invokes a method with an optional block. It pops its receiver and
    # the arguments for the method off the stack and pushes the return value
    # onto the stack. It has two arguments: the calldata for the call site and
    # the optional block instruction sequence.
    #
    # ### Usage
    #
    # ~~~ruby
    # "hello".tap { |i| p i }
    # ~~~
    #
    class Send < Instruction
      attr_reader :calldata, :block_iseq

      def initialize(calldata, block_iseq)
        @calldata = calldata
        @block_iseq = block_iseq
      end

      def disasm(fmt)
        fmt.enqueue(block_iseq) if block_iseq
        fmt.instruction(
          "send",
          [fmt.calldata(calldata), block_iseq&.name || "nil"]
        )
      end

      def to_a(_iseq)
        [:send, calldata.to_h, block_iseq&.to_a]
      end

      def deconstruct_keys(_keys)
        { calldata: calldata, block_iseq: block_iseq }
      end

      def ==(other)
        other.is_a?(Send) && other.calldata == calldata &&
          other.block_iseq == block_iseq
      end

      def length
        3
      end

      def pops
        argb = (calldata.flag?(CallData::CALL_ARGS_BLOCKARG) ? 1 : 0)
        argb + calldata.argc + 1
      end

      def pushes
        1
      end

      def call(vm)
        block =
          if (iseq = block_iseq)
            frame = vm.frame
            ->(*args, **kwargs, &blk) do
              vm.run_block_frame(iseq, frame, *args, **kwargs, &blk)
            end
          elsif calldata.flag?(CallData::CALL_ARGS_BLOCKARG)
            vm.pop
          end

        keywords =
          if calldata.kw_arg
            calldata.kw_arg.zip(vm.pop(calldata.kw_arg.length)).to_h
          else
            {}
          end

        arguments = vm.pop(calldata.argc)
        receiver = vm.pop

        vm.push(
          receiver.__send__(calldata.method, *arguments, **keywords, &block)
        )
      end
    end

    # ### Summary
    #
    # `setblockparam` sets the value of a block local variable on a frame
    # determined by the level and index arguments. The level is the number of
    # frames back to look and the index is the index in the local table. It pops
    # the value it is setting off the stack.
    #
    # ### Usage
    #
    # ~~~ruby
    # def foo(&bar)
    #   bar = baz
    # end
    # ~~~
    #
    class SetBlockParam < Instruction
      attr_reader :index, :level

      def initialize(index, level)
        @index = index
        @level = level
      end

      def disasm(fmt)
        fmt.instruction("setblockparam", [fmt.local(index, explicit: level)])
      end

      def to_a(iseq)
        current = iseq
        level.times { current = current.parent_iseq }
        [:setblockparam, current.local_table.offset(index), level]
      end

      def deconstruct_keys(_keys)
        { index: index, level: level }
      end

      def ==(other)
        other.is_a?(SetBlockParam) && other.index == index &&
          other.level == level
      end

      def length
        3
      end

      def pops
        1
      end

      def call(vm)
        vm.local_set(index, level, vm.pop)
      end
    end

    # ### Summary
    #
    # `setclassvariable` looks for a class variable in the current class and
    # sets its value to the value it pops off the top of the stack. It uses an
    # inline cache to reduce the need to lookup the class variable in the class
    # hierarchy every time.
    #
    # ### Usage
    #
    # ~~~ruby
    # @@class_variable = 1
    # ~~~
    #
    class SetClassVariable < Instruction
      attr_reader :name, :cache

      def initialize(name, cache)
        @name = name
        @cache = cache
      end

      def disasm(fmt)
        fmt.instruction(
          "setclassvariable",
          [fmt.object(name), fmt.inline_storage(cache)]
        )
      end

      def to_a(_iseq)
        [:setclassvariable, name, cache]
      end

      def deconstruct_keys(_keys)
        { name: name, cache: cache }
      end

      def ==(other)
        other.is_a?(SetClassVariable) && other.name == name &&
          other.cache == cache
      end

      def length
        3
      end

      def pops
        1
      end

      def call(vm)
        clazz = vm.frame._self
        clazz = clazz.class unless clazz.is_a?(Class)
        clazz.class_variable_set(name, vm.pop)
      end
    end

    # ### Summary
    #
    # `setconstant` pops two values off the stack: the value to set the
    # constant to and the constant base to set it in.
    #
    # ### Usage
    #
    # ~~~ruby
    # Constant = 1
    # ~~~
    #
    class SetConstant < Instruction
      attr_reader :name

      def initialize(name)
        @name = name
      end

      def disasm(fmt)
        fmt.instruction("setconstant", [fmt.object(name)])
      end

      def to_a(_iseq)
        [:setconstant, name]
      end

      def deconstruct_keys(_keys)
        { name: name }
      end

      def ==(other)
        other.is_a?(SetConstant) && other.name == name
      end

      def length
        2
      end

      def pops
        2
      end

      def call(vm)
        value, parent = vm.pop(2)
        parent.const_set(name, value)
      end
    end

    # ### Summary
    #
    # `setglobal` sets the value of a global variable to a value popped off the
    # top of the stack.
    #
    # ### Usage
    #
    # ~~~ruby
    # $global = 5
    # ~~~
    #
    class SetGlobal < Instruction
      attr_reader :name

      def initialize(name)
        @name = name
      end

      def disasm(fmt)
        fmt.instruction("setglobal", [fmt.object(name)])
      end

      def to_a(_iseq)
        [:setglobal, name]
      end

      def deconstruct_keys(_keys)
        { name: name }
      end

      def ==(other)
        other.is_a?(SetGlobal) && other.name == name
      end

      def length
        2
      end

      def pops
        1
      end

      def call(vm)
        # Evaluating the name of the global variable because there isn't a
        # reflection API for global variables.
        eval("#{name} = vm.pop", binding, __FILE__, __LINE__)
      end
    end

    # ### Summary
    #
    # `setinstancevariable` pops a value off the top of the stack and then sets
    # the instance variable associated with the instruction to that value.
    #
    # This instruction has two forms, but both have the same structure. Before
    # Ruby 3.2, the inline cache corresponded to both the get and set
    # instructions and could be shared. Since Ruby 3.2, it uses object shapes
    # instead so the caches are unique per instruction.
    #
    # ### Usage
    #
    # ~~~ruby
    # @instance_variable = 1
    # ~~~
    #
    class SetInstanceVariable < Instruction
      attr_reader :name, :cache

      def initialize(name, cache)
        @name = name
        @cache = cache
      end

      def disasm(fmt)
        fmt.instruction(
          "setinstancevariable",
          [fmt.object(name), fmt.inline_storage(cache)]
        )
      end

      def to_a(_iseq)
        [:setinstancevariable, name, cache]
      end

      def deconstruct_keys(_keys)
        { name: name, cache: cache }
      end

      def ==(other)
        other.is_a?(SetInstanceVariable) && other.name == name &&
          other.cache == cache
      end

      def length
        3
      end

      def pops
        1
      end

      def call(vm)
        method = Object.instance_method(:instance_variable_set)
        method.bind(vm.frame._self).call(name, vm.pop)
      end
    end

    # ### Summary
    #
    # `setlocal` sets the value of a local variable on a frame determined by the
    # level and index arguments. The level is the number of frames back to
    # look and the index is the index in the local table. It pops the value it
    # is setting off the stack.
    #
    # ### Usage
    #
    # ~~~ruby
    # value = 5
    # tap { tap { value = 10 } }
    # ~~~
    #
    class SetLocal < Instruction
      attr_reader :index, :level

      def initialize(index, level)
        @index = index
        @level = level
      end

      def disasm(fmt)
        fmt.instruction("setlocal", [fmt.local(index, explicit: level)])
      end

      def to_a(iseq)
        current = iseq
        level.times { current = current.parent_iseq }
        [:setlocal, current.local_table.offset(index), level]
      end

      def deconstruct_keys(_keys)
        { index: index, level: level }
      end

      def ==(other)
        other.is_a?(SetLocal) && other.index == index && other.level == level
      end

      def length
        3
      end

      def pops
        1
      end

      def call(vm)
        vm.local_set(index, level, vm.pop)
      end
    end

    # ### Summary
    #
    # `setlocal_WC_0` is a specialized version of the `setlocal` instruction. It
    # sets the value of a local variable on the current frame to the value at
    # the top of the stack as determined by the index given as its only
    # argument.
    #
    # ### Usage
    #
    # ~~~ruby
    # value = 5
    # ~~~
    #
    class SetLocalWC0 < Instruction
      attr_reader :index

      def initialize(index)
        @index = index
      end

      def disasm(fmt)
        fmt.instruction("setlocal_WC_0", [fmt.local(index, implicit: 0)])
      end

      def to_a(iseq)
        [:setlocal_WC_0, iseq.local_table.offset(index)]
      end

      def deconstruct_keys(_keys)
        { index: index }
      end

      def ==(other)
        other.is_a?(SetLocalWC0) && other.index == index
      end

      def length
        2
      end

      def pops
        1
      end

      def canonical
        SetLocal.new(index, 0)
      end

      def call(vm)
        canonical.call(vm)
      end
    end

    # ### Summary
    #
    # `setlocal_WC_1` is a specialized version of the `setlocal` instruction. It
    # sets the value of a local variable on the parent frame to the value at the
    # top of the stack as determined by the index given as its only argument.
    #
    # ### Usage
    #
    # ~~~ruby
    # value = 5
    # self.then { value = 10 }
    # ~~~
    #
    class SetLocalWC1 < Instruction
      attr_reader :index

      def initialize(index)
        @index = index
      end

      def disasm(fmt)
        fmt.instruction("setlocal_WC_1", [fmt.local(index, implicit: 1)])
      end

      def to_a(iseq)
        [:setlocal_WC_1, iseq.parent_iseq.local_table.offset(index)]
      end

      def deconstruct_keys(_keys)
        { index: index }
      end

      def ==(other)
        other.is_a?(SetLocalWC1) && other.index == index
      end

      def length
        2
      end

      def pops
        1
      end

      def canonical
        SetLocal.new(index, 1)
      end

      def call(vm)
        canonical.call(vm)
      end
    end

    # ### Summary
    #
    # `setn` sets a value in the stack to a value popped off the top of the
    # stack. It then pushes that value onto the top of the stack as well.
    #
    # ### Usage
    #
    # ~~~ruby
    # {}[:key] = 'val'
    # ~~~
    #
    class SetN < Instruction
      attr_reader :number

      def initialize(number)
        @number = number
      end

      def disasm(fmt)
        fmt.instruction("setn", [fmt.object(number)])
      end

      def to_a(_iseq)
        [:setn, number]
      end

      def deconstruct_keys(_keys)
        { number: number }
      end

      def ==(other)
        other.is_a?(SetN) && other.number == number
      end

      def length
        2
      end

      def pops
        1
      end

      def pushes
        1
      end

      def call(vm)
        vm.stack[-number - 1] = vm.stack.last
      end
    end

    # ### Summary
    #
    # `setspecial` pops a value off the top of the stack and sets a special
    # local variable to that value. The special local variable is determined by
    # the key given as its only argument.
    #
    # ### Usage
    #
    # ~~~ruby
    # baz if (foo == 1) .. (bar == 1)
    # ~~~
    #
    class SetSpecial < Instruction
      attr_reader :key

      def initialize(key)
        @key = key
      end

      def disasm(fmt)
        fmt.instruction("setspecial", [fmt.object(key)])
      end

      def to_a(_iseq)
        [:setspecial, key]
      end

      def deconstruct_keys(_keys)
        { key: key }
      end

      def ==(other)
        other.is_a?(SetSpecial) && other.key == key
      end

      def length
        2
      end

      def pops
        1
      end

      def call(vm)
        case key
        when GetSpecial::SVAR_LASTLINE
          raise NotImplementedError, "setspecial SVAR_LASTLINE"
        when GetSpecial::SVAR_BACKREF
          raise NotImplementedError, "setspecial SVAR_BACKREF"
        when GetSpecial::SVAR_FLIPFLOP_START
          vm.frame_svar.svars[GetSpecial::SVAR_FLIPFLOP_START]
        end
      end
    end

    # ### Summary
    #
    # `splatarray` coerces the array object at the top of the stack into Array
    # by calling `to_a`. It pushes a duplicate of the array if there is a flag,
    # and the original array if there isn't one.
    #
    # ### Usage
    #
    # ~~~ruby
    # x = *(5)
    # ~~~
    #
    class SplatArray < Instruction
      attr_reader :flag

      def initialize(flag)
        @flag = flag
      end

      def disasm(fmt)
        fmt.instruction("splatarray", [fmt.object(flag)])
      end

      def to_a(_iseq)
        [:splatarray, flag]
      end

      def deconstruct_keys(_keys)
        { flag: flag }
      end

      def ==(other)
        other.is_a?(SplatArray) && other.flag == flag
      end

      def length
        2
      end

      def pops
        1
      end

      def pushes
        1
      end

      def call(vm)
        value = vm.pop

        vm.push(
          if Array === value
            value.instance_of?(Array) ? value.dup : Array[*value]
          elsif value.nil?
            value.to_a
          else
            if value.respond_to?(:to_a, true)
              result = value.to_a

              if result.nil?
                [value]
              elsif !result.is_a?(Array)
                raise TypeError, "expected to_a to return an Array"
              end
            else
              [value]
            end
          end
        )
      end
    end

    # ### Summary
    #
    # `swap` swaps the top two elements in the stack.
    #
    # ### TracePoint
    #
    # `swap` does not dispatch any events.
    #
    # ### Usage
    #
    # ~~~ruby
    # !!defined?([[]])
    # ~~~
    #
    class Swap < Instruction
      def disasm(fmt)
        fmt.instruction("swap")
      end

      def to_a(_iseq)
        [:swap]
      end

      def deconstruct_keys(_keys)
        {}
      end

      def ==(other)
        other.is_a?(Swap)
      end

      def pops
        2
      end

      def pushes
        2
      end

      def call(vm)
        left, right = vm.pop(2)
        vm.push(right, left)
      end
    end

    # ### Summary
    #
    # `throw` pops a value off the top of the stack and throws it. It is caught
    # using the instruction sequence's (or an ancestor's) catch table. It pushes
    # on the result of throwing the value.
    #
    # ### Usage
    #
    # ~~~ruby
    # [1, 2, 3].map { break 2 }
    # ~~~
    #
    class Throw < Instruction
      RUBY_TAG_NONE = 0x0
      RUBY_TAG_RETURN = 0x1
      RUBY_TAG_BREAK = 0x2
      RUBY_TAG_NEXT = 0x3
      RUBY_TAG_RETRY = 0x4
      RUBY_TAG_REDO = 0x5
      RUBY_TAG_RAISE = 0x6
      RUBY_TAG_THROW = 0x7
      RUBY_TAG_FATAL = 0x8

      VM_THROW_NO_ESCAPE_FLAG = 0x8000
      VM_THROW_STATE_MASK = 0xff

      attr_reader :type

      def initialize(type)
        @type = type
      end

      def disasm(fmt)
        fmt.instruction("throw", [fmt.object(type)])
      end

      def to_a(_iseq)
        [:throw, type]
      end

      def deconstruct_keys(_keys)
        { type: type }
      end

      def ==(other)
        other.is_a?(Throw) && other.type == type
      end

      def length
        2
      end

      def pops
        1
      end

      def pushes
        1
      end

      def call(vm)
        state = type & VM_THROW_STATE_MASK
        value = vm.pop

        case state
        when RUBY_TAG_NONE
          case value
          when nil
            # do nothing
          when Exception
            raise value
          else
            raise NotImplementedError
          end
        when RUBY_TAG_RETURN
          raise VM::ReturnError.new(value, error_backtrace(vm))
        when RUBY_TAG_BREAK
          raise VM::BreakError.new(value, error_backtrace(vm))
        when RUBY_TAG_NEXT
          raise VM::NextError.new(value, error_backtrace(vm))
        else
          raise NotImplementedError, "Unknown throw kind #{state}"
        end
      end

      private

      def error_backtrace(vm)
        backtrace = []
        current = vm.frame

        while current
          backtrace << "#{current.iseq.file}:#{current.line}:in" \
            "`#{current.iseq.name}'"
          current = current.parent
        end

        [*backtrace, *caller]
      end
    end

    # ### Summary
    #
    # `topn` pushes a single value onto the stack that is a copy of the value
    # within the stack that is `number` of slots down from the top.
    #
    # ### Usage
    #
    # ~~~ruby
    # case 3
    # when 1..5
    #   puts "foo"
    # end
    # ~~~
    #
    class TopN < Instruction
      attr_reader :number

      def initialize(number)
        @number = number
      end

      def disasm(fmt)
        fmt.instruction("topn", [fmt.object(number)])
      end

      def to_a(_iseq)
        [:topn, number]
      end

      def deconstruct_keys(_keys)
        { number: number }
      end

      def ==(other)
        other.is_a?(TopN) && other.number == number
      end

      def length
        2
      end

      def pushes
        1
      end

      def call(vm)
        vm.push(vm.stack[-number - 1])
      end
    end

    # ### Summary
    #
    # `toregexp` pops a number of values off the stack, combines them into a new
    # regular expression, and pushes the new regular expression onto the stack.
    #
    # ### Usage
    #
    # ~~~ruby
    # /foo #{bar}/
    # ~~~
    #
    class ToRegExp < Instruction
      attr_reader :options, :length

      def initialize(options, length)
        @options = options
        @length = length
      end

      def disasm(fmt)
        fmt.instruction("toregexp", [fmt.object(options), fmt.object(length)])
      end

      def to_a(_iseq)
        [:toregexp, options, length]
      end

      def deconstruct_keys(_keys)
        { options: options, length: length }
      end

      def ==(other)
        other.is_a?(ToRegExp) && other.options == options &&
          other.length == length
      end

      def pops
        length
      end

      def pushes
        1
      end

      def call(vm)
        vm.push(Regexp.new(vm.pop(length).join, options))
      end
    end
  end
end