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,108 @@
1
+ module Houndstooth::SemanticNode
2
+ # A set of parameters accepted by a method definition or block.
3
+ class Parameters < Base
4
+ register_ast_converter :args do |ast_node|
5
+ parameters = Parameters.new(
6
+ ast_node: ast_node,
7
+ positional_parameters: [],
8
+ optional_parameters: [],
9
+ keyword_parameters: [],
10
+ optional_keyword_parameters: [],
11
+ rest_parameter: nil,
12
+ rest_keyword_parameter: nil,
13
+ block_parameter: nil,
14
+ only_proc_parameter: false,
15
+ has_forward_parameter: false,
16
+ )
17
+
18
+ ast_node.to_a.each do |arg|
19
+ case arg.type
20
+ when :arg
21
+ parameters.positional_parameters << arg.to_a.first
22
+ when :kwarg
23
+ parameters.keyword_parameters << arg.to_a.first
24
+ when :optarg
25
+ name, value = *arg
26
+ parameters.optional_parameters << [name, from_ast(value)]
27
+ when :kwoptarg
28
+ name, value = *arg
29
+ parameters.optional_keyword_parameters << [name, from_ast(value)]
30
+ when :restarg
31
+ parameters.rest_parameter = arg.to_a.first
32
+ when :kwrestarg
33
+ parameters.rest_keyword_parameter = arg.to_a.first
34
+ when :procarg0
35
+ parameters.only_proc_parameter = true
36
+ when :blockarg
37
+ parameters.block_parameter = arg.to_a.first
38
+ when :forward_arg
39
+ parameters.has_forward_parameter = true
40
+ else
41
+ Houndstooth::Errors::Error.new(
42
+ "Unsupported argument type",
43
+ [[arg.loc.expression, "unsupported"]]
44
+ ).push
45
+ next nil
46
+ end
47
+ end
48
+
49
+ parameters
50
+ end
51
+
52
+ # True if this block of parameters takes the magic "progarc0", which in blocks can represent
53
+ # all parameters given in an array.
54
+ # @return [Boolean]
55
+ attr_accessor :only_proc_parameter
56
+
57
+ # @return [<Symbol>]
58
+ attr_accessor :positional_parameters
59
+
60
+ # @return [<(Symbol, SemanticNode)>]
61
+ attr_accessor :optional_parameters
62
+
63
+ # @return [<Symbol>]
64
+ attr_accessor :keyword_parameters
65
+
66
+ # @return [<(Symbol, SemanticNode)>]
67
+ attr_accessor :optional_keyword_parameters
68
+
69
+ # @return [Symbol, nil]
70
+ attr_accessor :rest_parameter
71
+
72
+ # @return [Symbol, nil]
73
+ attr_accessor :rest_keyword_parameter
74
+
75
+ # @return [Symbol, nil]
76
+ attr_accessor :block_parameter
77
+
78
+ # True if this method has a `...` parameter.
79
+ # @return [Boolean]
80
+ attr_accessor :has_forward_parameter
81
+
82
+ def add_to_instruction_block(block)
83
+ if optional_parameters.any? ||
84
+ keyword_parameters.any? ||
85
+ optional_keyword_parameters.any? ||
86
+ rest_parameter ||
87
+ rest_keyword_parameter ||
88
+ has_forward_parameter ||
89
+ block_parameter ||
90
+ only_proc_parameter
91
+
92
+ # Replace call with a nil
93
+ Houndstooth::Errors::Error.new(
94
+ "Only required positional parameters are supported",
95
+ [[ast_node.loc.expression, "unsupported parameters in block"]]
96
+ ).push
97
+ return false
98
+ end
99
+
100
+ # Create parameters on this block
101
+ positional_parameters.each do |name|
102
+ block.parameters << I::Variable.new(name.to_s)
103
+ end
104
+
105
+ return true
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,349 @@
1
+ module Houndstooth::SemanticNode
2
+ # A method call, called a 'send' internally by Ruby and its parser, hence its name here.
3
+ class Send < Base
4
+ # @return [SemanticNode, nil]
5
+ attr_accessor :target
6
+
7
+ # @return [Symbol]
8
+ attr_accessor :method
9
+
10
+ # @return [<SemanticNode>]
11
+ attr_accessor :arguments
12
+
13
+ # @return [Boolean]
14
+ attr_accessor :safe_navigation
15
+
16
+ # If true, this isn't really a send, but instead a super call. The `target` and `method`
17
+ # of this instance should be ignored.
18
+ # @return [Boolean]
19
+ attr_accessor :super_call
20
+
21
+ # @return [Block, nil]
22
+ attr_accessor :block
23
+
24
+ register_ast_converter :send do |ast_node, multiple_assignment_lhs: false|
25
+ target, method, *arguments_nodes = *ast_node
26
+
27
+ # Let the target shift comments first!
28
+ # This is because you can break onto newlines on the dots if you need to apply a comment
29
+ # to another node.
30
+ #
31
+ # Say you need to apply a magic comment to all of the three sends in a chain:
32
+ #
33
+ # a.b.c
34
+ #
35
+ # Appying to the first send in the chain (the "deepest target") allows you to do this:
36
+ #
37
+ # # Comment A
38
+ # a
39
+ # # Comment B
40
+ # .b
41
+ # # Comment C
42
+ # .c
43
+ #
44
+ # Rather than what you've have to do if they apply to the end of the chain:
45
+ #
46
+ # # Comment A
47
+ # _a = a
48
+ # # Comment B
49
+ # _b = a.b
50
+ # # Comment C
51
+ # _c = b.c
52
+ #
53
+ target = from_ast(target) if target
54
+ comments = shift_comments(ast_node)
55
+
56
+ if multiple_assignment_lhs
57
+ next Send.new(
58
+ ast_node: ast_node,
59
+ comments: comments,
60
+
61
+ target: target,
62
+ method: method,
63
+ arguments: [PositionalArgument.new(MagicPlaceholder.new)],
64
+ safe_navigation: false,
65
+ )
66
+ end
67
+
68
+ if arguments_nodes.last&.type == :kwargs
69
+ arguments = arguments_nodes[0...-1].map { PositionalArgument.new(from_ast(_1)) }
70
+ arguments.concat(arguments_nodes.last.to_a.map do |kwarg|
71
+ next [:_, nil] if kwarg.type == :kwsplat
72
+
73
+ unless kwarg.type == :pair
74
+ Houndstooth::Errors::Error.new(
75
+ "Expected keyword argument list to contain only pairs",
76
+ [[kwarg.loc.expression, "did not parse as a pair"]]
77
+ ).push
78
+ next nil
79
+ end
80
+
81
+ name, value = *kwarg.to_a.map { from_ast(_1) }
82
+ KeywordArgument.new(value, name: name)
83
+ end)
84
+ else
85
+ arguments = arguments_nodes.map { PositionalArgument.new(from_ast(_1)) }
86
+ end
87
+
88
+ Send.new(
89
+ ast_node: ast_node,
90
+ comments: comments,
91
+
92
+ target: target,
93
+ method: method,
94
+ arguments: arguments,
95
+ safe_navigation: false,
96
+ )
97
+ end
98
+
99
+ register_ast_converter :csend do |ast_node, multiple_assignment_lhs: false|
100
+ # Convert this csend into a send
101
+ equivalent_send_node = Parser::AST::Node.new(:send, ast_node, location: ast_node.location)
102
+
103
+ # Convert that into a semantic node and set the safe flag
104
+ send = from_ast(equivalent_send_node, multiple_assignment_lhs: multiple_assignment_lhs)
105
+ send.safe_navigation = true
106
+
107
+ send
108
+ end
109
+
110
+ register_ast_converter :block do |ast_node|
111
+ send_ast_node, args_ast_node, block_body = *ast_node
112
+
113
+ # Parse the `send`, we'll set block properties afterwards
114
+ send = from_ast(send_ast_node)
115
+ send.ast_node = ast_node
116
+
117
+ send.block = Block.new(
118
+ ast_node: ast_node,
119
+ parameters: from_ast(args_ast_node),
120
+ body: block_body.nil? ? Body.new(ast_node: ast_node) : from_ast(block_body)
121
+ )
122
+
123
+ send
124
+ end
125
+
126
+ # Numblocks are just converted into regular blocks with the same parameter names, e.g.:
127
+ #
128
+ # array.map { _1 + 1 }
129
+ #
130
+ # Becomes:
131
+ #
132
+ # array.map { |_1| _1 + 1 }
133
+ #
134
+ register_ast_converter :numblock do |ast_node|
135
+ send_ast_node, args_count, block_body = *ast_node
136
+
137
+ # Parse the `send`, we'll set block properties afterwards
138
+ send = from_ast(send_ast_node)
139
+ send.ast_node = ast_node
140
+
141
+ # Build a fake set of parameters for the block
142
+ # We need to respect the "procarg0" semantics by checking if there's only 1 parameter
143
+ if args_count == 1
144
+ parameters = Parameters.new(
145
+ ast_node: block_body,
146
+ positional_parameters: [],
147
+ optional_parameters: [],
148
+ keyword_parameters: [],
149
+ optional_keyword_parameters: [],
150
+ rest_parameter: nil,
151
+ rest_keyword_parameter: nil,
152
+ only_proc_parameter: true,
153
+ )
154
+ else
155
+ parameters = Parameters.new(
156
+ ast_node: block_body,
157
+ positional_parameters: args_count.times.map { |i| :"_#{i + 1}" },
158
+ optional_parameters: [],
159
+ keyword_parameters: [],
160
+ optional_keyword_parameters: [],
161
+ rest_parameter: nil,
162
+ rest_keyword_parameter: nil,
163
+ only_proc_parameter: false,
164
+ )
165
+ end
166
+
167
+ send.block = Block.new(
168
+ ast_node: ast_node,
169
+ parameters: parameters,
170
+ body: from_ast(block_body)
171
+ )
172
+
173
+ send
174
+ end
175
+
176
+ # Supers are virtually identical to method calls in terms of the arguments they can take.
177
+ register_ast_converter :super do |ast_node|
178
+ # Convert this super into a fake send node
179
+ equivalent_send_node = Parser::AST::Node.new(
180
+ :send,
181
+ [
182
+ # Target
183
+ nil,
184
+
185
+ # Method
186
+ :super__NOT_A_REAL_METHOD,
187
+
188
+ # Arguments
189
+ *ast_node
190
+ ],
191
+ location: ast_node.location
192
+ )
193
+
194
+ # Convert that into a semantic node and set the super flag
195
+ send = from_ast(equivalent_send_node)
196
+ send.super_call = true
197
+
198
+ send
199
+ end
200
+
201
+ def to_instructions(block)
202
+ # Generate instructions for the method's target
203
+ # If it doesn't have one, then it's implicitly `self`
204
+ if target
205
+ target.to_instructions(block)
206
+ else
207
+ block.instructions << I::SelfInstruction.new(block: block, node: self)
208
+ end
209
+ target_variable = block.instructions.last.result
210
+
211
+ type_arguments = get_type_arguments
212
+
213
+ # If this call uses save navigation, we want to wrap everything else in a conditional
214
+ # which checks the target isn't nil
215
+ # (If safe navigation bails from a call because the target is nil, the arguments don't
216
+ # get evaluated either)
217
+ if safe_navigation
218
+ # Generates:
219
+ # $1 = ...target...
220
+ # if $2.nil?
221
+ # nil
222
+ # else
223
+ # $1.method
224
+ # end
225
+ block.instructions << I::SendInstruction.new(
226
+ block: block,
227
+ node: self,
228
+ target: target_variable,
229
+ method_name: :nil?,
230
+ )
231
+
232
+ true_blk = I::InstructionBlock.new(has_scope: false, parent: block)
233
+ true_blk.instructions << I::LiteralInstruction.new(block: true_blk, node: self, value: nil)
234
+ block.instructions << I::ConditionalInstruction.new(
235
+ block: block,
236
+ node: self,
237
+ condition: block.instructions.last.result,
238
+ true_branch: true_blk,
239
+ false_branch: I::InstructionBlock.new(has_scope: false, parent: block),
240
+ )
241
+
242
+ # Replace the working instruction block with the false branch, so we insert the
243
+ # actual send in there
244
+ block = block.instructions.last.false_branch
245
+ end
246
+
247
+ # Evaluate arguments
248
+ ins_args = arguments.map do |arg|
249
+ case arg
250
+ when PositionalArgument
251
+ arg.node.to_instructions(block)
252
+ I::PositionalArgument.new(block.instructions.last.result)
253
+ when KeywordArgument
254
+ if arg.name.is_a?(SymbolLiteral) && arg.name.components.length == 1 && arg.name.components.first.is_a?(String)
255
+ arg.node.to_instructions(block)
256
+ I::KeywordArgument.new(
257
+ block.instructions.last.result,
258
+ name: arg.name.components.first,
259
+ )
260
+ else
261
+ Houndstooth::Errors::Error.new(
262
+ "Keyword argument keys must be non-interpolated symbol literals",
263
+ [[arg.name.ast_node.loc.expression, "invalid key"]]
264
+ ).push
265
+
266
+ block.instructions << I::LiteralInstruction.new(block: block, node: arg.name, value: nil)
267
+ I::KeywordArgument.new(
268
+ block.instructions.last.result,
269
+ name: "__non_symbol_key_error_#{(rand * 10000).to_i}",
270
+ )
271
+ end
272
+ else
273
+ raise "unknown node argument type: #{arg}"
274
+ end
275
+ end
276
+
277
+ # Insert send instruction
278
+ si = I::SendInstruction.new(
279
+ block: block,
280
+ node: self,
281
+ target: target_variable,
282
+ method_name: method,
283
+ arguments: ins_args,
284
+ super_call: super_call,
285
+ type_arguments: type_arguments,
286
+ )
287
+
288
+ # Build up method block
289
+ if self.block
290
+ si.method_block =
291
+ I::InstructionBlock.new(has_scope: true, parent: si).tap do |blk|
292
+ if !self.block.parameters.add_to_instruction_block(blk)
293
+ block.instructions << I::LiteralInstruction.new(node: self, block: block, value: nil)
294
+ return
295
+ end
296
+
297
+ # Create body
298
+ self.block.body.to_instructions(blk)
299
+ end
300
+ end
301
+
302
+ block.instructions << si
303
+ end
304
+ end
305
+
306
+ # A block passed to a `Send`.
307
+ class Block < Base
308
+ # @return [Parameters]
309
+ attr_accessor :parameters
310
+
311
+ # @return [SemanticNode]
312
+ attr_accessor :body
313
+ end
314
+
315
+ # An argument to a `Send`.
316
+ # @abstract
317
+ class Argument
318
+ # The node for the argument's value.
319
+ # @return [SemanticNode]
320
+ attr_accessor :node
321
+
322
+ def initialize(node)
323
+ @node = node
324
+ end
325
+ end
326
+
327
+ # A standard, singular, positional argument.
328
+ class PositionalArgument < Argument; end
329
+
330
+ # A singular keyword argument.
331
+ class KeywordArgument < Argument
332
+ # The keyword.
333
+ # @return [SemanticNode]
334
+ attr_accessor :name
335
+
336
+ def initialize(node, name:)
337
+ super(node)
338
+ @name = name
339
+ end
340
+ end
341
+
342
+ # A special argument which may appear in the arguments to a `Send`, when arguments have been
343
+ # forwarded from the enclosing method into it.
344
+ class ForwardedArguments < Base
345
+ register_ast_converter :forwarded_args do |ast_node|
346
+ ForwardedArguments.new(ast_node: ast_node)
347
+ end
348
+ end
349
+ end
@@ -0,0 +1,12 @@
1
+ module Houndstooth::SemanticNode
2
+ # An implicit super call, without parentheses. This will forward arguments to the superclass'
3
+ # method automatically.
4
+ class ImplicitSuper < Base
5
+ register_ast_converter :zsuper do |ast_node|
6
+ self.new(ast_node: ast_node)
7
+ end
8
+ end
9
+
10
+ # ...Where's `ExplicitSuper`?
11
+ # We use `Send` for that, since the parameters are largely the same!
12
+ end
@@ -0,0 +1,119 @@
1
+ require 'parser/ruby30'
2
+
3
+ # Accuracy to Ruby 3
4
+ LEGACY_MODES = %i[lambda procarg0 encoding arg_inside_procarg0 forward_arg kwargs match_pattern]
5
+ LEGACY_MODES.each do |mode|
6
+ Parser::Builders::Default.send :"emit_#{mode}=", true
7
+ end
8
+ Parser::Builders::Default.emit_index = false
9
+ Parser::Builders::Default.emit_lambda = false
10
+
11
+ # Useful resource: https://docs.rs/lib-ruby-parser/3.0.12/lib_ruby_parser/index.html
12
+ # Based on whitequark/parser so gives good idea of what node types to expect
13
+
14
+ module Houndstooth::SemanticNode
15
+ # Shorthand for use by #to_instructions implementations
16
+ I = Houndstooth::Instructions
17
+
18
+ class Base
19
+ # @return [Parser::AST::Node]
20
+ attr_accessor :ast_node
21
+
22
+ # @return [<Parser::Source::Comment>]
23
+ attr_accessor :comments
24
+
25
+ def initialize(ast_node:, **kwargs)
26
+ @comments = []
27
+ @ast_node = ast_node
28
+
29
+ kwargs.each do |k, v|
30
+ send :"#{k}=", v
31
+ end
32
+ end
33
+
34
+ def self.from_ast(ast_node, **options)
35
+ converter = @@ast_converters[ast_node.type]
36
+
37
+ if converter.nil?
38
+ Houndstooth::Errors::Error.new(
39
+ "Unsupported AST node type #{ast_node.type}",
40
+ [[ast_node.loc.expression, "unsupported"]]
41
+ ).push
42
+ return
43
+ end
44
+
45
+ converter.(ast_node, **options)
46
+ end
47
+
48
+ def self.register_ast_converter(*types, &block)
49
+ @@ast_converters ||= {}
50
+ types.each do |type|
51
+ @@ast_converters[type] = block
52
+ end
53
+ end
54
+
55
+ # TODO: shouldn't use a global!!
56
+ def self.shift_comments(ast_node)
57
+ # TODO: don't pick *any* comment before this one, only ones on their own line
58
+ # In this case:
59
+ # x = 2 # foo
60
+ # y
61
+ # We shouldn't match the `# foo` comment to the `y` Send
62
+
63
+ if ast_node.type == :send && ast_node.location.respond_to?(:selector) && ast_node.location.selector
64
+ # Use name of the method as position reference, if available
65
+ reference_location = ast_node.location.selector
66
+ else
67
+ # Not sure what this is, just use the very start of the expression
68
+ reference_location = ast_node.location.expression
69
+ end
70
+
71
+ comments = []
72
+ comments << $comments.shift \
73
+ while $comments.first && $comments.first.location.expression < reference_location
74
+ comments
75
+ end
76
+
77
+ # Converts this semantic node into a sequence of equivalent instructions, and adds them to
78
+ # the given instruction block.
79
+ # It is expected that, after this call returns, the variable assigned by the final
80
+ # instruction in the block has an equivalent result to evaluating this expression.
81
+ # @param [InstructionBlock] block
82
+ def to_instructions(block)
83
+ raise "#to_instructions not implemented for #{self.class.name}"
84
+ end
85
+
86
+ protected
87
+
88
+ # Extracts type arguments from comments as strings.
89
+ def get_type_arguments
90
+ comments
91
+ .select { |c| c.text.start_with?('#!arg ') }
92
+ .map do |c|
93
+ unless /^#!arg\s+(.+)\s*$/ === c.text
94
+ Houndstooth::Errors::Error.new(
95
+ "Malformed #!arg definition",
96
+ [[c.loc.expression, "invalid"]]
97
+ ).push
98
+ return
99
+ end
100
+
101
+ $1
102
+ end
103
+ end
104
+ end
105
+
106
+ def self.from_ast(...)
107
+ Base.from_ast(...)
108
+ end
109
+ end
110
+
111
+ require_relative 'semantic_node/parameters'
112
+ require_relative 'semantic_node/control_flow'
113
+ require_relative 'semantic_node/operators'
114
+ require_relative 'semantic_node/identifiers'
115
+ require_relative 'semantic_node/keywords'
116
+ require_relative 'semantic_node/literals'
117
+ require_relative 'semantic_node/send'
118
+ require_relative 'semantic_node/definitions'
119
+ require_relative 'semantic_node/super'
@@ -0,0 +1,6 @@
1
+ module Houndstooth::Stdlib
2
+ def self.add_types(environment)
3
+ Houndstooth.process_file('stdlib.htt', File.read(File.join(__dir__, '..', '..', 'types', 'stdlib.htt')), environment)
4
+ environment.resolve_all_pending_types
5
+ end
6
+ end