gloss 0.0.2 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/.gitattributes +3 -0
  3. data/.github/workflows/{crystal.yml → crystal_specs.yml} +1 -1
  4. data/.github/workflows/{ruby.yml → ruby_specs.yml} +1 -1
  5. data/.gloss.yml +1 -0
  6. data/.rspec +1 -0
  7. data/Gemfile.lock +25 -27
  8. data/README.md +36 -5
  9. data/exe/gloss +13 -2
  10. data/ext/gloss/{src/lib → lib}/cr_ruby.cr +0 -0
  11. data/ext/gloss/lib/rbs_types.cr +3 -0
  12. data/ext/gloss/spec/parser_spec.cr +83 -50
  13. data/ext/gloss/src/cr_ast.cr +146 -72
  14. data/ext/gloss/src/gloss.cr +2 -2
  15. data/ext/gloss/src/lexer.cr +59 -1
  16. data/ext/gloss/src/parser.cr +4 -4
  17. data/ext/gloss/src/rb_ast.cr +152 -57
  18. data/lib/gloss.rb +15 -7
  19. data/lib/gloss/cli.rb +85 -27
  20. data/lib/gloss/config.rb +18 -10
  21. data/lib/gloss/errors.rb +13 -7
  22. data/lib/gloss/initializer.rb +11 -6
  23. data/lib/gloss/logger.rb +29 -0
  24. data/lib/gloss/parser.rb +22 -5
  25. data/lib/gloss/prog_loader.rb +141 -0
  26. data/lib/gloss/scope.rb +7 -2
  27. data/lib/gloss/source.rb +17 -14
  28. data/lib/gloss/type_checker.rb +105 -66
  29. data/lib/gloss/utils.rb +44 -0
  30. data/lib/gloss/version.rb +6 -1
  31. data/lib/gloss/visitor.rb +667 -0
  32. data/lib/gloss/watcher.rb +63 -19
  33. data/lib/gloss/writer.rb +35 -18
  34. data/sig/core.rbs +2 -0
  35. data/sig/fast_blank.rbs +4 -0
  36. data/sig/gls.rbs +3 -0
  37. data/sig/listen.rbs +1 -0
  38. data/sig/optparse.rbs +6 -0
  39. data/sig/rubygems.rbs +9 -0
  40. data/sig/yaml.rbs +3 -0
  41. data/src/exe/gloss +19 -0
  42. data/src/lib/gloss.gl +26 -0
  43. data/src/lib/gloss/cli.gl +70 -0
  44. data/src/lib/gloss/config.gl +21 -0
  45. data/src/lib/gloss/errors.gl +11 -0
  46. data/src/lib/gloss/initializer.gl +20 -0
  47. data/src/lib/gloss/logger.gl +21 -0
  48. data/src/lib/gloss/parser.gl +31 -0
  49. data/src/lib/gloss/prog_loader.gl +133 -0
  50. data/src/lib/gloss/scope.gl +7 -0
  51. data/src/lib/gloss/source.gl +32 -0
  52. data/src/lib/gloss/type_checker.gl +119 -0
  53. data/src/lib/gloss/utils.gl +38 -0
  54. data/src/lib/gloss/version.gl +3 -0
  55. data/src/lib/gloss/visitor.gl +575 -0
  56. data/src/lib/gloss/watcher.gl +66 -0
  57. data/src/lib/gloss/writer.gl +35 -0
  58. metadata +35 -8
  59. data/lib/gloss/builder.rb +0 -393
  60. data/src/lib/hrb/initializer.gl +0 -22
  61. data/src/lib/hrb/watcher.gl +0 -32
