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,323 @@
|
|
1
|
+
RSpec.describe Houndstooth::Environment do
|
2
|
+
E = Houndstooth::Environment
|
3
|
+
|
4
|
+
def resolve(t)
|
5
|
+
t.resolve_all_pending_types(subject, context: nil)
|
6
|
+
t
|
7
|
+
end
|
8
|
+
|
9
|
+
before :each do
|
10
|
+
Houndstooth::Stdlib.add_types(subject)
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'can resolve methods' do
|
14
|
+
cases = [
|
15
|
+
# Target type Method Valid?
|
16
|
+
['<Eigen:Object>', :new, true ], # Object.new - defined here
|
17
|
+
['Object', :new, false], # Object.new.new - not OK
|
18
|
+
|
19
|
+
['<Eigen:Class>', :new, true ], # Class.new - inherited from <Eigen:Object>
|
20
|
+
['Class', :new, true ], # Class.new.new - defined here
|
21
|
+
|
22
|
+
['<Eigen:Class>', :superclass, true ], # Class.superclass - defined here
|
23
|
+
['Class', :superclass, true ], # Class.new.superclass - defined here
|
24
|
+
|
25
|
+
['<Eigen:String>', :new, true ], # String.new - inherited from <Eigen:Object>
|
26
|
+
['<Eigen:String>', :superclass, true ], # String.superclass - inherited from <Eigen:Class>
|
27
|
+
['String', :superclass, false], # "foo".superclass - not OK
|
28
|
+
|
29
|
+
['String', :inspect, true ], # "foo".inspect - inherited from Object
|
30
|
+
['String', :length, true ], # "foo".length - defined here
|
31
|
+
|
32
|
+
['<Eigen:String>', :nesting, true ], # String.nesting - inherited from Module
|
33
|
+
['String', :nesting, false], # "foo" - not OK
|
34
|
+
]
|
35
|
+
|
36
|
+
cases.each do |type, method, valid|
|
37
|
+
expect(subject.types[type].resolve_instance_method(method, subject)).send(valid ? :not_to : :to, be_nil)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'can resolve types' do
|
42
|
+
subject.add_type E::DefinedType.new(path: 'A')
|
43
|
+
subject.add_type E::DefinedType.new(path: 'A::B')
|
44
|
+
subject.add_type E::DefinedType.new(path: 'A::B::A')
|
45
|
+
subject.add_type E::DefinedType.new(path: 'A::C')
|
46
|
+
subject.add_type E::DefinedType.new(path: 'A::D')
|
47
|
+
subject.add_type E::DefinedType.new(path: 'B')
|
48
|
+
subject.add_type E::DefinedType.new(path: 'B::A')
|
49
|
+
subject.add_type E::DefinedType.new(path: 'E')
|
50
|
+
|
51
|
+
t = subject.types
|
52
|
+
|
53
|
+
expect(subject.resolve_type('A')).to eq t['A']
|
54
|
+
expect(subject.resolve_type('A::B')).to eq t['A::B']
|
55
|
+
expect(subject.resolve_type('B::A')).to eq t['B::A']
|
56
|
+
|
57
|
+
expect(subject.resolve_type('A', type_context: t['A'])).to eq t['A']
|
58
|
+
expect(subject.resolve_type('E', type_context: t['A'])).to eq t['E']
|
59
|
+
expect(subject.resolve_type('B', type_context: t['A'])).to eq t['A::B']
|
60
|
+
expect(subject.resolve_type('B', type_context: t['A::B'])).to eq t['A::B']
|
61
|
+
expect(subject.resolve_type('A', type_context: t['A::B::A'])).to eq t['A::B::A']
|
62
|
+
expect(subject.resolve_type('::A', type_context: t['A::B::A'])).to eq t['A']
|
63
|
+
expect(subject.resolve_type('A', type_context: t['B'])).to eq t['B::A']
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'can parse RBS signatures into our type model' do
|
67
|
+
expect(resolve(E::TypeParser.parse_method_type('(String, Object) -> Integer'))).to m(
|
68
|
+
E::MethodType,
|
69
|
+
positional_parameters: [
|
70
|
+
m(E::PositionalParameter, name: nil, type: m(E::TypeInstance, type: m(E::DefinedType, path: "String"))),
|
71
|
+
m(E::PositionalParameter, name: nil, type: m(E::TypeInstance, type: m(E::DefinedType, path: "Object"))),
|
72
|
+
],
|
73
|
+
return_type: m(E::TypeInstance, type: m(E::DefinedType, path: "Integer")),
|
74
|
+
)
|
75
|
+
|
76
|
+
expect(E::TypeParser.parse_method_type '(A a, ?B b, *E e, c: C, ?d: D, **F f) -> R').to m(
|
77
|
+
E::MethodType,
|
78
|
+
positional_parameters: [
|
79
|
+
m(E::PositionalParameter, name: :a, type: m(E::TypeInstance, type: m(E::PendingDefinedType, path: "A"))),
|
80
|
+
m(E::PositionalParameter, name: :b, type: m(E::TypeInstance, type: m(E::PendingDefinedType, path: "B")), optional: true),
|
81
|
+
],
|
82
|
+
keyword_parameters: [
|
83
|
+
m(E::KeywordParameter, name: :c, type: m(E::TypeInstance, type: m(E::PendingDefinedType, path: "C"))),
|
84
|
+
m(E::KeywordParameter, name: :d, type: m(E::TypeInstance, type: m(E::PendingDefinedType, path: "D")), optional: true),
|
85
|
+
],
|
86
|
+
rest_positional_parameter: m(E::PositionalParameter, name: :e, type: m(E::TypeInstance, type: m(E::PendingDefinedType, path: "E"))),
|
87
|
+
rest_keyword_parameter: m(E::KeywordParameter, name: :f, type: m(E::TypeInstance, type: m(E::PendingDefinedType, path: "F"))),
|
88
|
+
return_type: m(E::TypeInstance, type: m(E::PendingDefinedType, path: "R")),
|
89
|
+
)
|
90
|
+
|
91
|
+
expect(resolve(E::TypeParser.parse_method_type('() { (Integer) -> Integer } -> void'))).to m(
|
92
|
+
E::MethodType,
|
93
|
+
block_parameter: m(
|
94
|
+
E::BlockParameter,
|
95
|
+
optional: false,
|
96
|
+
type: m(
|
97
|
+
E::MethodType,
|
98
|
+
positional_parameters: [
|
99
|
+
m(E::PositionalParameter, name: nil, type: m(E::TypeInstance, type: m(E::DefinedType, path: "Integer"))),
|
100
|
+
],
|
101
|
+
return_type: m(E::TypeInstance, type: m(E::DefinedType, path: "Integer")),
|
102
|
+
),
|
103
|
+
),
|
104
|
+
return_type: m(E::VoidType),
|
105
|
+
)
|
106
|
+
end
|
107
|
+
|
108
|
+
it 'can be built using the builder' do
|
109
|
+
include Houndstooth::SemanticNode
|
110
|
+
node = code_to_semantic_node("
|
111
|
+
module A
|
112
|
+
class B
|
113
|
+
class C
|
114
|
+
def c1
|
115
|
+
end
|
116
|
+
|
117
|
+
#: () -> String
|
118
|
+
def c2
|
119
|
+
'c2'
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
module D
|
125
|
+
class ::E
|
126
|
+
#: (Object) -> Object
|
127
|
+
#: (String) -> String
|
128
|
+
def e
|
129
|
+
magic!
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
class F
|
135
|
+
module G
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
")
|
140
|
+
E::Builder.new(node, subject).analyze
|
141
|
+
|
142
|
+
expect(subject.types.keys).to include(
|
143
|
+
"A",
|
144
|
+
"A::B",
|
145
|
+
"A::B::C",
|
146
|
+
"A::D",
|
147
|
+
"A::F",
|
148
|
+
"A::F::G",
|
149
|
+
"E",
|
150
|
+
)
|
151
|
+
|
152
|
+
expect(subject.types["A::B::C"].instance_methods).to include(
|
153
|
+
m(
|
154
|
+
E::Method,
|
155
|
+
name: :c1,
|
156
|
+
signatures: [],
|
157
|
+
),
|
158
|
+
m(
|
159
|
+
E::Method,
|
160
|
+
name: :c2,
|
161
|
+
signatures: [m(
|
162
|
+
E::MethodType,
|
163
|
+
positional_parameters: [],
|
164
|
+
return_type: m(E::TypeInstance, type: m(E::PendingDefinedType, path: "String")),
|
165
|
+
)],
|
166
|
+
)
|
167
|
+
)
|
168
|
+
|
169
|
+
expect(subject.types["E"].instance_methods).to include m(
|
170
|
+
E::Method,
|
171
|
+
name: :e,
|
172
|
+
signatures: include(
|
173
|
+
m(
|
174
|
+
E::MethodType,
|
175
|
+
positional_parameters: [m(
|
176
|
+
E::PositionalParameter,
|
177
|
+
type: m(E::TypeInstance, type: m(E::PendingDefinedType, path: "Object")),
|
178
|
+
)],
|
179
|
+
return_type: m(E::TypeInstance, type: m(E::PendingDefinedType, path: "Object")),
|
180
|
+
),
|
181
|
+
m(
|
182
|
+
E::MethodType,
|
183
|
+
positional_parameters: [m(
|
184
|
+
E::PositionalParameter,
|
185
|
+
type: m(E::TypeInstance, type: m(E::PendingDefinedType, path: "String")),
|
186
|
+
)],
|
187
|
+
return_type: m(E::TypeInstance, type: m(E::PendingDefinedType, path: "String")),
|
188
|
+
),
|
189
|
+
),
|
190
|
+
)
|
191
|
+
end
|
192
|
+
|
193
|
+
it 'can have acceptance checked' do
|
194
|
+
str = subject.resolve_type('String')
|
195
|
+
int = subject.resolve_type('Integer')
|
196
|
+
num = subject.resolve_type('Numeric')
|
197
|
+
obj = subject.resolve_type('Object')
|
198
|
+
|
199
|
+
# Strings
|
200
|
+
expect(str.accepts?(str)).to eq 0
|
201
|
+
expect(obj.accepts?(str)).to eq 1
|
202
|
+
expect(str.accepts?(obj)).to eq false
|
203
|
+
|
204
|
+
# Integers
|
205
|
+
expect(int.accepts?(int)).to eq 0
|
206
|
+
expect(num.accepts?(int)).to eq 1
|
207
|
+
expect(obj.accepts?(int)).to eq 2
|
208
|
+
expect(int.accepts?(num)).to eq false
|
209
|
+
expect(int.accepts?(str)).to eq false
|
210
|
+
|
211
|
+
# Untyped and void
|
212
|
+
expect(E::UntypedType.new.accepts?(int)).to eq 1
|
213
|
+
expect(E::VoidType.new.accepts?(int)).to eq 1
|
214
|
+
|
215
|
+
# Unions
|
216
|
+
int_str = E::UnionType.new([int, str])
|
217
|
+
expect(int_str.accepts?(int)).to eq 1
|
218
|
+
expect(int_str.accepts?(str)).to eq 1
|
219
|
+
expect(int_str.accepts?(num)).to eq false
|
220
|
+
expect(int_str.accepts?(obj)).to eq false
|
221
|
+
|
222
|
+
num_str = E::UnionType.new([num, str])
|
223
|
+
expect(num_str.accepts?(int)).to eq 2
|
224
|
+
expect(num_str.accepts?(str)).to eq 1
|
225
|
+
expect(num_str.accepts?(num)).to eq 1
|
226
|
+
expect(num_str.accepts?(obj)).to eq false
|
227
|
+
|
228
|
+
int_num_str = E::UnionType.new([int_str, num_str]).simplify
|
229
|
+
expect(int_num_str.accepts?(int)).to eq 1
|
230
|
+
expect(int_num_str.accepts?(str)).to eq 1
|
231
|
+
expect(int_num_str.accepts?(num)).to eq 1
|
232
|
+
expect(int_num_str.accepts?(obj)).to eq false
|
233
|
+
end
|
234
|
+
|
235
|
+
it 'can resolve signatures on methods based on arguments' do
|
236
|
+
mt = ->s do
|
237
|
+
m = Houndstooth::Environment::TypeParser.parse_method_type(s)
|
238
|
+
m.resolve_all_pending_types(subject, context: nil)
|
239
|
+
m
|
240
|
+
end
|
241
|
+
t = ->s{ subject.resolve_type(s).instantiate }
|
242
|
+
|
243
|
+
foo = E::Method.new(:foo, [
|
244
|
+
mt.('(String, Numeric) -> Numeric'),
|
245
|
+
mt.('(String, Integer) -> Integer'),
|
246
|
+
mt.('(String) -> String'),
|
247
|
+
])
|
248
|
+
inst = E::TypeInstance.new(nil)
|
249
|
+
|
250
|
+
# Exact signature matches
|
251
|
+
expect(foo.resolve_matching_signature(inst, [
|
252
|
+
[I::PositionalArgument.new(nil), t.('String')],
|
253
|
+
[I::PositionalArgument.new(nil), t.('Numeric')],
|
254
|
+
])).to eq foo.signatures[0]
|
255
|
+
|
256
|
+
expect(foo.resolve_matching_signature(inst, [
|
257
|
+
[I::PositionalArgument.new(nil), t.('String')],
|
258
|
+
[I::PositionalArgument.new(nil), t.('Integer')],
|
259
|
+
])).to eq foo.signatures[1]
|
260
|
+
|
261
|
+
expect(foo.resolve_matching_signature(inst, [
|
262
|
+
[I::PositionalArgument.new(nil), t.('String')],
|
263
|
+
])).to eq foo.signatures[2]
|
264
|
+
|
265
|
+
# Variant match (Numeric accepts Float)
|
266
|
+
expect(foo.resolve_matching_signature(inst, [
|
267
|
+
[I::PositionalArgument.new(nil), t.('String')],
|
268
|
+
[I::PositionalArgument.new(nil), t.('Float')],
|
269
|
+
])).to eq foo.signatures[0]
|
270
|
+
|
271
|
+
# Invalid, too many arguments
|
272
|
+
expect(foo.resolve_matching_signature(inst, [
|
273
|
+
[I::PositionalArgument.new(nil), t.('String')],
|
274
|
+
[I::PositionalArgument.new(nil), t.('Float')],
|
275
|
+
[I::PositionalArgument.new(nil), t.('Integer')],
|
276
|
+
])).to eq nil
|
277
|
+
|
278
|
+
# Invalid, too few arguments
|
279
|
+
expect(foo.resolve_matching_signature(inst, [])).to eq nil
|
280
|
+
|
281
|
+
# Invalid, incorrect argument type
|
282
|
+
expect(foo.resolve_matching_signature(inst, [
|
283
|
+
[I::PositionalArgument.new(nil), t.('String')],
|
284
|
+
[I::PositionalArgument.new(nil), t.('Object')],
|
285
|
+
])).to eq nil
|
286
|
+
end
|
287
|
+
|
288
|
+
it 'can be un-eigened' do
|
289
|
+
expect(subject.resolve_type('Object').eigen.uneigen).to eq 'Object'
|
290
|
+
end
|
291
|
+
|
292
|
+
it 'parses #!const' do
|
293
|
+
Houndstooth::Stdlib.add_types(subject)
|
294
|
+
meth = subject.resolve_type('Numeric').resolve_instance_method(:+, subject)
|
295
|
+
expect(meth.const).to eq :internal
|
296
|
+
end
|
297
|
+
|
298
|
+
it 'can parse type signatures with type parameters' do
|
299
|
+
include Houndstooth::SemanticNode
|
300
|
+
node = code_to_semantic_node("
|
301
|
+
class A
|
302
|
+
#: [T] (T) -> T
|
303
|
+
def identity(x)
|
304
|
+
x
|
305
|
+
end
|
306
|
+
end
|
307
|
+
")
|
308
|
+
E::Builder.new(node, subject).analyze
|
309
|
+
|
310
|
+
expect(subject.types["A"].instance_methods).to include(
|
311
|
+
m(
|
312
|
+
E::Method,
|
313
|
+
name: :identity,
|
314
|
+
signatures: [m(
|
315
|
+
E::MethodType,
|
316
|
+
type_parameters: ['T'],
|
317
|
+
positional_parameters: [m(E::PositionalParameter, type: m(E::TypeParameterPlaceholder, name: "T"))],
|
318
|
+
return_type: m(E::TypeParameterPlaceholder, name: "T"),
|
319
|
+
)],
|
320
|
+
)
|
321
|
+
)
|
322
|
+
end
|
323
|
+
end
|
@@ -0,0 +1,291 @@
|
|
1
|
+
RSpec.describe Houndstooth::Instructions do
|
2
|
+
I = Houndstooth::Instructions
|
3
|
+
|
4
|
+
it 'can be created from basic literals' do
|
5
|
+
# Keywords
|
6
|
+
expect(code_to_block("
|
7
|
+
true
|
8
|
+
false
|
9
|
+
nil
|
10
|
+
self
|
11
|
+
").instructions).to match_array [
|
12
|
+
m(I::LiteralInstruction, value: true),
|
13
|
+
m(I::LiteralInstruction, value: false),
|
14
|
+
m(I::LiteralInstruction, value: nil),
|
15
|
+
m(I::SelfInstruction),
|
16
|
+
]
|
17
|
+
|
18
|
+
# Literals
|
19
|
+
expect(code_to_block("
|
20
|
+
0
|
21
|
+
2.4
|
22
|
+
'Hello'
|
23
|
+
:hello
|
24
|
+
").instructions).to match_array [
|
25
|
+
m(I::LiteralInstruction, value: 0),
|
26
|
+
m(I::LiteralInstruction, value: 2.4),
|
27
|
+
m(I::LiteralInstruction, value: "Hello"),
|
28
|
+
m(I::LiteralInstruction, value: :hello),
|
29
|
+
]
|
30
|
+
|
31
|
+
# Interpolated string
|
32
|
+
string_interp = code_to_block("\"2 is \#{2}...\"").instructions
|
33
|
+
expect(string_interp).to match_array [
|
34
|
+
m(I::LiteralInstruction, value: "2 is "),
|
35
|
+
m(I::LiteralInstruction, value: 2),
|
36
|
+
m(I::ToStringInstruction, target: string_interp[1].result),
|
37
|
+
m(I::LiteralInstruction, value: "..."),
|
38
|
+
m(I::SendInstruction,
|
39
|
+
target: string_interp[0].result,
|
40
|
+
method_name: :+,
|
41
|
+
arguments: [m(I::PositionalArgument, variable: string_interp[2].result)],
|
42
|
+
),
|
43
|
+
m(I::SendInstruction,
|
44
|
+
target: string_interp[4].result,
|
45
|
+
method_name: :+,
|
46
|
+
arguments: [m(I::PositionalArgument, variable: string_interp[3].result)],
|
47
|
+
),
|
48
|
+
]
|
49
|
+
|
50
|
+
# Interpolated symbol
|
51
|
+
sym_interp = code_to_block(":\"2 is \#{2}...\"").instructions
|
52
|
+
expect(sym_interp).to match_array [
|
53
|
+
m(I::LiteralInstruction, value: "2 is "),
|
54
|
+
m(I::LiteralInstruction, value: 2),
|
55
|
+
m(I::ToStringInstruction, target: sym_interp[1].result),
|
56
|
+
m(I::LiteralInstruction, value: "..."),
|
57
|
+
m(I::SendInstruction,
|
58
|
+
target: sym_interp[0].result,
|
59
|
+
method_name: :+,
|
60
|
+
arguments: [m(I::PositionalArgument, variable: sym_interp[2].result)],
|
61
|
+
),
|
62
|
+
m(I::SendInstruction,
|
63
|
+
target: sym_interp[4].result,
|
64
|
+
method_name: :+,
|
65
|
+
arguments: [m(I::PositionalArgument, variable: sym_interp[3].result)],
|
66
|
+
),
|
67
|
+
m(I::SendInstruction,
|
68
|
+
target: sym_interp[5].result,
|
69
|
+
method_name: :to_sym,
|
70
|
+
)
|
71
|
+
]
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'can be created from conditionals' do
|
75
|
+
ins = code_to_block("if true; 2; end").instructions
|
76
|
+
expect(ins).to match_array [
|
77
|
+
m(I::LiteralInstruction, value: true),
|
78
|
+
m(I::ConditionalInstruction,
|
79
|
+
condition: ins[0].result,
|
80
|
+
true_branch: m(I::InstructionBlock, instructions: [
|
81
|
+
m(I::LiteralInstruction, value: 2),
|
82
|
+
]),
|
83
|
+
false_branch: m(I::InstructionBlock, instructions: [
|
84
|
+
m(I::LiteralInstruction, value: nil),
|
85
|
+
]),
|
86
|
+
)
|
87
|
+
]
|
88
|
+
|
89
|
+
# elsif always become an else with an if inside, so I'm not going to write a test for that
|
90
|
+
|
91
|
+
ins = code_to_block("true ? 2 : 4").instructions
|
92
|
+
expect(ins).to match_array [
|
93
|
+
m(I::LiteralInstruction, value: true),
|
94
|
+
m(I::ConditionalInstruction,
|
95
|
+
condition: ins[0].result,
|
96
|
+
true_branch: m(I::InstructionBlock, instructions: [
|
97
|
+
m(I::LiteralInstruction, value: 2),
|
98
|
+
]),
|
99
|
+
false_branch: m(I::InstructionBlock, instructions: [
|
100
|
+
m(I::LiteralInstruction, value: 4),
|
101
|
+
]),
|
102
|
+
)
|
103
|
+
]
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'can be created from method calls' do
|
107
|
+
# Implicit `self` target
|
108
|
+
ins = code_to_block("a").instructions
|
109
|
+
expect(ins).to match_array [
|
110
|
+
m(I::SelfInstruction),
|
111
|
+
m(I::SendInstruction,
|
112
|
+
target: ins[0].result,
|
113
|
+
method_name: :a,
|
114
|
+
arguments: [],
|
115
|
+
),
|
116
|
+
]
|
117
|
+
|
118
|
+
# Explicit target
|
119
|
+
ins = code_to_block("-3.abs").instructions
|
120
|
+
expect(ins).to match_array [
|
121
|
+
m(I::LiteralInstruction, value: -3),
|
122
|
+
m(I::SendInstruction,
|
123
|
+
target: ins[0].result,
|
124
|
+
method_name: :abs,
|
125
|
+
arguments: [],
|
126
|
+
),
|
127
|
+
]
|
128
|
+
|
129
|
+
# Arguments
|
130
|
+
ins = code_to_block("combine(1, 2, 3, strategy: :add)").instructions
|
131
|
+
expect(ins).to match_array [
|
132
|
+
# Target
|
133
|
+
m(I::SelfInstruction),
|
134
|
+
|
135
|
+
# Arguments
|
136
|
+
m(I::LiteralInstruction, value: 1),
|
137
|
+
m(I::LiteralInstruction, value: 2),
|
138
|
+
m(I::LiteralInstruction, value: 3),
|
139
|
+
m(I::LiteralInstruction, value: :add),
|
140
|
+
|
141
|
+
# Send
|
142
|
+
m(I::SendInstruction,
|
143
|
+
target: ins[0].result,
|
144
|
+
method_name: :combine,
|
145
|
+
arguments: [
|
146
|
+
m(I::PositionalArgument, variable: ins[1].result),
|
147
|
+
m(I::PositionalArgument, variable: ins[2].result),
|
148
|
+
m(I::PositionalArgument, variable: ins[3].result),
|
149
|
+
m(I::KeywordArgument, name: 'strategy', variable: ins[4].result),
|
150
|
+
],
|
151
|
+
),
|
152
|
+
]
|
153
|
+
|
154
|
+
# Safe navigation
|
155
|
+
ins = code_to_block("a&.b").instructions
|
156
|
+
expect(ins).to match_array [
|
157
|
+
# Target
|
158
|
+
m(I::SelfInstruction),
|
159
|
+
m(I::SendInstruction,
|
160
|
+
target: ins[0].result,
|
161
|
+
method_name: :a,
|
162
|
+
),
|
163
|
+
|
164
|
+
# Safe navigation
|
165
|
+
m(I::SendInstruction,
|
166
|
+
target: ins[1].result,
|
167
|
+
method_name: :nil?,
|
168
|
+
),
|
169
|
+
m(I::ConditionalInstruction,
|
170
|
+
condition: ins[2].result,
|
171
|
+
true_branch: m(I::InstructionBlock, instructions: [
|
172
|
+
m(I::LiteralInstruction, value: nil),
|
173
|
+
]),
|
174
|
+
false_branch: m(I::InstructionBlock, instructions: [
|
175
|
+
# Send
|
176
|
+
m(I::SendInstruction,
|
177
|
+
target: ins[1].result,
|
178
|
+
method_name: :b,
|
179
|
+
),
|
180
|
+
]),
|
181
|
+
)
|
182
|
+
]
|
183
|
+
end
|
184
|
+
|
185
|
+
it 'can be created for local variables' do
|
186
|
+
ins = code_to_block("a = 3; puts a").instructions
|
187
|
+
expect(ins).to match_array [
|
188
|
+
m(I::LiteralInstruction, value: 3),
|
189
|
+
m(I::AssignExistingInstruction, variable: ins[0].result, result: m(I::Variable, ruby_identifier: "a")),
|
190
|
+
m(I::SelfInstruction),
|
191
|
+
m(I::AssignExistingInstruction, variable: ins[1].result, result: ins[1].result),
|
192
|
+
m(I::SendInstruction, method_name: :puts, arguments: [
|
193
|
+
m(I::PositionalArgument, variable: ins[1].result)
|
194
|
+
]),
|
195
|
+
]
|
196
|
+
|
197
|
+
ins = code_to_block("x = 3; y = x").instructions
|
198
|
+
expect(ins).to match_array [
|
199
|
+
m(I::LiteralInstruction, value: 3),
|
200
|
+
m(I::AssignExistingInstruction, variable: ins[0].result, result: m(I::Variable, ruby_identifier: "x")),
|
201
|
+
m(I::AssignExistingInstruction, variable: ins[1].result, result: ins[1].result),
|
202
|
+
m(I::AssignExistingInstruction,
|
203
|
+
variable: ins[1].result,
|
204
|
+
result: m(I::Variable, ruby_identifier: 'y'),
|
205
|
+
),
|
206
|
+
]
|
207
|
+
end
|
208
|
+
|
209
|
+
it 'can parse method type arguments' do
|
210
|
+
ins = code_to_block("
|
211
|
+
#!arg String
|
212
|
+
a.b
|
213
|
+
").instructions
|
214
|
+
expect(ins).to match_array [
|
215
|
+
m(I::SelfInstruction),
|
216
|
+
m(I::SendInstruction, method_name: :a, type_arguments: ['String']),
|
217
|
+
m(I::SendInstruction, method_name: :b, type_arguments: []),
|
218
|
+
]
|
219
|
+
end
|
220
|
+
|
221
|
+
context 'can resolve types by traversing through instructions' do
|
222
|
+
# TODO: When implemented, make these test cases use actual Ruby code with local variables
|
223
|
+
|
224
|
+
it 'in simple sequential cases' do
|
225
|
+
env = Houndstooth::Environment.new
|
226
|
+
Houndstooth::Stdlib.add_types(env)
|
227
|
+
|
228
|
+
# One instruction, which has a typechange
|
229
|
+
block = I::InstructionBlock.new(parent: nil, has_scope: false)
|
230
|
+
block.instructions << I::LiteralInstruction.new(block: block, node: nil, value: 3)
|
231
|
+
block.instructions.last.type_change = env.resolve_type("Integer")
|
232
|
+
expect(
|
233
|
+
block.variable_type_at(block.instructions.last.result, block.instructions.last)
|
234
|
+
).to eq env.resolve_type("Integer")
|
235
|
+
|
236
|
+
# Add a second assignment to the same variable, also with a typechange
|
237
|
+
block.instructions << I::LiteralInstruction.new(block: block, node: nil, value: "foo")
|
238
|
+
block.instructions.last.result = block.instructions[0].result
|
239
|
+
block.instructions.last.type_change = env.resolve_type("String")
|
240
|
+
expect(
|
241
|
+
block.variable_type_at(block.instructions[0].result, block.instructions.last)
|
242
|
+
).to eq env.resolve_type("String")
|
243
|
+
expect(
|
244
|
+
block.variable_type_at(block.instructions[0].result, block.instructions[0])
|
245
|
+
).to eq env.resolve_type("Integer")
|
246
|
+
|
247
|
+
# Assignment to a new variable
|
248
|
+
block.instructions << I::LiteralInstruction.new(block: block, node: nil, value: true)
|
249
|
+
block.instructions.last.type_change = env.resolve_type("TrueClass")
|
250
|
+
expect(
|
251
|
+
block.variable_type_at(block.instructions[0].result, block.instructions.last)
|
252
|
+
).to eq env.resolve_type("String")
|
253
|
+
end
|
254
|
+
|
255
|
+
it 'in conditionals' do
|
256
|
+
env = Houndstooth::Environment.new
|
257
|
+
Houndstooth::Stdlib.add_types(env)
|
258
|
+
|
259
|
+
# Set the same variable to 3, then 'foo'
|
260
|
+
block = code_to_block("3; if a; 'foo'; end; puts")
|
261
|
+
block.instructions[3].true_branch.instructions[0].result = block.instructions[0].result
|
262
|
+
|
263
|
+
# Set up typechanges
|
264
|
+
block.instructions[0].type_change = env.resolve_type("Integer")
|
265
|
+
block.instructions[3].true_branch.instructions[0].type_change = env.resolve_type("String")
|
266
|
+
|
267
|
+
# Should be Integer at the start, String in true branch, and Integer in false branch
|
268
|
+
expect(
|
269
|
+
block.variable_type_at(block.instructions[0].result, block.instructions[0])
|
270
|
+
).to eq env.resolve_type("Integer")
|
271
|
+
|
272
|
+
tb = block.instructions[3].true_branch
|
273
|
+
expect(
|
274
|
+
tb.variable_type_at(block.instructions[0].result, tb.instructions[0])
|
275
|
+
).to eq env.resolve_type("String")
|
276
|
+
|
277
|
+
fb = block.instructions[3].false_branch
|
278
|
+
expect(
|
279
|
+
fb.variable_type_at(block.instructions[0].result, fb.instructions[0])
|
280
|
+
).to eq env.resolve_type("Integer")
|
281
|
+
|
282
|
+
# After the conditional, should be String | Integer
|
283
|
+
expect(
|
284
|
+
block.variable_type_at(block.instructions[0].result, block.instructions[4])
|
285
|
+
).to m(Houndstooth::Environment::UnionType, types: [
|
286
|
+
env.resolve_type("String"),
|
287
|
+
env.resolve_type("Integer"),
|
288
|
+
])
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|