houndstooth 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +5 -0
- data/.rspec +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +11 -0
- data/Gemfile.lock +49 -0
- data/README.md +99 -0
- data/bin/houndstooth.rb +183 -0
- data/fuzz/cases/x.rb +8 -0
- data/fuzz/cases/y.rb +8 -0
- data/fuzz/cases/z.rb +22 -0
- data/fuzz/ruby.dict +64 -0
- data/fuzz/run +21 -0
- data/lib/houndstooth/environment/builder.rb +260 -0
- data/lib/houndstooth/environment/type_parser.rb +149 -0
- data/lib/houndstooth/environment/types/basic/type.rb +85 -0
- data/lib/houndstooth/environment/types/basic/type_instance.rb +54 -0
- data/lib/houndstooth/environment/types/compound/union_type.rb +72 -0
- data/lib/houndstooth/environment/types/defined/base_defined_type.rb +23 -0
- data/lib/houndstooth/environment/types/defined/defined_type.rb +137 -0
- data/lib/houndstooth/environment/types/defined/pending_defined_type.rb +14 -0
- data/lib/houndstooth/environment/types/method/method.rb +79 -0
- data/lib/houndstooth/environment/types/method/method_type.rb +144 -0
- data/lib/houndstooth/environment/types/method/parameters.rb +53 -0
- data/lib/houndstooth/environment/types/method/special_constructor_method.rb +15 -0
- data/lib/houndstooth/environment/types/special/instance_type.rb +9 -0
- data/lib/houndstooth/environment/types/special/self_type.rb +9 -0
- data/lib/houndstooth/environment/types/special/type_parameter_placeholder.rb +38 -0
- data/lib/houndstooth/environment/types/special/untyped_type.rb +11 -0
- data/lib/houndstooth/environment/types/special/void_type.rb +12 -0
- data/lib/houndstooth/environment/types.rb +3 -0
- data/lib/houndstooth/environment.rb +74 -0
- data/lib/houndstooth/errors.rb +53 -0
- data/lib/houndstooth/instructions.rb +698 -0
- data/lib/houndstooth/interpreter/const_internal.rb +148 -0
- data/lib/houndstooth/interpreter/objects.rb +142 -0
- data/lib/houndstooth/interpreter/runtime.rb +309 -0
- data/lib/houndstooth/interpreter.rb +7 -0
- data/lib/houndstooth/semantic_node/control_flow.rb +218 -0
- data/lib/houndstooth/semantic_node/definitions.rb +253 -0
- data/lib/houndstooth/semantic_node/identifiers.rb +308 -0
- data/lib/houndstooth/semantic_node/keywords.rb +45 -0
- data/lib/houndstooth/semantic_node/literals.rb +226 -0
- data/lib/houndstooth/semantic_node/operators.rb +126 -0
- data/lib/houndstooth/semantic_node/parameters.rb +108 -0
- data/lib/houndstooth/semantic_node/send.rb +349 -0
- data/lib/houndstooth/semantic_node/super.rb +12 -0
- data/lib/houndstooth/semantic_node.rb +119 -0
- data/lib/houndstooth/stdlib.rb +6 -0
- data/lib/houndstooth/type_checker.rb +462 -0
- data/lib/houndstooth.rb +53 -0
- data/spec/ast_to_node_spec.rb +889 -0
- data/spec/environment_spec.rb +323 -0
- data/spec/instructions_spec.rb +291 -0
- data/spec/integration_spec.rb +785 -0
- data/spec/interpreter_spec.rb +170 -0
- data/spec/self_spec.rb +7 -0
- data/spec/spec_helper.rb +50 -0
- data/test/ruby_interpreter_test.rb +162 -0
- data/types/stdlib.htt +170 -0
- metadata +110 -0
| @@ -0,0 +1,137 @@ | |
| 1 | 
            +
            class Houndstooth::Environment
         | 
| 2 | 
            +
                class DefinedType < Type
         | 
| 3 | 
            +
                    def initialize(path: nil, node: nil, superclass: nil, instance_methods: nil, eigen: :generate, type_parameters: nil)
         | 
| 4 | 
            +
                        @path = path.to_s
         | 
| 5 | 
            +
                        @node = node
         | 
| 6 | 
            +
                        @superclass = superclass
         | 
| 7 | 
            +
                        @instance_methods = instance_methods || []
         | 
| 8 | 
            +
                        @type_parameters = type_parameters || []
         | 
| 9 | 
            +
                        @type_instance_variables = {}
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                        if eigen == :generate
         | 
