class Oso::Polar::Host

Translate between Polar and the host language (Ruby).

Constants

OPS

Attributes

accept_expression[RW]

@return [Boolean]

classes[R]

@return [Hash<String, Class>]

ffi_polar[R]

@return [FFI::Polar]

instances[R]

@return [Hash<Integer, Object>]

Public Class Methods

new(ffi_polar) click to toggle source
# File lib/oso/polar/host.rb, line 54
def initialize(ffi_polar)
  @ffi_polar = ffi_polar
  @classes = {}
  @instances = {}
  @accept_expression = false
end

Public Instance Methods

cache_class(cls, name:) click to toggle source

Store a Ruby class in the {#classes} cache.

@param cls [Class] the class to cache. @param name [String] the name to cache the class as. @return [String] the name the class is cached as. @raise [DuplicateClassAliasError] if attempting to register a class under a previously-registered name.

# File lib/oso/polar/host.rb, line 85
def cache_class(cls, name:)
  raise DuplicateClassAliasError.new name: name, old: get_class(name), new: cls if classes.key? name

  classes[name] = PolarClass.new(cls)
  name
end
cache_instance(instance, id: nil) click to toggle source

Cache a Ruby instance in the {#instances} cache, fetching a new id if one isn't provided.

@param instance [Object] @param id [Integer] the instance ID. Generated via FFI if not provided. @return [Integer] the instance ID.

# File lib/oso/polar/host.rb, line 125
def cache_instance(instance, id: nil)
  id = ffi_polar.new_id if id.nil?
  # Save the instance as a PolarClass if it is a non-anonymous class
  instance = PolarClass.new(instance) if instance.is_a?(Class)
  instances[id] = instance
  id
end
enrich_message(msg) click to toggle source
# File lib/oso/polar/host.rb, line 312
def enrich_message(msg)
  msg.gsub(/\^\{id: ([0-9]+)\}/) do
    get_instance(Regexp.last_match[1].to_i).to_s
  end
end
get_class(name) click to toggle source

Fetch a Ruby class from the {#classes} cache.

@param name [String] @return [Class] @raise [UnregisteredClassError] if the class has not been registered.

# File lib/oso/polar/host.rb, line 72
def get_class(name)
  raise UnregisteredClassError, name unless classes.key? name

  classes[name].get
end
get_instance(id) click to toggle source

Fetch a Ruby instance from the {#instances} cache.

@param id [Integer] @return [Object] @raise [UnregisteredInstanceError] if the ID has not been registered.

# File lib/oso/polar/host.rb, line 110
def get_instance(id)
  raise UnregisteredInstanceError, id unless instance? id

  instance = instances[id]
  return instance.get if instance.is_a? PolarClass

  instance
end
initialize_copy(other) click to toggle source
# File lib/oso/polar/host.rb, line 61
def initialize_copy(other)
  @ffi_polar = other.ffi_polar
  @classes = other.classes.dup
  @instances = other.instances.dup
end
instance?(id) click to toggle source

Check if an instance exists in the {#instances} cache.

@param id [Integer] @return [Boolean]

# File lib/oso/polar/host.rb, line 96
def instance?(id)
  case id
  when Integer
    instances.key? id
  else
    instances.value? id
  end
end
isa?(instance, class_tag:) click to toggle source

Check if instance is an instance of class.

@param instance [Hash<String, Object>] @param class_tag [String] @return [Boolean]

# File lib/oso/polar/host.rb, line 198
def isa?(instance, class_tag:)
  instance = to_ruby(instance)
  cls = get_class(class_tag)
  instance.is_a? cls
end
make_instance(cls_name, args:, kwargs:, id:) click to toggle source

Construct and cache a Ruby instance.

@param cls_name [String] name of the instance's class. @param args [Array<Object>] positional args to the constructor. @param kwargs [Hash<String, Object>] keyword args to the constructor. @param id [Integer] the instance ID. @raise [PolarRuntimeError] if instance construction fails. @return [Integer] the instance ID.

# File lib/oso/polar/host.rb, line 141
def make_instance(cls_name, args:, kwargs:, id:)
  instance = if kwargs.empty? # This check is for Ruby < 2.7.
               get_class(cls_name).__send__(:new, *args)
             else
               get_class(cls_name).__send__(:new, *args, **kwargs)
             end
  cache_instance(instance, id: id)
rescue StandardError => e
  raise PolarRuntimeError, "Error constructing instance of #{cls_name}: #{e}"
end
operator(operation, args) click to toggle source

Compare two values

@param op [String] operation to perform. @param args [Array<Object>] left and right args to operation. @raise [PolarRuntimeError] if operation fails or is unsupported. @return [Boolean]

# File lib/oso/polar/host.rb, line 167
def operator(operation, args)
  left, right = args
  op = OPS[operation]
  raise PolarRuntimeError, "Unsupported external operation '#{left.class} #{operation} #{right.class}'" if op.nil?

  begin
    left.__send__ op, right
  rescue StandardError
    raise PolarRuntimeError, "External operation '#{left.class} #{operation} #{right.class}' failed."
  end
end
subspecializer?(instance_id, left_tag:, right_tag:) click to toggle source

Check if the left class is more specific than the right class with respect to the given instance.

@param instance_id [Integer] @param left_tag [String] @param right_tag [String] @return [Boolean]

# File lib/oso/polar/host.rb, line 186
def subspecializer?(instance_id, left_tag:, right_tag:)
  mro = get_instance(instance_id).class.ancestors
  left_index = mro.index(get_class(left_tag))
  right_index = mro.index(get_class(right_tag))
  left_index && right_index && left_index < right_index
end
to_polar(value) click to toggle source

Turn a Ruby value into a Polar term that's ready to be sent across the FFI boundary.

@param value [Object] @return [Hash<String, Object>]

# File lib/oso/polar/host.rb, line 209
def to_polar(value) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
  value = case true # rubocop:disable Lint/LiteralAsCondition
          when value.instance_of?(TrueClass) || value.instance_of?(FalseClass)
            { 'Boolean' => value }
          when value.instance_of?(Integer)
            { 'Number' => { 'Integer' => value } }
          when value.instance_of?(Float)
            if value == Float::INFINITY
              value = 'Infinity'
            elsif value == -Float::INFINITY
              value = '-Infinity'
            elsif value.nan?
              value = 'NaN'
            end
            { 'Number' => { 'Float' => value } }
          when value.instance_of?(String)
            { 'String' => value }
          when value.instance_of?(Array)
            { 'List' => value.map { |el| to_polar(el) } }
          when value.instance_of?(Hash)
            { 'Dictionary' => { 'fields' => value.transform_values { |v| to_polar(v) } } }
          when value.instance_of?(Predicate)
            { 'Call' => { 'name' => value.name, 'args' => value.args.map { |el| to_polar(el) } } }
          when value.instance_of?(Variable)
            # This is supported so that we can query for unbound variables
            { 'Variable' => value.name }
          when value.instance_of?(Expression)
            { 'Expression' => { 'operator' => value.operator, 'args' => value.args.map { |el| to_polar(el) } } }
          when value.instance_of?(Pattern)
            dict = to_polar(value.fields)['value']
            if value.tag.nil?
              { 'Pattern' => dict }
            else
              { 'Pattern' => { 'Instance' => { 'tag' => value.tag, 'fields' => dict['Dictionary'] } } }
            end
          else
            { 'ExternalInstance' => { 'instance_id' => cache_instance(value), 'repr' => nil } }
          end
  { 'value' => value }
end
to_ruby(data) click to toggle source

Turn a Polar term passed across the FFI boundary into a Ruby value.

@param data [Hash<String, Object>] @option data [Integer] :id @option data [Integer] :offset Character offset of the term in its source string. @option data [Hash<String, Object>] :value @return [Object] @raise [UnexpectedPolarTypeError] if type cannot be converted to Ruby.

# File lib/oso/polar/host.rb, line 258
def to_ruby(data) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
  tag, value = data['value'].first
  case tag
  when 'String', 'Boolean'
    value
  when 'Number'
    num = value.values.first
    if value.key? 'Float'
      case num
      when 'Infinity'
        return Float::INFINITY
      when '-Infinity'
        return -Float::INFINITY
      when 'NaN'
        return Float::NAN
      else
        unless value['Float'].is_a? Float # rubocop:disable Metrics/BlockNesting
          raise PolarRuntimeError, "Expected a floating point number, got \"#{value['Float']}\""
        end
      end
    end
    num
  when 'List'
    value.map { |el| to_ruby(el) }
  when 'Dictionary'
    value['fields'].transform_values { |v| to_ruby(v) }
  when 'ExternalInstance'
    get_instance(value['instance_id'])
  when 'Call'
    Predicate.new(value['name'], args: value['args'].map { |a| to_ruby(a) })
  when 'Variable'
    Variable.new(value)
  when 'Expression'
    raise UnexpectedPolarTypeError, tag unless accept_expression

    args = value['args'].map { |a| to_ruby(a) }
    Expression.new(value['operator'], args)
  when 'Pattern'
    case value.keys.first
    when 'Instance'
      tag = value.values.first['tag']
      fields = value.values.first['fields']['fields'].transform_values { |v| to_ruby(v) }
      Pattern.new(tag, fields)
    when 'Dictionary'
      fields = value.values.first['fields'].transform_values { |v| to_ruby(v) }
      Pattern.new(nil, fields)
    else
      raise UnexpectedPolarTypeError, "#{value.keys.first} variant of Pattern"
    end
  else
    raise UnexpectedPolarTypeError, tag
  end
end