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,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