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.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +5 -0
  3. data/.rspec +1 -0
  4. data/.ruby-version +1 -0
  5. data/Gemfile +11 -0
  6. data/Gemfile.lock +49 -0
  7. data/README.md +99 -0
  8. data/bin/houndstooth.rb +183 -0
  9. data/fuzz/cases/x.rb +8 -0
  10. data/fuzz/cases/y.rb +8 -0
  11. data/fuzz/cases/z.rb +22 -0
  12. data/fuzz/ruby.dict +64 -0
  13. data/fuzz/run +21 -0
  14. data/lib/houndstooth/environment/builder.rb +260 -0
  15. data/lib/houndstooth/environment/type_parser.rb +149 -0
  16. data/lib/houndstooth/environment/types/basic/type.rb +85 -0
  17. data/lib/houndstooth/environment/types/basic/type_instance.rb +54 -0
  18. data/lib/houndstooth/environment/types/compound/union_type.rb +72 -0
  19. data/lib/houndstooth/environment/types/defined/base_defined_type.rb +23 -0
  20. data/lib/houndstooth/environment/types/defined/defined_type.rb +137 -0
  21. data/lib/houndstooth/environment/types/defined/pending_defined_type.rb +14 -0
  22. data/lib/houndstooth/environment/types/method/method.rb +79 -0
  23. data/lib/houndstooth/environment/types/method/method_type.rb +144 -0
  24. data/lib/houndstooth/environment/types/method/parameters.rb +53 -0
  25. data/lib/houndstooth/environment/types/method/special_constructor_method.rb +15 -0
  26. data/lib/houndstooth/environment/types/special/instance_type.rb +9 -0
  27. data/lib/houndstooth/environment/types/special/self_type.rb +9 -0
  28. data/lib/houndstooth/environment/types/special/type_parameter_placeholder.rb +38 -0
  29. data/lib/houndstooth/environment/types/special/untyped_type.rb +11 -0
  30. data/lib/houndstooth/environment/types/special/void_type.rb +12 -0
  31. data/lib/houndstooth/environment/types.rb +3 -0
  32. data/lib/houndstooth/environment.rb +74 -0
  33. data/lib/houndstooth/errors.rb +53 -0
  34. data/lib/houndstooth/instructions.rb +698 -0
  35. data/lib/houndstooth/interpreter/const_internal.rb +148 -0
  36. data/lib/houndstooth/interpreter/objects.rb +142 -0
  37. data/lib/houndstooth/interpreter/runtime.rb +309 -0
  38. data/lib/houndstooth/interpreter.rb +7 -0
  39. data/lib/houndstooth/semantic_node/control_flow.rb +218 -0
  40. data/lib/houndstooth/semantic_node/definitions.rb +253 -0
  41. data/lib/houndstooth/semantic_node/identifiers.rb +308 -0
  42. data/lib/houndstooth/semantic_node/keywords.rb +45 -0
  43. data/lib/houndstooth/semantic_node/literals.rb +226 -0
  44. data/lib/houndstooth/semantic_node/operators.rb +126 -0
  45. data/lib/houndstooth/semantic_node/parameters.rb +108 -0
  46. data/lib/houndstooth/semantic_node/send.rb +349 -0
  47. data/lib/houndstooth/semantic_node/super.rb +12 -0
  48. data/lib/houndstooth/semantic_node.rb +119 -0
  49. data/lib/houndstooth/stdlib.rb +6 -0
  50. data/lib/houndstooth/type_checker.rb +462 -0
  51. data/lib/houndstooth.rb +53 -0
  52. data/spec/ast_to_node_spec.rb +889 -0
  53. data/spec/environment_spec.rb +323 -0
  54. data/spec/instructions_spec.rb +291 -0
  55. data/spec/integration_spec.rb +785 -0
  56. data/spec/interpreter_spec.rb +170 -0
  57. data/spec/self_spec.rb +7 -0
  58. data/spec/spec_helper.rb +50 -0
  59. data/test/ruby_interpreter_test.rb +162 -0
  60. data/types/stdlib.htt +170 -0
  61. metadata +110 -0
