diff --git a/doc/shape.md b/doc/shape.md index 03b021e41..6d784964d 100644 --- a/doc/shape.md +++ b/doc/shape.md @@ -24,7 +24,9 @@ Shape (_Foo) { Note that the `self` type in the example is resolved to `_Foo` during shape calculation. The shape calculation of an object is straightforward. Calculate a `RBS::Definition` of a class singleton/instance, or an interface, and translate the data structure to a `Shape` object. But there are a few things to consider. + ## Tuple, record, and proc types + The shape of tuple, record, or proc types are based on their base types -- Array, Hash, or Proc classes --, but with specialized method types. ``` @@ -37,16 +39,22 @@ Shape ([Integer, String]) { ``` The specialization is implemented as a part of shape calculation. + ## Special methods + Steep recognizes some special methods for type narrowing, including `#is_a?`, `#===`, `#nil?`, ... These methods are defined with normal RBS syntax, but the method types in shapes are transformed to types using logic types. The shape calculation inserts the specialized methods with these special methods. + ## `self` types + There are two cases of `self` types to consider during shape calculation. 1. `self` types included in the shape of a type 2. `self` types included in given types + ### 1. `self` types included in the shape of a type + `self` types may be included in a class or interface definition. ```rbs @@ -62,7 +70,9 @@ Shape (_Foo) { itself: () -> _Foo } ``` + ### 2. `self` types included in given types + Unlike `self` types included in definitions, `self` types in given types should be preserved. ```rbs @@ -100,7 +110,9 @@ end ``` We want the type of `foo.get` to be `self`, not `Foo`, to avoid a type error being detected. + ## Shape of `self` types + We also want `self` type if `self` is the type of the shape. ```rb @@ -154,7 +166,9 @@ Shape (Foo | Bar) { So, the resulting type of `self.foo` where the type of `self` is `Foo | Bar`, would be `Integer | Foo | Bar`. But, actually, it won't be `Foo` because the `self` comes from `Bar`. This is an incorrect result, but Steep is doing this right now. + ## `class` and `instance` types + The shape calculation provides limited support for `class` and `instance` types. 1. `class`/`instance` types from the definition are resolved @@ -162,13 +176,17 @@ The shape calculation provides limited support for `class` and `instance` types. 3. Shape of `class`/`instance` types are resolved to configuration's `class_type` and `instance_type`, and the translated types are used to calculate the shape It's different from `self` types except case #2. The relationship between `self`/`class`/`instance` is not trivial in Ruby. All of them might be resolved to any type, which means calculating one from another of them is simply impossible. + ## Public methods, private methods + `Shape` objects have a flag of if the shape is for *public* method calls or *private* method calls. Private method call is a form of `foo()` or `self.foo()` -- when the receiver is omitted or `self`. Public method calls are anything else. The shape calculation starts with *private methods*, and the `Shape#public_shape` method returns another shape that only has *public* methods. > Note that the private shape calculation is required even on public method calls. This means a possible chance of future optimizations. + ## Lazy method type calculation + We rarely need all of the methods available for an object. If we want to type check a method call, we only need the method type of that method. All other methods can be just ignored. *Lazy method type calculation* is introduced for that case. Instead of calculating the types of all of the methods, it registers a block that computes the method type. diff --git a/lib/steep/interface/builder.rb b/lib/steep/interface/builder.rb index 4d5933300..859332b33 100644 --- a/lib/steep/interface/builder.rb +++ b/lib/steep/interface/builder.rb @@ -9,8 +9,6 @@ def initialize(self_type:, class_type: nil, instance_type: nil, variable_bounds: @class_type = class_type @instance_type = instance_type @variable_bounds = variable_bounds - - validate end def self.empty @@ -23,11 +21,16 @@ def subst end end - def validate + def validate_self_type validate_fvs(:self_type, self_type) + end + + def validate_instance_type validate_fvs(:instance_type, instance_type) + end + + def validate_class_type validate_fvs(:class_type, class_type) - self end def validate_fvs(name, type) @@ -37,6 +40,7 @@ def validate_fvs(name, type) raise "#{name} cannot include 'self' type: #{type}" end if fvs.include?(AST::Types::Instance.instance) + Steep.logger.fatal { "#{name} cannot include 'instance' type: #{type}" } raise "#{name} cannot include 'instance' type: #{type}" end if fvs.include?(AST::Types::Class.instance) @@ -87,12 +91,15 @@ def fetch_cache(cache, key) def raw_shape(type, config) case type when AST::Types::Self + config.validate_self_type self_type = config.self_type or raise self_shape(self_type, config) when AST::Types::Instance + config.validate_instance_type instance_type = config.instance_type or raise raw_shape(instance_type, config) when AST::Types::Class + config.validate_class_type klass_type = config.class_type or raise raw_shape(klass_type, config) when AST::Types::Name::Singleton diff --git a/lib/steep/signature/validator.rb b/lib/steep/signature/validator.rb index 22e9a881d..354d0201e 100644 --- a/lib/steep/signature/validator.rb +++ b/lib/steep/signature/validator.rb @@ -5,10 +5,23 @@ class Validator Declarations = RBS::AST::Declarations attr_reader :checker + attr_reader :context def initialize(checker:) @checker = checker @errors = [] + @context = [] + end + + def push_context(self_type: latest_context[0], class_type: latest_context[1], instance_type: latest_context[2]) + @context.push([self_type, class_type, instance_type]) + yield + ensure + @context.pop + end + + def latest_context + context.last || [nil, nil, nil] end def has_error? @@ -72,11 +85,13 @@ def validate_type_application_constraints(type_name, type_params, type_args, loc constraints = Subtyping::Constraints.empty + self_type, class_type, instance_type = latest_context + checker.check( Subtyping::Relation.new(sub_type: arg_type, super_type: upper_bound_type), - self_type: AST::Types::Self.instance, - class_type: nil, - instance_type: nil, + self_type: self_type, + class_type: class_type, + instance_type: instance_type, constraints: constraints ).else do |result| @errors << Diagnostic::Signature::UnsatisfiableTypeApplication.new( @@ -236,25 +251,113 @@ def validate_definition_type(definition) end end - def validate_one_class_decl(name) + def validate_one_class_decl(name, entry) rescue_validation_errors(name) do Steep.logger.debug { "Validating class definition `#{name}`..." } + class_type = AST::Types::Name::Singleton.new(name: name, location: nil) + instance_type = AST::Types::Name::Instance.new( + name: name, + args: entry.type_params.map { AST::Types::Any.new(location: nil) }, + location: nil + ) + Steep.logger.tagged "#{name}" do builder.build_instance(name).tap do |definition| upper_bounds = definition.type_params_decl.each.with_object({}) do |param, bounds| bounds[param.name] = factory.type_opt(param.upper_bound) end - checker.push_variable_bounds(upper_bounds) do + self_type = AST::Types::Name::Instance.new( + name: name, + args: entry.type_params.map { AST::Types::Var.new(name: _1.name) }, + location: nil + ) + + push_context(self_type: self_type, class_type: class_type, instance_type: instance_type) do + checker.push_variable_bounds(upper_bounds) do + definition.instance_variables.each do |name, var| + if parent = var.parent_variable + var_type = checker.factory.type(var.type) + parent_type = checker.factory.type(parent.type) + + relation = Subtyping::Relation.new(sub_type: var_type, super_type: parent_type) + result1 = checker.check(relation, self_type: nil, instance_type: nil, class_type: nil, constraints: Subtyping::Constraints.empty) + result2 = checker.check(relation.flip, self_type: nil, instance_type: nil, class_type: nil, constraints: Subtyping::Constraints.empty) + + unless result1.success? and result2.success? + @errors << Diagnostic::Signature::InstanceVariableTypeError.new( + name: name, + location: var.type.location, + var_type: var_type, + parent_type: parent_type + ) + end + end + end + + ancestors = builder.ancestor_builder.one_instance_ancestors(name) + mixin_constraints(definition, ancestors.included_modules || raise, immediate_self_types: ancestors.self_types).each do |relation, ancestor| + checker.check( + relation, + self_type: AST::Types::Self.instance, + instance_type: AST::Types::Instance.instance, + class_type: AST::Types::Class.instance, + constraints: Subtyping::Constraints.empty + ).else do + raise if ancestor.source.is_a?(Symbol) + + @errors << Diagnostic::Signature::ModuleSelfTypeError.new( + name: name, + location: ancestor.source&.location || raise, + ancestor: ancestor, + relation: relation + ) + end + end + + ancestors.each_ancestor do |ancestor| + case ancestor + when RBS::Definition::Ancestor::Instance + validate_ancestor_application(name, ancestor) + end + end + + validate_definition_type(definition) + end + end + end + + builder.build_singleton(name).tap do |definition| + entry = + case definition.entry + when RBS::Environment::ClassEntry, RBS::Environment::ModuleEntry + definition.entry + else + raise + end + + push_context(self_type: class_type, class_type: class_type, instance_type: instance_type) do definition.instance_variables.each do |name, var| if parent = var.parent_variable var_type = checker.factory.type(var.type) parent_type = checker.factory.type(parent.type) relation = Subtyping::Relation.new(sub_type: var_type, super_type: parent_type) - result1 = checker.check(relation, self_type: nil, instance_type: nil, class_type: nil, constraints: Subtyping::Constraints.empty) - result2 = checker.check(relation.flip, self_type: nil, instance_type: nil, class_type: nil, constraints: Subtyping::Constraints.empty) + result1 = checker.check( + relation, + self_type: AST::Types::Self.instance, + instance_type: AST::Types::Instance.instance, + class_type: AST::Types::Class.instance, + constraints: Subtyping::Constraints.empty + ) + result2 = checker.check( + relation.flip, + self_type: AST::Types::Self.instance, + instance_type: AST::Types::Instance.instance, + class_type: AST::Types::Class.instance, + constraints: Subtyping::Constraints.empty + ) unless result1.success? and result2.success? @errors << Diagnostic::Signature::InstanceVariableTypeError.new( @@ -267,11 +370,32 @@ def validate_one_class_decl(name) end end - ancestors = builder.ancestor_builder.one_instance_ancestors(name) - mixin_constraints(definition, ancestors.included_modules || raise, immediate_self_types: ancestors.self_types).each do |relation, ancestor| + definition.class_variables.each do |name, var| + if var.declared_in == definition.type_name + if (parent = var.parent_variable) && var.declared_in != parent.declared_in + class_var = entry.decls.flat_map {|decl| decl.decl.members }.find do |member| + member.is_a?(RBS::AST::Members::ClassVariable) && member.name == name + end + + if class_var + loc = class_var.location #: RBS::Location[untyped, untyped]? + @errors << Diagnostic::Signature::ClassVariableDuplicationError.new( + class_name: definition.type_name, + other_class_name: parent.declared_in, + variable_name: name, + location: loc&.[](:name) || raise + ) + end + end + end + end + + ancestors = builder.ancestor_builder.one_singleton_ancestors(name) + ancestors.extended_modules or raise + mixin_constraints(definition, ancestors.extended_modules, immediate_self_types: ancestors.self_types).each do |relation, ancestor| checker.check( relation, - self_type: AST::Types::Self.instance, + self_type: AST::Types::Self.instance , instance_type: AST::Types::Instance.instance, class_type: AST::Types::Class.instance, constraints: Subtyping::Constraints.empty @@ -286,7 +410,6 @@ def validate_one_class_decl(name) ) end end - ancestors.each_ancestor do |ancestor| case ancestor when RBS::Definition::Ancestor::Instance @@ -297,97 +420,6 @@ def validate_one_class_decl(name) validate_definition_type(definition) end end - - builder.build_singleton(name).tap do |definition| - entry = - case definition.entry - when RBS::Environment::ClassEntry, RBS::Environment::ModuleEntry - definition.entry - else - raise - end - - definition.instance_variables.each do |name, var| - if parent = var.parent_variable - var_type = checker.factory.type(var.type) - parent_type = checker.factory.type(parent.type) - - relation = Subtyping::Relation.new(sub_type: var_type, super_type: parent_type) - result1 = checker.check( - relation, - self_type: AST::Types::Self.instance, - instance_type: AST::Types::Instance.instance, - class_type: AST::Types::Class.instance, - constraints: Subtyping::Constraints.empty - ) - result2 = checker.check( - relation.flip, - self_type: AST::Types::Self.instance, - instance_type: AST::Types::Instance.instance, - class_type: AST::Types::Class.instance, - constraints: Subtyping::Constraints.empty - ) - - unless result1.success? and result2.success? - @errors << Diagnostic::Signature::InstanceVariableTypeError.new( - name: name, - location: var.type.location, - var_type: var_type, - parent_type: parent_type - ) - end - end - end - - definition.class_variables.each do |name, var| - if var.declared_in == definition.type_name - if (parent = var.parent_variable) && var.declared_in != parent.declared_in - class_var = entry.decls.flat_map {|decl| decl.decl.members }.find do |member| - member.is_a?(RBS::AST::Members::ClassVariable) && member.name == name - end - - if class_var - loc = class_var.location #: RBS::Location[untyped, untyped]? - @errors << Diagnostic::Signature::ClassVariableDuplicationError.new( - class_name: definition.type_name, - other_class_name: parent.declared_in, - variable_name: name, - location: loc&.[](:name) || raise - ) - end - end - end - end - - ancestors = builder.ancestor_builder.one_singleton_ancestors(name) - ancestors.extended_modules or raise - mixin_constraints(definition, ancestors.extended_modules, immediate_self_types: ancestors.self_types).each do |relation, ancestor| - checker.check( - relation, - self_type: AST::Types::Self.instance , - instance_type: AST::Types::Instance.instance, - class_type: AST::Types::Class.instance, - constraints: Subtyping::Constraints.empty - ).else do - raise if ancestor.source.is_a?(Symbol) - - @errors << Diagnostic::Signature::ModuleSelfTypeError.new( - name: name, - location: ancestor.source&.location || raise, - ancestor: ancestor, - relation: relation - ) - end - end - ancestors.each_ancestor do |ancestor| - case ancestor - when RBS::Definition::Ancestor::Instance - validate_ancestor_application(name, ancestor) - end - end - - validate_definition_type(definition) - end end end end @@ -397,7 +429,7 @@ def validate_one_class(name) case entry when RBS::Environment::ClassEntry, RBS::Environment::ModuleEntry - validate_one_class_decl(name) + validate_one_class_decl(name, entry) when RBS::Environment::ClassAliasEntry, RBS::Environment::ModuleAliasEntry validate_one_class_alias(name, entry) end @@ -453,23 +485,31 @@ def validate_one_interface(name) bounds[param.name] = factory.type_opt(param.upper_bound) end - checker.push_variable_bounds(upper_bounds) do - validate_definition_type(definition) - - ancestors = builder.ancestor_builder.one_interface_ancestors(name) - ancestors.each_ancestor do |ancestor| - case ancestor - when RBS::Definition::Ancestor::Instance - # Interface ancestor cannot be other than Interface - ancestor.source.is_a?(Symbol) and raise - - defn = builder.build_interface(ancestor.name) - validate_type_application_constraints( - ancestor.name, - defn.type_params_decl, - ancestor.args, - location: ancestor.source&.location || raise - ) + self_type = AST::Types::Name::Interface.new( + name: name, + args: definition.type_params.map { AST::Types::Var.new(name: _1) }, + location: nil + ) + + push_context(self_type: self_type, class_type: nil, instance_type: nil) do + checker.push_variable_bounds(upper_bounds) do + validate_definition_type(definition) + + ancestors = builder.ancestor_builder.one_interface_ancestors(name) + ancestors.each_ancestor do |ancestor| + case ancestor + when RBS::Definition::Ancestor::Instance + # Interface ancestor cannot be other than Interface + ancestor.source.is_a?(Symbol) and raise + + defn = builder.build_interface(ancestor.name) + validate_type_application_constraints( + ancestor.name, + defn.type_params_decl, + ancestor.args, + location: ancestor.source&.location || raise + ) + end end end end @@ -519,21 +559,33 @@ def validate_one_global(name, entry) end def validate_one_alias(name, entry = env.type_alias_decls[name]) - rescue_validation_errors(name) do - Steep.logger.debug "Validating alias `#{name}`..." + *, inner_most_outer_module = entry.outer + if inner_most_outer_module + class_type = AST::Types::Name::Singleton.new(name: inner_most_outer_module.name, location: nil) + instance_type = AST::Types::Name::Instance.new( + name: inner_most_outer_module.name, + args: inner_most_outer_module.type_params.map { AST::Types::Any.new(location: nil) }, + location: nil + ) + end - unless name.namespace.empty? - outer = name.namespace.to_type_name - builder.validate_type_name(outer, entry.decl.location&.aref(:name)) - end + push_context(class_type: class_type, instance_type: instance_type, self_type: nil) do + rescue_validation_errors(name) do + Steep.logger.debug "Validating alias `#{name}`..." - upper_bounds = entry.decl.type_params.each.with_object({}) do |param, bounds| - bounds[param.name] = factory.type_opt(param.upper_bound) - end + unless name.namespace.empty? + outer = name.namespace.to_type_name + builder.validate_type_name(outer, entry.decl.location&.aref(:name)) + end + + upper_bounds = entry.decl.type_params.each.with_object({}) do |param, bounds| + bounds[param.name] = factory.type_opt(param.upper_bound) + end - validator.validate_type_alias(entry: entry) do |type| - checker.push_variable_bounds(upper_bounds) do - validate_type(entry.decl.type) + validator.validate_type_alias(entry: entry) do |type| + checker.push_variable_bounds(upper_bounds) do + validate_type(entry.decl.type) + end end end end diff --git a/lib/steep/subtyping/check.rb b/lib/steep/subtyping/check.rb index 11a813fbd..5dff60815 100644 --- a/lib/steep/subtyping/check.rb +++ b/lib/steep/subtyping/check.rb @@ -798,33 +798,37 @@ def check_constraints(relation, variables:, variance:) end def check_method_type(name, relation) - relation.method! + Steep.logger.tagged "#{name} : #{relation.sub_type} <: #{relation.super_type}" do + relation.method! - sub_type, super_type = relation + sub_type, super_type = relation - sub_type.type_params.empty? or raise "Expected monomorphic method type: #{sub_type}" - super_type.type_params.empty? or raise "Expected monomorphic method type: #{super_type}" + sub_type.type_params.empty? or raise "Expected monomorphic method type: #{sub_type}" + super_type.type_params.empty? or raise "Expected monomorphic method type: #{super_type}" - All(relation) do |result| - type_relation = Relation.new(sub_type: sub_type.type, super_type: super_type.type) - - ret = expand_block_given(name, Relation.new(sub_type: sub_type.block, super_type: super_type.block)) - - case ret - when true - result.add(type_relation) { check_function(name, type_relation) } - when Relation - result.add(type_relation) { check_function(name, type_relation) } - result.add(ret) do - All(ret) do |result| - result.add_result(check_self_type_binding(ret, ret.super_type.self_type, ret.sub_type.self_type)) - result.add(Relation(ret.super_type.type, ret.sub_type.type)) do |block_relation| - check_function(name, block_relation) + All(relation) do |result| + type_relation = Relation.new(sub_type: sub_type.type, super_type: super_type.type) + + ret = expand_block_given(name, Relation.new(sub_type: sub_type.block, super_type: super_type.block)) + + case ret + when true + result.add(type_relation) { check_function(name, type_relation) } + when Relation + result.add(type_relation) { check_function(name, type_relation) } + result.add(ret) do + All(ret) do |result| + result.add_result(check_self_type_binding(ret, ret.super_type.self_type, ret.sub_type.self_type)) + result.add(Relation(ret.super_type.type, ret.sub_type.type)) do |block_relation| + check_function(name, block_relation) + end end end + when Result::Failure + result.add(ret.relation) { ret } + end.tap do |ret| + Steep.logger.debug "result=#{ret.class}" end - when Result::Failure - result.add(ret.relation) { ret } end end end diff --git a/rbs_collection.steep.yaml b/rbs_collection.steep.yaml index 6e05ade13..54f164181 100644 --- a/rbs_collection.steep.yaml +++ b/rbs_collection.steep.yaml @@ -21,3 +21,5 @@ gems: - name: csv - name: pathname - name: securerandom + - name: ffi + ignore: true diff --git a/sig/steep/interface/builder.rbs b/sig/steep/interface/builder.rbs index 4915abf43..f31ef2367 100644 --- a/sig/steep/interface/builder.rbs +++ b/sig/steep/interface/builder.rbs @@ -28,9 +28,16 @@ module Steep def upper_bound: (Symbol) -> AST::Types::t? - private + # Validates `self_type` attribute, and raises an error if it's not valid + def validate_self_type: () -> void + + # Validates `instance_type` attribute, and raises an error if it's not valid + def validate_instance_type: () -> void - def validate: () -> self + # Validates `class_type` attribute, and raises an error if it's not valid + def validate_class_type: () -> void + + private def validate_fvs: (Symbol name, AST::Types::t?) -> void end diff --git a/sig/steep/signature/validator.rbs b/sig/steep/signature/validator.rbs index 577bd5ca7..142ade732 100644 --- a/sig/steep/signature/validator.rbs +++ b/sig/steep/signature/validator.rbs @@ -13,6 +13,14 @@ module Steep @validator: RBS::Validator? + # Stack of `self_type`, `class_type`, `instance_type` tuple + # + attr_reader context: Array[[AST::Types::t?, AST::Types::t?, AST::Types::t?]] + + def latest_context: -> [AST::Types::t?, AST::Types::t?, AST::Types::t?] + + def push_context: [T] (?self_type: AST::Types::t?, ?class_type: AST::Types::t?, ?instance_type: AST::Types::t?) { () -> T } -> T + def initialize: (checker: Subtyping::Check) -> void def has_error?: () -> bool @@ -70,10 +78,10 @@ module Steep # # 1. Make sure the outer namespace of given `name` exists # 2. Make sure the type alias is valid with respect to `RBS::Validator` - # + # def validate_one_alias: (RBS::TypeName name, ?RBS::Environment::TypeAliasEntry entry) -> void - def validate_one_class_decl: (RBS::TypeName) -> void + def validate_one_class_decl: (RBS::TypeName, RBS::Environment::ClassEntry | RBS::Environment::ModuleEntry) -> void def validate_one_class_alias: (RBS::TypeName, RBS::Environment::ClassAliasEntry | RBS::Environment::ModuleAliasEntry) -> void diff --git a/test/validation_test.rb b/test/validation_test.rb index 0fc4d8507..b68948e09 100644 --- a/test/validation_test.rb +++ b/test/validation_test.rb @@ -1156,4 +1156,27 @@ module Bar = Foo end end end + + def test_validate_type_app__classish_bounded + with_checker <<~RBS do |checker| + interface _Generic[T < Object] + end + + class Foo < BasicObject + class User + type t1 = _Generic[instance] + type t2 = _Generic[class] + + include _Generic[class] + extend _Generic[instance] + end + end + RBS + + Validator.new(checker: checker).tap do |validator| + validator.validate + assert_predicate validator.each_error.to_a, :empty? + end + end + end end