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