class RubyVM::RJIT::Context

Public Class Methods

new( stack_size: 0, sp_offset: 0, chain_depth: 0, local_types: [Type::Unknown] * MAX_LOCAL_TYPES, temp_types: [Type::Unknown] * MAX_TEMP_TYPES, self_type: Type::Unknown, temp_mapping: [MapToStack] * MAX_TEMP_TYPES ) click to toggle source
Calls superclass method
# File ruby_vm/rjit/context.rb, line 26
  def initialize(
    stack_size:   0,
    sp_offset:    0,
    chain_depth:  0,
    local_types:  [Type::Unknown] * MAX_LOCAL_TYPES,
    temp_types:   [Type::Unknown] * MAX_TEMP_TYPES,
    self_type:    Type::Unknown,
    temp_mapping: [MapToStack] * MAX_TEMP_TYPES
  ) = super

  # Deep dup by default for safety
  def dup
    ctx = super
    ctx.local_types = ctx.local_types.dup
    ctx.temp_types = ctx.temp_types.dup
    ctx.temp_mapping = ctx.temp_mapping.dup
    ctx
  end

  # Create a new Context instance with a given stack_size and sp_offset adjusted
  # accordingly. This is useful when you want to virtually rewind a stack_size for
  # generating a side exit while considering past sp_offset changes on gen_save_sp.
  def with_stack_size(stack_size)
    ctx = self.dup
    ctx.sp_offset -= ctx.stack_size - stack_size
    ctx.stack_size = stack_size
    ctx
  end

  def stack_opnd(depth_from_top)
    [SP, C.VALUE.size * (self.sp_offset - 1 - depth_from_top)]
  end

  def sp_opnd(offset_bytes = 0)
    [SP, (C.VALUE.size * self.sp_offset) + offset_bytes]
  end

  # Push one new value on the temp stack with an explicit mapping
  # Return a pointer to the new stack top
  def stack_push_mapping(mapping_temp_type)
    stack_size = self.stack_size

    # Keep track of the type and mapping of the value
    if stack_size < MAX_TEMP_TYPES
      mapping, temp_type = mapping_temp_type
      self.temp_mapping[stack_size] = mapping
      self.temp_types[stack_size] = temp_type

      case mapping
      in MapToLocal[idx]
        assert(idx < MAX_LOCAL_TYPES)
      else
      end
    end

    self.stack_size += 1
    self.sp_offset += 1

    return self.stack_opnd(0)
  end

  # Push one new value on the temp stack
  # Return a pointer to the new stack top
  def stack_push(val_type)
    return self.stack_push_mapping([MapToStack, val_type])
  end

  # Push the self value on the stack
  def stack_push_self
    return self.stack_push_mapping([MapToStack, Type::Unknown])
  end

  # Push a local variable on the stack
  def stack_push_local(local_idx)
    if local_idx >= MAX_LOCAL_TYPES
      return self.stack_push(Type::Unknown)
    end

    return self.stack_push_mapping([MapToLocal[local_idx], Type::Unknown])
  end

  # Pop N values off the stack
  # Return a pointer to the stack top before the pop operation
  def stack_pop(n = 1)
    assert(n <= self.stack_size)

    top = self.stack_opnd(0)

    # Clear the types of the popped values
    n.times do |i|
      idx = self.stack_size - i - 1

      if idx < MAX_TEMP_TYPES
        self.temp_types[idx] = Type::Unknown
        self.temp_mapping[idx] = MapToStack
      end
    end

    self.stack_size -= n
    self.sp_offset -= n

    return top
  end

  def shift_stack(argc)
    assert(argc < self.stack_size)

    method_name_index = self.stack_size - argc - 1

    (method_name_index...(self.stack_size - 1)).each do |i|
      if i + 1 < MAX_TEMP_TYPES
        self.temp_types[i] = self.temp_types[i + 1]
        self.temp_mapping[i] = self.temp_mapping[i + 1]
      end
    end
    self.stack_pop(1)
  end

  # Get the type of an instruction operand
  def get_opnd_type(opnd)
    case opnd
    in SelfOpnd
      self.self_type
    in StackOpnd[idx]
      assert(idx < self.stack_size)
      stack_idx = self.stack_size - 1 - idx

      # If outside of tracked range, do nothing
      if stack_idx >= MAX_TEMP_TYPES
        return Type::Unknown
      end

      mapping = self.temp_mapping[stack_idx]

      case mapping
      in MapToSelf
        self.self_type
      in MapToStack
        self.temp_types[self.stack_size - 1 - idx]
      in MapToLocal[idx]
        assert(idx < MAX_LOCAL_TYPES)
        self.local_types[idx]
      end
    end
  end

  # Get the currently tracked type for a local variable
  def get_local_type(idx)
    self.local_types[idx] || Type::Unknown
  end

  # Upgrade (or "learn") the type of an instruction operand
  # This value must be compatible and at least as specific as the previously known type.
  # If this value originated from self, or an lvar, the learned type will be
  # propagated back to its source.
  def upgrade_opnd_type(opnd, opnd_type)
    case opnd
    in SelfOpnd
      self.self_type = self.self_type.upgrade(opnd_type)
    in StackOpnd[idx]
      assert(idx < self.stack_size)
      stack_idx = self.stack_size - 1 - idx

      # If outside of tracked range, do nothing
      if stack_idx >= MAX_TEMP_TYPES
        return
      end

      mapping = self.temp_mapping[stack_idx]

      case mapping
      in MapToSelf
        self.self_type = self.self_type.upgrade(opnd_type)
      in MapToStack
        self.temp_types[stack_idx] = self.temp_types[stack_idx].upgrade(opnd_type)
      in MapToLocal[idx]
        assert(idx < MAX_LOCAL_TYPES)
        self.local_types[idx] = self.local_types[idx].upgrade(opnd_type)
      end
    end
  end

  # Get both the type and mapping (where the value originates) of an operand.
  # This is can be used with stack_push_mapping or set_opnd_mapping to copy
  # a stack value's type while maintaining the mapping.
  def get_opnd_mapping(opnd)
    opnd_type = self.get_opnd_type(opnd)

    case opnd
    in SelfOpnd
      return [MapToSelf, opnd_type]
    in StackOpnd[idx]
      assert(idx < self.stack_size)
      stack_idx = self.stack_size - 1 - idx

      if stack_idx < MAX_TEMP_TYPES
        return [self.temp_mapping[stack_idx], opnd_type]
      else
        # We can't know the source of this stack operand, so we assume it is
        # a stack-only temporary. type will be UNKNOWN
        assert(opnd_type == Type::Unknown)
        return [MapToStack, opnd_type]
      end
    end
  end

  # Overwrite both the type and mapping of a stack operand.
  def set_opnd_mapping(opnd, mapping_opnd_type)
    case opnd
    in SelfOpnd
      raise 'self always maps to self'
    in StackOpnd[idx]
      assert(idx < self.stack_size)
      stack_idx = self.stack_size - 1 - idx

      # If outside of tracked range, do nothing
      if stack_idx >= MAX_TEMP_TYPES
        return
      end

      mapping, opnd_type = mapping_opnd_type
      self.temp_mapping[stack_idx] = mapping

      # Only used when mapping == MAP_STACK
      self.temp_types[stack_idx] = opnd_type
    end
  end

  # Set the type of a local variable
  def set_local_type(local_idx, local_type)
    if local_idx >= MAX_LOCAL_TYPES
      return
    end

    # If any values on the stack map to this local we must detach them
    MAX_TEMP_TYPES.times do |stack_idx|
      case self.temp_mapping[stack_idx]
      in MapToStack
        # noop
      in MapToSelf
        # noop
      in MapToLocal[local_idx]
        if stack_idx == local_idx
          self.temp_types[stack_idx] = self.local_types[local_idx];
          self.temp_mapping[stack_idx] = MapToStack
        else
          # noop
        end
      end
    end

    self.local_types[local_idx] = local_type
  end

  # Erase local variable type information
  # eg: because of a call we can't track
  def clear_local_types
    # When clearing local types we must detach any stack mappings to those
    # locals. Even if local values may have changed, stack values will not.
    MAX_TEMP_TYPES.times do |stack_idx|
      case self.temp_mapping[stack_idx]
      in MapToStack
        # noop
      in MapToSelf
        # noop
      in MapToLocal[local_idx]
        self.temp_types[stack_idx] = self.local_types[local_idx]
        self.temp_mapping[stack_idx] = MapToStack
      end
    end

    # Clear the local types
    self.local_types = [Type::Unknown] * MAX_LOCAL_TYPES
  end

  # Compute a difference score for two context objects
  def diff(dst)
    # Self is the source context (at the end of the predecessor)
    src = self

    # Can only lookup the first version in the chain
    if dst.chain_depth != 0
      return TypeDiff::Incompatible
    end

    # Blocks with depth > 0 always produce new versions
    # Sidechains cannot overlap
    if src.chain_depth != 0
      return TypeDiff::Incompatible
    end

    if dst.stack_size != src.stack_size
      return TypeDiff::Incompatible
    end

    if dst.sp_offset != src.sp_offset
      return TypeDiff::Incompatible
    end

    # Difference sum
    diff = 0

    # Check the type of self
    diff += case src.self_type.diff(dst.self_type)
    in TypeDiff::Compatible[diff] then diff
    in TypeDiff::Incompatible then return TypeDiff::Incompatible
    end

    # For each local type we track
    src.local_types.size.times do |i|
      t_src = src.local_types[i]
      t_dst = dst.local_types[i]
      diff += case t_src.diff(t_dst)
      in TypeDiff::Compatible[diff] then diff
      in TypeDiff::Incompatible then return TypeDiff::Incompatible
      end
    end

    # For each value on the temp stack
    src.stack_size.times do |i|
      src_mapping, src_type = src.get_opnd_mapping(StackOpnd[i])
      dst_mapping, dst_type = dst.get_opnd_mapping(StackOpnd[i])

      # If the two mappings aren't the same
      if src_mapping != dst_mapping
        if dst_mapping == MapToStack
          # We can safely drop information about the source of the temp
          # stack operand.
          diff += 1
        else
          return TypeDiff::Incompatible
        end
      end

      diff += case src_type.diff(dst_type)
      in TypeDiff::Compatible[diff] then diff
      in TypeDiff::Incompatible then return TypeDiff::Incompatible
      end
    end

    return TypeDiff::Compatible[diff]
  end

  private

  def assert(cond)
    unless cond
      raise "'#{cond.inspect}' was not true"
    end
  end
