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.
- checksums.yaml +7 -0
- data/.gitignore +5 -0
- data/.rspec +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +11 -0
- data/Gemfile.lock +49 -0
- data/README.md +99 -0
- data/bin/houndstooth.rb +183 -0
- data/fuzz/cases/x.rb +8 -0
- data/fuzz/cases/y.rb +8 -0
- data/fuzz/cases/z.rb +22 -0
- data/fuzz/ruby.dict +64 -0
- data/fuzz/run +21 -0
- data/lib/houndstooth/environment/builder.rb +260 -0
- data/lib/houndstooth/environment/type_parser.rb +149 -0
- data/lib/houndstooth/environment/types/basic/type.rb +85 -0
- data/lib/houndstooth/environment/types/basic/type_instance.rb +54 -0
- data/lib/houndstooth/environment/types/compound/union_type.rb +72 -0
- data/lib/houndstooth/environment/types/defined/base_defined_type.rb +23 -0
- data/lib/houndstooth/environment/types/defined/defined_type.rb +137 -0
- data/lib/houndstooth/environment/types/defined/pending_defined_type.rb +14 -0
- data/lib/houndstooth/environment/types/method/method.rb +79 -0
- data/lib/houndstooth/environment/types/method/method_type.rb +144 -0
- data/lib/houndstooth/environment/types/method/parameters.rb +53 -0
- data/lib/houndstooth/environment/types/method/special_constructor_method.rb +15 -0
- data/lib/houndstooth/environment/types/special/instance_type.rb +9 -0
- data/lib/houndstooth/environment/types/special/self_type.rb +9 -0
- data/lib/houndstooth/environment/types/special/type_parameter_placeholder.rb +38 -0
- data/lib/houndstooth/environment/types/special/untyped_type.rb +11 -0
- data/lib/houndstooth/environment/types/special/void_type.rb +12 -0
- data/lib/houndstooth/environment/types.rb +3 -0
- data/lib/houndstooth/environment.rb +74 -0
- data/lib/houndstooth/errors.rb +53 -0
- data/lib/houndstooth/instructions.rb +698 -0
- data/lib/houndstooth/interpreter/const_internal.rb +148 -0
- data/lib/houndstooth/interpreter/objects.rb +142 -0
- data/lib/houndstooth/interpreter/runtime.rb +309 -0
- data/lib/houndstooth/interpreter.rb +7 -0
- data/lib/houndstooth/semantic_node/control_flow.rb +218 -0
- data/lib/houndstooth/semantic_node/definitions.rb +253 -0
- data/lib/houndstooth/semantic_node/identifiers.rb +308 -0
- data/lib/houndstooth/semantic_node/keywords.rb +45 -0
- data/lib/houndstooth/semantic_node/literals.rb +226 -0
- data/lib/houndstooth/semantic_node/operators.rb +126 -0
- data/lib/houndstooth/semantic_node/parameters.rb +108 -0
- data/lib/houndstooth/semantic_node/send.rb +349 -0
- data/lib/houndstooth/semantic_node/super.rb +12 -0
- data/lib/houndstooth/semantic_node.rb +119 -0
- data/lib/houndstooth/stdlib.rb +6 -0
- data/lib/houndstooth/type_checker.rb +462 -0
- data/lib/houndstooth.rb +53 -0
- data/spec/ast_to_node_spec.rb +889 -0
- data/spec/environment_spec.rb +323 -0
- data/spec/instructions_spec.rb +291 -0
- data/spec/integration_spec.rb +785 -0
- data/spec/interpreter_spec.rb +170 -0
- data/spec/self_spec.rb +7 -0
- data/spec/spec_helper.rb +50 -0
- data/test/ruby_interpreter_test.rb +162 -0
- data/types/stdlib.htt +170 -0
- 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
|