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