end

Public Instance Methods

assert(cond) click to toggle source
# File ruby_vm/rjit/context.rb, line 371
def assert(cond)
  unless cond
    raise "'#{cond.inspect}' was not true"
  end
end
clear_local_types() click to toggle source

Erase local variable type information eg: because of a call we can’t track

# File ruby_vm/rjit/context.rb, line 282
def clear_local_types
  # When clearing local types we must detach any stack mappings to those
  # locals. Even if local values may have changed, stack values will not.
  MAX_TEMP_TYPES.times do |stack_idx|
    case self.temp_mapping[stack_idx]
    in MapToStack
      # noop
    in MapToSelf
      # noop
    in MapToLocal[local_idx]
      self.temp_types[stack_idx] = self.local_types[local_idx]
      self.temp_mapping[stack_idx] = MapToStack
    end
  end

  # Clear the local types
  self.local_types = [Type::Unknown] * MAX_LOCAL_TYPES
end
diff(dst) click to toggle source

Compute a difference score for two context objects

# File ruby_vm/rjit/context.rb, line 302
def diff(dst)
  # Self is the source context (at the end of the predecessor)
  src = self

  # Can only lookup the first version in the chain
  if dst.chain_depth != 0
    return TypeDiff::Incompatible
  end

  # Blocks with depth > 0 always produce new versions
  # Sidechains cannot overlap
  if src.chain_depth != 0
    return TypeDiff::Incompatible
  end

  if dst.stack_size != src.stack_size
    return TypeDiff::Incompatible
  end

  if dst.sp_offset != src.sp_offset
    return TypeDiff::Incompatible
  end

  # Difference sum
  diff = 0

  # Check the type of self
  diff += case src.self_type.diff(dst.self_type)
  in TypeDiff::Compatible[diff] then diff
  in TypeDiff::Incompatible then return TypeDiff::Incompatible
  end

  # For each local type we track
  src.local_types.size.times do |i|
    t_src = src.local_types[i]
    t_dst = dst.local_types[i]
    diff += case t_src.diff(t_dst)
    in TypeDiff::Compatible[diff] then diff
    in TypeDiff::Incompatible then return TypeDiff::Incompatible
    end
  end

  # For each value on the temp stack
  src.stack_size.times do |i|
    src_mapping, src_type = src.get_opnd_mapping(StackOpnd[i])
    dst_mapping, dst_type = dst.get_opnd_mapping(StackOpnd[i])

    # If the two mappings aren't the same
    if src_mapping != dst_mapping
      if dst_mapping == MapToStack
        # We can safely drop information about the source of the temp
        # stack operand.
        diff += 1
      else
        return TypeDiff::Incompatible
      end
    end

    diff += case src_type.diff(dst_type)
    in TypeDiff::Compatible[diff] then diff
    in TypeDiff::Incompatible then return TypeDiff::Incompatible
    end
  end

  return TypeDiff::Compatible[diff]