@@ -0,0 +1,7 @@
1
+ module Gloss
2
+ class Scope < Hash[String, String]
3
+ def [](k)
4
+ fetch(k) { raise "Undefined expression for current scope: #{k}" }
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gloss
4
+ class Source < String
5
+ def initialize(@indent_level)
6
+ super()
7
+ end
8
+
9
+ def write(*args : String)
10
+ args.each do |a|
11
+ self << a
12
+ end
13
+ self
14
+ end
15
+
16
+ def write_indnt(*args : String)
17
+ write(*args.map { |a| "#{(" " * @indent_level)}#{a}" })
18
+ end
19
+
20
+ def write_ln(*args : String)
21
+ write_indnt(*args.map { |a| a.strip << "\n" })
22
+ end
23
+
24
+ def increment_indent
25
+ @indent_level += 1
26
+ end
27
+
28
+ def decrement_indent
29
+ @indent_level -= 1
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,119 @@
1
+ require "set"
2
+
3
+ module Gloss
4
+ class TypeChecker
5
+ Project = Struct.new :targets
6
+
7
+ attr_reader :steep_target, :top_level_decls, :env, :rbs_gem_dir
8
+
9
+ def initialize
10
+ @steep_target = Steep::Project::Target.new(
11
+ name: "gloss",
12
+ options: Steep::Project::Options.new.tap do |o|
13
+ o.allow_unknown_constant_assignment = true
14
+ end,
15
+ source_patterns: ["**/*.rb"],
16
+ ignore_patterns: Array.new,
17
+ signature_patterns: ["sig"]
18
+ )
19
+ @top_level_decls = Set.new
20
+ @rbs_gem_dir = Utils.gem_path_for("rbs")
21
+ env_loader = RBS::EnvironmentLoader.new
22
+ @env = RBS::Environment.from_loader(env_loader)
23
+ project = Steep::Project.new(steepfile_path: Pathname.new(Config.src_dir).realpath)
24
+ project.targets << @steep_target
25
+ loader = Steep::Project::FileLoader.new(project: project)
26
+ #loader.load_signatures
27
+ end
28
+
29
+ def run(filepath, rb_str)
30
+ begin
31
+ valid_types = check_types filepath, rb_str
32
+ rescue ParseError => e
33
+ throw :error, ""
34
+ rescue => e
35
+ throw :error, "Type checking Error: #{e.message} (#{e.class})"
36
+ end
37
+
38
+ unless valid_types
39
+ errors = @steep_target.errors.map { |e|
40
+ case e
41
+ when Steep::Diagnostic::Ruby::NoMethod
42
+ "Unknown method :#{e.method}, location: #{e.type.location.inspect}"
43
+ when Steep::Diagnostic::Ruby::MethodBodyTypeMismatch
44
+ "Invalid method body type - expected: #{e.expected}, actual: #{e.actual}"
45
+ when Steep::Diagnostic::Ruby::IncompatibleArguments
46
+ <<-ERR
47
+ Invalid argmuents - method type: #{e.method_types.first}
48
+ method name: #{e.method_name}
49
+ ERR
50
+ when Steep::Diagnostic::Ruby::ReturnTypeMismatch
51
+ "Invalid return type - expected: #{e.expected}, actual: #{e.actual}"
52
+ when Steep::Diagnostic::Ruby::IncompatibleAssignment
53
+ "Invalid assignment - cannot assign #{e.rhs_type} to type #{e.lhs_type}"
54
+ when Steep::Diagnostic::Ruby::UnexpectedBlockGiven
55
+ "Unexpected block given"
56
+ else
57
+ "#{e.header_line}\n#{e.inspect}"
58
+ end
59
+ }.join("\n")
60
+ throw :error, errors
61
+ end
62
+
63
+ true
64
+ end
65
+
66
+ def ready_for_checking!
67
+ @top_level_decls.each do |decl|
68
+ @env << decl
69
+ end
70
+ @env = @env.resolve_type_names
71
+
72
+ @steep_target.instance_variable_set("@environment", @env)
73
+ end
74
+
75
+ def check_types(filepath, rb_str)
76
+ @steep_target.add_source(filepath, rb_str)
77
+
78
+ ready_for_checking!
79
+
80
+ @steep_target.type_check
81
+
82
+ if @steep_target.status.is_a? Steep::Project::Target::SignatureErrorStatus
83
+ throw :error, @steep_target.status.errors.map { |e|
84
+ msg = case e
85
+ when Steep::Diagnostic::Signature::UnknownTypeName
86
+ "Unknown type name: #{e.name.name} (#{e.location.source[/^.*$/]})"
87
+ when Steep::Diagnostic::Signature::InvalidTypeApplication
88
+ "Invalid type application: #{e.header_line}"
89
+ when Steep::Diagnostic::Signature::DuplicatedMethodDefinition
90
+ "Duplicated method: #{e.header_line}"
91
+ else
92
+ e.header_line
93
+ end
94
+ <<~MSG
95
+ SignatureSyntaxError:
96
+ Location: #{e.location}
97
+ Message: "#{msg}"
98
+ MSG
99
+ }.join("\n")
100
+ end
101
+
102
+ @steep_target.source_files.each do |path, f|
103
+ if f.status.is_a? Steep::Project::SourceFile::ParseErrorStatus
104
+ e = f.status.error
105
+ throw :error, "#{e.class}: #{e.message}"
106
+ end
107
+ end
108
+
109
+ @steep_target.status.is_a?(Steep::Project::Target::TypeCheckStatus) &&
110
+ @steep_target.no_error? &&
111
+ @steep_target.errors.empty?
112
+ end
113
+
114
+ def load_sig_path(path : String)
115
+ Gloss.logger.debug "Loading signature file for #{path}"
116
+ @steep_target.add_signature path, File.open(path).read
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,38 @@
1
+ require "rubygems"
2
+
3
+ module Gloss
4
+ module Utils
5
+ module_function
6
+
7
+ def absolute_path(path)
8
+ pn = Pathname.new(path)
9
+ if pn.absolute?
10
+ pn.to_s
11
+ else
12
+ ap = File.absolute_path path
13
+ if File.exist? ap
14
+ ap
15
+ else
16
+ throw :error, "File path #{path} does not exist (also looked for #{ap})"
17
+ end
18
+ end
19
+ end
20
+
21
+ def gem_path_for(gem_name)
22
+ Gem.ui.instance_variable_set :"@outs", StringIO.new
23
+ Gem::GemRunner.new.run(["which", gem_name])
24
+ Gem.ui.outs.string
25
+ rescue SystemExit => e
26
+ nil
27
+ end
28
+
29
+ def with_file_header(str)
30
+ "#{Visitor::FILE_HEADER}\n\n#{str}"
31
+ end
32
+
33
+ def src_path_to_output_path(src_path : String) : String
34
+ src_path.sub("#{Config.src_dir}/", "")
35
+ .sub(/\.gl$/, ".rb")
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,3 @@
1
+ module Gloss
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,575 @@
1
+ module Gloss
2
+ class Visitor
3
+ FILE_HEADER = <<~RUBY
4
+ #{"# frozen_string_literal: true\n" if Config.frozen_string_literals}
5
+ ##### This file was generated by Gloss; any changes made here will be overwritten.
6
+ ##### See #{Config.src_dir}/ to make changes
7
+ RUBY
8
+
9
+ attr_reader :tree
10
+
11
+ def initialize(tree_hash, type_checker = nil, @on_new_file_referenced = nil)
12
+ @indent_level = 0
13
+ @inside_macro = false
14
+ @eval_vars = false
15
+ @current_scope = nil
16
+ @tree = tree_hash
17
+ @type_checker = type_checker
18
+ @after_module_function = false
19
+ end
20
+
21
+ def run
22
+ rb_output = visit_node(@tree)
23
+ Utils.with_file_header(rb_output)
24
+ end
25
+
26
+ # type node = Hash[Symbol, String | Array[String | node] | Hash[Symbol, node]] | true | false
27
+
28
+ def visit_node(node : Hash[Symbol, Any], scope = Scope.new) : String
29
+ src = Source.new(@indent_level)
30
+ case node[:type]
31
+ when "ClassNode"
32
+ class_name = visit_node(node[:name])
33
+ current_namespace = @current_scope ? @current_scope.name.to_namespace : RBS::Namespace.root
34
+ superclass_type = nil
35
+ superclass_output = ""
36
+ if node[:superclass]
37
+ @eval_vars = true
38
+ superclass_output = visit_node(node[:superclass])
39
+ @eval_vars = false
40
+ args = Array.new
41
+ if node.dig(:superclass, :type) == "Generic"
42
+ superclass_output = superclass_output[/^[^\[]+/] || superclass_output
43
+ args = node.dig(:superclass, :args).map { |n| RBS::Parser.parse_type(visit_node(n)) }
44
+ end
45
+
46
+ class_name_index = superclass_output.index(/[^(?:::)]+\z/) || 0
47
+ namespace = superclass_output[0, class_name_index]
48
+ superclass_name = superclass_output[/[^(?:::)]+\z/] || superclass_output
49
+ superclass_type = RBS::AST::Declarations::Class::Super.new(
50
+ name: RBS::TypeName.new(
51
+ namespace: method(:Namespace).call(namespace),
52
+ name: superclass_name.to_sym,
53
+ ),
54
+ args: args,
55
+ location: build_location(node),
56
+ )
57
+ end
58
+
59
+ src.write_ln "class #{class_name}#{" < #{superclass_output}" unless superclass_output.blank?}"
60
+
61
+ class_type = RBS::AST::Declarations::Class.new(
62
+ name: RBS::TypeName.new(
63
+ namespace: current_namespace,
64
+ name: class_name.to_sym
65
+ ),
66
+ type_params: RBS::AST::Declarations::ModuleTypeParams.new, # responds to #add to add params
67
+ super_class: superclass_type,
68
+ members: Array.new, # TODO
69
+ annotations: Array.new, # TODO
70
+ location: build_location(node),
71
+ comment: node[:comment]
72
+ )
73
+ old_parent_scope = @current_scope
74
+ @current_scope = class_type
75
+
76
+ indented(src) { src.write_ln visit_node(node[:body]) if node[:body] }
77
+
78
+ src.write_ln "end"
79
+
80
+ @current_scope = old_parent_scope
81
+
82
+ @current_scope.members << class_type if @current_scope
83
+
84
+ if @type_checker && !@current_scope
85
+ @type_checker.top_level_decls.add(class_type)
86
+ end
87
+ when "ModuleNode"
88
+ existing_module_function_state = @after_module_function.dup
89
+ @after_module_function = false
90
+ module_name = visit_node node[:name]
91
+ src.write_ln "module #{module_name}"
92
+
93
+ current_namespace = @current_scope ? @current_scope.name.to_namespace : RBS::Namespace.root
94
+
95
+ module_type = RBS::AST::Declarations::Module.new(
96
+ name: RBS::TypeName.new(
97
+ namespace: current_namespace,
98
+ name: module_name.to_sym
99
+ ),
100
+ type_params: RBS::AST::Declarations::ModuleTypeParams.new, # responds to #add to add params
101
+ self_types: Array.new, # TODO
102
+ members: Array.new, # TODO
103
+ annotations: Array.new, # TODO
104
+ location: build_location(node),
105
+ comment: node[:comment]
106
+ )
107
+ old_parent_scope = @current_scope
108
+ @current_scope = module_type
109
+
110
+ indented(src) { src.write_ln visit_node(node[:body]) if node[:body] }
111
+
112
+ @current_scope = old_parent_scope
113
+
114
+ @current_scope.members << module_type if @current_scope
115
+
116
+ if @type_checker && !@current_scope
117
+ @type_checker.top_level_decls.add(module_type)
118
+ end
119
+ src.write_ln "end"
120
+ @after_module_function = existing_module_function_state
121
+ when "DefNode"
122
+ args = render_args(node)
123
+ receiver = node[:receiver] ? visit_node(node[:receiver]) : nil
124
+ src.write_ln "def #{"#{receiver}." if receiver}#{node[:name]}#{args[:representation]}"
125
+
126
+ return_type = if node[:return_type]
127
+ RBS::Types::ClassInstance.new(
128
+ name: RBS::TypeName.new(
129
+ name: eval(visit_node(node[:return_type])).to_s.to_sym,
130
+ namespace: RBS::Namespace.root
131
+ ),
132
+ args: EMPTY_ARRAY, # TODO
133
+ location: build_location(node)
134
+ )
135
+ else
136
+ RBS::Types::Bases::Any.new(
137
+ location: build_location(node)
138
+ )
139
+ end
140
+
141
+ method_types = [
142
+ RBS::MethodType.new(
143
+ type_params: EMPTY_ARRAY, # TODO
144
+ type: RBS::Types::Function.new(
145
+ required_positionals: args.dig(:types, :required_positionals),
146
+ optional_positionals: args.dig(:types, :optional_positionals),
147
+ rest_positionals: args.dig(:types, :rest_positionals),
148
+ trailing_positionals: args.dig(:types, :trailing_positionals),
149
+ required_keywords: args.dig(:types, :required_keywords),
150
+ optional_keywords: args.dig(:types, :optional_keywords),
151
+ rest_keywords: args.dig(:types, :rest_keywords),
152
+ return_type: return_type
153
+ ),
154
+ block: node[:yield_arg_count] ?
155
+ RBS::Types::Block.new(
156
+ type: RBS::Types::Function.new(
157
+ required_positionals: Array.new,
158
+ optional_positionals: Array.new,
159
+ rest_positionals: nil,
160
+ trailing_positionals: Array.new,
161
+ required_keywords: Hash.new,
162
+ optional_keywords: Hash.new,
163
+ rest_keywords: nil,
164
+ return_type: RBS::Types::Bases::Any.new(location: build_location(node))
165
+ ),
166
+ required: !!(node[:block_arg] || node[:yield_arg_count])
167
+ ) : nil,
168
+ location: build_location(node)
169
+ )
170
+ ]
171
+ method_definition = RBS::AST::Members::MethodDefinition.new(
172
+ name: node[:name].to_sym,
173
+ kind: @after_module_function ? :singleton_instance : receiver ? :singleton : :instance,
174
+ types: method_types,
175
+ annotations: EMPTY_ARRAY, # TODO
176
+ location: build_location(node),
177
+ comment: node[:comment],
178
+ overload: false
179
+ )
180
+
181
+ if @current_scope
182
+ @current_scope.members << method_definition
183
+ else
184
+ @type_checker.type_env << method_definition if @type_checker # should be new class declaration for Object with method_definition as private method
185
+ end
186
+
187
+ indented(src) { src.write_ln visit_node(node[:body]) if node[:body] }
188
+
189
+ src.write_ln "end"
190
+
191
+ when "VisibilityModifier"
192
+
193
+ src.write_ln "#{node[:visibility]} #{visit_node(node[:exp])}"
194
+ when "CollectionNode"
195
+ node[:children].each { |a| src.write visit_node(a, scope) }
196
+ when "Call"
197
+ obj = node[:object] ? "#{visit_node(node[:object], scope)}." : ""
198
+ arg_arr = node.fetch(:args) { EMPTY_ARRAY }
199
+ arg_arr += node[:named_args] if node[:named_args]
200
+ args = if !arg_arr.empty? || node[:block_arg]
201
+ "#{arg_arr.map { |a| visit_node(a, scope).strip }.reject(&:blank?).join(", ")}#{"&#{visit_node(node[:block_arg]).strip}" if node[:block_arg]}"
202
+ else
203
+ nil
204
+ end
205
+ block = node[:block] ? " #{visit_node(node[:block])}" : nil
206
+ has_parens = !!(node[:has_parentheses] || args || block)
207
+ opening_delimiter = if has_parens
208
+ "("
209
+ else
210
+ nil
211
+ end
212
+ name = node[:name]
213
+ call = "#{obj}#{name}#{opening_delimiter}#{args}#{")" if has_parens}#{block}"
214
+ case name
215
+ when "require_relative"
216
+ @on_new_file_referenced.call(name, true) if @on_new_file_referenced
217
+ when "module_function"
218
+ @after_module_function = true
219
+ end
220
+ src.write_ln(call)
221
+
222
+ when "Block"
223
+ args = render_args node
224
+ src.write "{ #{args[:representation].gsub(/(\A\(|\)\z)/,'|')}\n"
225
+
226
+ indented(src) { src.write visit_node(node[:body]) }
227
+
228
+ src.write_ln "}"
229
+
230
+ when "RangeLiteral"
231
+ dots = node[:exclusive] ? "..." : ".."
232
+
233
+ # parentheses around the whole thing help the compatibility with precendence of operators in some situations
234
+ # eg. (1..3).cover? 2 vs. 1..3.cover? 2
235
+ # parentheses around either number help with things like arithemtic ((x-1)..(y+5))
236
+ src.write "(", "(", visit_node(node[:from]), ")", dots, "(", visit_node(node[:to]), ")", ")"
237
+
238
+ when "LiteralNode"
239
+
240
+ src.write node[:value]
241
+
242
+ when "ArrayLiteral"
243
+
244
+ src.write("[", node[:elements].map { |e| visit_node(e).strip }.join(", "), "]")
245
+ src.write ".freeze" if node[:frozen]
246
+
247
+ when "StringInterpolation"
248
+
249
+ contents = node[:contents].inject(String.new) do |str, c|
250
+ str << case c[:type]
251
+ when "LiteralNode"
252
+ c[:value][1...-1]
253
+ else
254
+ [%q|#{|, visit_node(c).strip, "}"].join
255
+ end
256
+ end
257
+ src.write '"', contents, '"'
258
+
259
+ when "Path"
260
+
261
+ src.write node[:value]
262
+
263
+ when "Require"
264
+ path = node[:value]
265
+ src.write_ln %(require "#{path}")
266
+
267
+ @on_new_file_referenced.call(path, false) if @on_new_file_referenced
268
+ when "Assign", "OpAssign"
269
+
270
+ src.write_ln "#{visit_node(node[:target])} #{node[:op]}= #{visit_node(node[:value]).strip}"
271
+
272
+ when "MultiAssign"
273
+
274
+ src.write_ln "#{node[:targets].map{ |t| visit_node(t).strip }.join(", ")} = #{node[:values].map { |v| visit_node(v).strip }.join(", ")}"
275
+
276
+ when "Var"
277
+
278
+ if @eval_vars
279
+ src.write scope[node[:name]]
280
+ else
281
+ src.write node[:name]
282
+ end
283
+
284
+ when "InstanceVar"
285
+
286
+ src.write node[:name]
287
+
288
+ when "GlobalVar"
289
+
290
+ src.write node[:name]
291
+
292
+ when "Arg"
293
+ val = node[:external_name]
294
+ if node[:keyword_arg]
295
+ val += ":"
296
+ val += " #{visit_node(node[:value])}" if node[:value]
297
+ elsif node[:value]
298
+ val += " = #{visit_node(node[:value])}"
299
+ end
300
+
301
+ src.write val
302
+
303
+ when "UnaryExpr"
304
+
305
+ src.write "#{node[:op]}#{visit_node(node[:value]).strip}"
306
+
307
+ when "BinaryOp"
308
+
309
+ src.write visit_node(node[:left]).strip, " #{node[:op]} ", visit_node(node[:right]).strip
310
+
311
+ when "HashLiteral"
312
+
313
+ contents = node[:elements].map do |k, v|
314
+ key = case k
315
+ when String
316
+ k.to_sym.inspect
317
+ else
318
+ visit_node k
319
+ end
320
+ value = visit_node v
321
+ "#{key} => #{value}"
322
+ end
323
+
324
+ src.write "{#{contents.join(",\n")}}"
325
+ src.write ".freeze" if node[:frozen]
326
+
327
+ when "Enum"
328
+ src.write_ln "module #{node[:name]}"
329
+ node[:members].each_with_index do |m, i|
330
+ indented(src) { src.write_ln(visit_node(m) + (!m[:value] ? " = #{i}" : "")) }
331
+ end
332
+ src.write_ln "end"
333
+ when "If"
334
+ src.write_ln "(if #{visit_node(node[:condition]).strip}"
335
+
336
+ indented(src) { src.write_ln visit_node(node[:then]) }
337
+
338
+ if node[:else]
339
+ src.write_ln "else"
340
+ indented(src) { src.write_ln visit_node(node[:else]) }
341
+ end
342
+
343
+ src.write_ln "end)"
344
+ when "Unless"
345
+ src.write_ln "unless #{visit_node node[:condition]}"
346
+ indented(src) { src.write_ln visit_node(node[:then]) }
347
+
348
+ if node[:else]
349
+ src.write_ln "else"
350
+ indented(src) { src.write_ln visit_node(node[:else]) }
351
+ end
352
+
353
+ src.write_ln "end"
354
+ when "Case"
355
+ src.write "case"
356
+ src.write " #{visit_node(node[:condition]).strip}\n" if node[:condition]
357
+ indented(src) do
358
+ node[:whens].each do |w|
359
+ src.write_ln visit_node(w)
360
+ end
361
+ if node[:else]
362
+ src.write_ln "else"
363
+ indented(src) do
364
+ src.write_ln visit_node(node[:else])
365
+ end
366
+ end
367
+ end
368
+ src.write_ln "end"
369
+ when "When"
370
+ src.write_ln "when #{node[:conditions].map { |n| visit_node(n) }.join(", ")}"
371
+
372
+ indented(src) { src.write_ln(node[:body] ? visit_node(node[:body]) : "# no op") }
373
+ when "MacroFor"
374
+ vars, expr, body = node[:vars], node[:expr], node[:body]
375
+ var_names = vars.map { |v| visit_node v }
376
+ @inside_macro = true
377
+ indent_level = @indent_level
378
+ @indent_level -= 1 unless indent_level.zero?
379
+ expanded : Array[String] = eval(visit_node(expr)).map do |*a|
380
+ locals = [var_names.join(%(", "))].zip(a).to_h
381
+ locals.merge!(scope) if @inside_macro
382
+ visit_node(body, locals)
383
+ end.flatten
384
+ @indent_level += 1 unless indent_level.zero?
385
+ expanded.each { |e| src.write e }
386
+ @inside_macro = false
387
+ when "MacroLiteral"
388
+ src.write node[:value]
389
+ when "MacroExpression"
390
+ if node[:output]
391
+ expr = visit_node node[:expr], scope
392
+ val = scope[expr]
393
+ src.write val
394
+ end
395
+ when "MacroIf"
396
+ if evaluate_macro_condition(node[:condition], scope)
397
+ src.write_ln visit_node(node[:then], scope) if node[:then]
398
+ else
399
+ src.write_ln visit_node(node[:else], scope) if node[:else]
400
+ end
401
+ when "Return"
402
+ val = node[:value] ? " #{visit_node(node[:value]).strip}" : nil
403
+ src.write "return#{val}"
404
+ when "TypeDeclaration"
405
+ src.write_ln "# @type var #{visit_node(node[:var])}: #{visit_node(node[:declared_type])}"
406
+ value = node[:value] ? " = #{visit_node node[:value]}" : nil
407
+ src.write_ln "#{visit_node(node[:var])}#{value}"
408
+ when "ExceptionHandler"
409
+ src.write_ln "begin"
410
+ indented src do
411
+ src.write_ln visit_node(node[:body])
412
+ end
413
+ if node[:rescues]
414
+ node[:rescues].each do |r|
415
+ src.write_ln "rescue #{r[:types].map { |n| visit_node n }.join(", ") if r[:types]}#{" => #{r[:name]}" if r[:name]}"
416
+ indented(src) { src.write_ln visit_node(r[:body]) } if r[:body]
417
+ end
418
+ end
419
+ if node[:else]
420
+ src.write_ln "else"
421
+ indented(src) { src.write_ln visit_node(node[:else]) }
422
+ end
423
+ if node[:ensure]
424
+ src.write_ln "ensure"
425
+ indented(src) { src.write_ln visit_node(node[:ensure]) }
426
+ end
427
+ src.write_ln "end"
428
+ when "Generic"
429
+ src.write "#{visit_node(node[:name])}[#{node[:args].map { |a| visit_node a }.join(", ")}]"
430
+ when "Proc"
431
+ fn = node[:function]
432
+ src.write "->#{render_args(fn)} { #{visit_node fn[:body]} }"
433
+ when "Include"
434
+ current_namespace = @current_scope ? @current_scope.name.to_namespace : RBS::Namespace.root
435
+ name = visit_node node[:name]
436
+ src.write_ln "include #{name}"
437
+ type = RBS::AST::Members::Include.new(
438
+ name: method(:TypeName).call(name),
439
+ args: Array.new,
440
+ annotations: Array.new,
441
+ location: build_location(node),
442
+ comment: node[:comment]
443
+ )
444
+ if @current_scope
445
+ @current_scope.members << type
446
+ else
447
+ @type_checker.type_env << type
448
+ end
449
+ when "Extend"
450
+ current_namespace = @current_scope ? @current_scope.name.to_namespace : RBS::Namespace.root
451
+ name = visit_node node[:name]
452
+ src.write_ln "extend #{name}"
453
+ type = RBS::AST::Members::Extend.new(
454
+ name: method(:TypeName).call(name),
455
+ args: Array.new,
456
+ annotations: Array.new,
457
+ location: build_location(node),
458
+ comment: node[:comment]
459
+ )
460
+ if @current_scope
461
+ @current_scope.members << type
462
+ else
463
+ @type_checker.type_env << type
464
+ end
465
+ when "RegexLiteral"
466
+ contents = visit_node node[:value]
467
+ src.write Regexp.new(contents.undump).inspect
468
+ when "Union"
469
+ types = node[:types]
470
+ output = if types.length == 2 && types[1][:type] == "Path" && types[1]["value"] == nil
471
+ "#{visit_node(types[0])}?"
472
+ else
473
+ types.map { |t| visit_node(t) }.join(" | ")
474
+ end
475
+ src.write output
476
+ when "Next"
477
+ val = " #{node[:value]}" if node[:value]
478
+ src.write "next#{val}"
479
+ when "EmptyNode"
480
+ # pass
481
+ else
482
+ raise "Not implemented: #{node[:type]}"
483
+ end
484
+
485
+ src
486
+ end
487
+
488
+ private def evaluate_macro_condition(condition_node, scope)
489
+ @eval_vars = true
490
+ eval(visit_node(condition_node, scope))
491
+ @eval_vars = false
492
+ end
493
+
494
+ private def indented(src)
495
+ increment_indent(src)
496
+ yield
497
+ decrement_indent(src)
498
+ end
499
+
500
+ private def increment_indent(src)
501
+ @indent_level += 1
502
+ src.increment_indent
503
+ end
504
+
505
+ private def decrement_indent(src)
506
+ @indent_level -= 1
507
+ src.decrement_indent
508
+ end
509
+
510
+ # TODO: allow NamedTuple as return type
511
+ private def render_args(node)# : { representation: String, types: Hash[Symbol, Any] }
512
+ rp : Array[Hash[Symbol, Any]] = node.fetch(:positional_args) { EMPTY_ARRAY }.filter { |a| !a[:value] }
513
+ op : Array[Hash[Symbol, Any]] = node.fetch(:positional_args) { EMPTY_ARRAY }.filter { |a| a[:value] }
514
+ rkw : Hash[Symbol, Any] = node.fetch(:req_kw_args) { EMPTY_HASH }
515
+ okw : Hash[Symbol, Any] = node.fetch(:opt_kw_args) { EMPTY_HASH }
516
+ rest_p : String? = node[:rest_p_args] ? visit_node(node[:rest_p_args]) : nil
517
+ rest_kw : Hash[Symbol, Any]? = node[:rest_kw_args]
518
+ return nil if [rp, op, rkw, okw, rest_p, rest_kw].all? { |a| a && a.empty? }
519
+
520
+ contents = [
521
+ rp.map { |a| visit_node(a) },
522
+ op.map { |a| "#{a[:name]} = #{visit_node(a[:value]).strip}" },
523
+ rkw.map { |name, _| "#{name}:" },
524
+ okw.map { |name, value| "#{name}: #{value}" },
525
+ rest_p ? "*#{rest_p}" : "",
526
+ rest_kw ? "**#{visit_node(rest_kw)}" : ""
527
+ ].reject(&:empty?).flatten.join(", ")
528
+ representation = "(#{contents})"
529
+ rp_args = rp.map do |a|
530
+ RBS::Types::Function::Param.new(
531
+ name: visit_node(a).to_sym,
532
+ type: RBS::Types::Bases::Any.new(
533
+ location: build_location(a)
534
+ )
535
+ )
536
+ end
537
+ op_args = op.map do |a|
538
+ RBS::Types::Function::Param.new(
539
+ name: visit_node(a).to_sym,
540
+ type: RBS::Types::Bases::Any.new(location: build_location(a))
541
+ )
542
+ end
543
+ rpa = rest_p ? RBS::Types::Function::Param.new(
544
+ name: rest_p.to_sym,
545
+ type: RBS::Types::Bases::Any.new(location: build_location(node))
546
+ ) : nil
547
+ {
548
+ representation: representation,
549
+ types: {
550
+ required_positionals: rp_args,
551
+ optional_positionals: op_args,
552
+ rest_positionals: rpa,
553
+ trailing_positionals: EMPTY_ARRAY, # TODO
554
+ required_keywords: node[:req_kw_args] || EMPTY_HASH,
555
+ optional_keywords: node[:opt_kw_args] || EMPTY_HASH,
556
+ rest_keywords: node[:rest_kw_args] ?
557
+ RBS::Types::Function::Param.new(
558
+ name: visit_node(node[:rest_kw_args]).to_sym,
559
+ type: RBS::Types::Bases::Any.new(location: build_location(node))
560
+ ) : nil
561
+ }
562
+ }
563
+ end
564
+
565
+ def build_location(node)
566
+ return nil unless node[:location]
567
+
568
+ # RBS::Location.new(
569
+ # buffer: RBS::Buffer.new(@file_path, @file_content),
570
+ # start_pos: node[:location][:start],
571
+ # end_pos: node[:location][:end]
572
+ # )
573
+ end
574
+ end
575
+ end