# typed: strict # frozen_string_literal: true module Tapioca # This class is responsible for storing and looking up information related to generic types. # # The class stores 2 different kinds of data, in two separate lookup tables: # 1. a lookup of generic type instances by name: `@generic_instances` # 2. a lookup of type variable serializer by constant and type variable # instance: `@type_variables` # # By storing the above data, we can cheaply query each constant against this registry # to see if it declares any generic type variables. This becomes a simple lookup in the # `@type_variables` hash table with the given constant. # # If there is no entry, then we can cheaply know that we can skip generic type # information generation for this type. # # On the other hand, if we get a result, then the result will be a hash of type # variable to type variable serializers. This allows us to associate type variables # to the constant names that represent them, easily. module GenericTypeRegistry @generic_instances = T.let( {}, T::Hash[String, Module] ) @type_variables = T.let( {}, T::Hash[Integer, T::Hash[Integer, String]] ) class << self extend T::Sig # This method is responsible for building the name of the instantiated concrete type # and cloning the given constant so that we can return a type that is the same # as the current type but is a different instance and has a different name method. # # We cache those cloned instances by their name in `@generic_instances`, so that # we don't keep instantiating a new type every single time it is referenced. # For example, `[Foo[Integer], Foo[Integer], Foo[Integer], Foo[String]]` will only # result in 2 clones (1 for `Foo[Integer]` and another for `Foo[String]`) and # 2 hash lookups (for the other two `Foo[Integer]`s). # # This method returns the created or cached clone of the constant. sig { params(constant: T.untyped, types: T.untyped).returns(Module) } def register_type(constant, types) # Build the name of the instantiated generic type, # something like `"Foo[X, Y, Z]"` type_list = types.map { |type| T::Utils.coerce(type).name }.join(", ") name = "#{name_of(constant)}[#{type_list}]" # Create a clone of the constant with an overridden `name` # method that returns the name we constructed above. # # Also, we try to memoize the clone based on the name, so that # we don't have to keep recreating clones all the time. @generic_instances[name] ||= constant.clone.tap do |clone| clone.define_singleton_method(:name) { name } end end sig do params( constant: T.untyped, type_member: T::Types::TypeVariable, fixed: T.untyped, lower: T.untyped, upper: T.untyped ).void end def register_type_member(constant, type_member, fixed, lower, upper) register_type_variable(constant, :type_member, type_member, fixed, lower, upper) end sig do params( constant: T.untyped, type_template: T::Types::TypeVariable, fixed: T.untyped, lower: T.untyped, upper: T.untyped ).void end def register_type_template(constant, type_template, fixed, lower, upper) register_type_variable(constant, :type_template, type_template, fixed, lower, upper) end sig { params(constant: Module).returns(T.nilable(T::Hash[Integer, String])) } def lookup_type_variables(constant) @type_variables[object_id_of(constant)] end private # This method is called from intercepted calls to `type_member` and `type_template`. # We get passed all the arguments to those methods, as well as the `T::Types::TypeVariable` # instance generated by the Sorbet defined `type_member`/`type_template` call on `T::Generic`. # # This method creates a `String` with that data and stores it in the # `@type_variables` lookup table, keyed by the `constant` and `type_variable`. # # Finally, the original `type_variable` is returned from this method, so that the caller # can return it from the original methods as well. sig do params( constant: T.untyped, type_variable_type: T.enum([:type_member, :type_template]), type_variable: T::Types::TypeVariable, fixed: T.untyped, lower: T.untyped, upper: T.untyped ).void end # rubocop:disable Metrics/ParameterLists def register_type_variable(constant, type_variable_type, type_variable, fixed, lower, upper) # rubocop:enable Metrics/ParameterLists type_variables = lookup_or_initialize_type_variables(constant) type_variables[object_id_of(type_variable)] = serialize_type_variable( type_variable_type, type_variable.variance, fixed, lower, upper ) end sig { params(constant: Module).returns(T::Hash[Integer, String]) } def lookup_or_initialize_type_variables(constant) @type_variables[object_id_of(constant)] ||= {} end sig do params( type_variable_type: Symbol, variance: Symbol, fixed: T.untyped, lower: T.untyped, upper: T.untyped ).returns(String) end def serialize_type_variable(type_variable_type, variance, fixed, lower, upper) parts = [] parts << ":#{variance}" unless variance == :invariant parts << "fixed: #{fixed}" if fixed parts << "lower: #{lower}" unless lower == T.untyped parts << "upper: #{upper}" unless upper == BasicObject parameters = parts.join(", ") serialized = T.let(type_variable_type.to_s, String) serialized += "(#{parameters})" unless parameters.empty? serialized end sig { params(constant: Module).returns(T.nilable(String)) } def name_of(constant) Module.instance_method(:name).bind(constant).call end sig { params(object: BasicObject).returns(Integer) } def object_id_of(object) Object.instance_method(:object_id).bind(object).call end end end end