houndstooth 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +5 -0
  3. data/.rspec +1 -0
  4. data/.ruby-version +1 -0
  5. data/Gemfile +11 -0
  6. data/Gemfile.lock +49 -0
  7. data/README.md +99 -0
  8. data/bin/houndstooth.rb +183 -0
  9. data/fuzz/cases/x.rb +8 -0
  10. data/fuzz/cases/y.rb +8 -0
  11. data/fuzz/cases/z.rb +22 -0
  12. data/fuzz/ruby.dict +64 -0
  13. data/fuzz/run +21 -0
  14. data/lib/houndstooth/environment/builder.rb +260 -0
  15. data/lib/houndstooth/environment/type_parser.rb +149 -0
  16. data/lib/houndstooth/environment/types/basic/type.rb +85 -0
  17. data/lib/houndstooth/environment/types/basic/type_instance.rb +54 -0
  18. data/lib/houndstooth/environment/types/compound/union_type.rb +72 -0
  19. data/lib/houndstooth/environment/types/defined/base_defined_type.rb +23 -0
  20. data/lib/houndstooth/environment/types/defined/defined_type.rb +137 -0
  21. data/lib/houndstooth/environment/types/defined/pending_defined_type.rb +14 -0
  22. data/lib/houndstooth/environment/types/method/method.rb +79 -0
  23. data/lib/houndstooth/environment/types/method/method_type.rb +144 -0
  24. data/lib/houndstooth/environment/types/method/parameters.rb +53 -0
  25. data/lib/houndstooth/environment/types/method/special_constructor_method.rb +15 -0
  26. data/lib/houndstooth/environment/types/special/instance_type.rb +9 -0
  27. data/lib/houndstooth/environment/types/special/self_type.rb +9 -0
  28. data/lib/houndstooth/environment/types/special/type_parameter_placeholder.rb +38 -0
  29. data/lib/houndstooth/environment/types/special/untyped_type.rb +11 -0
  30. data/lib/houndstooth/environment/types/special/void_type.rb +12 -0
  31. data/lib/houndstooth/environment/types.rb +3 -0
  32. data/lib/houndstooth/environment.rb +74 -0
  33. data/lib/houndstooth/errors.rb +53 -0
  34. data/lib/houndstooth/instructions.rb +698 -0
  35. data/lib/houndstooth/interpreter/const_internal.rb +148 -0
  36. data/lib/houndstooth/interpreter/objects.rb +142 -0
  37. data/lib/houndstooth/interpreter/runtime.rb +309 -0
  38. data/lib/houndstooth/interpreter.rb +7 -0
  39. data/lib/houndstooth/semantic_node/control_flow.rb +218 -0
  40. data/lib/houndstooth/semantic_node/definitions.rb +253 -0
  41. data/lib/houndstooth/semantic_node/identifiers.rb +308 -0
  42. data/lib/houndstooth/semantic_node/keywords.rb +45 -0
  43. data/lib/houndstooth/semantic_node/literals.rb +226 -0
  44. data/lib/houndstooth/semantic_node/operators.rb +126 -0
  45. data/lib/houndstooth/semantic_node/parameters.rb +108 -0
  46. data/lib/houndstooth/semantic_node/send.rb +349 -0
  47. data/lib/houndstooth/semantic_node/super.rb +12 -0
  48. data/lib/houndstooth/semantic_node.rb +119 -0
  49. data/lib/houndstooth/stdlib.rb +6 -0
  50. data/lib/houndstooth/type_checker.rb +462 -0
  51. data/lib/houndstooth.rb +53 -0
  52. data/spec/ast_to_node_spec.rb +889 -0
  53. data/spec/environment_spec.rb +323 -0
  54. data/spec/instructions_spec.rb +291 -0
  55. data/spec/integration_spec.rb +785 -0
  56. data/spec/interpreter_spec.rb +170 -0
  57. data/spec/self_spec.rb +7 -0
  58. data/spec/spec_helper.rb +50 -0
  59. data/test/ruby_interpreter_test.rb +162 -0
  60. data/types/stdlib.htt +170 -0
  61. metadata +110 -0
@@ -0,0 +1,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