end
dup() click to toggle source

Deep dup by default for safety

Calls superclass method
# File ruby_vm/rjit/context.rb, line 37
def dup
  ctx = super
  ctx.local_types = ctx.local_types.dup
  ctx.temp_types = ctx.temp_types.dup
  ctx.temp_mapping = ctx.temp_mapping.dup
  ctx
end
get_local_type(idx) click to toggle source

Get the currently tracked type for a local variable

# File ruby_vm/rjit/context.rb, line 173
def get_local_type(idx)
  self.local_types[idx] || Type::Unknown
end
get_opnd_mapping(opnd) click to toggle source

Get both the type and mapping (where the value originates) of an operand. This is can be used with stack_push_mapping or set_opnd_mapping to copy a stack value’s type while maintaining the mapping.

# File ruby_vm/rjit/context.rb, line 211
def get_opnd_mapping(opnd)
  opnd_type = self.get_opnd_type(opnd)

  case opnd
  in SelfOpnd
    return [MapToSelf, opnd_type]
  in StackOpnd[idx]
    assert(idx < self.stack_size)
    stack_idx = self.stack_size - 1 - idx

    if stack_idx < MAX_TEMP_TYPES
      return [self.temp_mapping[stack_idx], opnd_type]
    else
      # We can't know the source of this stack operand, so we assume it is
      # a stack-only temporary. type will be UNKNOWN
      assert(opnd_type == Type::Unknown)
      return [MapToStack, opnd_type]
    end
  end