| 12 | 
            +
                            @eigen = DefinedType.new(
         | 
| 13 | 
            +
                                path: "<Eigen:#{path}>",
         | 
| 14 | 
            +
                                superclass:
         | 
| 15 | 
            +
                                    if superclass.is_a?(PendingDefinedType)
         | 
| 16 | 
            +
                                        PendingDefinedType.new("<Eigen:#{superclass.path}>")
         | 
| 17 | 
            +
                                    else 
         | 
| 18 | 
            +
                                        superclass&.eigen
         | 
| 19 | 
            +
                                    end,
         | 
| 20 | 
            +
                                eigen: nil,
         | 
| 21 | 
            +
                            )
         | 
| 22 | 
            +
                        else
         | 
| 23 | 
            +
                            @eigen = eigen
         | 
| 24 | 
            +
                        end
         | 
| 25 | 
            +
                    end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                    # @return [String]
         | 
| 28 | 
            +
                    attr_reader :path
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                    # @return [String]
         | 
| 31 | 
            +
                    def name
         | 
| 32 | 
            +
                        path.split("::").last
         | 
| 33 | 
            +
                    end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                    # @return [SemanticNode]
         | 
| 36 | 
            +
                    attr_reader :node
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                    # @return [Type, nil]
         | 
| 39 | 
            +
                    attr_accessor :superclass
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                    # @return [Type]
         | 
| 42 | 
            +
                    attr_accessor :eigen
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                    # @return [<Method>]
         | 
| 45 | 
            +
                    attr_reader :instance_methods
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                    # @return [<String>]
         | 
| 48 | 
            +
                    attr_reader :type_parameters
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                    # @return [{String => Type}]
         | 
| 51 | 
            +
                    attr_reader :type_instance_variables
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                    def resolve_instance_method(method_name, env, instance: nil, top_level: true)            
         | 
| 54 | 
            +
                        # Is it available on this type?
         | 
| 55 | 
            +
                        # If not, check the superclass
         | 
| 56 | 
            +
                        # If there's no superclass, then there is no method to be found, so return nil
         | 
| 57 | 
            +
                        instance_method = instance_methods.find { _1.name == method_name }
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                        found = if instance_method
         | 
| 60 | 
            +
                            instance_method
         | 
| 61 | 
            +
                        else
         | 
| 62 | 
            +
                            superclass&.resolve_instance_method(method_name, env, instance: instance, top_level: false)
         | 
| 63 | 
            +
                        end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                        # If the upper chain returned a special constructor method, we need to convert this by
         | 
| 66 | 
            +
                        # grabbing our instance's `initialize` type
         | 
| 67 | 
            +
                        if top_level && found && found.is_a?(SpecialConstructorMethod)
         | 
| 68 | 
            +
                            initialize_sig = env.resolve_type(uneigen).resolve_instance_method(:initialize, env, instance: instance)
         | 
| 69 | 
            +
                            @new_sig ||= Method.new(
         | 
| 70 | 
            +
                                :new,
         | 
| 71 | 
            +
                                initialize_sig.signatures.map do |sig|
         | 
| 72 | 
            +
                                    # Same parameters, but returns `instance`
         | 
| 73 | 
            +
                                    new_sig = sig.clone
         | 
| 74 | 
            +
                                    new_sig.return_type = InstanceType.new
         | 
| 75 | 
            +
                                    new_sig
         | 
| 76 | 
            +
                                end,
         | 
| 77 | 
            +
                                const: initialize_sig.const,
         | 
| 78 | 
            +
                            )
         | 
| 79 | 
            +
                            @new_sig
         | 
| 80 | 
            +
                        else
         | 
| 81 | 
            +
                            found
         | 
| 82 | 
            +
                        end
         | 
| 83 | 
            +
                    end
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                    def resolve_instance_variable(name)
         | 
| 86 | 
            +
                        var_here = type_instance_variables[name]
         | 
| 87 | 
            +
                        return var_here if var_here
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                        superclass&.resolve_instance_variable(name)
         | 
| 90 | 
            +
                    end
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                    # A path to this type, but with one layer of "eigen-ness" removed from the final element.
         | 
| 93 | 
            +
                    # A bit cursed, but used for constant resolution.
         | 
| 94 | 
            +
                    # @return [String]
         | 
| 95 | 
            +
                    def uneigen
         | 
| 96 | 
            +
                        path_parts = path.split("::")
         | 
| 97 | 
            +
                        *rest, name = path_parts
         | 
| 98 | 
            +
             | 
| 99 | 
            +
                        raise "internal error: can't uneigen a non-eigen type" unless /^<Eigen:(.+)>$/ === name
         | 
| 100 | 
            +
                        uneigened_name = $1
         | 
| 101 | 
            +
             | 
| 102 | 
            +
                        [*rest, uneigened_name].join("::")
         | 
| 103 | 
            +
                    end
         | 
