houndstooth 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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