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,308 @@
1
+ module Houndstooth::SemanticNode
2
+ # A constant access, either with (`X::Y`) or without (`Y`) a leading target.
3
+ class Constant < Base
4
+ # @return [SemanticNode, nil]
5
+ attr_accessor :target
6
+
7
+ # @return [Symbol]
8
+ attr_accessor :name
9
+
10
+ register_ast_converter :const do |ast_node|
11
+ target, name = *ast_node
12
+ target = from_ast(target) if target
13
+ comments = shift_comments(ast_node)
14
+
15
+ Constant.new(
16
+ ast_node: ast_node,
17
+ comments: comments,
18
+
19
+ target: target,
20
+ name: name,
21
+ )
22
+ end
23
+
24
+ def to_instructions(block)
25
+ if target.nil?
26
+ target_value = nil
27
+ else
28
+ target.to_instructions(block)
29
+ target_value = block.instructions.last.result
30
+ end
31
+
32
+ type_arguments = get_type_arguments
33
+
34
+ block.instructions << I::ConstantAccessInstruction.new(
35
+ block: block,
36
+ node: self,
37
+ name: name,
38
+ target: target_value,
39
+ type_arguments: type_arguments,
40
+ )
41
+ end
42
+ end
43
+
44
+ # A special node which is only valid as the target of a `Constant`. Represents the `::A` syntax
45
+ # used to access a constant from the root namespace.
46
+ class ConstantBase < Base
47
+ register_ast_converter :cbase do |ast_node|
48
+ ConstantBase.new(ast_node: ast_node)
49
+ end
50
+
51
+ def to_instructions(block)
52
+ block.instructions << I::ConstantBaseAccessInstruction.new(block: block, node: self)
53
+ end
54
+ end
55
+
56
+ # Assignment to a constant: `X::Y = 3`
57
+ class ConstantAssignment < Base
58
+ # @return [Constant, nil]
59
+ attr_accessor :target
60
+
61
+ # @return [Symbol]
62
+ attr_accessor :name
63
+
64
+ # @return [SemanticNode]
65
+ attr_accessor :value
66
+
67
+ register_ast_converter :casgn do |ast_node|
68
+ target, name, value = *ast_node
69
+ target = from_ast(target) if target
70
+ value = from_ast(value)
71
+
72
+ ConstantAssignment.new(
73
+ ast_node: ast_node,
74
+ target: target,
75
+ name: name,
76
+ value: value,
77
+ )
78
+ end
79
+ end
80
+
81
+ # A utility mixin to model variable accesses and assignments, since the code is shared between
82
+ # local, instance, class, and global variables.
83
+ #
84
+ # This is not implemented as a class because an ambiguous `VariableAccess` would not be a valid
85
+ # node - the kind of variable must always be known at parse-time.
86
+ module VariableMixin
87
+ def variable_mixin(type)
88
+ # @return [Symbol]
89
+ attr_accessor :name
90
+
91
+ register_ast_converter type do |ast_node|
92
+ self.new(ast_node: ast_node, name: ast_node.to_a.first)
93
+ end
94
+ end
95
+ end
96
+
97
+ # Represents a local variable. The parser looks at surrounding assignments and knows to generate
98
+ # these instead of `Send`s in the correct places.
99
+ class LocalVariable < Base
100
+ extend VariableMixin
101
+ variable_mixin :lvar
102
+
103
+ # @return [Boolean]
104
+ attr_accessor :fabricated
105
+ alias fabricated? fabricated
106
+
107
+ def self.fabricate
108
+ @@fabricate_counter ||= 0
109
+ @@fabricate_counter += 1
110
+
111
+ name = "___fabricated_#{@@fabricate_counter}"
112
+ LocalVariable.new(ast_node: nil, name: name, fabricated: true)
113
+ end
114
+
115
+ def to_instructions(block)
116
+ variable = block.resolve_local_variable(name.to_s, create: false)
117
+ block.instructions << I::AssignExistingInstruction.new(
118
+ block: block,
119
+ node: self,
120
+ variable: variable,
121
+ result: variable,
122
+ )
123
+ end
124
+ end
125
+
126
+ # Represents an instance variable.
127
+ class InstanceVariable < Base
128
+ extend VariableMixin
129
+ variable_mixin :ivar
130
+
131
+ def to_instructions(block)
132
+ block.instructions << I::InstanceVariableReadInstruction.new(
133
+ block: block,
134
+ node: self,
135
+ name: name.to_s,
136
+ )
137
+ end
138
+ end
139
+
140
+ # Represents a class variable.
141
+ class ClassVariable < Base
142
+ extend VariableMixin
143
+ variable_mixin :cvar
144
+ end
145
+
146
+ # Represents a global variable.
147
+ class GlobalVariable < Base
148
+ extend VariableMixin
149
+ variable_mixin :gvar
150
+ end
151
+
152
+ # Represents an assignment to a variable.
153
+ class VariableAssignment < Base
154
+ # @return [SemanticNode]
155
+ attr_accessor :target
156
+
157
+ # @return [SemanticNode]
158
+ attr_accessor :value
159
+
160
+ def self.from_ast_assignment(ast_node, variable_type, multiple_assignment_lhs: false, **_)
161
+ name, value = *ast_node
162
+
163
+ target = variable_type.new(
164
+ ast_node: ast_node,
165
+ name: name,
166
+ )
167
+
168
+ return target if multiple_assignment_lhs && value.nil?
169
+
170
+ value = from_ast(value)
171
+
172
+ VariableAssignment.new(
173
+ ast_node: ast_node,
174
+ target: target,
175
+ value: value,
176
+ )
177
+ end
178
+
179
+ register_ast_converter(:lvasgn) { |n, **o| from_ast_assignment(n, LocalVariable, **o) }
180
+ register_ast_converter(:ivasgn) { |n, **o| from_ast_assignment(n, InstanceVariable, **o) }
181
+ register_ast_converter(:cvasgn) { |n, **o| from_ast_assignment(n, ClassVariable, **o) }
182
+ register_ast_converter(:gvasgn) { |n, **o| from_ast_assignment(n, GlobalVariable, **o) }
183
+
184
+ # Convert `x += 3` into `x = x + 3`
185
+ register_ast_converter :op_asgn do |ast_node, **o|
186
+ target, op, value = *ast_node
187
+
188
+ # Get target as an e.g. LocalVariable
189
+ target = from_ast(target, multiple_assignment_lhs: true)
190
+
191
+ # TODO: No nice way of converting between =/non-= versions of method names currently
192
+ # Also would need same considerations as noted for ||=/&&= to support
193
+ if target.is_a?(Send)
194
+ Houndstooth::Errors::Error.new(
195
+ "Operator-assignment with a call on the left-hand side is not yet supported",
196
+ [[ast_node.loc.dot.join(ast_node.loc.operator), "calls a method"]]
197
+ ).push
198
+ next nil
199
+ end
200
+
201
+ value = from_ast(value)
202
+
203
+ VariableAssignment.new(
204
+ ast_node: ast_node,
205
+ target: target,
206
+ value: Send.new(
207
+ # Yeah, this is the same as the parent, but there's not really a better option
208
+ ast_node: ast_node,
209
+
210
+ method: op,
211
+ target: target,
212
+
213
+ arguments: [PositionalArgument.new(value)]
214
+ )
215
+ )
216
+ end
217
+
218
+ def to_instructions(block)
219
+ if !target.is_a?(LocalVariable) && !target.is_a?(InstanceVariable)
220
+ Houndstooth::Errors::Error.new(
221
+ "Only local or instance variables are currently supported",
222
+ [[target.ast_node.loc.name, "unsupported kind of variable"]]
223
+ ).push
224
+
225
+ block.instructions << I::LiteralInstruction.new(block: block, node: self, value: nil)
226
+ return
227
+ end
228
+
229
+ value.to_instructions(block)
230
+ if target.is_a?(LocalVariable)
231
+ block.instructions << I::AssignExistingInstruction.new(
232
+ block: block,
233
+ node: self,
234
+ variable: block.instructions.last.result,
235
+ result: block.resolve_local_variable(target.name.to_s, create: true),
236
+ )
237
+ else
238
+ block.instructions << I::InstanceVariableWriteInstruction.new(
239
+ block: block,
240
+ node: self,
241
+ name: target.name.to_s,
242
+ value: block.instructions.last.result,
243
+ )
244
+ end
245
+ end
246
+ end
247
+
248
+ # An assignment to multiple variables at once, destructuring the right-hand-side across the
249
+ # targets on the left-hand-side.
250
+ #
251
+ # A multiple assignment with one target is NOT the same as a single variable assignment!
252
+ #
253
+ # a = [1, 2, 3]
254
+ # p a # => [1, 2, 3]
255
+ #
256
+ # a, = [1, 2, 3]
257
+ # p a # => 1
258
+ #
259
+ class MultipleAssignment < Base
260
+ # @return [<SemanticNode>]
261
+ attr_accessor :targets
262
+
263
+ # Yep, value singular - "a, b = 1, 2" is desugared by the parser to have an array as the RHS
264
+ # @return [SemanticNode]
265
+ attr_accessor :value
266
+
267
+ register_ast_converter :masgn do |ast_node|
268
+ lhs, rhs = *ast_node
269
+
270
+ if lhs.type != :mlhs
271
+ Houndstooth::Errors::Error.new(
272
+ "Unexpected left-hand side of multiple assignment",
273
+ [[lhs.loc.expression, "expected list of variables to assign to"]]
274
+ ).push
275
+ next nil
276
+ end
277
+
278
+ targets = lhs.to_a.map { |n| from_ast(n, multiple_assignment_lhs: true) }
279
+ value = from_ast(rhs)
280
+
281
+ MultipleAssignment.new(
282
+ ast_node: ast_node,
283
+ targets: targets,
284
+ value: value,
285
+ )
286
+ end
287
+ end
288
+
289
+ # Represents a node which will be filled in magically by a parent node at code runtime, in cases
290
+ # where complex runtime behaviour means that desugaring isn't possible at parse-time.
291
+ #
292
+ # Currently, this is only used where the left-hand-side of a multiple assignment will call a
293
+ # method (with `Send`), e.g.:
294
+ #
295
+ # self.a, self.b = *[1, 2]
296
+ #
297
+ # This calls self.a=(1) and self.b=(2), but for other RHS values, we might not be able to
298
+ # determine what the parameters will be at parse-time. So, this parses as a multiple assignment
299
+ # to targets (Send self.a=(MagicPlaceholder)) and (Send self.b=(MagicPlaceholder)).
300
+ #
301
+ # If you encounter this during node processing, something has probably gone wrong, and you
302
+ # should have processed the enclosing multiple assignment earlier!
303
+ class MagicPlaceholder < Base
304
+ def initialize(**kwargs)
305
+ super(ast_node: nil, **kwargs)
306
+ end
307
+ end
308
+ end
@@ -0,0 +1,45 @@
1
+ module Houndstooth::SemanticNode
2
+ # The `true` keyword.
3
+ class TrueKeyword < Base
4
+ register_ast_converter :true do |ast_node|
5
+ TrueKeyword.new(ast_node: ast_node)
6
+ end
7
+
8
+ def to_instructions(block)
9
+ block.instructions << I::LiteralInstruction.new(block: block, node: self, value: true)
10
+ end
11
+ end
12
+
13
+ # The `false` keyword.
14
+ class FalseKeyword < Base
15
+ register_ast_converter :false do |ast_node|
16
+ FalseKeyword.new(ast_node: ast_node)
17
+ end
18
+
19
+ def to_instructions(block)
20
+ block.instructions << I::LiteralInstruction.new(block: block, node: self, value: false)
21
+ end
22
+ end
23
+
24
+ # The `self` keyword.
25
+ class SelfKeyword < Base
26
+ register_ast_converter :self do |ast_node|
27
+ SelfKeyword.new(ast_node: ast_node)
28
+ end
29
+
30
+ def to_instructions(block)
31
+ block.instructions << I::SelfInstruction.new(block: block, node: self)
32
+ end
33
+ end
34
+
35
+ # The `nil` keyword.
36
+ class NilKeyword < Base
37
+ register_ast_converter :nil do |ast_node|
38
+ NilKeyword.new(ast_node: ast_node)
39
+ end
40
+
41
+ def to_instructions(block)
42
+ block.instructions << I::LiteralInstruction.new(block: block, node: self, value: nil)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,226 @@
1
+ module Houndstooth::SemanticNode
2
+ # An integer literal.
3
+ class IntegerLiteral < Base
4
+ # @return [Integer]
5
+ attr_accessor :value
6
+
7
+ register_ast_converter :int do |ast_node|
8
+ IntegerLiteral.new(ast_node: ast_node, value: ast_node.to_a.first)
9
+ end
10
+
11
+ def to_instructions(block)
12
+ block.instructions << I::LiteralInstruction.new(block: block, node: self, value: value)
13
+ end
14
+ end
15
+
16
+ # A floating-point number literal.
17
+ class FloatLiteral < Base
18
+ # @return [Float]
19
+ attr_accessor :value
20
+
21
+ register_ast_converter :float do |ast_node|
22
+ FloatLiteral.new(ast_node: ast_node, value: ast_node.to_a.first)
23
+ end
24
+
25
+ def to_instructions(block)
26
+ block.instructions << I::LiteralInstruction.new(block: block, node: self, value: value)
27
+ end
28
+ end
29
+
30
+ # A string literal, possibly with interpolated components.
31
+ class StringLiteral < Base
32
+ # @return [<String, SemanticNode>]
33
+ attr_accessor :components
34
+
35
+ register_ast_converter :str do |ast_node|
36
+ StringLiteral.new(ast_node: ast_node, components: [ast_node.to_a.first])
37
+ end
38
+
39
+ register_ast_converter :dstr do |ast_node|
40
+ components = ast_node.to_a.map do |part|
41
+ if part.type == :str
42
+ part.to_a.first
43
+ else
44
+ from_ast(part)
45
+ end
46
+ end
47
+
48
+ StringLiteral.new(ast_node: ast_node, components: components)
49
+ end
50
+
51
+ def to_instructions(block)
52
+ # There are a few different ways to compile this, depending on what the string's made
53
+ # up of...
54
+ if components.all? { |c| c.is_a?(String) }
55
+ # All literals
56
+ value = components.join
57
+ block.instructions << I::LiteralInstruction.new(block: block, node: self, value: value)
58
+ else
59
+ # We need to actually generate instructions to concatenate the strings at runtime
60
+ # First evaluate each part of the string into a variable
61
+ string_part_variables = components.map do |c|
62
+ if c.is_a?(String)
63
+ block.instructions << I::LiteralInstruction.new(block: block, node: self, value: c)
64
+ else
65
+ c.to_instructions(block)
66
+ block.instructions << I::ToStringInstruction.new(
67
+ block: block,
68
+ node: c,
69
+ target: block.instructions.last.result,
70
+ )
71
+ end
72
+ block.instructions.last.result
73
+ end
74
+
75
+ # Now generate code to concatenate these variables together
76
+ previous_variable = string_part_variables.first
77
+ string_part_variables[1..].each do |variable|
78
+ block.instructions << I::SendInstruction.new(
79
+ block: block,
80
+ node: self,
81
+ target: previous_variable,
82
+ method_name: :+,
83
+ arguments: [I::PositionalArgument.new(variable)],
84
+ )
85
+ previous_variable = block.instructions.last.result
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ # A symbol literal, possibly with interpolated components.
92
+ class SymbolLiteral < Base
93
+ # @return [<String, SemanticNode>]
94
+ attr_accessor :components
95
+
96
+ register_ast_converter :sym do |ast_node|
97
+ SymbolLiteral.new(ast_node: ast_node, components: [ast_node.to_a.first.to_s])
98
+ end
99
+
100
+ register_ast_converter :dsym do |ast_node|
101
+ components = ast_node.to_a.map do |part|
102
+ if part.type == :str
103
+ part.to_a.first.to_s
104
+ else
105
+ from_ast(part)
106
+ end
107
+ end
108
+
109
+ SymbolLiteral.new(ast_node: ast_node, components: components)
110
+ end
111
+
112
+ def to_instructions(block)
113
+ # This logic is identical to translating a string literal - if it's all symbols we'll do
114
+ # it directly, otherwise we'll pretend we're a string and hand it off
115
+ if components.all? { |c| c.is_a?(String) }
116
+ # All literals
117
+ value = components.join
118
+ block.instructions << I::LiteralInstruction.new(block: block, node: self, value: value.to_sym)
119
+ else
120
+ # Pretend we're a string, and convert to a symbol with a call at the end
121
+ # Not 100% equivalent in terms of allocations, but Good Enough
122
+ StringLiteral.new(ast_node: ast_node, components: components).to_instructions(block)
123
+ block.instructions << I::SendInstruction.new(
124
+ block: block,
125
+ node: self,
126
+ target: block.instructions.last.result,
127
+ method_name: :to_sym,
128
+ )
129
+ end
130
+ end
131
+ end
132
+
133
+ # An array literal.
134
+ class ArrayLiteral < Base
135
+ # @return [<SemanticNode>]
136
+ attr_accessor :nodes
137
+
138
+ register_ast_converter :array do |ast_node|
139
+ comments = shift_comments(ast_node)
140
+
141
+ ArrayLiteral.new(
142
+ ast_node: ast_node,
143
+ comments: comments,
144
+ nodes: ast_node.to_a.map { |node| from_ast(node) }
145
+ )
146
+ end
147
+
148
+ def to_instructions(block)
149
+ # Translate the array into `Array.new` followed by a sequence of calls to Array#push
150
+ type_arguments = get_type_arguments
151
+
152
+ block.instructions << I::ConstantBaseAccessInstruction.new(
153
+ block: block,
154
+ node: self,
155
+ )
156
+ block.instructions << I::ConstantAccessInstruction.new(
157
+ block: block,
158
+ node: self,
159
+ name: 'Array',
160
+ target: block.instructions.last.result,
161
+ type_arguments: type_arguments,
162
+ )
163
+ block.instructions << I::SendInstruction.new(
164
+ block: block,
165
+ node: self,
166
+ target: block.instructions.last.result,
167
+ method_name: :new,
168
+ )
169
+ array_var = block.instructions.last.result
170
+
171
+ nodes.each do |node|
172
+ node.to_instructions(block)
173
+ block.instructions << I::SendInstruction.new(
174
+ block: block,
175
+ node: node,
176
+ target: array_var,
177
+ method_name: :<<,
178
+ arguments: [I::PositionalArgument.new(block.instructions.last.result)],
179
+ )
180
+ end
181
+
182
+ block.instructions << I::AssignExistingInstruction.new(
183
+ block: block,
184
+ node: self,
185
+ result: array_var,
186
+ variable: array_var,
187
+ )
188
+ end
189
+ end
190
+
191
+ # A hash literal.
192
+ class HashLiteral < Base
193
+ # @return [<(SemanticNode, SemanticNode)>]
194
+ attr_accessor :pairs
195
+
196
+ register_ast_converter :hash do |ast_node|
197
+ HashLiteral.new(
198
+ ast_node: ast_node,
199
+ pairs: ast_node.to_a.map { |pair| pair.to_a.map { from_ast(_1) } }
200
+ )
201
+ end
202
+ end
203
+
204
+ # A range literal.
205
+ class RangeLiteral < Base
206
+ # @return [SemanticNode, nil]
207
+ attr_accessor :first
208
+
209
+ # @return [SemanticNode, nil]
210
+ attr_accessor :last
211
+
212
+ # @return [bool]
213
+ attr_accessor :inclusive
214
+
215
+ register_ast_converter :irange, :erange do |ast_node|
216
+ first, last = ast_node.to_a.map { from_ast(_1) if _1 }
217
+
218
+ RangeLiteral.new(
219
+ ast_node: ast_node,
220
+ first: first,
221
+ last: last,
222
+ inclusive: (ast_node.type == :irange),
223
+ )
224
+ end
225
+ end
226
+ end
@@ -0,0 +1,126 @@
1
+ module Houndstooth::SemanticNode
2
+ # A boolean AND operation: `a && b`,
3
+ #
4
+ # Unlike other infix operators, boolean AND and OR are not translated to a `Send`.
5
+ class BooleanAnd < Base
6
+ # @return [SemanticNode]
7
+ attr_accessor :left
8
+
9
+ # @return [SemanticNode]
10
+ attr_accessor :right
11
+
12
+ register_ast_converter :and do |ast_node|
13
+ left, right = ast_node.to_a.map { from_ast(_1) }
14
+
15
+ BooleanAnd.new(ast_node: ast_node, left: left, right: right)
16
+ end
17
+ end
18
+
19
+ # A boolean OR operation: `a || b`,
20
+ #
21
+ # Unlike other infix operators, boolean AND and OR are not translated to a `Send`.
22
+ class BooleanOr < Base
23
+ # @return [SemanticNode]
24
+ attr_accessor :left
25
+
26
+ # @return [SemanticNode]
27
+ attr_accessor :right
28
+
29
+ register_ast_converter :or do |ast_node|
30
+ left, right = ast_node.to_a.map { from_ast(_1) }
31
+
32
+ BooleanOr.new(ast_node: ast_node, left: left, right: right)
33
+ end
34
+ end
35
+
36
+ # Note: It is *probably* possible to desugar the boolean-assign operators.
37
+ #
38
+ # I think `x ||= y` can be translated into:
39
+ #
40
+ # if !(defined?(x) && x)
41
+ # x = y
42
+ # end
43
+ #
44
+ # If the source of `x` is not idempotent, this is not _strictly_ correct - if our `x`
45
+ # was `do_something_random.x`, then we'd call `do_something_random` twice rather than
46
+ # once. But it might be close enough for static analysis purposes?
47
+
48
+ # An assignment using a boolean AND operator: `x &&= 3`
49
+ #
50
+ # This is NOT equivalent to `x = x && 3`.
51
+ class BooleanAndAssignment < Base
52
+ # @return [SemanticNode]
53
+ attr_accessor :target
54
+
55
+ # @return [SemanticNode]
56
+ attr_accessor :value
57
+
58
+ register_ast_converter :and_asgn do |ast_node|
59
+ target, value = *ast_node
60
+
61
+ # It's not *really* a multiple assignment LHS, but it's the easiest way to get e.g. a
62
+ # LocalVariable from (lvasgn :x), so we'll just pretend
63
+ target = from_ast(target, multiple_assignment_lhs: true)
64
+ value = from_ast(value)
65
+
66
+ BooleanAndAssignment.new(
67
+ ast_node: ast_node,
68
+ target: target,
69
+ value: value,
70
+ )
71
+ end
72
+ end
73
+
74
+ # An assignment using a boolean OR operator: `x ||= 3`
75
+ #
76
+ # This is NOT equivalent to `x = x || 3`.
77
+ class BooleanOrAssignment < Base
78
+ # @return [SemanticNode]
79
+ attr_accessor :target
80
+
81
+ # @return [SemanticNode]
82
+ attr_accessor :value
83
+
84
+ register_ast_converter :or_asgn do |ast_node|
85
+ target, value = *ast_node
86
+
87
+ # It's not *really* a multiple assignment LHS, but it's the easiest way to get e.g. a
88
+ # LocalVariable from (lvasgn :x), so we'll just pretend
89
+ target = from_ast(target, multiple_assignment_lhs: true)
90
+ value = from_ast(value)
91
+
92
+ BooleanOrAssignment.new(
93
+ ast_node: ast_node,
94
+ target: target,
95
+ value: value,
96
+ )
97
+ end
98
+ end
99
+
100
+ # A splat: `a = *b` or `[1, 2, *[3, 4], 5]`
101
+ class Splat < Base
102
+ # @return [SemanticNode]
103
+ attr_accessor :value
104
+
105
+ # It is possible for splats to appear on the LHS of an assigment, so we need to handle that
106
+ register_ast_converter :splat do |ast_node, multiple_assignment_lhs: false|
107
+ Splat.new(
108
+ ast_node: ast_node,
109
+ value: from_ast(
110
+ ast_node.to_a.first,
111
+ multiple_assignment_lhs: multiple_assignment_lhs,
112
+ )
113
+ )
114
+ end
115
+ end
116
+
117
+ # A `defined?` check: `defined? x`
118
+ class IsDefined < Base
119
+ # @return [SemanticNode]
120
+ attr_accessor :value
121
+
122
+ register_ast_converter :defined? do |ast_node|
123
+ IsDefined.new(ast_node: ast_node, value: from_ast(ast_node.to_a.first))
124
+ end
125
+ end
126
+ end