houndstooth 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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