| 104 | 
            +
             | 
| 105 | 
            +
                    def resolve_all_pending_types(environment, context: nil)
         | 
| 106 | 
            +
                        @superclass = resolve_type_if_pending(superclass, self, environment)
         | 
| 107 | 
            +
                        @eigen = resolve_type_if_pending(eigen, self, environment)
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                        instance_methods.map do |method|
         | 
| 110 | 
            +
                            method.resolve_all_pending_types(environment, context: self)
         | 
| 111 | 
            +
                        end
         | 
| 112 | 
            +
             | 
| 113 | 
            +
                        type_instance_variables.keys.each do |k|
         | 
| 114 | 
            +
                            type_instance_variables[k] = resolve_type_if_pending(type_instance_variables[k], self, environment)
         | 
| 115 | 
            +
                        end
         | 
| 116 | 
            +
                    end
         | 
| 117 | 
            +
             | 
| 118 | 
            +
                    def accepts?(other)
         | 
| 119 | 
            +
                        return false unless other.is_a?(DefinedType)
         | 
| 120 | 
            +
                        
         | 
| 121 | 
            +
                        distance = 0
         | 
| 122 | 
            +
                        current = other
         | 
| 123 | 
            +
                        until current.nil?
         | 
| 124 | 
            +
                            return distance if current == self
         | 
| 125 | 
            +
             | 
| 126 | 
            +
                            current = current&.superclass
         | 
| 127 | 
            +
                            distance += 1
         | 
| 128 | 
            +
                        end
         | 
| 129 | 
            +
             | 
| 130 | 
            +
                        false
         | 
| 131 | 
            +
                    end
         | 
| 132 | 
            +
             | 
| 133 | 
            +
                    def rbs
         | 
| 134 | 
            +
                        path
         | 
| 135 | 
            +
                    end
         | 
| 136 | 
            +
                end
         | 
| 137 | 
            +
            end
         | 
| @@ -0,0 +1,79 @@ | |
| 1 | 
            +
            class Houndstooth::Environment
         | 
| 2 | 
            +
                class Method
         | 
| 3 | 
            +
                    # @return [String]
         | 
| 4 | 
            +
                    attr_reader :name
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                    # @return [<MethodType>]
         | 
| 7 | 
            +
                    attr_reader :signatures
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                    # :public, :protected or :private
         | 
| 10 | 
            +
                    # @return [Symbol]
         | 
| 11 | 
            +
                    attr_reader :visibility
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                    # If a symbol, the kind of constness this method has:
         | 
| 14 | 
            +
                    #   - :normal, defined as user-specified source, can be used anywhere
         | 
| 15 | 
            +
                    #   - :internal, defined in Houndstooth, can be used anywhere
         | 
| 16 | 
            +
                    #   - :required, defined as user-specified source, can only be used from a const context
         | 
| 17 | 
            +
                    #   - :required_internal, defined in Houndstooth, can only be used from a const context
         | 
| 18 | 
            +
                    # If nil, this method is not const.
         | 
| 19 | 
            +
                    # @return [Symbol, nil]
         | 
| 20 | 
            +
                    attr_reader :const
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                    def const?; !const.nil?; end
         | 
| 23 | 
            +
                    def const_internal?; const == :internal || const == :required_internal; end
         | 
| 24 | 
            +
                    def const_required?; const == :required || const == :required_internal; end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                    # The instruction block which implements this method.
         | 
| 27 | 
            +
                    # @return [InstructionBlock]
         | 
| 28 | 
            +
                    attr_accessor :instruction_block
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                    def initialize(name, signatures = nil, visibility: :public, const: false)
         | 
| 31 | 
            +
                        @name = name
         | 
| 32 | 
            +
                        @signatures = signatures || []
         | 
| 33 | 
            +
                        @visibility = visibility
         | 
| 34 | 
            +
                        @const = const
         | 
| 35 | 
            +
                    end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                    def resolve_all_pending_types(environment, context:)
         | 
| 38 | 
            +
                        signatures.map do |sig|
         | 
| 39 | 
            +
                            sig.resolve_all_pending_types(environment, context: context)
         | 
| 40 | 
            +
                        end
         | 
| 41 | 
            +
                    end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                    def substitute_type_parameters(instance, call_type_args)
         | 
| 44 | 
            +
                        raise 'internal error: tried to substitute parameters on a Method; too high in the hierarchy for this to be sensible'
         | 
| 45 | 
            +
                    end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                    # Given a set of arguments and their types, resolves and returns the best matching signature
         | 
| 48 | 
            +
                    # of this method.
         | 
| 49 | 
            +
                    #
         | 
| 50 | 
            +
                    # If multiple signatures match, the "best" is chosen according to the distance rules used
         | 
