houndstooth 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|