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,462 @@
1
+ module Houndstooth
2
+ class TypeChecker
3
+ attr_reader :env
4
+
5
+ def initialize(env)
6
+ @env = env
7
+ end
8
+
9
+ def process_block(block, lexical_context:, self_type:, const_context:, type_parameters:)
10
+ block.environment = env
11
+ block.instructions.each do |ins|
12
+ process_instruction(ins,
13
+ lexical_context: lexical_context,
14
+ self_type: self_type,
15
+ const_context: const_context,
16
+ type_parameters: type_parameters,
17
+ )
18
+ end
19
+ end
20
+
21
+ def process_instruction(ins, lexical_context:, self_type:, const_context:, type_parameters:)
22
+ case ins
23
+ when Instructions::LiteralInstruction
24
+ assign_type_to_literal_instruction(ins)
25
+
26
+ when Instructions::ConditionalInstruction
27
+ # Special case - did the conditional of this `if` statement come from an `is_a?`
28
+ # call immediately preceding it?
29
+ index = ins.block.instructions.index { _1 == ins }
30
+ if index != 0
31
+ last_ins = ins.block.instructions[index - 1]
32
+ if last_ins.is_a?(Instructions::SendInstruction) \
33
+ && last_ins.method_name == :is_a? \
34
+ && last_ins.result == ins.condition \
35
+ && last_ins.target.ruby_identifier \
36
+ && last_ins.arguments.length == 1
37
+
38
+ # Yep, we just checked the type of a local variable!
39
+ # In the true branch, we can refine the type
40
+ # What was the argument type? Uneigen it to get the type of the variable
41
+ arg_var = last_ins.arguments.first.variable
42
+ arg_type = ins.block.variable_type_at!(arg_var, ins)
43
+ checked_var_type = env.resolve_type(arg_type.type.uneigen).instantiate
44
+
45
+ ins.true_branch.type_refinements[last_ins.target] = checked_var_type
46
+ end
47
+ end
48
+
49
+ process_block(ins.true_branch, lexical_context: lexical_context, self_type: self_type, const_context: const_context, type_parameters: type_parameters)
50
+ process_block(ins.false_branch, lexical_context: lexical_context, self_type: self_type, const_context: const_context, type_parameters: type_parameters)
51
+
52
+ # A conditional could return either of its branches
53
+ ins.type_change = Environment::UnionType.new([
54
+ ins.true_branch.return_type!,
55
+ ins.false_branch.return_type!,
56
+ ]).simplify
57
+
58
+ when Instructions::AssignExistingInstruction
59
+ # If the assignment is to a different variable, set a typechange
60
+ if ins.result != ins.variable
61
+ ins.type_change = ins.block.variable_type_at!(ins.variable, ins)
62
+ end
63
+
64
+ when Instructions::SendInstruction
65
+ # Parse type arguments, if any
66
+ type_args = Environment::TypeParser.parse_type_arguments(env, ins.type_arguments, type_parameters)
67
+
68
+ # Get type of target
69
+ target_type = ins.block.variable_type_at!(ins.target, ins)
70
+
71
+ # Look up method on target
72
+ method = target_type.resolve_instance_method(ins.method_name, env)
73
+ if method.nil?
74
+ Houndstooth::Errors::Error.new(
75
+ "`#{target_type.rbs}` has no method named `#{ins.method_name}`",
76
+ [[ins.node.ast_node.loc.expression, "no such method"]]
77
+ ).push
78
+
79
+ # Assign result to untyped so type checking can continue
80
+ # TODO: create a special "abandoned" type specifically for this purpose
81
+ ins.type_change = Environment::UntypedType.new
82
+
83
+ return
84
+ end
85
+
86
+ # Get type of all arguments
87
+ arguments_with_types = ins.arguments.map do |arg|
88
+ [arg, ins.block.variable_type_at!(arg.variable, ins)]
89
+ end
90
+
91
+ # Resolve the best method signature with these
92
+ sig = method.resolve_matching_signature(target_type, arguments_with_types, type_args)
93
+ if sig.nil?
94
+ # Special case - if the node isn't a send, then it's a generated insertion to
95
+ # an array
96
+ if !ins.node.is_a?(Houndstooth::SemanticNode::Send)
97
+ # TODO: improve error
98
+ Houndstooth::Errors::Error.new(
99
+ "Incorrect type of element in array literal",
100
+ [[ins.node.ast_node.loc.expression, "type does not match annotation"]]
101
+ ).push
102
+ else
103
+ error_message =
104
+ "`#{target_type.rbs}` method `#{ins.method_name}` has no signature matching the given arguments\n"
105
+
106
+ if method.signatures.any?
107
+ error_message += "Available signatures are:\n" \
108
+ + method.signatures.map { |s| " - #{s.substitute_type_parameters(target_type, Hash.new).rbs}" }.join("\n")
109
+ else
110
+ error_message += "Method has no signatures - did you use a #: comment?"
111
+ end
112
+
113
+ Houndstooth::Errors::Error.new(
114
+ error_message,
115
+ # TODO: feels a bit dodgy
116
+ [[ins.node.ast_node.loc.expression, "no matching signature"]] \
117
+ + ins.node.arguments.zip(arguments_with_types).map do |node, (_, t)|
118
+ [node.node.ast_node.loc.expression, "argument type is `#{t.rbs}`"]
119
+ end
120
+ ).push
121
+ end
122
+
123
+ # Assign result to untyped so type checking can continue
124
+ # TODO: create a special "abandoned" type specifically for this purpose
125
+ ins.type_change = Environment::UntypedType.new
126
+
127
+ return
128
+ end
129
+
130
+ # Resolve type arguments - don't need to perform length check here, since the sig
131
+ # wouldn't have matched if the lengths mismatched
132
+ call_type_args = sig.type_parameters.zip(type_args).to_h
133
+ sig = sig.substitute_type_parameters(target_type, call_type_args)
134
+
135
+ # Handle block
136
+ catch :abort_block do
137
+ if sig.block_parameter && ins.method_block
138
+ # This method takes a block, and was given one
139
+ # Add parameter types
140
+ throw :abort_block \
141
+ if !add_parameter_type_instructions(ins, ins.method_block, sig.block_parameter.type)
142
+
143
+ # Recurse type checking into it
144
+ # We don't support overridden block self types, so we need to special-case
145
+ # this for `define_method`, where the block's `self` is uneigened
146
+ # `define_method` also strips constness
147
+ if method == env.resolve_type('::Module').resolve_instance_method(:define_method, env)
148
+ # HACK: Won't work with generics
149
+ inner_self_type = env.resolve_type(self_type.type.uneigen).instantiate
150
+ inner_const_context = false
151
+ else
152
+ inner_self_type = self_type
153
+ inner_const_context = const_context
154
+ end
155
+ process_block(ins.method_block, lexical_context: lexical_context, self_type: inner_self_type, const_context: inner_const_context, type_parameters: type_parameters)
156
+
157
+ # Check return type
158
+ if !sig.block_parameter.type.return_type.accepts?(ins.method_block.return_type!)
159
+ Houndstooth::Errors::Error.new(
160
+ "Incorrect return type for block, expected `#{sig.block_parameter.type.return_type.rbs}`",
161
+ [[
162
+ ins.method_block.instructions.last.node.ast_node.loc.expression,
163
+ "got `#{ins.method_block.return_type!.rbs}`"
164
+ ]]
165
+ ).push
166
+ throw :abort_block
167
+ end
168
+
169
+ elsif sig.block_parameter && !ins.method_block
170
+ # This method takes a block, but wasn't given one
171
+ # If the block is not optional, error
172
+ if !sig.block_parameter.optional?
173
+ Houndstooth::Errors::Error.new(
174
+ "`#{target_type.rbs}` method `#{ins.method_name}` requires a block, but none was given",
175
+ [[ins.node.ast_node.loc.expression, "expected block"]]
176
+ ).push
177
+ end
178
+ elsif !sig.block_parameter && ins.method_block
179
+ # This method doesn't take a block, but was given one
180
+ # That's not allowed!
181
+ # (Well, Ruby allows it, but it doesn't make sense for us - 99% of the time,
182
+ # this will be a bug)
183
+ if ins.method_block
184
+ Houndstooth::Errors::Error.new(
185
+ "`#{target_type.rbs}` method `#{ins.method_name}` does not accept a block",
186
+ [[ins.node.ast_node.loc.expression, "unexpected block"]]
187
+ ).push
188
+ end
189
+ end
190
+ end
191
+
192
+ # Check for return type special cases
193
+ case sig.return_type
194
+ when Environment::SelfType
195
+ ins.type_change = target_type
196
+ when Environment::InstanceType
197
+ ins.type_change = env.resolve_type(target_type.type.uneigen).instantiate(target_type.type_arguments)
198
+ else
199
+ # No special cases, set result variable to return type
200
+ ins.type_change = sig.return_type
201
+ end
202
+
203
+ # If we're in a const context, check that the target method is const
204
+ if const_context && !method.const?
205
+ Houndstooth::Errors::Error.new(
206
+ "`#{target_type.rbs}` method `#{ins.method_name}` is not const, so cannot be called from a const context",
207
+ [[ins.node.ast_node.loc.expression, "call inside a const context"]]
208
+ ).push
209
+ end
210
+
211
+ # If this method is const-required, check that the call was const-considered, or the
212
+ # method we're currently in is const-required
213
+ if method.const_required? && !ins.const_considered?
214
+ Houndstooth::Errors::Error.new(
215
+ "`#{target_type.rbs}` method `#{ins.method_name}` is const-required, but this call is not within a const-required context",
216
+ [[ins.node.ast_node.loc.expression, "call outside a const context"]]
217
+ ).push
218
+ end
219
+
220
+ when Instructions::ConstantBaseAccessInstruction
221
+ ins.type_change = Environment::BaseDefinedType.new
222
+
223
+ when Instructions::ConstantAccessInstruction
224
+ if ins.target
225
+ # TODO: will only work with types, not actual constants
226
+ target = ins.block.variable_type_at!(ins.target, ins)
227
+ else
228
+ target = lexical_context
229
+ end
230
+ resolved = env.resolve_type(ins.name.to_s, type_context: env.resolve_type(target.uneigen))
231
+
232
+ if resolved.nil?
233
+ Houndstooth::Errors::Error.new(
234
+ "No constant named `#{ins.name}` on `#{target.rbs}`",
235
+ [[ins.node.ast_node.loc.expression, "no such constant"]]
236
+ ).push
237
+
238
+ # Assign result to untyped so type checking can continue
239
+ # TODO: another use for "abandoned" type
240
+ ins.type_change = Environment::UntypedType.new
241
+ return
242
+ end
243
+
244
+ # Check type parameter numbers (even if the type doesn't have any - we don't want
245
+ # to allow type arguments when none are expected)
246
+ el = resolved.type_parameters.length
247
+ gl = ins.type_arguments.length
248
+ if el != gl
249
+ Houndstooth::Errors::Error.new(
250
+ "Insufficient type arguments for `#{ins.name}` (expected #{el}, got #{gl})",
251
+ [[ins.node.ast_node.loc.expression, "incorrect number of arguments"]]
252
+ ).push
253
+
254
+ # Assign result to untyped so type checking can continue
255
+ # TODO: another use for "abandoned" type
256
+ ins.type_change = Environment::UntypedType.new
257
+ return
258
+ end
259
+
260
+ # Does the type require type parameters?
261
+ if resolved.type_parameters.any?
262
+ # Yep - parse them
263
+ type_args = Environment::TypeParser.parse_type_arguments(env, ins.type_arguments, type_parameters)
264
+ else
265
+ type_args = []
266
+ end
267
+
268
+ ins.type_change = resolved.eigen.instantiate(type_args)
269
+
270
+ when Instructions::SelfInstruction
271
+ ins.type_change = self_type
272
+
273
+ when Instructions::TypeDefinitionInstruction
274
+ if ins.target
275
+ base_type = ins.block.variable_type_at!(ins.target, ins)
276
+ else
277
+ base_type = lexical_context
278
+ end
279
+
280
+ type_being_defined = env.resolve_type("#{base_type.uneigen}::#{ins.name}").eigen
281
+ type_being_defined_inst = type_being_defined.instantiate
282
+
283
+ process_block(
284
+ ins.body,
285
+ lexical_context: type_being_defined,
286
+ self_type: type_being_defined_inst,
287
+ # Type definitions bodies are always const
288
+ const_context: true,
289
+ type_parameters: type_parameters + type_being_defined.type_parameters,
290
+ )
291
+
292
+ # Returns the just-defined type
293
+ ins.type_change = type_being_defined_inst
294
+
295
+ when Instructions::MethodDefinitionInstruction
296
+ method, inner_self_type = ins.resolve_method_and_type(self_type, env)
297
+
298
+ # If this method is const-required, mark its body as const-considered
299
+ if method.const_required?
300
+ ins.body.mark_const_considered
301
+ end
302
+
303
+ # Does it have any signatures?
304
+ if method.signatures.empty?
305
+ Houndstooth::Errors::Error.new(
306
+ "No signatures provided",
307
+ [[ins.node.ast_node.loc.expression, "no signatures"]]
308
+ ).push
309
+ end
310
+
311
+ # Check each signature
312
+ method.signatures.map do |sig|
313
+ # Assign parameter types
314
+ number_of_type_ins = add_parameter_type_instructions(ins, ins.body, sig)
315
+ if number_of_type_ins == false
316
+ next
317
+ end
318
+
319
+ # Recurse into body
320
+ process_block(
321
+ ins.body,
322
+ self_type: inner_self_type,
323
+ lexical_context: lexical_context,
324
+ # Method definition bodies are const iff the method being defined is
325
+ const_context: method.const?,
326
+ type_parameters: type_parameters + sig.type_parameters,
327
+ )
328
+
329
+ # Check return type
330
+ if !sig.return_type.accepts?(ins.body.return_type!)
331
+ Houndstooth::Errors::Error.new(
332
+ "Incorrect return type for method, expected `#{sig.return_type.rbs}`",
333
+ [[
334
+ ins.body.instructions.last.node.ast_node.loc.expression,
335
+ "got `#{ins.body.return_type!.rbs}`"
336
+ ]]
337
+ ).push
338
+ end
339
+
340
+ # If there's more than one, remove type instructions so the next loop can add
341
+ # them again
342
+ # (We could remove them if there's only one too, but it's handy to leave in for
343
+ # debugging)
344
+ if method.signatures.length > 1
345
+ number_of_type_ins.times { ins.body.instructions.shift }
346
+ end
347
+ end
348
+
349
+ # Returns a symbol of the method's name
350
+ ins.type_change = env.resolve_type("Symbol").instantiate
351
+
352
+ when Instructions::ToStringInstruction
353
+ ins.type_change = env.resolve_type("String").instantiate
354
+
355
+ when Instructions::InstanceVariableReadInstruction
356
+ var_type = self_type.type.resolve_instance_variable(ins.name)
357
+ if var_type.nil?
358
+ Houndstooth::Errors::Error.new(
359
+ "Instance variable #{ins.name} is not defined",
360
+ [[ins.node.ast_node.loc.expression, "undefined"]]
361
+ ).push
362
+ ins.type_change = Environment::UntypedType.new
363
+ return
364
+ end
365
+
366
+ ins.type_change = var_type
367
+
368
+ when Instructions::InstanceVariableWriteInstruction
369
+ var_type = self_type.type.resolve_instance_variable(ins.name)
370
+ if var_type.nil?
371
+ Houndstooth::Errors::Error.new(
372
+ "Instance variable #{ins.name} is not defined",
373
+ [[ins.node.ast_node.loc.expression, "undefined"]]
374
+ ).push
375
+ ins.type_change = Environment::UntypedType.new
376
+ return
377
+ end
378
+
379
+ value_type = ins.block.variable_type_at!(ins.value, ins)
380
+
381
+ if !var_type.accepts?(value_type)
382
+ Houndstooth::Errors::Error.new(
383
+ "Cannot assign `#{value_type.rbs}` to #{ins.name}",
384
+ [[ins.node.ast_node.loc.expression, "Expected `#{var_type.rbs}`"]]
385
+ ).push
386
+ end
387
+
388
+ ins.type_change = var_type
389
+
390
+ else
391
+ raise "internal error: don\'t know how to type check #{ins.class.to_s}"
392
+ end
393
+ end
394
+
395
+ # Given a `LiteralInstruction`, assigns a result type based on the value. Assumes that the
396
+ # stdlib is loaded into the environment.
397
+ # @param [LiteralInstruction] ins
398
+ def assign_type_to_literal_instruction(ins)
399
+ ins.type_change =
400
+ case ins.value
401
+ when Integer
402
+ env.resolve_type("Integer").instantiate
403
+ when Float
404
+ env.resolve_type("Float").instantiate
405
+ when String
406
+ env.resolve_type("String").instantiate
407
+ when Symbol
408
+ env.resolve_type("Symbol").instantiate
409
+ when TrueClass
410
+ env.resolve_type("TrueClass").instantiate
411
+ when FalseClass
412
+ env.resolve_type("FalseClass").instantiate
413
+ when NilClass
414
+ env.resolve_type("NilClass").instantiate
415
+ else
416
+ Houndstooth::Errors::Error.new(
417
+ "Internal bug - encountered a literal with an unknown type",
418
+ [[ins.node.ast_node.loc.expression, "literal"]]
419
+ ).push
420
+ end
421
+ end
422
+
423
+ # Inserts instructions into the beginning of block to assign types to a set of parameter
424
+ # variables.
425
+ #
426
+ # @param [Instruction] ins The instruction this is relevant to. Only used for error
427
+ # reporting and for the nodes of the new instructions.
428
+ # @param [InstructionBlock] block The instruction block to prepend the new instructions to.
429
+ # @param [MethodType] method_type The method type to retrieve parameter types from.
430
+ # @return [Integer, false] False if an error occurred, otherwise the number of instructions
431
+ # added to the beginning.
432
+ def add_parameter_type_instructions(ins, block, method_type)
433
+ # Check parameter count
434
+ # TODO: this won't work if we support other kinds of parameter
435
+ expected_ps = method_type.positional_parameters
436
+ got_ps = block.parameters
437
+ expected_l = expected_ps.length
438
+ got_l = got_ps.length
439
+ if expected_l != got_l
440
+ Houndstooth::Errors::Error.new(
441
+ "Incorrect number of parameters (expected #{expected_l}, got #{got_l})",
442
+ [[ins.node.ast_node.loc.expression, "incorrect parameters"]]
443
+ ).push
444
+ return false
445
+ end
446
+
447
+ # Insert an instruction to assign each parameter's type
448
+ expected_ps.zip(got_ps).each do |type_param, var|
449
+ i = Instructions::AssignExistingInstruction.new(
450
+ block: block,
451
+ node: ins.node,
452
+ result: var,
453
+ variable: var,
454
+ )
455
+ i.type_change = type_param.type
456
+ block.instructions.unshift(i)
457
+ end
458
+
459
+ expected_ps.length
460
+ end
461
+ end
462
+ end
@@ -0,0 +1,53 @@
1
+ module Houndstooth; end
2
+
3
+ require_relative 'houndstooth/errors'
4
+ require_relative 'houndstooth/instructions'
5
+ require_relative 'houndstooth/semantic_node'
6
+ require_relative 'houndstooth/environment'
7
+ require_relative 'houndstooth/stdlib'
8
+ require_relative 'houndstooth/type_checker'
9
+ require_relative 'houndstooth/interpreter'
10
+
11
+ module Houndstooth
12
+ # Parses a complete file, and adds its type definitions to the given environment.
13
+ # Returns the parsed `SemanticNode`, or if a syntax error occurs, returns `nil`.
14
+ def self.process_file(file_name, file_contents, env)
15
+ # Build parser buffer
16
+ begin
17
+ buffer = Parser::Source::Buffer.new(file_name)
18
+ buffer.source = file_contents
19
+ rescue => e
20
+ Houndstooth::Errors::Error.new("Error building parse buffer: #{e}", []).push
21
+ abort_on_error!
22
+ end
23
+
24
+ # Parse file into AST nodes
25
+ any_errors = false
26
+ parser = Parser::Ruby30.new
27
+
28
+ parser.diagnostics.consumer = ->(diag) do
29
+ any_errors = true
30
+ Houndstooth::Errors::Error.new(
31
+ "Syntax error",
32
+ [[diag.location, diag.message]]
33
+ ).push
34
+ end
35
+ begin
36
+ ast_node, comments = parser.parse_with_comments(buffer)
37
+ rescue Parser::SyntaxError => e
38
+ # We already got a diagnostic for this, don't need to handle it again
39
+ end
40
+ $comments = comments
41
+
42
+ return nil if any_errors
43
+
44
+ # Convert to semantic nodes
45
+ node = Houndstooth::SemanticNode.from_ast(ast_node)
46
+
47
+ # Build environment items
48
+ Houndstooth::Environment::Builder.new(node, env).analyze
49
+
50
+ node
51
+ end
52
+ end
53
+