| 51 | 
            +
                    # by `Type#accepts?` - the type with the lowest distance over all arguments is returned.
         | 
| 52 | 
            +
                    # If no signatures match, returns nil.
         | 
| 53 | 
            +
                    #
         | 
| 54 | 
            +
                    # @param [TypeInstance] instance
         | 
| 55 | 
            +
                    # @param [<(Instructions::Argument, Type)>] arguments
         | 
| 56 | 
            +
                    # @param [<Type>] type_arguments
         | 
| 57 | 
            +
                    # @return [MethodType, nil]
         | 
| 58 | 
            +
                    def resolve_matching_signature(instance, arguments, type_arguments = nil)
         | 
| 59 | 
            +
                        type_arguments ||= []
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                        sigs_with_scores = signatures
         | 
| 62 | 
            +
                            .map do |sig|
         | 
| 63 | 
            +
                                # Create {name => type} type argument mapping, or if the numbers mismatch
         | 
| 64 | 
            +
                                # return false, as this signature cannot match
         | 
| 65 | 
            +
                                next [nil, false] if type_arguments.length != sig.type_parameters.length
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                                call_type_args = sig.type_parameters.zip(type_arguments).to_h
         | 
| 68 | 
            +
                                [sig, sig.substitute_type_parameters(instance, call_type_args).accepts_arguments?(arguments)]
         | 
| 69 | 
            +
                            end
         | 
| 70 | 
            +
                            .reject { |_, r| r == false }
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                        if sigs_with_scores.any?
         | 
| 73 | 
            +
                            sigs_with_scores.min_by { |sig, score| score }[0]
         | 
| 74 | 
            +
                        else
         | 
| 75 | 
            +
                            nil
         | 
| 76 | 
            +
                        end
         | 
| 77 | 
            +
                    end
         | 
| 78 | 
            +
                end
         | 
| 79 | 
            +
            end
         | 
| @@ -0,0 +1,144 @@ | |
| 1 | 
            +
            class Houndstooth::Environment
         | 
| 2 | 
            +
                class MethodType < Type
         | 
| 3 | 
            +
                    # @return [<PositionalParameter>]
         | 
| 4 | 
            +
                    attr_accessor :positional_parameters
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                    # @return [<KeywordParameter>]
         | 
| 7 | 
            +
                    attr_accessor :keyword_parameters
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                    # @return [PositionalParameter, nil]
         | 
| 10 | 
            +
                    attr_accessor :rest_positional_parameter
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                    # @return [KeywordParameter, nil]
         | 
| 13 | 
            +
                    attr_accessor :rest_keyword_parameter
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                    # @return [BlockParameter, nil]
         | 
| 16 | 
            +
                    attr_accessor :block_parameter
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                    # @return [Type]
         | 
| 19 | 
            +
                    attr_accessor :return_type
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                    # @return [<String>]
         | 
| 22 | 
            +
                    attr_accessor :type_parameters
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                    def initialize(positional: [], keyword: [], rest_positional: nil, rest_keyword: nil, block: nil, return_type: nil, type_parameters: nil)
         | 
| 25 | 
            +
                        super()
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                        @positional_parameters = positional
         | 
| 28 | 
            +
                        @keyword_parameters = keyword
         | 
| 29 | 
            +
                        @rest_positional_parameter = rest_positional
         | 
| 30 | 
            +
                        @rest_keyword_parameter = rest_keyword
         | 
| 31 | 
            +
                        @block_parameter = block
         | 
| 32 | 
            +
                        @return_type = return_type || VoidType.new
         | 
| 33 | 
            +
                        @type_parameters = type_parameters || []
         | 
| 34 | 
            +
                    end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                    def resolve_all_pending_types(environment, context:)
         | 
| 37 | 
            +
                        @return_type = resolve_type_if_pending(return_type, context, environment)
         | 
| 38 | 
            +
                        
         | 
| 39 | 
            +
                        positional_parameters.map do |param|
         | 
| 40 | 
            +
                            param.resolve_all_pending_types(environment, context: context)
         | 
| 41 | 
            +
                        end
         | 
| 42 | 
            +
                        
         | 
| 43 | 
            +
                        keyword_parameters.map do |param|
         | 
| 44 | 
            +
                            param.resolve_all_pending_types(environment, context: context)
         | 
| 45 | 
            +
                        end
         | 
| 46 | 
            +
                        
         | 
| 47 | 
            +
                        rest_positional_parameter&.resolve_all_pending_types(environment, context: context)
         | 
| 48 | 
            +
                        rest_keyword_parameter&.resolve_all_pending_types(environment, context: context)
         | 
| 49 | 
            +
                        block_parameter&.resolve_all_pending_types(environment, context: context)
         | 
