# typed: strict # frozen_string_literal: true module Tapioca module Runtime # 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( {}.compare_by_identity, T::Hash[Module, T::Array[TypeVariableModule]], ) class GenericType < T::Types::Simple extend T::Sig sig { params(raw_type: Module, underlying_type: Module).void } def initialize(raw_type, underlying_type) super(raw_type) @underlying_type = T.let(underlying_type, Module) end sig { override.params(obj: T.untyped).returns(T::Boolean) } def valid?(obj) obj.is_a?(@underlying_type) end end 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 = "#{Reflection.name_of(constant)}[#{type_list}]" # Create a generic type with an overridden `name` # method that returns the name we constructed above. # # Also, we try to memoize the generic type based on the name, so that # we don't have to keep recreating them all the time. @generic_instances[name] ||= create_generic_type(constant, name) end sig { params(instance: Object).returns(T::Boolean) } def generic_type_instance?(instance) @generic_instances.values.any? { |generic_type| generic_type === instance } end sig { params(constant: Module).returns(T.nilable(T::Array[TypeVariableModule])) } def lookup_type_variables(constant) @type_variables[constant] end # 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: TypeVariableModule, ).void end def register_type_variable(constant, type_variable) type_variables = lookup_or_initialize_type_variables(constant) type_variables << type_variable end private sig { params(constant: Module, name: String).returns(Module) } def create_generic_type(constant, name) generic_type = case constant when Class # For classes, we want to create a subclass, so that an instance of # the generic class `Foo[Bar]` is still a `Foo`. That is: # `Foo[Bar].new.is_a?(Foo)` should be true, which isn't the case # if we just clone the class. But subclassing works just fine. create_safe_subclass(constant) else # This can only be a module and it is fine to just clone modules # since they can't have instances and will not have `is_a?` relationships. # Moreover, we never `include`/`extend` any generic modules into the # ancestor tree, so this doesn't become a problem with checking the # instance of a class being `is_a?` of a module type. constant.clone end # Let's set the `name` and `to_s` methods to return the proper generic name name_proc = -> { name } generic_type.define_singleton_method(:name, name_proc) generic_type.define_singleton_method(:to_s, name_proc) override_type = GenericType.new(generic_type, constant) override_type_proc = -> { override_type } generic_type.define_singleton_method(:__tapioca_override_type, override_type_proc) # We need to define a `<=` method on the cloned constant, so that Sorbet # can do covariance/contravariance checks on the type variables. # # Normally, we would be doing proper covariance/contravariance checks here, but # that is not necessary, since we are not implementing a runtime type checker # here. It is just enough for the checks to pass, so that we can serialize the # signatures, assuming the sigs were well-formed. # # So we act like all subtype checks pass. generic_type.define_singleton_method(:<=) { |_| true } # Return the generic type we created generic_type end sig { params(constant: T::Class[T.anything]).returns(T::Class[T.anything]) } def create_safe_subclass(constant) # Lookup the "inherited" class method inherited_method = constant.method(:inherited) # and the module that defines it owner = inherited_method.owner # If no one has overridden the inherited method yet, just subclass return Class.new(constant) if Class == owner begin # Otherwise, some inherited method could be preventing us # from creating subclasses, so let's override it and rescue owner.send(:define_method, :inherited) do |s| inherited_method.call(s) rescue # Ignoring errors end # return a subclass Class.new(constant) ensure # Reinstate the original inherited method back. owner.send(:define_method, :inherited, inherited_method) end end sig { params(constant: Module).returns(T::Array[TypeVariableModule]) } def lookup_or_initialize_type_variables(constant) @type_variables[constant] ||= [] end end end end end