class Yadriggy::RubyTypeInferer

Type checker for Ruby with type inference. The type system is slightly different from Ruby's one. For example, once an integer is assigned to a local variable, assigning a String value to it causes a type error.

Public Instance Methods

binary_type(bin_expr, right_t, left_t) click to toggle source

@api private

# File lib/yadriggy/ruby_typeinfer.rb, line 230
def binary_type(bin_expr, right_t, left_t)
  op = bin_expr.op
  case op
  when :'&&', :'||', :and, :or      # not overridable
    return UnionType.new([right_t, left_t])
  when :>, :>=, :<, :<=, :==, :===, :!=
    if left_t <= RubyClass::Numeric
      return RubyClass::Boolean
    end
  when :**, :*, :/, :%, :+, :-
    if left_t <= RubyClass::Numeric
      if left_t <= RubyClass::Float || right_t <= RubyClass::Float
        return RubyClass::Float
      else
        return RubyClass::Integer
      end
    end
  when :<<, :>>, :&, :|, :^
    return RubyClass::Integer if left_t <= RubyClass::Integer
  # when :=~, :!~, :<=>
  end

  if left_t <= RubyClass::String
    if op == :% || op == :+ || op == :<<
      return RubyClass::String
    elsif op == :=~ || op == :<=>
      return UnionType.new(RubyClass::Integer, RubyClass::NilClass)
    elsif op == :!~
      return RubyClass::Boolean
    end
  end

  call_expr = Call.make(receiver: bin_expr.left, name: op,
                        args: [bin_expr.right], parent: bin_expr.parent)
  return get_call_expr_type(call_expr, type_env, op)
end
bind_local_var(env, ast, var_type, is_def=true) click to toggle source

Binds a local variable name to a type. @param [TypeEnv] env a type environment. @param [ASTnode] ast a local variable name. @param [Type] var_type a type. @param [Boolean] is_def true if the variable is initialized there.

# File lib/yadriggy/ruby_typeinfer.rb, line 20
def bind_local_var(env, ast, var_type, is_def=true)
  unless var_type.nil?
    t = if UnionType.role(var_type)
          ts = UnionType.role(var_type).types
          UnionType.new(ts.map {|t| to_non_instance_type(t) })
        else
          ins_t = InstanceType.role(var_type)
          to_non_instance_type(var_type)
        end
    lvt = LocalVarType.new(t.copy(OptionalRole), is_def ? ast : nil)
    env.bind_name(ast, lvt)
    @typetable[ast] = lvt
  end
end
get_instance_variable_type(key, ivar, is_valid_type, value_type) click to toggle source

Obtains the type of the given instance variable `ivar` declared in the given class (i.e. module) or the instance object `key`. If the type of `ivar` is not defined, `value_type` is recorded as its type.

@param [Module|Object] key the key when looking into the typedef table. @param [InstanceVariable] ivar an instance variable. @param [Boolean] is_valid_type true if `value_type` is valid. @param [Type] value_type the type suggested for `ivar`. @return [Type] the type of `ivar`.

# File lib/yadriggy/ruby_typeinfer.rb, line 193
def get_instance_variable_type(key, ivar, is_valid_type, value_type)
  td = add_typedef(key)
  ivar_t = td[ivar]
  if ivar_t.nil?
    td[ivar] = value_type
  else
    type_assert_subsume(ivar_t, value_type,
              "bad type value for #{ivar.name}") if is_valid_type
    ivar_t
  end
end
get_return_type(an_ast, mthd, new_tenv, arg_types) click to toggle source