| 50 | 
            +
                    end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                    # Determines whether this method can be called with the given arguments and their types.
         | 
| 53 | 
            +
                    # Follows the same return-value rules as `accepts?`.
         | 
| 54 | 
            +
                    #
         | 
| 55 | 
            +
                    # @param [<(Instructions::Argument, Type)>] arguments
         | 
| 56 | 
            +
                    # @return [Integer, Boolean]
         | 
| 57 | 
            +
                    def accepts_arguments?(arguments)
         | 
| 58 | 
            +
                        distance_total = 0
         | 
| 59 | 
            +
                        args_index = 0
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                        # Check the positional parameters first
         | 
| 62 | 
            +
                        positional_parameters.each do |param|
         | 
| 63 | 
            +
                            # Is there also a positional argument in this index slot?
         | 
| 64 | 
            +
                            this_arg, this_type = arguments[args_index]
         | 
| 65 | 
            +
                            if this_arg.is_a?(Houndstooth::Instructions::PositionalArgument)
         | 
| 66 | 
            +
                                # Yes, so this argument was definitely passed to this parameter
         | 
| 67 | 
            +
                                # Are the types compatible?
         | 
| 68 | 
            +
                                dist = param.type.accepts?(this_type)
         | 
| 69 | 
            +
                                if dist
         | 
| 70 | 
            +
                                    # Yep! All is well. Add to total distance
         | 
| 71 | 
            +
                                    distance_total += dist
         | 
| 72 | 
            +
                                    args_index += 1
         | 
| 73 | 
            +
                                else
         | 
| 74 | 
            +
                                    # Nope, this isn't valid. Bail
         | 
| 75 | 
            +
                                    return false
         | 
| 76 | 
            +
                                end
         | 
| 77 | 
            +
                            else
         | 
| 78 | 
            +
                                # No positional argument - but that's OK if this parameter is optional
         | 
| 79 | 
            +
                                if !param.optional?
         | 
| 80 | 
            +
                                    # Missing argument not allowed
         | 
| 81 | 
            +
                                    return false
         | 
| 82 | 
            +
                                end
         | 
| 83 | 
            +
                            end
         | 
| 84 | 
            +
                        end
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                        # Are there any positional arguments left over?
         | 
| 87 | 
            +
                        while arguments[args_index] && arguments[args_index][0].is_a?(Houndstooth::Instructions::PositionalArgument)
         | 
| 88 | 
            +
                            this_arg, this_type = arguments[args_index]
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                            # Is there a rest-parameter to take these?
         | 
| 91 | 
            +
                            if !rest_positional_parameter.nil?
         | 
| 92 | 
            +
                                # Yep, but does this argument match the type of the rest positional?
         | 
| 93 | 
            +
                                dist = param.type.accepts?(this_type)
         | 
| 94 | 
            +
                                if dist
         | 
| 95 | 
            +
                                    # Correct - this is passed into the splat!
         | 
| 96 | 
            +
                                    distance_total += dist
         | 
| 97 | 
            +
                                    args_index += 1
         | 
| 98 | 
            +
                                else
         | 
| 99 | 
            +
                                    # Not the right type for the splat, invalid
         | 
| 100 | 
            +
                                    return false
         | 
| 101 | 
            +
                                end
         | 
| 102 | 
            +
                            else
         | 
| 103 | 
            +
                                # No, error - too many arguments
         | 
| 104 | 
            +
                                return false
         | 
| 105 | 
            +
                            end
         | 
| 106 | 
            +
                        end
         | 
| 107 | 
            +
             | 
| 108 | 
            +
                        # TODO: keyword arguments
         | 
| 109 | 
            +
                        raise "keyword argument checking not implemeneted" \
         | 
| 110 | 
            +
                            if arguments.find { |x, _| x.is_a?(Houndstooth::Instructions::KeywordArgument) }
         | 
| 111 | 
            +
             | 
| 112 | 
            +
                        distance_total
         | 
| 113 | 
            +
                    end
         | 
| 114 | 
            +
             | 
| 115 | 
            +
                    def substitute_type_parameters(instance, call_type_args)
         | 
| 116 | 
            +
                        clone.tap do |t|
         | 
| 117 | 
            +
                            t.positional_parameters = t.positional_parameters.map { |p| p.substitute_type_parameters(instance, call_type_args) }
         | 
| 118 | 
            +
                            t.keyword_parameters = t.keyword_parameters.map { |p| p.substitute_type_parameters(instance, call_type_args) }
         | 
| 119 | 
            +
             | 
| 120 | 
            +
                            t.rest_positional_parameter = t.rest_positional_parameter&.substitute_type_parameters(instance, call_type_args)
         | 
