def test_object_structural_subtyping( self, attributes, other_attributes ) -> None: x1, x2 = IndividualVariable(), IndividualVariable() object1 = ObjectType(x1, {**other_attributes, **attributes}) object2 = ObjectType(x2, attributes) self.assertLessEqual(object1, object2)
def _generate_module_type(components: Sequence[str], _full_name: Optional[str] = None, source_dir='.') -> ObjectType: if _full_name is None: _full_name = '.'.join(components) if len(components) > 1: module_t = ObjectType( IndividualVariable(), { components[1]: _generate_module_type(components[1:], _full_name, source_dir)[_seq_var, ], }, nominal_supertypes=[module_type], ) effect = StackEffect([_seq_var], [_seq_var, module_type]) return ObjectType(IndividualVariable(), { '__call__': effect, }, [_seq_var]) else: innermost_type = _generate_type_of_innermost_module( _full_name, source_dir) return ObjectType(IndividualVariable(), { '__call__': innermost_type, }, [_seq_var])
def _generate_type_of_innermost_module(qualified_name: str, source_dir) -> StackEffect: # We resolve imports as if we are the source file. sys.path, old_path = [source_dir, *sys.path], sys.path try: module = importlib.import_module(qualified_name) except ModuleNotFoundError: raise TypeError( 'module {} not found during type checking'.format(qualified_name)) finally: sys.path = old_path module_attributes = {} for name in dir(module): attribute_type = object_type if isinstance(getattr(module, name), int): attribute_type = int_type elif callable(getattr(module, name)): attribute_type = py_function_type module_attributes[name] = attribute_type module_t = ObjectType( IndividualVariable(), module_attributes, nominal_supertypes=[module_type], ) return StackEffect([_seq_var], [_seq_var, module_type])
def test_attribute_word(self, attr_word) -> None: _, type = concat.typecheck.infer( concat.typecheck.Environment(), [attr_word], initial_stack=TypeSequence( [ ObjectType( IndividualVariable(), {attr_word.value: StackEffect([], [int_type]),}, ), ] ), ) self.assertEqual(list(type.output), [int_type])
def test_object_subtype_of_py_function(self, type1, type2) -> None: x = IndividualVariable() py_function = py_function_type[TypeSequence([type1]), type2] object = ObjectType(x, {'__call__': py_function}) self.assertLessEqual(object, py_function)
def test_object_subtype_of_stack_effect(self, effect) -> None: x = IndividualVariable() object = ObjectType(x, {'__call__': effect}) self.assertLessEqual(object, effect)
'pick': ForAll( [_rest_var, _a_var, _b_var, _c_var], StackEffect( [_rest_var, _a_var, _b_var, _c_var], [_rest_var, _a_var, _b_var, _c_var, _a_var], ), ), 'nip': ForAll( [_rest_var, _a_var], StackEffect([_rest_var, object_type, _a_var], [_rest_var, _a_var]), ), 'nip_2': ObjectType( _a_var, { '__call__': StackEffect( [_rest_var, object_type, object_type, _b_var], [_rest_var, _b_var], ) }, [_rest_var, _b_var], ), 'drop': ForAll( [_rest_var], StackEffect([_rest_var, object_type], [_rest_var]) ), 'dup': ForAll( [_rest_var, _a_var], StackEffect([_rest_var, _a_var], [_rest_var, _a_var, _a_var]), ), 'open': ForAll( [_rest_var], StackEffect([_rest_var, dict_type, str_type], [_rest_var, file_type]), ),
def infer( gamma: Environment, e: 'concat.astutils.WordsOrStatements', extensions: Optional[Tuple[Callable]] = None, is_top_level=False, source_dir='.', initial_stack: Optional[TypeSequence] = None, ) -> Tuple[Substitutions, StackEffect]: """The infer function described by Kleffner.""" e = list(e) current_subs = Substitutions() if initial_stack is None: initial_stack = TypeSequence( [] if is_top_level else [SequenceVariable()]) current_effect = StackEffect(initial_stack, initial_stack) for node in e: try: S, (i, o) = current_subs, current_effect if isinstance(node, concat.operators.AddWordNode): # rules: # require object_type because the methods should return # NotImplemented for most types # FIXME: Make the rules safer... somehow # ... a b => (... {__add__(object) -> s} t) # --- # a b + => (... s) # ... a b => (... t {__radd__(object) -> s}) # --- # a b + => (... s) *rest, type1, type2 = current_effect.output try_radd = False try: add_type = type1.get_type_of_attribute('__add__') except AttributeError: try_radd = True else: if not isinstance(add_type, ObjectType): raise TypeError( '__add__ method of type {} is not of an object type, instead has type {}' .format(type1, add_type)) if add_type.head != py_function_type: raise TypeError( '__add__ method of type {} is not a Python function, instead it has type {}' .format(type1, add_type)) if [*add_type.type_arguments[0]] != [object_type]: raise TypeError( '__add__ method of type {} does not have type (object) -> `t, instead it has type {}' .format(type1, add_type)) current_effect = StackEffect( current_effect.input, [*rest, add_type.output], ) if try_radd: radd_type = type2.get_type_of_attribute('__radd__') if (not isinstance(radd_type, ObjectType) or radd_type.head != py_function_type or [*radd_type.type_arguments[0]] != [object_type]): raise TypeError( '__radd__ method of type {} does not have type (object) -> `t, instead it has type {} (left operand is of type {})' .format(type2, radd_type, type1)) current_effect = StackEffect( current_effect.input, [*rest, radd_type.output], ) elif isinstance(node, concat.parse.PushWordNode): S1, (i1, o1) = S, (i, o) # special case for pushing an attribute accessor child = node.children[0] if isinstance(child, concat.parse.AttributeWordNode): top = o1[-1] attr_type = top.get_type_of_attribute(child.value) rest_types = o1[:-1] current_subs, current_effect = ( S1, StackEffect(i1, [*rest_types, attr_type]), ) # special case for name words elif isinstance(child, concat.parse.NameWordNode): if child.value not in gamma: raise NameError(child) name_type = gamma[child.value].instantiate() current_effect = StackEffect( current_effect.input, [*current_effect.output, current_subs(name_type)], ) elif isinstance(child, concat.parse.SliceWordNode): sliceable_object_type = o[-1] # This doesn't match the evaluation order used by the # transpiler. # FIXME: Change the transpiler to fit the type checker. sub1, start_effect = infer( gamma, list(child.start_children), extensions=extensions, source_dir=source_dir, initial_stack=TypeSequence(o[:-1]), ) start_type = start_effect.output[-1] o = tuple(start_effect.output[:-1]) sub2, stop_effect = infer( sub1(gamma), list(child.stop_children), extensions=extensions, source_dir=source_dir, initial_stack=TypeSequence(o), ) stop_type = stop_effect.output[-1] o = tuple(stop_effect.output[:-1]) sub3, step_effect = infer( sub2(sub1(gamma)), list(child.step_children), extensions=extensions, source_dir=source_dir, initial_stack=TypeSequence(o), ) step_type = step_effect.output[-1] o = tuple(step_effect.output[:-1]) this_slice_type = slice_type[start_type, stop_type, step_type] getitem_type = sliceable_object_type.get_type_of_attribute( '__getitem__') getitem_type = getitem_type.get_type_of_attribute( '__call__') getitem_type = getitem_type.instantiate() if (not isinstance(getitem_type, PythonFunctionType) or len(getitem_type.input) != 1): raise TypeError( '__getitem__ method of {} has incorrect type {}'. format(node, getitem_type)) getitem_type, overload_subs = getitem_type.select_overload( (this_slice_type, )) result_type = getitem_type.output current_subs = overload_subs(sub3(sub2( sub1(current_subs)))) current_effect = current_subs( StackEffect(i, [*o, result_type])) # special case for subscription words elif isinstance(child, concat.parse.SubscriptionWordNode): S2, (i2, o2) = infer( current_subs(gamma), child.children, extensions=extensions, is_top_level=False, source_dir=source_dir, initial_stack=current_effect.output, ) # FIXME: Should be generic subscriptable_interface = subscriptable_type[ int_type, IndividualVariable(), ] rest_var = SequenceVariable() expected_o2 = TypeSequence([ rest_var, subscriptable_interface, int_type, ]) o2[-1].constrain(int_type) getitem_type = (o2[-2].get_type_of_attribute( '__getitem__').instantiate().get_type_of_attribute( '__call__').instantiate()) if not isinstance(getitem_type, PythonFunctionType): raise TypeError( '__getitem__ of type {} is not a Python function (has type {})' .format(o2[-2], getitem_type)) getitem_type, overload_subs = getitem_type.select_overload( [int_type]) current_subs = overload_subs(S2(current_subs)) current_effect = current_subs( StackEffect( current_effect.input, [*o2[:-2], getitem_type.output], )) else: if (isinstance(child, concat.parse.QuoteWordNode) and child.input_stack_type is not None): input_stack, _ = child.input_stack_type.to_type(gamma) else: # The majority of quotations I've written don't comsume # anything on the stack, so make that the default. input_stack = TypeSequence([SequenceVariable()]) S2, fun_type = infer( S1(gamma), child.children, extensions=extensions, source_dir=source_dir, initial_stack=input_stack, ) current_subs, current_effect = ( S2(S1), StackEffect( S2(TypeSequence(i1)), [*S2(TypeSequence(o1)), QuotationType(fun_type)], ), ) elif isinstance(node, concat.parse.WithWordNode): a_bar, b_bar = SequenceVariable(), SequenceVariable() body_type = StackEffect([a_bar, object_type], [b_bar]) phi = current_effect.output.constrain_and_bind_supertype_variables( TypeSequence([a_bar, body_type, context_manager_type]), set(), ) assert b_bar in phi current_subs, current_effect = ( phi(current_subs), phi( StackEffect(current_effect.input, TypeSequence([b_bar]))), ) elif isinstance(node, concat.parse.TryWordNode): a_bar, b_bar = SequenceVariable(), SequenceVariable() phi = TypeSequence(o).constrain_and_bind_supertype_variables( TypeSequence([ a_bar, iterable_type[StackEffect([a_bar], [b_bar]), ], StackEffect([a_bar], [b_bar]), ]), set(), ) assert b_bar in phi current_subs, current_effect = ( phi(S), phi(StackEffect(i, [b_bar])), ) elif isinstance(node, concat.parse.DictWordNode): phi = current_subs collected_type = current_effect.output for key, value in node.dict_children: phi1, (i1, o1) = infer( phi(gamma), key, extensions=extensions, source_dir=source_dir, initial_stack=collected_type, ) phi = phi1(phi) collected_type = phi(o1) # drop the top of the stack to use as the key collected_type, key_type = ( collected_type[:-1], collected_type.as_sequence()[-1], ) phi2, (i2, o2) = infer( phi(gamma), value, extensions=extensions, source_dir=source_dir, initial_stack=collected_type, ) phi = phi2(phi) collected_type = phi(o2) # drop the top of the stack to use as the value collected_type, value_type = ( collected_type[:-1], collected_type.as_sequence()[-1], ) current_subs, current_effect = ( phi, phi( StackEffect(current_effect.input, [*collected_type, dict_type])), ) elif isinstance(node, concat.parse.ListWordNode): phi = S collected_type = TypeSequence(o) element_type: IndividualType = object_type for item in node.list_children: phi1, fun_type = infer( phi(gamma), item, extensions=extensions, source_dir=source_dir, initial_stack=collected_type, ) collected_type = fun_type.output # FIXME: Infer the type of elements in the list based on # ALL the elements. if element_type == object_type: assert isinstance(collected_type[-1], IndividualType) element_type = collected_type[-1] # drop the top of the stack to use as the item collected_type = collected_type[:-1] phi = phi1(phi) current_subs, current_effect = ( phi, phi( StackEffect( i, [*collected_type, list_type[element_type, ]])), ) elif isinstance(node, concat.operators.InvertWordNode): out_types = current_effect.output[:-1] invert_attr_type = current_effect.output[ -1].get_type_of_attribute('__invert__') if not isinstance(invert_attr_type, PythonFunctionType): raise TypeError( '__invert__ of type {} must be a Python function'. format(current_effect.output[-1])) result_type = invert_attr_type.output current_effect = StackEffect(current_effect.input, [*out_types, result_type]) elif isinstance(node, concat.parse.NoneWordNode): current_effect = StackEffect(i, [*o, none_type]) elif isinstance(node, concat.parse.NotImplWordNode): current_effect = StackEffect(i, [*o, not_implemented_type]) elif isinstance(node, concat.parse.EllipsisWordNode): current_effect = StackEffect(i, [*o, ellipsis_type]) elif isinstance(node, concat.parse.SliceWordNode): sliceable_object_type = o[-1] # This doesn't match the evaluation order used by the # transpiler. # FIXME: Change the transpiler to fit the type checker. sub1, start_effect = infer( gamma, list(node.start_children), source_dir=source_dir, initial_stack=TypeSequence(o[:-1]), ) start_type = start_effect.output[-1] o = tuple(start_effect.output[:-1]) sub2, stop_effect = infer( sub1(gamma), list(node.stop_children), source_dir=source_dir, initial_stack=TypeSequence(o), ) stop_type = stop_effect.output[-1] o = tuple(stop_effect.output[:-1]) sub3, step_effect = infer( sub2(sub1(gamma)), list(node.step_children), source_dir=source_dir, initial_stack=TypeSequence(o), ) step_type = step_effect.output[-1] o = tuple(step_effect.output[:-1]) this_slice_type = slice_type[start_type, stop_type, step_type] getitem_type = sliceable_object_type.get_type_of_attribute( '__getitem__') getitem_type = getitem_type.get_type_of_attribute('__call__') getitem_type = getitem_type.instantiate() if (not isinstance(getitem_type, PythonFunctionType) or len(getitem_type.input) != 1): raise TypeError( '__getitem__ method of {} has incorrect type {}'. format(node, getitem_type)) getitem_type, overload_subs = getitem_type.select_overload( (this_slice_type, )) result_type = getitem_type.output current_subs = overload_subs(sub3(sub2(sub1(current_subs)))) current_effect = overload_subs( StackEffect(i, [*o, result_type])) elif isinstance(node, concat.parse.FromImportStatementNode): imported_name = node.asname or node.imported_name # mutate type environment gamma[imported_name] = object_type # We will try to find a more specific type. sys.path, old_path = [source_dir, *sys.path], sys.path module = importlib.import_module(node.value) sys.path = old_path # For now, assume the module's written in Python. try: gamma[imported_name] = current_subs( getattr(module, '@@types')[node.imported_name]) except (KeyError, builtins.AttributeError): # attempt introspection to get a more specific type if callable(getattr(module, node.imported_name)): args_var = SequenceVariable() gamma[imported_name] = ObjectType( IndividualVariable(), { '__call__': py_function_type[TypeSequence([args_var]), object_type], }, type_parameters=[args_var], nominal=True, ) elif isinstance(node, concat.parse.ImportStatementNode): # TODO: Support all types of import correctly. if node.asname is not None: gamma[node.asname] = current_subs( _generate_type_of_innermost_module( node.value, source_dir).generalized_wrt(current_subs(gamma))) else: imported_name = node.value # mutate type environment components = node.value.split('.') # FIXME: This replaces whatever was previously imported. I really # should implement namespaces properly. gamma[components[0]] = current_subs( _generate_module_type(components, source_dir=source_dir)) elif isinstance(node, concat.parse.SubscriptionWordNode): seq = current_effect.output[:-1] index_type_var = IndividualVariable() result_type_var = IndividualVariable() subscriptable_interface = subscriptable_type[index_type_var, result_type_var] ( index_subs, (index_input, index_output), ) = infer( gamma, node.children, initial_stack=current_effect.output, extensions=extensions, ) index_output_typeseq = index_output subs = index_output_typeseq[ -2].constrain_and_bind_supertype_variables( subscriptable_interface, set(), )(index_subs(current_subs)) subs(index_output_typeseq[-1]).constrain(subs(index_type_var)) result_type = subs(result_type_var).get_type_of_attribute( '__call__') if not isinstance(result_type, StackEffect): raise TypeError( 'result of subscription is not callable as a Concat function (has type {})' .format(result_type)) subs = index_output_typeseq[: -2].constrain_and_bind_supertype_variables( result_type.input, set())(subs) current_subs, current_effect = ( subs, subs(StackEffect(current_effect.input, result_type.output)), ) elif isinstance(node, concat.operators.SubtractWordNode): # FIXME: We should check if the other operand supports __rsub__ if the # first operand doesn't support __sub__. other_operand_type_var = IndividualVariable() result_type_var = IndividualVariable() subtractable_interface = subtractable_type[ other_operand_type_var, result_type_var] seq_var = SequenceVariable() final_subs = current_effect.output.constrain_and_bind_supertype_variables( TypeSequence([ seq_var, subtractable_interface, other_operand_type_var, ]), set(), ) assert seq_var in final_subs current_subs, current_effect = ( final_subs(current_subs), final_subs( StackEffect(current_effect.input, [seq_var, result_type_var])), ) elif isinstance(node, concat.parse.FuncdefStatementNode): S = current_subs f = current_effect name = node.name declared_type: Optional[StackEffect] if node.stack_effect: declared_type, _ = node.stack_effect.to_type(S(gamma)) declared_type = S(declared_type) else: # NOTE: To continue the "bidirectional" bent, we will require a # type annotation. # TODO: Make the return types optional? # FIXME: Should be a parse error. raise TypeError( 'must have type annotation on function definition') recursion_env = gamma.copy() recursion_env[name] = declared_type.generalized_wrt(S(gamma)) phi1, inferred_type = infer( S(recursion_env), node.body, is_top_level=False, extensions=extensions, initial_stack=declared_type.input, ) # We want to check that the inferred outputs are subtypes of # the declared outputs. Thus, inferred_type.output should be a subtype # declared_type.output. try: inferred_type.output.constrain(declared_type.output) except TypeError: message = ( 'declared function type {} is not compatible with ' 'inferred type {}') raise TypeError( message.format(declared_type, inferred_type)) effect = declared_type # we *mutate* the type environment gamma[name] = effect.generalized_wrt(S(gamma)) elif isinstance(node, concat.operators.GreaterThanOrEqualToWordNode): a_type, b_type = current_effect.output[-2:] try: ge_type = a_type.get_type_of_attribute('__ge__') if not isinstance(ge_type, PythonFunctionType): raise TypeError( 'method __ge__ of type {} should be a Python function' .format(ge_type)) _, current_subs = ge_type.select_overload([b_type]) except TypeError: le_type = b_type.get_type_of_attribute('__le__') if not isinstance(le_type, PythonFunctionType): raise TypeError( 'method __le__ of type {} should be a Python function' .format(le_type)) _, current_subs = le_type.select_overload([a_type]) current_subs, current_effect = ( current_subs, StackEffect( current_effect.input, TypeSequence([*current_effect.output[:-2], bool_type]), ), ) elif isinstance( node, ( concat.operators.IsWordNode, concat.operators.AndWordNode, concat.operators.OrWordNode, concat.operators.EqualToWordNode, ), ): # TODO: I should be more careful here, since at least __eq__ can be # deleted, if I remember correctly. if not isinstance(current_effect.output[-1], IndividualType) or not isinstance( current_effect.output[-2], IndividualType): raise StackMismatchError( TypeSequence(current_effect.output), TypeSequence([object_type, object_type]), ) current_effect = StackEffect( current_effect.input, TypeSequence([*current_effect.output[:-2], bool_type]), ) elif isinstance(node, concat.parse.NumberWordNode): if isinstance(node.value, int): current_effect = StackEffect(i, [*o, int_type]) else: raise UnhandledNodeTypeError elif isinstance(node, concat.parse.NameWordNode): (i1, o1) = current_effect if node.value not in current_subs(gamma): raise NameError(node) type_of_name = current_subs(gamma)[node.value].instantiate() type_of_name = type_of_name.get_type_of_attribute('__call__') if not isinstance(type_of_name, StackEffect): raise UnhandledNodeTypeError( 'name {} of type {} (repr {!r})'.format( node.value, type_of_name, type_of_name)) constraint_subs = o1.constrain_and_bind_supertype_variables( type_of_name.input, set()) current_subs = constraint_subs(current_subs) current_effect = current_subs( StackEffect(i1, type_of_name.output)) elif isinstance(node, concat.parse.QuoteWordNode): quotation = cast(concat.parse.QuoteWordNode, node) # make sure any annotation matches the current stack if quotation.input_stack_type is not None: input_stack, _ = quotation.input_stack_type.to_type(gamma) S = TypeSequence(o).constrain_and_bind_supertype_variables( input_stack, set(), )(S) else: input_stack = TypeSequence(o) S1, (i1, o1) = infer( gamma, [*quotation.children], extensions=extensions, source_dir=source_dir, initial_stack=input_stack, ) current_subs, current_effect = ( S1(S), S1(StackEffect(i, o1)), ) elif isinstance(node, concat.parse.StringWordNode): current_subs, current_effect = ( S, StackEffect( current_effect.input, [*current_effect.output, str_type], ), ) elif isinstance(node, concat.parse.AttributeWordNode): stack_top_type = o[-1] out_types = o[:-1] attr_function_type = stack_top_type.get_type_of_attribute( node.value).instantiate() if not isinstance(attr_function_type, StackEffect): raise UnhandledNodeTypeError( 'attribute {} of type {} (repr {!r})'.format( node.value, attr_function_type, attr_function_type)) R = TypeSequence( out_types).constrain_and_bind_supertype_variables( attr_function_type.input, set(), ) current_subs, current_effect = ( R(S), R(StackEffect(i, attr_function_type.output)), ) elif isinstance(node, concat.parse.CastWordNode): new_type, _ = node.type.to_type(gamma) rest = current_effect.output[:-1] current_effect = current_subs( StackEffect(current_effect.input, [*rest, new_type])) else: raise UnhandledNodeTypeError( "don't know how to handle '{}'".format(node)) except TypeError as e: e.set_location_if_missing(node.location) raise return current_subs, current_effect