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,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
|