end
get_opnd_type(opnd) click to toggle source

Get the type of an instruction operand

# File ruby_vm/rjit/context.rb, line 145
def get_opnd_type(opnd)
  case opnd
  in SelfOpnd
    self.self_type
  in StackOpnd[idx]
    assert(idx < self.stack_size)
    stack_idx = self.stack_size - 1 - idx

    # If outside of tracked range, do nothing
    if stack_idx >= MAX_TEMP_TYPES
      return Type::Unknown
    end

    mapping = self.temp_mapping[stack_idx]

    case mapping
    in MapToSelf
      self.self_type
    in MapToStack
      self.temp_types[self.stack_size - 1 - idx]
    in MapToLocal[idx]
      assert(idx < MAX_LOCAL_TYPES)
      self.local_types[idx]
    end
  end
end
set_local_type(local_idx, local_type) click to toggle source

Set the type of a local variable

# File ruby_vm/rjit/context.rb, line 255
def set_local_type(local_idx, local_type)
  if local_idx >= MAX_LOCAL_TYPES
    return
  end

  # If any values on the stack map to this local we must detach them
  MAX_TEMP_TYPES.times do |stack_idx|
    case self.temp_mapping[stack_idx]
    in MapToStack
      # noop
    in MapToSelf
      # noop
    in MapToLocal[local_idx]
      if stack_idx == local_idx
        self.temp_types[stack_idx] = self.local_types[local_idx];
        self.temp_mapping[stack_idx] = MapToStack
      else
        # noop
      end
    end
  end

  self.local_types[local_idx] = local_type
end
set_opnd_mapping(opnd, mapping_opnd_type) click to toggle source

Overwrite both the type and mapping of a stack operand.

# File ruby_vm/rjit/context.rb, line 233
def set_opnd_mapping(opnd, mapping_opnd_type)
  case opnd
  in SelfOpnd
    raise 'self always maps to self'
  in StackOpnd[idx]
    assert(idx < self.stack_size)
    stack_idx = self.stack_size - 1 - idx

    # If outside of tracked range, do nothing
    if stack_idx >= MAX_TEMP_TYPES
      return
    end

    mapping, opnd_type = mapping_opnd_type
    self.temp_mapping[stack_idx] = mapping

    # Only used when mapping == MAP_STACK
    self.temp_types[stack_idx] = opnd_type
  end
end
shift_stack(argc) click to toggle source
# File ruby_vm/rjit/context.rb, line 130
def shift_stack(argc)
  assert(argc < self.stack_size)

  method_name_index = self.stack_size - argc - 1

  (method_name_index...(self.stack_size - 1)).each do |i|
    if i + 1 < MAX_TEMP_TYPES
      self.temp_types[i] = self.temp_types[i + 1]
      self.temp_mapping[i] = self.temp_mapping[i + 1]
    end
  end
  self.stack_pop(1)
