gloss 0.0.3 → 0.1.1

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 (59) 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} +2 -2
  5. data/.gloss.yml +1 -0
  6. data/.rspec +1 -0
  7. data/Gemfile.lock +10 -12
  8. data/README.md +36 -5
  9. data/Rakefile +1 -1
  10. data/exe/gloss +13 -2
  11. data/ext/gloss/Makefile +8 -19
  12. data/ext/gloss/{src/lib → lib}/cr_ruby.cr +0 -0
  13. data/ext/gloss/lib/rbs_types.cr +3 -0
  14. data/ext/gloss/spec/parser_spec.cr +83 -83
  15. data/ext/gloss/src/cr_ast.cr +96 -77
  16. data/ext/gloss/src/gloss.cr +2 -2
  17. data/ext/gloss/src/lexer.cr +59 -1
  18. data/ext/gloss/src/rb_ast.cr +114 -63
  19. data/lib/gloss.rb +15 -7
  20. data/lib/gloss/cli.rb +85 -28
  21. data/lib/gloss/config.rb +13 -7
  22. data/lib/gloss/errors.rb +3 -4
  23. data/lib/gloss/initializer.rb +9 -9
  24. data/lib/gloss/logger.rb +29 -0
  25. data/lib/gloss/parser.rb +19 -5
  26. data/lib/gloss/prog_loader.rb +141 -0
  27. data/lib/gloss/scope.rb +7 -2
  28. data/lib/gloss/source.rb +17 -14
  29. data/lib/gloss/type_checker.rb +86 -33
  30. data/lib/gloss/utils.rb +44 -0
  31. data/lib/gloss/version.rb +1 -2
  32. data/lib/gloss/visitor.rb +667 -0
  33. data/lib/gloss/watcher.rb +51 -16
  34. data/lib/gloss/writer.rb +24 -15
  35. data/sig/core.rbs +2 -0
  36. data/sig/fast_blank.rbs +4 -0
  37. data/sig/{gloss.rbs → gls.rbs} +0 -0
  38. data/sig/listen.rbs +1 -0
  39. data/sig/optparse.rbs +6 -0
  40. data/sig/rubygems.rbs +9 -0
  41. data/sig/yaml.rbs +3 -0
  42. data/src/exe/gloss +19 -0
  43. data/src/lib/gloss.gl +26 -0
  44. data/src/lib/gloss/cli.gl +70 -0
  45. data/src/lib/gloss/config.gl +9 -3
  46. data/src/lib/gloss/initializer.gl +4 -6
  47. data/src/lib/gloss/logger.gl +21 -0
  48. data/src/lib/gloss/parser.gl +17 -5
  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 +85 -36
  53. data/src/lib/gloss/utils.gl +38 -0
  54. data/src/lib/gloss/version.gl +1 -1
  55. data/src/lib/gloss/visitor.gl +575 -0
  56. data/src/lib/gloss/watcher.gl +44 -10
  57. data/src/lib/gloss/writer.gl +16 -14
  58. metadata +28 -8
  59. data/lib/gloss/builder.rb +0 -447
@@ -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
@@ -1,10 +1,10 @@
1
- # frozen_string_literal: true
1
+ require "set"
2
2
 
3
3
  module Gloss
4
4
  class TypeChecker
5
5
  Project = Struct.new :targets
6
6
 
7
- attr_reader :steep_target, :top_level_decls
7
+ attr_reader :steep_target, :top_level_decls, :env, :rbs_gem_dir
8
8
 
9
9
  def initialize
10
10
  @steep_target = Steep::Project::Target.new(
@@ -12,59 +12,108 @@ module Gloss
12
12
  options: Steep::Project::Options.new.tap do |o|
13
13
  o.allow_unknown_constant_assignment = true
14
14
  end,
15
- source_patterns: ["gloss.rb"],
15
+ source_patterns: ["**/*.rb"],
16
16
  ignore_patterns: Array.new,
17
17
  signature_patterns: ["sig"]
18
18
  )
19
- @top_level_decls = {}
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
20
27
  end
21
28
 
22
- def run(rb_str)
23
- unless check_types(rb_str)
24
- raise Errors::TypeError,
25
- @steep_target.errors.map { |e|
26
- case e
27
- when Steep::Errors::NoMethod
28
- "Unknown method :#{e.method}, location: #{e.type.location.inspect}"
29
- when Steep::Errors::MethodBodyTypeMismatch
30
- "Invalid method body type - expected: #{e.expected}, actual: #{e.actual}"
31
- when Steep::Errors::IncompatibleArguments
32
- "Invalid argmuents - method type: #{e.method_type}, receiver type: #{e.receiver_type}"
33
- when Steep::Errors::ReturnTypeMismatch
34
- "Invalid return type - expected: #{e.expected}, actual: #{e.actual}"
35
- when Steep::Errors::IncompatibleAssignment
36
- "Invalid assignment - cannot assign #{e.rhs_type} to type #{e.lhs_type}"
37
- else
38
- e.inspect
39
- end
40
- }.join("\n")
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
41
61
  end
42
62
 
43
63
  true
44
64
  end
45
65
 
46
- def check_types(rb_str)
47
- env_loader = RBS::EnvironmentLoader.new
48
- env = RBS::Environment.from_loader(env_loader)
49
- project = Steep::Project.new(steepfile_path: Pathname.new(Config.src_dir).realpath)
50
- project.targets << @steep_target
51
- loader = Steep::Project::FileLoader.new(project: project)
52
- loader.load_signatures
66
+ def ready_for_checking!
67
+ @top_level_decls.each do |decl|
68
+ @env << decl
69
+ end
70
+ @env = @env.resolve_type_names
53
71
 
54
- @steep_target.add_source("gloss.rb", rb_str)
72
+ @steep_target.instance_variable_set("@environment", @env)
73
+ end
55
74
 
56
- @top_level_decls.each do |_, decl|
57
- env << decl
58
- end
59
- env = env.resolve_type_names
75
+ def check_types(filepath, rb_str)
76
+ @steep_target.add_source(filepath, rb_str)
60
77
 
61
- @steep_target.instance_variable_set("@environment", env)
78
+ ready_for_checking!
62
79
 
63
80
  @steep_target.type_check
64
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
+
65
109
  @steep_target.status.is_a?(Steep::Project::Target::TypeCheckStatus) &&
66
110
  @steep_target.no_error? &&
67
111
  @steep_target.errors.empty?
68
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
69
118
  end
70
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
@@ -1,3 +1,3 @@
1
1
  module Gloss
2
- VERSION = "0.0.3"
2
+ VERSION = "0.1.1"
3
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