@api private Overrides {RubyTypeChecker#get_return_type}.

# File lib/yadriggy/ruby_typeinfer.rb, line 304
def get_return_type(an_ast, mthd, new_tenv, arg_types)
  m_ast = an_ast.root.reify(mthd)
  type_assert_false(m_ast.nil?, "no source code: for #{mthd}")
  (@syntax.check(m_ast.tree) || @syntax.raise_error) if @syntax

  m_ast.tree.params.each_with_index do |p, i|
    bind_local_var(new_tenv, p, arg_types[i])
  end

  nparams = m_ast.tree.params.length
  m_ast.tree.optionals.each_with_index do |p, i|
    bind_local_var(new_tenv, p, arg_types[nparams + i])
  end

  mtype = MethodType.role(type(m_ast.tree, new_tenv))
  type_assert(mtype, 'not a method type')
  type_assert_params(mtype.params, arg_types, 'argument type mismatch')
  mtype.result
end
is_attr_accessor?(tenv, name) click to toggle source

@api private

# File lib/yadriggy/ruby_typeinfer.rb, line 134
def is_attr_accessor?(tenv, name)
  self_t = type_env.context
  !self_t.nil? &&
    (self_t.method_defined?(name) ||
     self_t.private_method_defined?(name))
end
to_non_instance_type(t) click to toggle source

@api private When the initial value of a variable is an InstanceType, the type of the variable has to be a RubyCass type corresponding to that instance type. The variable type can be set to that InstanceType only when it is guaranteed that the value of the variable is never changed later.

# File lib/yadriggy/ruby_typeinfer.rb, line 42
def to_non_instance_type(t)
  ins_t = InstanceType.role(t)
  if ins_t.nil?
    t
  else
    ins_t.supertype
  end
end
type_assign(left_expr, ast_op, right_expr, rtype, ast_parent) click to toggle source

@api private Overriding and overloading.

# File lib/yadriggy/ruby_typeinfer.rb, line 93
def type_assign(left_expr, ast_op, right_expr, rtype, ast_parent)
  if ast_op != :'='    # if op is += etc.
    ltype = type(left_expr)
    LocalVarType.role(ltype)&.definition = left_expr
    ltype
  elsif left_expr.is_a?(IdentifierOrCall)
    vtype = type_env.bound_name?(left_expr)
    if vtype.nil?  # if a new name is found,
      method_name = left_expr.name  + '='
      if is_attr_accessor?(type_env, method_name.to_sym)  # self.name=()?
        call_expr = Call.make(name: method_name, args: [ right_expr ],
                              parent: ast_parent)
        get_call_expr_type_with_argtypes(call_expr, type_env, method_name,
                                         [ rtype ])
      else
        bind_local_var(type_env, left_expr, rtype)
      end
    else
      type_assert(rtype <= vtype, 'incompatible assignment type')
      LocalVarType.role(vtype)&.definition = left_expr
      vtype
    end
  elsif left_expr.is_a?(Call) && left_expr.op == :'.'      # obj.name=()?
    method_name = left_expr.name.name + '='
    call_expr = Call.make(receiver: left_expr.receiver,
                          name: method_name, args: [ right_expr ],
                          parent: ast_parent)
    get_call_expr_type_with_argtypes(call_expr, type_env, method_name,
                                     [ rtype ])
  elsif left_expr.is_a?(InstanceVariable)     # @var = ..., @@cvar = ..., @var += ...
    get_instance_variable_type(type_env.context, left_expr, true,
                               InstanceType.role(rtype)&.supertype || rtype)
  elsif left_expr.is_a?(GlobalVariable)
    get_instance_variable_type(:global_variables, left_expr, true,
                               InstanceType.role(rtype)&.supertype || rtype)
  else
    type(left_expr)           # a[i] = ..., <expr> = ... or <expr> += ...
  end
end
type_multi_assign(ast_left, ast_op, ast_right, ast_parent) click to toggle source

@api private Overriding and overloading

# File lib/yadriggy/ruby_typeinfer.rb, line 70
def type_multi_assign(ast_left, ast_op, ast_right, ast_parent)
  if ast_right.is_a?(Array)
    rtypes = ast_right.map {|e| type(e) }
    ast_left.each_with_index do |v, i|
      if i < rtypes.size
        type_assign(v, ast_op, ast_right[i], rtypes[i], ast_parent)
      else
        # passing "ast_right" is wrong but it will not be used..
        type_assign(v, ast_op, ast_right, RubyClass::NilClass, ast_parent)
      end
    end
  else
    type(ast_right)
    ast_left.each_with_index do |v, i|
        # passing "ast_right" is wrong but it will not be used..
      type_assign(v, ast_op, ast_right, DynType, ast_parent)
    end
  end
  DynType
end