end
sp_opnd(offset_bytes = 0) click to toggle source
# File ruby_vm/rjit/context.rb, line 59
def sp_opnd(offset_bytes = 0)
  [SP, (C.VALUE.size * self.sp_offset) + offset_bytes]
end
stack_opnd(depth_from_top) click to toggle source
# File ruby_vm/rjit/context.rb, line 55
def stack_opnd(depth_from_top)
  [SP, C.VALUE.size * (self.sp_offset - 1 - depth_from_top)]
end
stack_pop(n = 1) click to toggle source

Pop N values off the stack Return a pointer to the stack top before the pop operation

# File ruby_vm/rjit/context.rb, line 109
def stack_pop(n = 1)
  assert(n <= self.stack_size)

  top = self.stack_opnd(0)

  # Clear the types of the popped values
  n.times do |i|
    idx = self.stack_size - i - 1

    if idx < MAX_TEMP_TYPES
      self.temp_types[idx] = Type::Unknown
      self.temp_mapping[idx] = MapToStack
    end
  end

  self.stack_size -= n
  self.sp_offset -= n

  return top
end
stack_push(val_type) click to toggle source

Push one new value on the temp stack Return a pointer to the new stack top

# File ruby_vm/rjit/context.rb, line 89
def stack_push(val_type)
  return self.stack_push_mapping([MapToStack, val_type])
end
stack_push_local(local_idx) click to toggle source

Push a local variable on the stack

# File ruby_vm/rjit/context.rb, line 99
def stack_push_local(local_idx)
  if local_idx >= MAX_LOCAL_TYPES
    return self.stack_push(Type::Unknown)
  end

  return self.stack_push_mapping([MapToLocal[local_idx], Type::Unknown])
end
stack_push_mapping(mapping_temp_type) click to toggle source

Push one new value on the temp stack with an explicit mapping Return a pointer to the new stack top

# File ruby_vm/rjit/context.rb, line 65
def stack_push_mapping(mapping_temp_type)
  stack_size = self.stack_size

  # Keep track of the type and mapping of the value
  if stack_size < MAX_TEMP_TYPES
    mapping, temp_type = mapping_temp_type
    self.temp_mapping[stack_size] = mapping
    self.temp_types[stack_size] = temp_type

    case mapping
    in MapToLocal[idx]
      assert(idx < MAX_LOCAL_TYPES)
    else
    end
  end

  self.stack_size += 1
  self.sp_offset += 1

  return self.stack_opnd(0)
end
stack_push_self() click to toggle source

Push the self value on the stack

# File ruby_vm/rjit/context.rb, line 94
def stack_push_self
  return self.stack_push_mapping([MapToStack, Type::Unknown])
end
upgrade_opnd_type(opnd, opnd_type) click to toggle source

Upgrade (or “learn”) the type of an instruction operand This value must be compatible and at least as specific as the previously known type. If this value originated from self, or an lvar, the learned type will be propagated back to its source.

# File ruby_vm/rjit/context.rb, line 181
def upgrade_opnd_type(opnd, opnd_type)
  case opnd
  in SelfOpnd
    self.self_type = self.self_type.upgrade(opnd_type)
  in StackOpnd[idx]
    assert(idx < self.stack_size)
    stack_idx = self.stack_size - 1 - idx

    # If outside of tracked range, do nothing
    if stack_idx >= MAX_TEMP_TYPES
      return
    end

    mapping = self.temp_mapping[stack_idx]

    case mapping
    in MapToSelf
      self.self_type = self.self_type.upgrade(opnd_type)
    in MapToStack
      self.temp_types[stack_idx] = self.temp_types[stack_idx].upgrade(opnd_type)
    in MapToLocal[idx]
      assert(idx < MAX_LOCAL_TYPES)
      self.local_types[idx] = self.local_types[idx].upgrade(opnd_type)
    end
  end
end
with_stack_size(stack_size) click to toggle source

Create a new Context instance with a given stack_size and sp_offset adjusted accordingly. This is useful when you want to virtually rewind a stack_size for generating a side exit while considering past sp_offset changes on gen_save_sp.

# File ruby_vm/rjit/context.rb, line 48
def with_stack_size(stack_size)
  ctx = self.dup
  ctx.sp_offset -= ctx.stack_size - stack_size
  ctx.stack_size = stack_size
  ctx
end