| 121 | 
            +
                            t.rest_keyword_parameter = t.rest_keyword_parameter&.substitute_type_parameters(instance, call_type_args)
         | 
| 122 | 
            +
                            t.block_parameter = t.block_parameter&.substitute_type_parameters(instance, call_type_args)
         | 
| 123 | 
            +
             | 
| 124 | 
            +
                            t.return_type = t.return_type.substitute_type_parameters(instance, call_type_args)
         | 
| 125 | 
            +
                        end
         | 
| 126 | 
            +
                    end
         | 
| 127 | 
            +
             | 
| 128 | 
            +
                    # TODO: implement accepts?
         | 
| 129 | 
            +
             | 
| 130 | 
            +
                    def rbs
         | 
| 131 | 
            +
                        params = 
         | 
| 132 | 
            +
                            [positional_parameters.map(&:rbs), keyword_parameters.map(&:rbs)].flatten.join(", ")
         | 
| 133 | 
            +
             | 
| 134 | 
            +
                        if type_parameters.any?
         | 
| 135 | 
            +
                            type_params = "[#{type_parameters.join(', ')}] "
         | 
| 136 | 
            +
                        else
         | 
| 137 | 
            +
                            type_params = ''
         | 
| 138 | 
            +
                        end
         | 
| 139 | 
            +
                            
         | 
| 140 | 
            +
                        "#{type_params}(#{params}) #{block_parameter ? "#{block_parameter.rbs} " : ''}-> #{return_type.rbs}"
         | 
| 141 | 
            +
                    end
         | 
| 142 | 
            +
                end
         | 
| 143 | 
            +
            end
         | 
| 144 | 
            +
                
         | 
| @@ -0,0 +1,53 @@ | |
| 1 | 
            +
            class Houndstooth::Environment
         | 
| 2 | 
            +
                class Parameter < Type
         | 
| 3 | 
            +
                    # Note: Parameters aren't *really* a type, but we need `resolve_type_if_pending`
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                    # @return [Name]
         | 
| 6 | 
            +
                    attr_reader :name
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                    # @return [Type]
         | 
| 9 | 
            +
                    attr_accessor :type
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                    # @return [Boolean]
         | 
| 12 | 
            +
                    attr_reader :optional
         | 
| 13 | 
            +
                    alias optional? optional
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                    def initialize(name, type, optional: false)
         | 
| 16 | 
            +
                        @name = name
         | 
| 17 | 
            +
                        @type = type
         | 
| 18 | 
            +
                        @optional = optional
         | 
| 19 | 
            +
                    end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                    def resolve_all_pending_types(environment, context:)
         | 
| 22 | 
            +
                        @type = resolve_type_if_pending(type, context, environment)
         | 
| 23 | 
            +
                    end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                    def substitute_type_parameters(instance, call_type_args)
         | 
| 26 | 
            +
                        clone.tap do |t|
         | 
| 27 | 
            +
                            t.type = t.type.substitute_type_parameters(instance, call_type_args)
         | 
| 28 | 
            +
                        end
         | 
| 29 | 
            +
                    end
         | 
| 30 | 
            +
                end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                class PositionalParameter < Parameter
         | 
| 33 | 
            +
                    def rbs
         | 
| 34 | 
            +
                        if name
         | 
| 35 | 
            +
                            "#{optional? ? '?' : ''}#{type.rbs} #{name}"
         | 
| 36 | 
            +
                        else
         | 
| 37 | 
            +
                            "#{optional? ? '?' : ''}#{type.rbs}"
         | 
| 38 | 
            +
                        end
         | 
| 39 | 
            +
                    end
         | 
| 40 | 
            +
                end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                class KeywordParameter < Parameter
         | 
| 43 | 
            +
                    def rbs
         | 
| 44 | 
            +
                        "#{optional? ? '?' : ''}#{name}: #{type.rbs}"
         | 
| 45 | 
            +
                    end
         | 
| 46 | 
            +
                end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                class BlockParameter < Parameter
         | 
| 49 | 
            +
                    def rbs
         | 
| 50 | 
            +
                        "#{optional? ? '?' : ''}{ #{type.rbs} }"
         | 
| 51 | 
            +
                    end
         | 
| 52 | 
            +
                end
         | 
| 53 | 
            +
            end
         | 
| @@ -0,0 +1,15 @@ | |
| 1 | 
            +
            class Houndstooth::Environment
         | 
| 2 | 
            +
                # A special type which can be used in place of a method, typically only `new`. Specifies
         | 
| 3 | 
            +
                # that the resolved instance method should actually be taken from an uneigened `initialize`.
         | 
| 4 | 
            +
                class SpecialConstructorMethod < Type
         | 
