class Yadriggy::RubyTypeChecker

Type checker for Ruby. Most values are typed as DynType but local variables are identified. So `type(ast)` returns a {LocalVarType} object if `ast` is of a local variable. A {LocalVarType} is a {Type} object that represents not only the value's type but also the fact that the value comes from a local variable.

The type of a free variable is the type of the current value of that variable. {#type} returns an {InstanceType} object.

This checker also attempts to recursivly trace a call-graph to reify and type-check the ASTs of the called methods.

Public Class Methods

new(syntax=nil) click to toggle source
Calls superclass method
# File lib/yadriggy/ruby_typecheck.rb, line 21
def initialize(syntax=nil)
  super()
  @syntax = syntax
end

Public Instance Methods

bind_local_var(env, ast, var_type) click to toggle source

Helper methods for rules They can be overridden.

# File lib/yadriggy/ruby_typecheck.rb, line 369
def bind_local_var(env, ast, var_type)
  unless var_type.nil?
    env.bind_name(ast, LocalVarType.new(var_type.copy(OptionalRole), ast))
    @typetable[ast] = var_type
  end
end
get_call_expr_type(call_ast, type_env, method_name) click to toggle source

Computes the type of {Call} expression. If it finds `method_name` in `type_env`, it returns its type recorded in `type_env`.

@param [Call] call_ast an AST. @param [TypeEnv] type_env a type environment. @param [String|Symbol] method_name a method name. @return [ResultType] the type of the resulting value.

# File lib/yadriggy/ruby_typecheck.rb, line 411
def get_call_expr_type(call_ast, type_env, method_name)
  arg_types = call_ast.args.map {|t| type(t) }
  get_call_expr_type_with_argtypes(call_ast, type_env, method_name,
                                   arg_types)
end
get_call_expr_type_with_argtypes(call_ast, type_env, method_name, arg_types) click to toggle source

@api private

# File lib/yadriggy/ruby_typecheck.rb, line 418
def get_call_expr_type_with_argtypes(call_ast, type_env, method_name,
                                     arg_types)
  type(call_ast.block_arg)
  type(call_ast.block)

  if call_ast.receiver.nil?
    found_t = type_env.bound_name?(method_name)
    unless found_t.nil?
      recv_type = DynType
    else
      recv_obj = call_ast.get_receiver_object
      recv_type = if recv_obj.nil?
                    if type_env.context.nil?
                      DynType
                    else
                      RubyClass[type_env.context]    # self's type
                    end
                  else
                    InstanceType.new(recv_obj)
                  end
    end
  else
    found_t = nil
    recv_type = type(call_ast.receiver)
  end

  if !found_t.nil?
    found_t
  elsif DynType == recv_type || DynType == recv_type.exact_type
    DynType
  else
    lookup_builtin(recv_type, method_name) ||
    lookup_ruby_classes(type_env, arg_types, recv_type, method_name)
  end
end
get_name_type(name_ast, tenv) click to toggle source

Gets the type of a given name, which may be a local variable or a free variable.

@param [Name] name_ast an AST. @param [TypeEnv] tenv a type environment.

# File lib/yadriggy/ruby_typecheck.rb, line 90
def get_name_type(name_ast, tenv)
  type = tenv.bound_name?(name_ast)
  if type
    type
  else
    v = name_ast.value
    if v == Undef
      DynType
    else
      InstanceType.new(v)
    end
  end
end
get_return_type(an_ast, mthd, new_tenv, arg_types) click to toggle source

Type-checks whether the argument types match parameter types. It returns a {ResultType}.

Override this method to delimit reification. The implementation in this class reifies any method. If its source code is not found, {#get_return_type} reports an error.

This method {#get_return_type} does not have to return a ResultType, which can be used in a later phase to obtain the invoked method. This method is invoked by rule(Call). See rule(Call) for more details.

@param [Call] an_ast the Call node. @param [Proc|Method|UnboundMethod] mthd the method invoked by an_ast.

if `mthd` is nil, {#get_return_type} reports an error.

@param [TypeEnv] new_tenv a type environment. @param [Array<Type>] arg_types the types of the actual arguments. @return [ResultType] the result type.

# File lib/yadriggy/ruby_typecheck.rb, line 509
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
  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
lookup_builtin(recv_type, method_name) click to toggle source

@api private Attempts to find a method by {TypeChecker#typedef}, which searches the method table in this typechecker.

# File lib/yadriggy/ruby_typecheck.rb, line 458
def lookup_builtin(recv_type, method_name)
  et = recv_type.exact_type
  if DynType == et
    nil
  else
    mt = typedef(et)&.[](method_name)
    if mt.nil?
      nil
    else
      MethodType.role(mt)&.result
    end
  end
end
lookup_ruby_classes(type_env, arg_types, recv_type, method_name) click to toggle source

Computes the type of the {Call} expression by searching the receiver class for the called method.

@param [TypeEnv] type_env a type environment. @param [Array<Type>] arg_types the types of the actual arguments. @param [Type] recv_type the receiver type. @param [String|Symbol] method_name the name of the called method. @raise [CheckError] if the method is not found in the receiver class. @return [ResultType] the result type.

# File lib/yadriggy/ruby_typecheck.rb, line 481
def lookup_ruby_classes(type_env, arg_types, recv_type, method_name)
  begin
    mth = Type.get_instance_method_object(recv_type, method_name)
  rescue CheckError => evar
    error_found!(ast, evar.message)
  end
  new_tenv = type_env.new_base_tenv(recv_type.exact_type)
  get_return_type(ast, mth, new_tenv, arg_types)
end
type_args_and_block(call_ast) click to toggle source
# File lib/yadriggy/ruby_typecheck.rb, line 226
def type_args_and_block(call_ast)
  call_ast.args.each {|t| type(t) }
  type(call_ast.block)
end
type_assert_params(params, args, errmsg='') click to toggle source
# File lib/yadriggy/ruby_typecheck.rb, line 376
def type_assert_params(params, args, errmsg='')
  unless params == DynType
    type_assert(params.is_a?(Array), errmsg)
    type_assert(args.is_a?(Array), errmsg)
    type_assert(params.length <= args.length, errmsg)  # ignore keyword params
    params.each_with_index do |p, i|
      type_assert_subsume(p, args[i], errmsg)
    end
  end
end
type_assert_subsume(expected_type, actual_type, errmsg='') click to toggle source
# File lib/yadriggy/ruby_typecheck.rb, line 387
def type_assert_subsume(expected_type, actual_type, errmsg='')
  type_assert(actual_type <= expected_type, errmsg)
end
type_assign(ast_left, ast_op, rtype) click to toggle source

@api private @param [ASTnode] ast_left the left operand @param [Type] rtype the type of the right operand.

# File lib/yadriggy/ruby_typecheck.rb, line 66
def type_assign(ast_left, ast_op, rtype)
  ltype = type(ast_left)
  if ast_op != :'='   # if op is += etc.
    LocalVarType.role(ltype)&.definition = ast_left
  elsif ast_left.is_a?(IdentifierOrCall)
    vtype = type_env.bound_name?(ast_left)
    if vtype.nil?
      bind_local_var(type_env, ast_left, DynType)
    else
      LocalVarType.role(vtype)&.definition = ast_left
    end
  end
  DynType
end
type_multi_assign(ast_left, ast_op, ast_right) click to toggle source

@api private @param [Array(ASTnode)] ast_left left operand

# File lib/yadriggy/ruby_typecheck.rb, line 49
def type_multi_assign(ast_left, ast_op, ast_right)
  if ast_right.is_a?(Array)
    rtypes = ast_right.map {|e| type(e) }
    ast_left.each_with_index do |v, i|
      type_assign(v, ast_op,
                  i < rtypes.size ? rtypes[i] : RubyClass::NilClass)
    end
  else
    type(ast_right)
    ast_left.each_with_index {|v, i| type_assign(v, ast_op, DynType) }
  end
  DynType
end
type_parameters(an_ast, tenv) click to toggle source
# File lib/yadriggy/ruby_typecheck.rb, line 391
def type_parameters(an_ast, tenv)
  an_ast.params.each {|v| bind_local_var(tenv, v, DynType) }
  an_ast.optionals.each {|v| bind_local_var(tenv, v[0], DynType) }
  bind_local_var(tenv, an_ast.rest_of_params,
                 DynType) unless an_ast.rest_of_params.nil?
  an_ast.keywords.each {|v| bind_local_var(tenv, v, DynType) }
  bind_local_var(tenv, an_ast.rest_of_keywords,
                 DynType) unless an_ast.rest_of_keywords.nil?
  bind_local_var(tenv, an_ast.block_param,
                 DynType) unless an_ast.block_param.nil?
end