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,218 @@
1
+ module Houndstooth::SemanticNode
2
+ # Used to group a sequence of nodes into one node - for example, when the body of a method
3
+ # definition contains more than one statement.
4
+ #
5
+ # In an ideal world, this class wouldn't exist, and instead we'd use an array everywhere it's
6
+ # possible for multiple nodes to exist. However, it turns out bodies are valid virtually
7
+ # everywhere! The following are valid snippets of Ruby code...
8
+ #
9
+ # - 1 + (x = 2; x * x)
10
+ # - something((a; b; c), (d; e; f))
11
+ # - class (s = :IO; Object.const_get(s))::Something; end
12
+ class Body < Base
13
+ # @return [<SemanticNode>]
14
+ attr_accessor :nodes
15
+
16
+ register_ast_converter :begin do |ast_node|
17
+ if ast_node.to_a.length == 1
18
+ from_ast(ast_node.to_a.first)
19
+ else
20
+ Body.new(
21
+ ast_node: ast_node,
22
+
23
+ # Use a flat map so that we can flatten inner Body nodes into this one
24
+ nodes: ast_node.to_a.flat_map do |ast_node|
25
+ sem_node = from_ast(ast_node)
26
+ if sem_node.is_a?(Body)
27
+ sem_node.nodes
28
+ else
29
+ [sem_node]
30
+ end
31
+ end
32
+ )
33
+ end
34
+ end
35
+
36
+ def to_instructions(block)
37
+ # A body could signify a new scope, but not always, so we'll let the upper node in the
38
+ # tree create one if needed
39
+
40
+ if nodes.any?
41
+ nodes&.each do |node|
42
+ node.to_instructions(block)
43
+ end
44
+ else
45
+ block << I::LiteralInstruction.new(block: block, node: self, value: nil)
46
+ end
47
+ end
48
+ end
49
+
50
+ # A conditional with true and false branches, used to represent `if` statements, ternary
51
+ # conditionals, and `case/when` constructs.
52
+ class Conditional < Base
53
+ # @return [SemanticNode]
54
+ attr_accessor :condition
55
+
56
+ # @return [SemanticNode]
57
+ attr_accessor :true_branch
58
+
59
+ # @return [SemanticNode, nil]
60
+ attr_accessor :false_branch
61
+
62
+ register_ast_converter :if do |ast_node|
63
+ condition, true_branch, false_branch = ast_node.to_a.map { from_ast(_1) if _1 }
64
+
65
+ Conditional.new(
66
+ ast_node: ast_node,
67
+ condition: condition,
68
+ true_branch: true_branch,
69
+ false_branch: false_branch,
70
+ )
71
+ end
72
+
73
+ register_ast_converter :case do |ast_node|
74
+ subject, *ast_whens, else_case = *ast_node
75
+
76
+ subject = from_ast(subject)
77
+ whens = ast_whens.map { |w| w.to_a.map { from_ast(_1) if _1 } } # [[value, body], ...]
78
+ else_case = from_ast(else_case) if else_case
79
+
80
+ # Convert into assignment and conditional chain
81
+ fabricated_subject_var = LocalVariable.fabricate
82
+ fabricated_subject_var_asgn = VariableAssignment.new(
83
+ ast_node: nil,
84
+ target: fabricated_subject_var,
85
+ value: subject,
86
+ )
87
+
88
+ # Add each `when` as the false branch of the previous one
89
+ root_conditional = nil
90
+ last_conditional = nil
91
+
92
+ whens.each.with_index do |_when, i|
93
+ value, body = *_when
94
+
95
+ this_conditional = Conditional.new(
96
+ ast_node: ast_whens[i],
97
+
98
+ # `when x` is equivalent to `x === subject`
99
+ condition: Send.new(
100
+ ast_node: ast_whens[i],
101
+
102
+ target: value,
103
+ method: :===,
104
+ arguments: [PositionalArgument.new(fabricated_subject_var)],
105
+ ),
106
+ true_branch: body,
107
+ false_branch: nil,
108
+ )
109
+
110
+ if last_conditional
111
+ last_conditional.false_branch = this_conditional
112
+ last_conditional = this_conditional
113
+ else
114
+ root_conditional = this_conditional
115
+ last_conditional = this_conditional
116
+ end
117
+ end
118
+
119
+ # It is syntactically enforced that a `case` will have at least one `when`, so this is safe
120
+ last_conditional.false_branch = else_case
121
+
122
+ Body.new(
123
+ ast_node: ast_node,
124
+ nodes: [
125
+ fabricated_subject_var_asgn,
126
+ root_conditional,
127
+ ]
128
+ )
129
+ end
130
+
131
+ def to_instructions(block)
132
+ condition.to_instructions(block)
133
+ ci = I::ConditionalInstruction.new(
134
+ block: block,
135
+ node: self,
136
+ condition: block.instructions.last.result,
137
+ true_branch: nil,
138
+ false_branch: nil,
139
+ )
140
+ ci.true_branch =
141
+ I::InstructionBlock.new(has_scope: false, parent: ci).tap do |blk|
142
+ true_branch.to_instructions(blk)
143
+ end
144
+ ci.false_branch =
145
+ I::InstructionBlock.new(has_scope: false, parent: ci).tap do |blk|
146
+ if false_branch.nil?
147
+ blk.instructions << I::LiteralInstruction.new(block: blk, node: self, value: nil)
148
+ else
149
+ false_branch.to_instructions(blk)
150
+ end
151
+ end
152
+ block.instructions << ci
153
+ end
154
+ end
155
+
156
+ # A while loop.
157
+ #
158
+ # TODO: It's possible this can be desugared into Kernel.loop { break unless condition; body }
159
+ class While < Base
160
+ # @return [SemanticNode]
161
+ attr_accessor :condition
162
+
163
+ # @return [SemanticNode]
164
+ attr_accessor :body
165
+
166
+ register_ast_converter :while do |ast_node|
167
+ condition, body = ast_node.to_a.map { from_ast(_1) if _1 }
168
+
169
+ While.new(
170
+ ast_node: ast_node,
171
+ condition: condition,
172
+ body: body,
173
+ )
174
+ end
175
+ end
176
+
177
+ # A mixin for defining expressions which affect the control of their enclosing contexts, e.g.
178
+ # `return` and `break`. These all take one optional arguments, so we can deduplicate their
179
+ # definitions.
180
+ module ControlExpressionMixin
181
+ def control_exp_mixin(type)
182
+ # @return [SemanticNode, nil]
183
+ attr_accessor :value
184
+
185
+ register_ast_converter type do |ast_node|
186
+ if ast_node.to_a.length > 1
187
+ value = ArrayLiteral.new(
188
+ ast_node: ast_node,
189
+ nodes: ast_node.to_a.map { from_ast(_1) },
190
+ )
191
+ else
192
+ value = ast_node.to_a.first
193
+ value = from_ast(value) if value
194
+ end
195
+
196
+ self.new(ast_node: ast_node, value: value)
197
+ end
198
+ end
199
+ end
200
+
201
+ # A return expression.
202
+ class Return < Base
203
+ extend ControlExpressionMixin
204
+ control_exp_mixin :return
205
+ end
206
+
207
+ # A break expression.
208
+ class Break < Base
209
+ extend ControlExpressionMixin
210
+ control_exp_mixin :break
211
+ end
212
+
213
+ # A next expression.
214
+ class Next < Base
215
+ extend ControlExpressionMixin
216
+ control_exp_mixin :next
217
+ end
218
+ end
@@ -0,0 +1,253 @@
1
+ module Houndstooth::SemanticNode
2
+ # A method definition. Used for both standard method definitions (`def x()`) or definitions
3
+ # on a singleton (`def something.x()`).
4
+ class MethodDefinition < Base
5
+ # @return [Symbol]
6
+ attr_accessor :name
7
+
8
+ # @return [Parameters]
9
+ attr_accessor :parameters
10
+
11
+ # @return [SemanticNode, nil]
12
+ attr_accessor :target
13
+
14
+ # @return [SemanticNode]
15
+ attr_accessor :body
16
+
17
+ register_ast_converter :def do |ast_node|
18
+ name, parameters, body = *ast_node
19
+ comments = shift_comments(ast_node)
20
+
21
+ body = from_ast(body) if body
22
+ parameters = from_ast(parameters)
23
+
24
+ MethodDefinition.new(
25
+ ast_node: ast_node,
26
+ comments: comments,
27
+
28
+ name: name,
29
+ body: body,
30
+ parameters: parameters,
31
+ target: nil,
32
+ )
33
+ end
34
+
35
+ register_ast_converter :defs do |ast_node|
36
+ target, name, parameters, body = *ast_node
37
+ comments = shift_comments(ast_node)
38
+
39
+ target = from_ast(target)
40
+ body = from_ast(body) if body
41
+ parameters = from_ast(parameters)
42
+
43
+ MethodDefinition.new(
44
+ ast_node: ast_node,
45
+ comments: comments,
46
+
47
+ name: name,
48
+ body: body,
49
+ parameters: parameters,
50
+ target: target,
51
+ )
52
+ end
53
+
54
+ def to_instructions(block)
55
+ if target
56
+ target.to_instructions(block)
57
+ target_var = block.instructions.last.result
58
+ else
59
+ target_var = nil
60
+ end
61
+
62
+ mdi = I::MethodDefinitionInstruction.new(
63
+ node: self,
64
+ block: block,
65
+ name: name,
66
+ target: target_var,
67
+ body: nil,
68
+ )
69
+ mdi.body =
70
+ I::InstructionBlock.new(has_scope: true, parent: mdi).tap do |blk|
71
+ if !parameters.add_to_instruction_block(blk)
72
+ block.instructions << I::LiteralInstruction.new(node: self, block: block, value: nil)
73
+ return
74
+ end
75
+
76
+ if body
77
+ body.to_instructions(blk)
78
+ else
79
+ blk.instructions << I::LiteralInstruction.new(node: self, block: block, value: nil)
80
+ end
81
+ end
82
+ block.instructions << mdi
83
+ end
84
+ end
85
+
86
+ module TypeDefinitionMixin
87
+ def type_definition_instructions(block, kind)
88
+ # Generate the name as instructions, but remove the last one to discover the name
89
+ # (Because the name will lead up to the target, e.g. class A::B)
90
+ # We measure how many instructions are generated to figure out if there was actually a
91
+ # leading path
92
+ instruction_count = block.instructions.length
93
+ name.to_instructions(block)
94
+ instruction_count = block.instructions.length - instruction_count
95
+ unless block.instructions.last.is_a?(I::ConstantAccessInstruction)
96
+ Houndstooth::Errors::Error.new(
97
+ "Type name must be a constant",
98
+ [[name.loc.expression, "unsupported"]]
99
+ ).push
100
+ return
101
+ end
102
+ type_name = block.instructions.pop.name.to_sym
103
+ if instruction_count > 1
104
+ type_target = block.instructions.last&.result
105
+ else
106
+ type_target = nil
107
+ end
108
+
109
+ if kind == :class
110
+ # Generate superclass, or if there isn't one, use Object
111
+ if superclass
112
+ superclass.to_instructions(block)
113
+ type_superclass = block.instructions.last.result
114
+ else
115
+ block.instructions << I::ConstantBaseAccessInstruction.new(block: block, node: self)
116
+ block.instructions << I::ConstantAccessInstruction.new(
117
+ block: block,
118
+ node: self,
119
+ target: block.instructions.last.result,
120
+ name: :Object,
121
+ )
122
+ type_superclass = block.instructions.last.result
123
+ end
124
+ end
125
+
126
+ # Build type
127
+ tdi = I::TypeDefinitionInstruction.new(
128
+ block: block,
129
+ node: self,
130
+ name: type_name,
131
+ kind: kind,
132
+ target: type_target,
133
+ superclass: type_superclass,
134
+ body: nil,
135
+ )
136
+ tdi.body =
137
+ I::InstructionBlock.new(has_scope: true, parent: tdi).tap do |blk|
138
+ body&.to_instructions(blk)
139
+ end
140
+
141
+ block.instructions << tdi
142
+ end
143
+ end
144
+
145
+ # A class definition.
146
+ class ClassDefinition < Base
147
+ # @return [SemanticNode]
148
+ attr_accessor :name
149
+
150
+ # @return [SemanticNode, nil]
151
+ attr_accessor :superclass
152
+
153
+ # @return [SemanticNode, nil]
154
+ attr_accessor :body
155
+
156
+ register_ast_converter :class do |ast_node|
157
+ name, superclass, body = *ast_node
158
+ comments = shift_comments(ast_node)
159
+
160
+ name = from_ast(name)
161
+ superclass = from_ast(superclass) if superclass
162
+ body = from_ast(body) if body
163
+
164
+ ClassDefinition.new(
165
+ ast_node: ast_node,
166
+ comments: comments,
167
+
168
+ name: name,
169
+ superclass: superclass,
170
+ body: body,
171
+ )
172
+ end
173
+
174
+ include TypeDefinitionMixin
175
+ def to_instructions(block)
176
+ type_definition_instructions(block, :class)
177
+ end
178
+ end
179
+
180
+ # A singleton class accessor: `class << x`.
181
+ class SingletonClass < Base
182
+ # @return [SemanticNode]
183
+ attr_accessor :target
184
+
185
+ # @return [SemanticNode, nil]
186
+ attr_accessor :body
187
+
188
+ register_ast_converter :sclass do |ast_node|
189
+ target, body = *ast_node
190
+
191
+ target = from_ast(target)
192
+ body = from_ast(body) if body
193
+
194
+ SingletonClass.new(
195
+ ast_node: ast_node,
196
+ target: target,
197
+ body: body,
198
+ )
199
+ end
200
+ end
201
+
202
+ # A module definition.
203
+ class ModuleDefinition < Base
204
+ # @return [SemanticNode]
205
+ attr_accessor :name
206
+
207
+ # @return [SemanticNode, nil]
208
+ attr_accessor :body
209
+
210
+ register_ast_converter :module do |ast_node|
211
+ name, body = *ast_node
212
+ comments = shift_comments(ast_node)
213
+
214
+ name = from_ast(name)
215
+ body = from_ast(body) if body
216
+
217
+ ModuleDefinition.new(
218
+ ast_node: ast_node,
219
+ comments: comments,
220
+
221
+ name: name,
222
+ body: body,
223
+ )
224
+ end
225
+
226
+ include TypeDefinitionMixin
227
+ def to_instructions(block)
228
+ type_definition_instructions(block, :module)
229
+ end
230
+ end
231
+
232
+ # An alias.
233
+ #
234
+ # Aliases are usually between statically-named methods given with just identifiers, but they
235
+ # can also be between methods named with dynamic symbols, and even between global variables.
236
+ class Alias < Base
237
+ # @return [SemanticNode]
238
+ attr_accessor :from
239
+
240
+ # @return [SemanticNode]
241
+ attr_accessor :to
242
+
243
+ register_ast_converter :alias do |ast_node|
244
+ to, from = ast_node.to_a.map { from_ast(_1) }
245
+
246
+ Alias.new(
247
+ ast_node: ast_node,
248
+ from: from,
249
+ to: to,
250
+ )
251
+ end
252
+ end
253
+ end