@@ -0,0 +1,260 @@
1
+ class Houndstooth::Environment
2
+ # Analyses a `SemanticNode` tree and builds a set of types and definitions for an `Environment`.
3
+ #
4
+ # It's likely this entire class will be unnecessary when CTFE is implemented, but it's a nice
5
+ # starting point for a basic typing checker.
6
+ class Builder
7
+ def initialize(root, environment)
8
+ @root = root
9
+ @environment = environment
10
+ end
11
+
12
+ # @return [SemanticNode]
13
+ attr_reader :root
14
+
15
+ # @return [Environment]
16
+ attr_reader :environment
17
+
18
+ def analyze(node: root, type_context: :root)
19
+ # Note for type_context:
20
+ # - An instance of `DefinedType` means that new methods, subtypes etc encountered in
21
+ # the node tree will be defined there
22
+ # - The symbol `:root` means they're defined at the top-level of the environment
23
+ # - `nil` means types and methods cannot be defined here
24
+
25
+ # TODO: consider if there's things which will "invalidate" a type context, turning it
26
+ # to nil - do we actually bother traversing into such things?
27
+ # Even if we don't, the CTFE component presumably will
28
+
29
+ case node
30
+ when Houndstooth::SemanticNode::Body
31
+ node.nodes.each do |child_node|
32
+ analyze(node: child_node, type_context: type_context)
33
+ end
34
+
35
+ when Houndstooth::SemanticNode::ClassDefinition
36
+ is_magic_basic_object = node.comments.find { |c| c.text.strip == "#!magic basicobject" }
37
+
38
+ name = constant_to_string(node.name)
39
+ if name.nil?
40
+ Houndstooth::Errors::Error.new(
41
+ "Class name is not a constant",
42
+ [[node.name.ast_node.loc.expression, "not a constant"]]
43
+ ).push
44
+ return
45
+ end
46
+
47
+ if node.superclass
48
+ superclass = constant_to_string(node.superclass)
49
+ if superclass.nil?
50
+ Houndstooth::Errors::Error.new(
51
+ "Superclass is not a constant",
52
+ [[node.superclass.ast_node.loc.expression, "not a constant"]]
53
+ ).push
54
+ return
55
+ end
56
+ else
57
+ # Special case used only for BasicObject
58
+ if is_magic_basic_object
59
+ superclass = nil
60
+ else
61
+ superclass = "Object"
62
+ end
63
+ end
64
+
65
+ new_type = DefinedType.new(
66
+ node: node,
67
+ path: append_type_and_rel_path(type_context, name),
68
+ type_parameters: type_parameter_definitions(node),
69
+ superclass: superclass ? PendingDefinedType.new(superclass) : nil
70
+ )
71
+
72
+ if is_magic_basic_object
73
+ new_type.eigen = DefinedType.new(
74
+ path: "<Eigen:BasicObject>",
75
+ superclass: PendingDefinedType.new("Class"),
76
+ instance_methods: [
77
+ SpecialConstructorMethod.new(:new),
78
+ ],
79
+ )
80
+ end
81
+
82
+ # Find instance variable definitions
83
+ node.comments
84
+ .select { |c| c.text.start_with?('#!var ') }
85
+ .each do |c|
86
+ unless /^#!var\s+(@[a-zA-Z_][a-zA-Z0-9_]*)\s+(.+)\s*$/ === c.text
87
+ Houndstooth::Errors::Error.new(
88
+ "Malformed #!var definition",
89
+ [[c.loc.expression, "invalid"]]
90
+ ).push
91
+ next
92
+ end
93
+
94
+ var_name = $1
95
+ type = $2
96
+
97
+ new_type.type_instance_variables[var_name] = TypeParser.parse_type(type)
98
+ end
99
+
100
+ environment.add_type(new_type)
101
+
102
+ analyze(node: node.body, type_context: new_type)
103
+
104
+ when Houndstooth::SemanticNode::ModuleDefinition
105
+ name = constant_to_string(node.name)
106
+ if name.nil?
107
+ Houndstooth::Errors::Error.new(
108
+ "Class name is not a constant",
109
+ [[node.name.ast_node.loc.expression, "not a constant"]]
110
+ ).push
111
+ return
112
+ end
113
+
114
+ new_type = DefinedType.new(
115
+ node: node,
116
+ type_parameters: type_parameter_definitions(node),
117
+ path: append_type_and_rel_path(type_context, name),
118
+ )
119
+ new_type.eigen.superclass = PendingDefinedType.new("Module")
120
+ environment.add_type(new_type)
121
+
122
+ analyze(node: node.body, type_context: new_type)
123
+
124
+ when Houndstooth::SemanticNode::MethodDefinition
125
+ if node.target.nil?
126
+ target = type_context
127
+ elsif node.target.is_a?(Houndstooth::SemanticNode::SelfKeyword)
128
+ target = type_context.eigen
129
+ else
130
+ # TODO
131
+ Houndstooth::Errors::Error.new(
132
+ "`self` is the only supported explicit target",
133
+ [[node.target.ast_node.loc.expression, "unsupported"]]
134
+ ).push
135
+ return
136
+ end
137
+
138
+ name = node.name
139
+
140
+ # Look for signature comments attached to this definition - those beginning with:
141
+ # #:
142
+ # The rest of the comment is an RBS signature attached to that method
143
+ signatures = node.comments
144
+ .filter { |comment| comment.text.start_with?('#: ') }
145
+ .map do |comment|
146
+ TypeParser.parse_method_type(
147
+ comment.text[3...].strip,
148
+ type_parameters:
149
+ type_context.is_a?(DefinedType) \
150
+ ? type_context.type_parameters
151
+ : nil,
152
+ method_definition_parameters: node.parameters
153
+ )
154
+ end
155
+
156
+ # Look for a declaration that this method is const
157
+ const = nil
158
+ const_decls = node.comments
159
+ .filter { |c| c.text.start_with?('#!const') }
160
+ .map { |c| c.text }
161
+ if const_decls.length == 1
162
+ parts = const_decls.first.strip.split
163
+ if parts.length != 1 && parts.length != 2
164
+ Houndstooth::Errors::Error.new(
165
+ "Malformed #!const definition",
166
+ [[node.ast_node.loc.expression, "too many parts"]],
167
+ ).push
168
+ elsif parts[1] && !['required', 'internal', 'required_internal'].include?(parts[1])
169
+ Houndstooth::Errors::Error.new(
170
+ "Malformed #!const definition",
171
+ [[node.ast_node.loc.expression, "don't understand '#{parts[1]}'"]],
172
+ ).push
173
+ else
174
+ if parts[1]
175
+ const = parts[1].to_sym
176
+ else
177
+ const = :normal
178
+ end
179
+ end
180
+ elsif const_decls.length == 0
181
+ # That's fine, just leave `const` as nil
182
+ else
183
+ Houndstooth::Errors::Error.new(
184
+ "Only one #!const comment is allowed",
185
+ [[node.ast_node.loc.expression, "multiple #!const comments given"]],
186
+ ).push
187
+ end
188
+
189
+ if type_context.nil? || type_context == :root
190
+ # TODO method definitions should definitely be allowed at the root!
191
+ Houndstooth::Errors::Error.new(
192
+ "Method definition not allowed here",
193
+ [[node.ast_node.loc.keyword, "not allowed"]]
194
+ ).push
195
+ return
196
+ end
197
+
198
+ # TODO: Don't allow methods with duplicate names
199
+ target.instance_methods << Method.new(name, signatures, const: const)
200
+ end
201
+ end
202
+
203
+ # Tries to convert a series of nested `Constant`s into a String path.
204
+ #
205
+ # If one of the items in the constant tree is not a `Constant` (or `ConstantRoot`), returns
206
+ # nil.
207
+ #
208
+ # @param [SemanticNode::Base] node
209
+ # @return [String, nil]
210
+ def constant_to_string(node)
211
+ case node
212
+ when Houndstooth::SemanticNode::Constant
213
+ if node.target.nil?
214
+ node.name.to_s
215
+ else
216
+ target_as_str = constant_to_string(node.target) or return nil
217
+ "#{target_as_str}::#{node.name}"
218
+ end
219
+ when Houndstooth::SemanticNode::ConstantBase
220
+ ''
221
+ else
222
+ nil
223
+ end
224
+ end
225
+
226
+ # Given a `DefinedType`, appends a relative path to its path.
227
+ #
228
+ # @param [DefinedType] type
229
+ # @param [String] rel
230
+ # @return [String]
231
+ def append_type_and_rel_path(type, rel)
232
+ if rel.start_with?('::')
233
+ rel[2..]
234
+ else
235
+ if type == :root
236
+ rel
237
+ else
238
+ "#{type.path}::#{rel}"
239
+ end
240
+ end
241
+ end
242
+
243
+ # Given a node, gets any type parameters defined on it.
244
+ def type_parameter_definitions(node)
245
+ node.comments
246
+ .select { |c| c.text.start_with?('#!param ') }
247
+ .map do |c|
248
+ unless /^#!param\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*$/ === c.text
249
+ Houndstooth::Errors::Error.new(
250
+ "Malformed #!param definition",
251
+ [[c.loc.expression, "invalid"]]
252
+ ).push
253
+ return
254
+ end
255
+
256
+ $1
257
+ end
258
+ end
259
+ end
260
+ end
@@ -0,0 +1,149 @@
1
+ require 'rbs'
2
+
3
+ class Houndstooth::Environment
4
+ module TypeParser
5
+ # Parses an RBS type signature, e.g. "(String) -> Integer", and returns it as a `Type` in
6
+ # this project's type model.
7
+ #
8
+ # The types used do not necessarily need to be defined - all type references in the
9
+ # returned signature will be instances of `PendingDefinedType`, which can be converted to
10
+ # `DefinedType` using `Type#resolve_all_pending_types`.
11
+ #
12
+ # @param [String] input
13
+ # @return [Type]
14
+ def self.parse_method_type(input, type_parameters: nil, method_definition_parameters: nil)
15
+ types_from_rbs(
16
+ RBS::Parser.parse_method_type(input),
17
+ type_parameters: type_parameters,
18
+ method_definition_parameters: method_definition_parameters
19
+ )
20
+ end
21
+
22
+ # Same as `parse_method_type`, but parses a singular type, such as `String`.
23
+ #
24
+ # @param [String] input
25
+ # @return [Type]
26
+ def self.parse_type(input, type_parameters: nil, method_definition_parameters: nil)
27
+ types_from_rbs(
28
+ RBS::Parser.parse_type(input),
29
+ type_parameters: type_parameters,
30
+ method_definition_parameters: method_definition_parameters
31
+ )
32
+ end
33
+
34
+ # Converts an RBS type to this project's type model.
35
+ def self.types_from_rbs(rbs_type, type_parameters: nil, method_definition_parameters: nil)
36
+ type_parameters ||= []
37
+
38
+ case rbs_type
39
+
40
+ when RBS::MethodType, RBS::Types::Function, RBS::Types::Proc
41
+ conv = ->(klass, name, rbs, opt) do
42
+ klass.new(name, types_from_rbs(rbs.type, type_parameters: type_parameters), optional: opt)
43
+ end
44
+
45
+ # `MethodType` has a `Function` instance in its #type field
46
+ # It also has a block and type parameters, whereas `Function` does not
47
+ if rbs_type.is_a?(RBS::MethodType) || rbs_type.is_a?(RBS::Types::Proc)
48
+ # Get block parameter
49
+ block_parameter = rbs_type.block&.then { |bp| conv.(BlockParameter, nil, bp, !bp.required) }
50
+
51
+ # Get method type parameters
52
+ method_type_parameters = rbs_type.type_params.map(&:name).map(&:to_s)
53
+
54
+ # Replace `rbs_type` used throughout this method with the inner `Function`
55
+ rbs_type = rbs_type.type
56
+ else
57
+ block_parameter = nil
58
+ end
59
+
60
+ # Build up lists of positional and keyword parameters
61
+ positional_parameters =
62
+ rbs_type.required_positionals.map { |rp| conv.(PositionalParameter, rp.name, rp, false) } \
63
+ + rbs_type.optional_positionals.map { |op| conv.(PositionalParameter, op.name, op, true) }
64
+
65
+ keyword_parameters =
66
+ rbs_type.required_keywords.map { |n, rk| conv.(KeywordParameter, n, rk, false) } \
67
+ + rbs_type.optional_keywords.map { |n, ok| conv.(KeywordParameter, n, ok, true) }
68
+
69
+ # Get rest parameters
70
+ rest_positional_parameter = rbs_type.rest_positionals&.then { |rsp| conv.(PositionalParameter, rsp.name, rsp, false) }
71
+ rest_keyword_parameter = rbs_type.rest_keywords&.then { |rsk| conv.(KeywordParameter, rsk.name, rsk, false) }
72
+
73
+ # Get return type
74
+ return_type = types_from_rbs(rbs_type.return_type, type_parameters: type_parameters)
75
+
76
+ # TODO: If method definition parameter list given, check that the counts and names
77
+ # line up (or fill in the names if the definition doesn't have them)
78
+
79
+ MethodType.new(
80
+ positional: positional_parameters,
81
+ keyword: keyword_parameters,
82
+ rest_positional: rest_positional_parameter,
83
+ rest_keyword: rest_keyword_parameter,
84
+ block: block_parameter,
85
+ return_type: return_type,
86
+ type_parameters: method_type_parameters,
87
+ )
88
+
89
+ when RBS::Types::ClassInstance
90
+ # rbs_type.name is not a String, it's a RBS::TypeName which also has a #namespace
91
+ # property
92
+ # Just converting to a String with #to_s simplifies things, since it includes the
93
+ # namespace for us
94
+ if type_parameters.include?(rbs_type.name.to_s)
95
+ TypeParameterPlaceholder.new(rbs_type.name.to_s)
96
+ else
97
+ TypeInstance.new(
98
+ PendingDefinedType.new(rbs_type.name.to_s),
99
+ type_arguments: rbs_type.args.map { |t| types_from_rbs(t, type_parameters: type_parameters) }
100
+ )
101
+ end
102
+
103
+ when RBS::Types::Variable
104
+ TypeParameterPlaceholder.new(rbs_type.name.to_s)
105
+
106
+ when RBS::Types::Bases::Void
107
+ VoidType.new
108
+
109
+ when RBS::Types::Bases::Self
110
+ SelfType.new
111
+
112
+ when RBS::Types::Bases::Instance
113
+ InstanceType.new
114
+
115
+ when RBS::Types::Bases::Any # written as `untyped`
116
+ UntypedType.new
117
+
118
+ else
119
+ # TODO: handle errors like this better
120
+ raise "RBS type construct #{rbs_type.class} is not supported (usage: #{rbs_type.location.source})"
121
+ end
122
+ end
123
+
124
+ # Given a list of types as strings, conventionally a list of type arguments (but it doesn't
125
+ # actually matter), parses them into types and returns the new array. The original array is
126
+ # not modified. If any of the items in the array are not strings, they are left unchanged in
127
+ # the new array.
128
+ # @param [<String, Type>] type_args
129
+ # @return [<Type>]
130
+ def self.parse_type_arguments(env, type_args, type_parameters)
131
+ type_args.map do |arg|
132
+ if arg.is_a?(String)
133
+ # TODO: as specified in comment at instruction-generation-time, not ideal
134
+ # We don't know about other type arguments, nor the correct context
135
+ t = parse_type(arg, type_parameters: type_parameters)
136
+ t.resolve_all_pending_types(env, context: nil)
137
+
138
+ if t.is_a?(TypeInstance)
139
+ t
140
+ else
141
+ t.instantiate
142
+ end
143
+ else
144
+ arg
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,85 @@
1
+ class Houndstooth::Environment
2
+ class Type
3
+ # Looks for methods on an instance of this type.
4
+ # For example, you would resolve :+ on Integer, and :new on <Class:Integer>.
5
+ #
6
+ # @param [Symbol] method_name
7
+ # @return [Method, nil]
8
+ def resolve_instance_method(method_name, env, **_)
9
+ nil
10
+ end
11
+
12
+ # Resolves the type of an instance variable by checking this type and its superclasses.
13
+ # Returns nil if no type could be resolved.
14
+ # @param [String] name
15
+ # @return [Type, nil]
16
+ def resolve_instance_variable(name)
17
+ nil
18
+ end
19
+
20
+ def resolve_all_pending_types(environment, context: nil); end
21
+
22
+ # If the given type is an instance of `PendingDefinedType`, uses the given environment to
23
+ # resolve the type. If the type couldn't be resolved, throws an exception.
24
+ #
25
+ # @param [Type] type
26
+ # @param [Environment] environment
27
+ # @return [DefinedType]
28
+ def resolve_type_if_pending(type, context, environment)
29
+ if type.is_a?(PendingDefinedType)
30
+ new_type = environment.resolve_type(type.path, type_context: context)
31
+ raise "could not resolve type '#{type.path}'" if new_type.nil? # TODO better error
32
+ new_type
33
+ else
34
+ # Do not recurse into DefinedTypes, this could cause infinite loops if classes are
35
+ # superclasses of each other
36
+ if !type.is_a?(DefinedType)
37
+ type&.resolve_all_pending_types(environment, context: context)
38
+ end
39
+ type
40
+ end
41
+ end
42
+
43
+ # Determine whether the type `other` can be passed into a "slot" (e.g. function parameter)
44
+ # which takes this type.
45
+ #
46
+ # The return value is either:
47
+ # - A positive (or zero) integer, indicating the "distance" between the two types. Zero
48
+ # indicates an exact type match (e.g. Integer and Integer), while every increment
49
+ # indicates a level of cast (e.g. Integer -> Numeric = 1, Integer -> Object = 2).
50
+ # This can be used to select an overload which is closest to the given set of arguments
51
+ # if multiple overloads match.
52
+ # - False, if the types do not match.
53
+ def accepts?(other)
54
+ raise "unimplemented for #{self.class.name}"
55
+ end
56
+
57
+ # Returns a copy of this type with any type parameters substituted for their actual values
58
+ # based on the given instance, and if provided, the call-specific type arguments.
59
+ #
60
+ # The instance is required, but the call-specific type arguments are not, and should be
61
+ # passed as `nil` for everything except methods.
62
+ #
63
+ # Because this has no link back the method type on which arguments are being substituted,
64
+ # the caller must construct a hash of call-specific type arguments which includes their
65
+ # name.
66
+ #
67
+ # The returned type could be a partial clone, deep clone, or even not a copy at all (just
68
+ # `self`) - the implementor makes no guarantees. As such, do NOT modify the returned type.
69
+ #
70
+ # @param [TypeInstance] instance
71
+ # @param [{String => Type}] call_type_args
72
+ # @return [Type]
73
+ def substitute_type_parameters(instance, call_type_args) = self
74
+
75
+ # Returns an RBS representation of this type. Subclasses should override this.
76
+ # This will not have the same formatting as the input string this is parsed from.
77
+ def rbs
78
+ "???"
79
+ end
80
+
81
+ def instantiate(type_arguments = nil)
82
+ TypeInstance.new(self, type_arguments: type_arguments || [])
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,54 @@
1
+ class Houndstooth::Environment
2
+ # Represents type arguments passed with a usage of a type. This doesn't necessarily need to be
3
+ # an "instance" of a class - "instance" refers to a usage of a type.
4
+ class TypeInstance < Type
5
+ def initialize(type, type_arguments: nil)
6
+ @type = type
7
+ @type_arguments = type_arguments || []
8
+ end
9
+
10
+ # @return [DefinedType]
11
+ attr_accessor :type
12
+
13
+ # @return [<Type>]
14
+ attr_accessor :type_arguments
15
+
16
+ def ==(other)
17
+ other.is_a?(TypeInstance) \
18
+ && type == other.type \
19
+ && type_arguments == other.type_arguments
20
+ end
21
+
22
+ def hash = [type, type_arguments].hash
23
+
24
+ def accepts?(other)
25
+ return false unless other.is_a?(TypeInstance)
26
+
27
+ type.accepts?(other.type)
28
+ end
29
+
30
+ def resolve_instance_method(method_name, env, top_level: true)
31
+ type.resolve_instance_method(method_name, env, instance: self, top_level: top_level)
32
+ end
33
+
34
+ def resolve_all_pending_types(environment, context: nil)
35
+ @type = resolve_type_if_pending(type, context, environment)
36
+ type_arguments.map! { |type| resolve_type_if_pending(type, context, environment) }
37
+ end
38
+
39
+ def substitute_type_parameters(instance, call_type_args)
40
+ clone.tap do |t|
41
+ t.type = t.type.substitute_type_parameters(instance, call_type_args)
42
+ t.type_arguments = t.type_arguments.map { |arg| arg.substitute_type_parameters(instance, call_type_args) }
43
+ end
44
+ end
45
+
46
+ def rbs
47
+ if type_arguments.any?
48
+ "#{type.rbs}[#{type_arguments.map(&:rbs).join(', ')}]"
49
+ else
50
+ type.rbs
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,72 @@
1
+ class Houndstooth::Environment
2
+ class UnionType < Type
3
+ # The types which this union is made up of.
4
+ # @return [<Type>]
5
+ attr_accessor :types
6
+
7
+ def initialize(types)
8
+ @types = types
9
+ end
10
+
11
+ # Simplifies this union using a couple of different strategies:
12
+ # - If any of the child types is also a `UnionType`, flattens it into one longer union.
13
+ # - If some children are the same, combines them.
14
+ # - If there is only one child, returns the child.
15
+ # Returns a new type with the same references, since the latter step could return something
16
+ # other than `UnionType`.
17
+ def simplify
18
+ new_types = types.flat_map do |type|
19
+ if type.is_a?(UnionType)
20
+ type.types
21
+ else
22
+ [type]
23
+ end
24
+ end
25
+
26
+ new_types.uniq! { |x| x.hash }
27
+
28
+ if new_types.length == 1
29
+ new_types.first
30
+ else
31
+ UnionType.new(new_types)
32
+ end
33
+ end
34
+
35
+ def resolve_instance_method(method_name, env, **_)
36
+ env.resolve_type('::BasicObject').resolve_instance_method(method_name, env)
37
+ end
38
+
39
+ def resolve_all_pending_types(environment, context: nil)
40
+ types.map! { |type| resolve_type_if_pending(type, self, environment) }
41
+ end
42
+
43
+ def substitute_type_parameters(instance, call_type_args)
44
+ clone.tap do |t|
45
+ t.types = t.types.map { |type| type.substitute_type_parameters(instance, call_type_args) }
46
+ end
47
+ end
48
+
49
+ def accepts?(other)
50
+ # Normalise into an array
51
+ if other.is_a?(UnionType)
52
+ other_types = other.types
53
+ else
54
+ other_types = [other]
55
+ end
56
+
57
+ # Each of the other types should fit into one of this type's options
58
+ # Find minimum distances from each candidate and sum them, plus one since this union is
59
+ # itself a "hop"
60
+ other_types.map do |ot|
61
+ candidates = types.map { |mt| mt.accepts?(ot) }.reject { |r| r == false }
62
+ return false if candidates.empty?
63
+
64
+ candidates.min
65
+ end.sum + 1
66
+ end
67
+
68
+ def rbs
69
+ types.map(&:rbs).join(" | ")
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,23 @@
1
+ class Houndstooth::Environment
2
+ # A slightly hacky type which represents the base namespace, such as when a constant is accessed
3
+ # using ::A syntax.
4
+ # This only appears in one place; the type change of a `ConstantBaseAccessInstruction`. This is
5
+ # invalid if it appears in any context where an actual type is expected.
6
+ # Unlike other constant accesses, this does NOT represent an *instance* of the base namespace,
7
+ # because that cannot exist.
8
+ class BaseDefinedType < Type
9
+ def accepts?(other)
10
+ false
11
+ end
12
+
13
+ def name
14
+ ""
15
+ end
16
+ alias path name
17
+ alias uneigen name
18
+
19
+ def rbs
20
+ "(base)"
21
+ end
22
+ end
23
+ end