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
# File lib/yadriggy/ruby_typecheck.rb, line 21 def initialize(syntax=nil) super() @syntax = syntax end
Public Instance Methods
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
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
@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
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
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
@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
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
# 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
# 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
# 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
@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
@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
# 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