| 5 | 
            +
                    def initialize(name)
         | 
| 6 | 
            +
                        @name = name
         | 
| 7 | 
            +
                    end
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                    attr_accessor :name
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                    def rbs
         | 
| 12 | 
            +
                        "<special constructor '#{name}'>"
         | 
| 13 | 
            +
                    end 
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
            end
         | 
| @@ -0,0 +1,38 @@ | |
| 1 | 
            +
            class Houndstooth::Environment
         | 
| 2 | 
            +
                # A type which will be replaced by a type argument later.
         | 
| 3 | 
            +
                class TypeParameterPlaceholder < Type
         | 
| 4 | 
            +
                    def initialize(name)
         | 
| 5 | 
            +
                        @name = name
         | 
| 6 | 
            +
                    end
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                    attr_accessor :name
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                    def accepts?(other)
         | 
| 11 | 
            +
                        if other.is_a?(TypeParameterPlaceholder) && name == other.name
         | 
| 12 | 
            +
                            1
         | 
| 13 | 
            +
                        else
         | 
| 14 | 
            +
                            false
         | 
| 15 | 
            +
                        end
         | 
| 16 | 
            +
                    end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                    def rbs
         | 
| 19 | 
            +
                        name
         | 
| 20 | 
            +
                    end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                    def substitute_type_parameters(instance, call_type_args)
         | 
| 23 | 
            +
                        # Call type arguments take priority, check those first
         | 
| 24 | 
            +
                        return call_type_args[name] if call_type_args[name]
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                        # Get index of type parameter
         | 
| 27 | 
            +
                        index = instance.type.type_parameters.index { |tp| tp == name } or return self
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                        # Replace with type argument, which should be an instance
         | 
| 30 | 
            +
                        instance.type_arguments[index] or self
         | 
| 31 | 
            +
                    end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                    # Yikes!
         | 
| 34 | 
            +
                    # It doesn't ever make sense to instantiate a type parameter, and trying to do so was
         | 
| 35 | 
            +
                    # causing problems when passing type arguments around functions, so just don't allow it
         | 
| 36 | 
            +
                    def instantiate(...) = self
         | 
| 37 | 
            +
                end
         | 
| 38 | 
            +
            end
         | 
| @@ -0,0 +1,74 @@ | |
| 1 | 
            +
            class Houndstooth::Environment
         | 
| 2 | 
            +
                def initialize
         | 
| 3 | 
            +
                    @types = {}
         | 
| 4 | 
            +
                end
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                def add_type(type)
         | 
| 7 | 
            +
                    # Add the type and its entire eigen chain
         | 
| 8 | 
            +
                    @types[type.path] = type
         | 
| 9 | 
            +
                    add_type(type.eigen) if type.respond_to?(:eigen) && type.eigen
         | 
| 10 | 
            +
                end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                # @return [{String, DefinedType}] 
         | 
| 13 | 
            +
                attr_reader :types
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                def resolve_all_pending_types
         | 
| 16 | 
            +
                    types.each do |_, type|
         | 
| 17 | 
            +
                        type.resolve_all_pending_types(self)
         | 
| 18 | 
            +
                    end
         | 
| 19 | 
            +
                end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                # Resolve a type by path; either an absolute path from the root namespace, or optionally as a
         | 
| 22 | 
            +
                # relative path from the context of it being used within the given type.
         | 
| 23 | 
            +
                # If the type does not exist, returns nil.
         | 
| 24 | 
            +
                #
         | 
| 25 | 
            +
                # @param [String] path The path to resolve. If `type_context` is nil, this is interpreted as an
         | 
| 26 | 
            +
                #   absolute path regardless of whether it is prefixed with `::`. If `type_context` is given,
         | 
| 27 | 
            +
                #   this is interpreted as a relative path without a `::` prefix, or an absolute path with one.
         | 
| 28 | 
            +
                # @param [DefinedType] type_context Optional: The context to search from.
         | 
| 29 | 
            +
                #
         | 
| 30 | 
            +
                # @return [DefinedType, nil]
         | 
| 31 | 
            +
                def resolve_type(path, type_context: nil)
         | 
| 32 | 
            +
                    if path.start_with?('::') || type_context.nil?
         | 
| 33 | 
            +
                        # Our `#types` field is indexed by absolute path, let's just look at that!
         | 
| 34 | 
            +
                        # Prune the :: if present
         | 
| 35 | 
            +
                        path = path[2..] if path.start_with?('::')
         | 
| 36 | 
            +
                        return types[path]
         | 
| 37 | 
            +
                    end
         | 
| 38 | 
            +
                    
         | 
| 39 | 
            +
                    # This is a relative path - split into parts
         | 
| 40 | 
            +
                    path_parts = path.split('::')
         | 
