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,462 @@
|
|
1
|
+
module Houndstooth
|
2
|
+
class TypeChecker
|
3
|
+
attr_reader :env
|
4
|
+
|
5
|
+
def initialize(env)
|
6
|
+
@env = env
|
7
|
+
end
|
8
|
+
|
9
|
+
def process_block(block, lexical_context:, self_type:, const_context:, type_parameters:)
|
10
|
+
block.environment = env
|
11
|
+
block.instructions.each do |ins|
|
12
|
+
process_instruction(ins,
|
13
|
+
lexical_context: lexical_context,
|
14
|
+
self_type: self_type,
|
15
|
+
const_context: const_context,
|
16
|
+
type_parameters: type_parameters,
|
17
|
+
)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def process_instruction(ins, lexical_context:, self_type:, const_context:, type_parameters:)
|
22
|
+
case ins
|
23
|
+
when Instructions::LiteralInstruction
|
24
|
+
assign_type_to_literal_instruction(ins)
|
25
|
+
|
26
|
+
when Instructions::ConditionalInstruction
|
27
|
+
# Special case - did the conditional of this `if` statement come from an `is_a?`
|
28
|
+
# call immediately preceding it?
|
29
|
+
index = ins.block.instructions.index { _1 == ins }
|
30
|
+
if index != 0
|
31
|
+
last_ins = ins.block.instructions[index - 1]
|
32
|
+
if last_ins.is_a?(Instructions::SendInstruction) \
|
33
|
+
&& last_ins.method_name == :is_a? \
|
34
|
+
&& last_ins.result == ins.condition \
|
35
|
+
&& last_ins.target.ruby_identifier \
|
36
|
+
&& last_ins.arguments.length == 1
|
37
|
+
|
38
|
+
# Yep, we just checked the type of a local variable!
|
39
|
+
# In the true branch, we can refine the type
|
40
|
+
# What was the argument type? Uneigen it to get the type of the variable
|
41
|
+
arg_var = last_ins.arguments.first.variable
|
42
|
+
arg_type = ins.block.variable_type_at!(arg_var, ins)
|
43
|
+
checked_var_type = env.resolve_type(arg_type.type.uneigen).instantiate
|
44
|
+
|
45
|
+
ins.true_branch.type_refinements[last_ins.target] = checked_var_type
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
process_block(ins.true_branch, lexical_context: lexical_context, self_type: self_type, const_context: const_context, type_parameters: type_parameters)
|
50
|
+
process_block(ins.false_branch, lexical_context: lexical_context, self_type: self_type, const_context: const_context, type_parameters: type_parameters)
|
51
|
+
|
52
|
+
# A conditional could return either of its branches
|
53
|
+
ins.type_change = Environment::UnionType.new([
|
54
|
+
ins.true_branch.return_type!,
|
55
|
+
ins.false_branch.return_type!,
|
56
|
+
]).simplify
|
57
|
+
|
58
|
+
when Instructions::AssignExistingInstruction
|
59
|
+
# If the assignment is to a different variable, set a typechange
|
60
|
+
if ins.result != ins.variable
|
61
|
+
ins.type_change = ins.block.variable_type_at!(ins.variable, ins)
|
62
|
+
end
|
63
|
+
|
64
|
+
when Instructions::SendInstruction
|
65
|
+
# Parse type arguments, if any
|
66
|
+
type_args = Environment::TypeParser.parse_type_arguments(env, ins.type_arguments, type_parameters)
|
67
|
+
|
68
|
+
# Get type of target
|
69
|
+
target_type = ins.block.variable_type_at!(ins.target, ins)
|
70
|
+
|
71
|
+
# Look up method on target
|
72
|
+
method = target_type.resolve_instance_method(ins.method_name, env)
|
73
|
+
if method.nil?
|
74
|
+
Houndstooth::Errors::Error.new(
|
75
|
+
"`#{target_type.rbs}` has no method named `#{ins.method_name}`",
|
76
|
+
[[ins.node.ast_node.loc.expression, "no such method"]]
|
77
|
+
).push
|
78
|
+
|
79
|
+
# Assign result to untyped so type checking can continue
|
80
|
+
# TODO: create a special "abandoned" type specifically for this purpose
|
81
|
+
ins.type_change = Environment::UntypedType.new
|
82
|
+
|
83
|
+
return
|
84
|
+
end
|
85
|
+
|
86
|
+
# Get type of all arguments
|
87
|
+
arguments_with_types = ins.arguments.map do |arg|
|
88
|
+
[arg, ins.block.variable_type_at!(arg.variable, ins)]
|
89
|
+
end
|
90
|
+
|
91
|
+
# Resolve the best method signature with these
|
92
|
+
sig = method.resolve_matching_signature(target_type, arguments_with_types, type_args)
|
93
|
+
if sig.nil?
|
94
|
+
# Special case - if the node isn't a send, then it's a generated insertion to
|
95
|
+
# an array
|
96
|
+
if !ins.node.is_a?(Houndstooth::SemanticNode::Send)
|
97
|
+
# TODO: improve error
|
98
|
+
Houndstooth::Errors::Error.new(
|
99
|
+
"Incorrect type of element in array literal",
|
100
|
+
[[ins.node.ast_node.loc.expression, "type does not match annotation"]]
|
101
|
+
).push
|
102
|
+
else
|
103
|
+
error_message =
|
104
|
+
"`#{target_type.rbs}` method `#{ins.method_name}` has no signature matching the given arguments\n"
|
105
|
+
|
106
|
+
if method.signatures.any?
|
107
|
+
error_message += "Available signatures are:\n" \
|
108
|
+
+ method.signatures.map { |s| " - #{s.substitute_type_parameters(target_type, Hash.new).rbs}" }.join("\n")
|
109
|
+
else
|
110
|
+
error_message += "Method has no signatures - did you use a #: comment?"
|
111
|
+
end
|
112
|
+
|
113
|
+
Houndstooth::Errors::Error.new(
|
114
|
+
error_message,
|
115
|
+
# TODO: feels a bit dodgy
|
116
|
+
[[ins.node.ast_node.loc.expression, "no matching signature"]] \
|
117
|
+
+ ins.node.arguments.zip(arguments_with_types).map do |node, (_, t)|
|
118
|
+
[node.node.ast_node.loc.expression, "argument type is `#{t.rbs}`"]
|
119
|
+
end
|
120
|
+
).push
|
121
|
+
end
|
122
|
+
|
123
|
+
# Assign result to untyped so type checking can continue
|
124
|
+
# TODO: create a special "abandoned" type specifically for this purpose
|
125
|
+
ins.type_change = Environment::UntypedType.new
|
126
|
+
|
127
|
+
return
|
128
|
+
end
|
129
|
+
|
130
|
+
# Resolve type arguments - don't need to perform length check here, since the sig
|
131
|
+
# wouldn't have matched if the lengths mismatched
|
132
|
+
call_type_args = sig.type_parameters.zip(type_args).to_h
|
133
|
+
sig = sig.substitute_type_parameters(target_type, call_type_args)
|
134
|
+
|
135
|
+
# Handle block
|
136
|
+
catch :abort_block do
|
137
|
+
if sig.block_parameter && ins.method_block
|
138
|
+
# This method takes a block, and was given one
|
139
|
+
# Add parameter types
|
140
|
+
throw :abort_block \
|
141
|
+
if !add_parameter_type_instructions(ins, ins.method_block, sig.block_parameter.type)
|
142
|
+
|
143
|
+
# Recurse type checking into it
|
144
|
+
# We don't support overridden block self types, so we need to special-case
|
145
|
+
# this for `define_method`, where the block's `self` is uneigened
|
146
|
+
# `define_method` also strips constness
|
147
|
+
if method == env.resolve_type('::Module').resolve_instance_method(:define_method, env)
|
148
|
+
# HACK: Won't work with generics
|
149
|
+
inner_self_type = env.resolve_type(self_type.type.uneigen).instantiate
|
150
|
+
inner_const_context = false
|
151
|
+
else
|
152
|
+
inner_self_type = self_type
|
153
|
+
inner_const_context = const_context
|
154
|
+
end
|
155
|
+
process_block(ins.method_block, lexical_context: lexical_context, self_type: inner_self_type, const_context: inner_const_context, type_parameters: type_parameters)
|
156
|
+
|
157
|
+
# Check return type
|
158
|
+
if !sig.block_parameter.type.return_type.accepts?(ins.method_block.return_type!)
|
159
|
+
Houndstooth::Errors::Error.new(
|
160
|
+
"Incorrect return type for block, expected `#{sig.block_parameter.type.return_type.rbs}`",
|
161
|
+
[[
|
162
|
+
ins.method_block.instructions.last.node.ast_node.loc.expression,
|
163
|
+
"got `#{ins.method_block.return_type!.rbs}`"
|
164
|
+
]]
|
165
|
+
).push
|
166
|
+
throw :abort_block
|
167
|
+
end
|
168
|
+
|
169
|
+
elsif sig.block_parameter && !ins.method_block
|
170
|
+
# This method takes a block, but wasn't given one
|
171
|
+
# If the block is not optional, error
|
172
|
+
if !sig.block_parameter.optional?
|
173
|
+
Houndstooth::Errors::Error.new(
|
174
|
+
"`#{target_type.rbs}` method `#{ins.method_name}` requires a block, but none was given",
|
175
|
+
[[ins.node.ast_node.loc.expression, "expected block"]]
|
176
|
+
).push
|
177
|
+
end
|
178
|
+
elsif !sig.block_parameter && ins.method_block
|
179
|
+
# This method doesn't take a block, but was given one
|
180
|
+
# That's not allowed!
|
181
|
+
# (Well, Ruby allows it, but it doesn't make sense for us - 99% of the time,
|
182
|
+
# this will be a bug)
|
183
|
+
if ins.method_block
|
184
|
+
Houndstooth::Errors::Error.new(
|
185
|
+
"`#{target_type.rbs}` method `#{ins.method_name}` does not accept a block",
|
186
|
+
[[ins.node.ast_node.loc.expression, "unexpected block"]]
|
187
|
+
).push
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
# Check for return type special cases
|
193
|
+
case sig.return_type
|
194
|
+
when Environment::SelfType
|
195
|
+
ins.type_change = target_type
|
196
|
+
when Environment::InstanceType
|
197
|
+
ins.type_change = env.resolve_type(target_type.type.uneigen).instantiate(target_type.type_arguments)
|
198
|
+
else
|
199
|
+
# No special cases, set result variable to return type
|
200
|
+
ins.type_change = sig.return_type
|
201
|
+
end
|
202
|
+
|
203
|
+
# If we're in a const context, check that the target method is const
|
204
|
+
if const_context && !method.const?
|
205
|
+
Houndstooth::Errors::Error.new(
|
206
|
+
"`#{target_type.rbs}` method `#{ins.method_name}` is not const, so cannot be called from a const context",
|
207
|
+
[[ins.node.ast_node.loc.expression, "call inside a const context"]]
|
208
|
+
).push
|
209
|
+
end
|
210
|
+
|
211
|
+
# If this method is const-required, check that the call was const-considered, or the
|
212
|
+
# method we're currently in is const-required
|
213
|
+
if method.const_required? && !ins.const_considered?
|
214
|
+
Houndstooth::Errors::Error.new(
|
215
|
+
"`#{target_type.rbs}` method `#{ins.method_name}` is const-required, but this call is not within a const-required context",
|
216
|
+
[[ins.node.ast_node.loc.expression, "call outside a const context"]]
|
217
|
+
).push
|
218
|
+
end
|
219
|
+
|
220
|
+
when Instructions::ConstantBaseAccessInstruction
|
221
|
+
ins.type_change = Environment::BaseDefinedType.new
|
222
|
+
|
223
|
+
when Instructions::ConstantAccessInstruction
|
224
|
+
if ins.target
|
225
|
+
# TODO: will only work with types, not actual constants
|
226
|
+
target = ins.block.variable_type_at!(ins.target, ins)
|
227
|
+
else
|
228
|
+
target = lexical_context
|
229
|
+
end
|
230
|
+
resolved = env.resolve_type(ins.name.to_s, type_context: env.resolve_type(target.uneigen))
|
231
|
+
|
232
|
+
if resolved.nil?
|
233
|
+
Houndstooth::Errors::Error.new(
|
234
|
+
"No constant named `#{ins.name}` on `#{target.rbs}`",
|
235
|
+
[[ins.node.ast_node.loc.expression, "no such constant"]]
|
236
|
+
).push
|
237
|
+
|
238
|
+
# Assign result to untyped so type checking can continue
|
239
|
+
# TODO: another use for "abandoned" type
|
240
|
+
ins.type_change = Environment::UntypedType.new
|
241
|
+
return
|
242
|
+
end
|
243
|
+
|
244
|
+
# Check type parameter numbers (even if the type doesn't have any - we don't want
|
245
|
+
# to allow type arguments when none are expected)
|
246
|
+
el = resolved.type_parameters.length
|
247
|
+
gl = ins.type_arguments.length
|
248
|
+
if el != gl
|
249
|
+
Houndstooth::Errors::Error.new(
|
250
|
+
"Insufficient type arguments for `#{ins.name}` (expected #{el}, got #{gl})",
|
251
|
+
[[ins.node.ast_node.loc.expression, "incorrect number of arguments"]]
|
252
|
+
).push
|
253
|
+
|
254
|
+
# Assign result to untyped so type checking can continue
|
255
|
+
# TODO: another use for "abandoned" type
|
256
|
+
ins.type_change = Environment::UntypedType.new
|
257
|
+
return
|
258
|
+
end
|
259
|
+
|
260
|
+
# Does the type require type parameters?
|
261
|
+
if resolved.type_parameters.any?
|
262
|
+
# Yep - parse them
|
263
|
+
type_args = Environment::TypeParser.parse_type_arguments(env, ins.type_arguments, type_parameters)
|
264
|
+
else
|
265
|
+
type_args = []
|
266
|
+
end
|
267
|
+
|
268
|
+
ins.type_change = resolved.eigen.instantiate(type_args)
|
269
|
+
|
270
|
+
when Instructions::SelfInstruction
|
271
|
+
ins.type_change = self_type
|
272
|
+
|
273
|
+
when Instructions::TypeDefinitionInstruction
|
274
|
+
if ins.target
|
275
|
+
base_type = ins.block.variable_type_at!(ins.target, ins)
|
276
|
+
else
|
277
|
+
base_type = lexical_context
|
278
|
+
end
|
279
|
+
|
280
|
+
type_being_defined = env.resolve_type("#{base_type.uneigen}::#{ins.name}").eigen
|
281
|
+
type_being_defined_inst = type_being_defined.instantiate
|
282
|
+
|
283
|
+
process_block(
|
284
|
+
ins.body,
|
285
|
+
lexical_context: type_being_defined,
|
286
|
+
self_type: type_being_defined_inst,
|
287
|
+
# Type definitions bodies are always const
|
288
|
+
const_context: true,
|
289
|
+
type_parameters: type_parameters + type_being_defined.type_parameters,
|
290
|
+
)
|
291
|
+
|
292
|
+
# Returns the just-defined type
|
293
|
+
ins.type_change = type_being_defined_inst
|
294
|
+
|
295
|
+
when Instructions::MethodDefinitionInstruction
|
296
|
+
method, inner_self_type = ins.resolve_method_and_type(self_type, env)
|
297
|
+
|
298
|
+
# If this method is const-required, mark its body as const-considered
|
299
|
+
if method.const_required?
|
300
|
+
ins.body.mark_const_considered
|
301
|
+
end
|
302
|
+
|
303
|
+
# Does it have any signatures?
|
304
|
+
if method.signatures.empty?
|
305
|
+
Houndstooth::Errors::Error.new(
|
306
|
+
"No signatures provided",
|
307
|
+
[[ins.node.ast_node.loc.expression, "no signatures"]]
|
308
|
+
).push
|
309
|
+
end
|
310
|
+
|
311
|
+
# Check each signature
|
312
|
+
method.signatures.map do |sig|
|
313
|
+
# Assign parameter types
|
314
|
+
number_of_type_ins = add_parameter_type_instructions(ins, ins.body, sig)
|
315
|
+
if number_of_type_ins == false
|
316
|
+
next
|
317
|
+
end
|
318
|
+
|
319
|
+
# Recurse into body
|
320
|
+
process_block(
|
321
|
+
ins.body,
|
322
|
+
self_type: inner_self_type,
|
323
|
+
lexical_context: lexical_context,
|
324
|
+
# Method definition bodies are const iff the method being defined is
|
325
|
+
const_context: method.const?,
|
326
|
+
type_parameters: type_parameters + sig.type_parameters,
|
327
|
+
)
|
328
|
+
|
329
|
+
# Check return type
|
330
|
+
if !sig.return_type.accepts?(ins.body.return_type!)
|
331
|
+
Houndstooth::Errors::Error.new(
|
332
|
+
"Incorrect return type for method, expected `#{sig.return_type.rbs}`",
|
333
|
+
[[
|
334
|
+
ins.body.instructions.last.node.ast_node.loc.expression,
|
335
|
+
"got `#{ins.body.return_type!.rbs}`"
|
336
|
+
]]
|
337
|
+
).push
|
338
|
+
end
|
339
|
+
|
340
|
+
# If there's more than one, remove type instructions so the next loop can add
|
341
|
+
# them again
|
342
|
+
# (We could remove them if there's only one too, but it's handy to leave in for
|
343
|
+
# debugging)
|
344
|
+
if method.signatures.length > 1
|
345
|
+
number_of_type_ins.times { ins.body.instructions.shift }
|
346
|
+
end
|
347
|
+
end
|
348
|
+
|
349
|
+
# Returns a symbol of the method's name
|
350
|
+
ins.type_change = env.resolve_type("Symbol").instantiate
|
351
|
+
|
352
|
+
when Instructions::ToStringInstruction
|
353
|
+
ins.type_change = env.resolve_type("String").instantiate
|
354
|
+
|
355
|
+
when Instructions::InstanceVariableReadInstruction
|
356
|
+
var_type = self_type.type.resolve_instance_variable(ins.name)
|
357
|
+
if var_type.nil?
|
358
|
+
Houndstooth::Errors::Error.new(
|
359
|
+
"Instance variable #{ins.name} is not defined",
|
360
|
+
[[ins.node.ast_node.loc.expression, "undefined"]]
|
361
|
+
).push
|
362
|
+
ins.type_change = Environment::UntypedType.new
|
363
|
+
return
|
364
|
+
end
|
365
|
+
|
366
|
+
ins.type_change = var_type
|
367
|
+
|
368
|
+
when Instructions::InstanceVariableWriteInstruction
|
369
|
+
var_type = self_type.type.resolve_instance_variable(ins.name)
|
370
|
+
if var_type.nil?
|
371
|
+
Houndstooth::Errors::Error.new(
|
372
|
+
"Instance variable #{ins.name} is not defined",
|
373
|
+
[[ins.node.ast_node.loc.expression, "undefined"]]
|
374
|
+
).push
|
375
|
+
ins.type_change = Environment::UntypedType.new
|
376
|
+
return
|
377
|
+
end
|
378
|
+
|
379
|
+
value_type = ins.block.variable_type_at!(ins.value, ins)
|
380
|
+
|
381
|
+
if !var_type.accepts?(value_type)
|
382
|
+
Houndstooth::Errors::Error.new(
|
383
|
+
"Cannot assign `#{value_type.rbs}` to #{ins.name}",
|
384
|
+
[[ins.node.ast_node.loc.expression, "Expected `#{var_type.rbs}`"]]
|
385
|
+
).push
|
386
|
+
end
|
387
|
+
|
388
|
+
ins.type_change = var_type
|
389
|
+
|
390
|
+
else
|
391
|
+
raise "internal error: don\'t know how to type check #{ins.class.to_s}"
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
395
|
+
# Given a `LiteralInstruction`, assigns a result type based on the value. Assumes that the
|
396
|
+
# stdlib is loaded into the environment.
|
397
|
+
# @param [LiteralInstruction] ins
|
398
|
+
def assign_type_to_literal_instruction(ins)
|
399
|
+
ins.type_change =
|
400
|
+
case ins.value
|
401
|
+
when Integer
|
402
|
+
env.resolve_type("Integer").instantiate
|
403
|
+
when Float
|
404
|
+
env.resolve_type("Float").instantiate
|
405
|
+
when String
|
406
|
+
env.resolve_type("String").instantiate
|
407
|
+
when Symbol
|
408
|
+
env.resolve_type("Symbol").instantiate
|
409
|
+
when TrueClass
|
410
|
+
env.resolve_type("TrueClass").instantiate
|
411
|
+
when FalseClass
|
412
|
+
env.resolve_type("FalseClass").instantiate
|
413
|
+
when NilClass
|
414
|
+
env.resolve_type("NilClass").instantiate
|
415
|
+
else
|
416
|
+
Houndstooth::Errors::Error.new(
|
417
|
+
"Internal bug - encountered a literal with an unknown type",
|
418
|
+
[[ins.node.ast_node.loc.expression, "literal"]]
|
419
|
+
).push
|
420
|
+
end
|
421
|
+
end
|
422
|
+
|
423
|
+
# Inserts instructions into the beginning of block to assign types to a set of parameter
|
424
|
+
# variables.
|
425
|
+
#
|
426
|
+
# @param [Instruction] ins The instruction this is relevant to. Only used for error
|
427
|
+
# reporting and for the nodes of the new instructions.
|
428
|
+
# @param [InstructionBlock] block The instruction block to prepend the new instructions to.
|
429
|
+
# @param [MethodType] method_type The method type to retrieve parameter types from.
|
430
|
+
# @return [Integer, false] False if an error occurred, otherwise the number of instructions
|
431
|
+
# added to the beginning.
|
432
|
+
def add_parameter_type_instructions(ins, block, method_type)
|
433
|
+
# Check parameter count
|
434
|
+
# TODO: this won't work if we support other kinds of parameter
|
435
|
+
expected_ps = method_type.positional_parameters
|
436
|
+
got_ps = block.parameters
|
437
|
+
expected_l = expected_ps.length
|
438
|
+
got_l = got_ps.length
|
439
|
+
if expected_l != got_l
|
440
|
+
Houndstooth::Errors::Error.new(
|
441
|
+
"Incorrect number of parameters (expected #{expected_l}, got #{got_l})",
|
442
|
+
[[ins.node.ast_node.loc.expression, "incorrect parameters"]]
|
443
|
+
).push
|
444
|
+
return false
|
445
|
+
end
|
446
|
+
|
447
|
+
# Insert an instruction to assign each parameter's type
|
448
|
+
expected_ps.zip(got_ps).each do |type_param, var|
|
449
|
+
i = Instructions::AssignExistingInstruction.new(
|
450
|
+
block: block,
|
451
|
+
node: ins.node,
|
452
|
+
result: var,
|
453
|
+
variable: var,
|
454
|
+
)
|
455
|
+
i.type_change = type_param.type
|
456
|
+
block.instructions.unshift(i)
|
457
|
+
end
|
458
|
+
|
459
|
+
expected_ps.length
|
460
|
+
end
|
461
|
+
end
|
462
|
+
end
|
data/lib/houndstooth.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
module Houndstooth; end
|
2
|
+
|
3
|
+
require_relative 'houndstooth/errors'
|
4
|
+
require_relative 'houndstooth/instructions'
|
5
|
+
require_relative 'houndstooth/semantic_node'
|
6
|
+
require_relative 'houndstooth/environment'
|
7
|
+
require_relative 'houndstooth/stdlib'
|
8
|
+
require_relative 'houndstooth/type_checker'
|
9
|
+
require_relative 'houndstooth/interpreter'
|
10
|
+
|
11
|
+
module Houndstooth
|
12
|
+
# Parses a complete file, and adds its type definitions to the given environment.
|
13
|
+
# Returns the parsed `SemanticNode`, or if a syntax error occurs, returns `nil`.
|
14
|
+
def self.process_file(file_name, file_contents, env)
|
15
|
+
# Build parser buffer
|
16
|
+
begin
|
17
|
+
buffer = Parser::Source::Buffer.new(file_name)
|
18
|
+
buffer.source = file_contents
|
19
|
+
rescue => e
|
20
|
+
Houndstooth::Errors::Error.new("Error building parse buffer: #{e}", []).push
|
21
|
+
abort_on_error!
|
22
|
+
end
|
23
|
+
|
24
|
+
# Parse file into AST nodes
|
25
|
+
any_errors = false
|
26
|
+
parser = Parser::Ruby30.new
|
27
|
+
|
28
|
+
parser.diagnostics.consumer = ->(diag) do
|
29
|
+
any_errors = true
|
30
|
+
Houndstooth::Errors::Error.new(
|
31
|
+
"Syntax error",
|
32
|
+
[[diag.location, diag.message]]
|
33
|
+
).push
|
34
|
+
end
|
35
|
+
begin
|
36
|
+
ast_node, comments = parser.parse_with_comments(buffer)
|
37
|
+
rescue Parser::SyntaxError => e
|
38
|
+
# We already got a diagnostic for this, don't need to handle it again
|
39
|
+
end
|
40
|
+
$comments = comments
|
41
|
+
|
42
|
+
return nil if any_errors
|
43
|
+
|
44
|
+
# Convert to semantic nodes
|
45
|
+
node = Houndstooth::SemanticNode.from_ast(ast_node)
|
46
|
+
|
47
|
+
# Build environment items
|
48
|
+
Houndstooth::Environment::Builder.new(node, env).analyze
|
49
|
+
|
50
|
+
node
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|