| 41 | 
            +
                    return nil if path_parts.empty?
         | 
| 42 | 
            +
                    next_part, *rest_parts = *path_parts
         | 
| 43 | 
            +
                
         | 
| 44 | 
            +
                    # Does the current type context contain the next part of the path?
         | 
| 45 | 
            +
                    maybe_inner_type = types[type_context.path + '::' + next_part]
         | 
| 46 | 
            +
                    if maybe_inner_type
         | 
| 47 | 
            +
                        # Yes - either return if there's no more parts, or advance into that type and continue
         | 
| 48 | 
            +
                        # the search
         | 
| 49 | 
            +
                        if rest_parts.empty?
         | 
| 50 | 
            +
                            maybe_inner_type
         | 
| 51 | 
            +
                        else
         | 
| 52 | 
            +
                            resolve_type(rest_parts.join('::'), type_context: maybe_inner_type)
         | 
| 53 | 
            +
                        end
         | 
| 54 | 
            +
                    else
         | 
| 55 | 
            +
                        # No - check the current type context's parent
         | 
| 56 | 
            +
                        # (Or, if there's no parent, we'll try searching for it as absolute as a last-ditch
         | 
| 57 | 
            +
                        # attempt)
         | 
| 58 | 
            +
                        if type_context.path.include?('::')
         | 
| 59 | 
            +
                            resolve_type(path, type_context: types[type_context.path.split('::')[...-1].join('::')])
         | 
| 60 | 
            +
                        else
         | 
| 61 | 
            +
                            resolve_type(path, type_context: nil)
         | 
| 62 | 
            +
                        end
         | 
| 63 | 
            +
                    end
         | 
| 64 | 
            +
                end
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                def inspect
         | 
| 67 | 
            +
                    "#<Houndstooth::Environment (#{types.length} types)>"
         | 
| 68 | 
            +
                end
         | 
| 69 | 
            +
                alias to_s inspect
         | 
| 70 | 
            +
            end
         | 
| 71 | 
            +
             | 
| 72 | 
            +
            require_relative 'environment/types'
         | 
| 73 | 
            +
            require_relative 'environment/type_parser'
         | 
| 74 | 
            +
            require_relative 'environment/builder'
         | 
| @@ -0,0 +1,53 @@ | |
| 1 | 
            +
            module Houndstooth
         | 
| 2 | 
            +
                module Errors
         | 
| 3 | 
            +
                    class Error
         | 
| 4 | 
            +
                        def initialize(message, tagged_ranges)
         | 
| 5 | 
            +
                            @message = message
         | 
| 6 | 
            +
                            @tagged_ranges = tagged_ranges
         | 
| 7 | 
            +
                        end
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                        # @return [String]
         | 
| 10 | 
            +
                        attr_reader :message
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                        # @return [(Parser::Source::Range, String)]
         | 
| 13 | 
            +
                        attr_reader :tagged_ranges
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                        def format
         | 
| 16 | 
            +
                            # TODO: merge nearby errors
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                            (["Error: #{message}"] \
         | 
| 19 | 
            +
                            + tagged_ranges.flat_map do |range, hint|
         | 
| 20 | 
            +
                                # TODO: won't work if the error spans multiple lines
         | 
| 21 | 
            +
                                line_range = range.source_buffer.line_range(range.line)
         | 
| 22 | 
            +
                                begin_pos_on_line = range.begin_pos - line_range.begin_pos
         | 
| 23 | 
            +
                                length = range.end_pos - range.begin_pos
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                                [
         | 
| 26 | 
            +
                                    "",
         | 
| 27 | 
            +
                                    "  #{range.source_buffer.name}",
         | 
| 28 | 
            +
                                    "  #{range.line}  |  #{range.source_line}",
         | 
| 29 | 
            +
                                    "  #{' ' * range.line.to_s.length}     #{' ' * begin_pos_on_line}#{'^' * length} #{hint}",
         | 
| 30 | 
            +
                                ]
         | 
| 31 | 
            +
                            end).join("\n")
         | 
| 32 | 
            +
                        end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                        def push
         | 
| 35 | 
            +
                            Errors.push(self)
         | 
| 36 | 
            +
                        end
         | 
| 37 | 
            +
                    end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                    @errors = []
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                    def self.reset
         | 
| 42 | 
            +
                        @errors = []
         | 
| 43 | 
            +
                    end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                    def self.push(error)
         | 
| 46 | 
            +
                        @errors << error
         | 
| 47 | 
            +
                    end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                    def self.errors
         | 
| 50 | 
            +
                        @errors
         | 
| 51 | 
            +
                    end
         | 
| 52 | 
            +
                end
         | 
| 53 | 
            +
            end
         |