gloss 0.0.4 → 0.1.2

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 (53) 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 +9 -11
  8. data/README.md +35 -5
  9. data/Rakefile +1 -1
  10. data/exe/gloss +13 -2
  11. data/ext/gloss/Makefile +8 -19
  12. data/ext/gloss/spec/parser_spec.cr +83 -83
  13. data/ext/gloss/src/cr_ast.cr +63 -77
  14. data/ext/gloss/src/gloss.cr +1 -2
  15. data/ext/gloss/src/rb_ast.cr +40 -38
  16. data/lib/gloss.rb +15 -7
  17. data/lib/gloss/cli.rb +75 -32
  18. data/lib/gloss/config.rb +10 -3
  19. data/lib/gloss/errors.rb +1 -1
  20. data/lib/gloss/initializer.rb +6 -5
  21. data/lib/gloss/logger.rb +29 -0
  22. data/lib/gloss/parser.rb +17 -2
  23. data/lib/gloss/prog_loader.rb +141 -0
  24. data/lib/gloss/scope.rb +1 -1
  25. data/lib/gloss/source.rb +1 -1
  26. data/lib/gloss/type_checker.rb +80 -32
  27. data/lib/gloss/utils.rb +44 -0
  28. data/lib/gloss/version.rb +5 -5
  29. data/lib/gloss/{builder.rb → visitor.rb} +125 -74
  30. data/lib/gloss/watcher.rb +47 -11
  31. data/lib/gloss/writer.rb +22 -11
  32. data/sig/core.rbs +2 -0
  33. data/sig/fast_blank.rbs +4 -0
  34. data/sig/{gloss.rbs → gls.rbs} +0 -0
  35. data/sig/optparse.rbs +6 -0
  36. data/sig/rubygems.rbs +9 -0
  37. data/sig/yaml.rbs +3 -0
  38. data/src/exe/gloss +19 -0
  39. data/src/lib/gloss.gl +26 -0
  40. data/src/lib/gloss/cli.gl +53 -24
  41. data/src/lib/gloss/config.gl +9 -3
  42. data/src/lib/gloss/initializer.gl +4 -6
  43. data/src/lib/gloss/logger.gl +21 -0
  44. data/src/lib/gloss/parser.gl +17 -5
  45. data/src/lib/gloss/prog_loader.gl +133 -0
  46. data/src/lib/gloss/scope.gl +0 -2
  47. data/src/lib/gloss/type_checker.gl +85 -39
  48. data/src/lib/gloss/utils.gl +38 -0
  49. data/src/lib/gloss/version.gl +1 -1
  50. data/src/lib/gloss/{builder.gl → visitor.gl} +123 -68
  51. data/src/lib/gloss/watcher.gl +44 -10
  52. data/src/lib/gloss/writer.gl +16 -14
  53. metadata +23 -8
@@ -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.4"
2
+ VERSION = "0.1.2"
3
3
  end
@@ -1,27 +1,26 @@
1
- # frozen_string_literal: true
2
-
3
1
  module Gloss
4
- class Builder
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
+
5
9
  attr_reader :tree
6
10
 
7
- def initialize(tree_hash, type_checker = nil)
11
+ def initialize(tree_hash, type_checker = nil, @on_new_file_referenced = nil)
8
12
  @indent_level = 0
9
13
  @inside_macro = false
10
14
  @eval_vars = false
11
15
  @current_scope = nil
12
16
  @tree = tree_hash
13
17
  @type_checker = type_checker
18
+ @after_module_function = false
14
19
  end
15
20
 
16
21
  def run
17
22
  rb_output = visit_node(@tree)
18
- <<~RUBY
19
- #{"# frozen_string_literal: true\n" if Config.frozen_string_literals}
20
- ##### This file was generated by Gloss; any changes made here will be overwritten.
21
- ##### See #{Config.src_dir}/ to make changes
22
-
23
- #{rb_output}
24
- RUBY
23
+ Utils.with_file_header(rb_output)
25
24
  end
26
25
 
27
26
  # type node = Hash[Symbol, String | Array[String | node] | Hash[Symbol, node]] | true | false
@@ -33,18 +32,31 @@ module Gloss
33
32
  class_name = visit_node(node[:name])
34
33
  current_namespace = @current_scope ? @current_scope.name.to_namespace : RBS::Namespace.root
35
34
  superclass_type = nil
36
- superclass_output = nil
35
+ superclass_output = ""
37
36
  if node[:superclass]
38
37
  @eval_vars = true
39
38
  superclass_output = visit_node(node[:superclass])
40
39
  @eval_vars = false
41
- superclass_type = RBS::Parser.parse_type superclass_output
40
+ args = Array.new
42
41
  if node.dig(:superclass, :type) == "Generic"
43
- superclass_output = superclass_output[/^[^\[]+/]
42
+ superclass_output = superclass_output[/^[^\[]+/] || superclass_output
43
+ args = node.dig(:superclass, :args).map { |n| RBS::Parser.parse_type(visit_node(n)) }
44
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
+ )
45
57
  end
46
58
 
47
- src.write_ln "class #{class_name}#{" < #{superclass_output}" if superclass_output}"
59
+ src.write_ln "class #{class_name}#{" < #{superclass_output}" unless superclass_output.blank?}"
48
60
 
49
61
  class_type = RBS::AST::Declarations::Class.new(
50
62
  name: RBS::TypeName.new(
@@ -55,7 +67,7 @@ module Gloss
55
67
  super_class: superclass_type,
56
68
  members: Array.new, # TODO
57
69
  annotations: Array.new, # TODO
58
- location: node[:location],
70
+ location: build_location(node),
59
71
  comment: node[:comment]
60
72
  )
61
73
  old_parent_scope = @current_scope
@@ -69,10 +81,12 @@ module Gloss
69
81
 
70
82
  @current_scope.members << class_type if @current_scope
71
83
 
72
- if @type_checker
73
- @type_checker.top_level_decls[class_type.name.name] = class_type unless @current_scope
84
+ if @type_checker && !@current_scope
85
+ @type_checker.top_level_decls.add(class_type)
74
86
  end
75
87
  when "ModuleNode"
88
+ existing_module_function_state = @after_module_function.dup
89
+ @after_module_function = false
76
90
  module_name = visit_node node[:name]
77
91
  src.write_ln "module #{module_name}"
78
92
 
@@ -87,7 +101,7 @@ module Gloss
87
101
  self_types: Array.new, # TODO
88
102
  members: Array.new, # TODO
89
103
  annotations: Array.new, # TODO
90
- location: node[:location],
104
+ location: build_location(node),
91
105
  comment: node[:comment]
92
106
  )
93
107
  old_parent_scope = @current_scope
@@ -99,13 +113,15 @@ module Gloss
99
113
 
100
114
  @current_scope.members << module_type if @current_scope
101
115
 
102
- if @type_checker
103
- @type_checker.top_level_decls[module_type.name.name] = module_type unless @current_scope
116
+ if @type_checker && !@current_scope
117
+ @type_checker.top_level_decls.add(module_type)
104
118
  end
105
119
  src.write_ln "end"
120
+ @after_module_function = existing_module_function_state
106
121
  when "DefNode"
107
122
  args = render_args(node)
108
- src.write_ln "def #{node[:name]}#{args}"
123
+ receiver = node[:receiver] ? visit_node(node[:receiver]) : nil
124
+ src.write_ln "def #{"#{receiver}." if receiver}#{node[:name]}#{args[:representation]}"
109
125
 
110
126
  return_type = if node[:return_type]
111
127
  RBS::Types::ClassInstance.new(
@@ -114,44 +130,25 @@ module Gloss
114
130
  namespace: RBS::Namespace.root
115
131
  ),
116
132
  args: EMPTY_ARRAY, # TODO
117
- location: node[:location]
133
+ location: build_location(node)
118
134
  )
119
135
  else
120
136
  RBS::Types::Bases::Any.new(
121
- location: node[:location]
137
+ location: build_location(node)
122
138
  )
123
139
  end
124
140
 
125
- rp : Array[Hash[Symbol, Any]] = node.fetch(:positional_args) { EMPTY_ARRAY }.filter { |a| !a[:value] }
126
- op : Array[Hash[Symbol, Any]] = node.fetch(:positional_args) { EMPTY_ARRAY }.filter { |a| a[:value] }
127
-
128
141
  method_types = [
129
142
  RBS::MethodType.new(
130
143
  type_params: EMPTY_ARRAY, # TODO
131
144
  type: RBS::Types::Function.new(
132
- required_positionals: rp.map do |a|
133
- RBS::Types::Function::Param.new(
134
- name: visit_node(a).to_sym,
135
- type: RBS::Types::Bases::Any.new(
136
- location: a[:location]
137
- )
138
- )
139
- end,
140
- optional_positionals: op.map do |a|
141
- RBS::Types::Function::Param.new(
142
- name: visit_node(a).to_sym,
143
- type: RBS::Types::Bases::Any.new(location: a[:location])
144
- )
145
- end,
146
- rest_positionals: (rpa = node[:rest_p_args]) ? RBS::Types::Function::Param.new(name: visit_node(rpa).to_sym, type: RBS::Types::Bases::Any.new(location: node[:location])) : nil,
147
- trailing_positionals: EMPTY_ARRAY, # TODO
148
- required_keywords: node[:req_kw_args] || EMPTY_HASH,
149
- optional_keywords: node[:opt_kw_args] || EMPTY_HASH,
150
- rest_keywords: node[:rest_kw_args] ?
151
- RBS::Types::Function::Param.new(
152
- name: visit_node(node[:rest_kw_args]).to_sym,
153
- type: RBS::Types::Bases::Any.new(location: node[:location])
154
- ) : nil,
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),
155
152
  return_type: return_type
156
153
  ),
157
154
  block: node[:yield_arg_count] ?
@@ -164,19 +161,19 @@ module Gloss
164
161
  required_keywords: Hash.new,
165
162
  optional_keywords: Hash.new,
166
163
  rest_keywords: nil,
167
- return_type: RBS::Types::Bases::Any.new(location: node[:location])
164
+ return_type: RBS::Types::Bases::Any.new(location: build_location(node))
168
165
  ),
169
166
  required: !!(node[:block_arg] || node[:yield_arg_count])
170
167
  ) : nil,
171
- location: node[:location]
168
+ location: build_location(node)
172
169
  )
173
170
  ]
174
171
  method_definition = RBS::AST::Members::MethodDefinition.new(
175
172
  name: node[:name].to_sym,
176
- kind: :instance,
173
+ kind: @after_module_function ? :singleton_instance : receiver ? :singleton : :instance,
177
174
  types: method_types,
178
175
  annotations: EMPTY_ARRAY, # TODO
179
- location: node[:location],
176
+ location: build_location(node),
180
177
  comment: node[:comment],
181
178
  overload: false
182
179
  )
@@ -212,12 +209,19 @@ module Gloss
212
209
  else
213
210
  nil
214
211
  end
215
- call = "#{obj}#{node[:name]}#{opening_delimiter}#{args}#{")" if has_parens}#{block}"
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
216
220
  src.write_ln(call)
217
221
 
218
222
  when "Block"
219
-
220
- src.write "{ |#{node[:args].map { |a| visit_node a }.join(", ")}|\n"
223
+ args = render_args node
224
+ src.write "{ #{args[:representation].gsub(/(\A\(|\)\z)/,'|')}\n"
221
225
 
222
226
  indented(src) { src.write visit_node(node[:body]) }
223
227
 
@@ -226,9 +230,10 @@ module Gloss
226
230
  when "RangeLiteral"
227
231
  dots = node[:exclusive] ? "..." : ".."
228
232
 
229
- # parentheses help the compatibility with precendence of operators in some situations
233
+ # parentheses around the whole thing help the compatibility with precendence of operators in some situations
230
234
  # eg. (1..3).cover? 2 vs. 1..3.cover? 2
231
- src.write "(", visit_node(node[:from]), dots, visit_node(node[:to]), ")"
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]), ")", ")"
232
237
 
233
238
  when "LiteralNode"
234
239
 
@@ -256,9 +261,10 @@ module Gloss
256
261
  src.write node[:value]
257
262
 
258
263
  when "Require"
264
+ path = node[:value]
265
+ src.write_ln %(require "#{path}")
259
266
 
260
- src.write_ln %(require "#{node[:value]}")
261
-
267
+ @on_new_file_referenced.call(path, false) if @on_new_file_referenced
262
268
  when "Assign", "OpAssign"
263
269
 
264
270
  src.write_ln "#{visit_node(node[:target])} #{node[:op]}= #{visit_node(node[:value]).strip}"
@@ -307,12 +313,12 @@ module Gloss
307
313
  contents = node[:elements].map do |k, v|
308
314
  key = case k
309
315
  when String
310
- k.to_sym
316
+ k.to_sym.inspect
311
317
  else
312
318
  visit_node k
313
319
  end
314
320
  value = visit_node v
315
- "#{key.inspect} => #{value}"
321
+ "#{key} => #{value}"
316
322
  end
317
323
 
318
324
  src.write "{#{contents.join(",\n")}}"
@@ -397,7 +403,8 @@ module Gloss
397
403
  src.write "return#{val}"
398
404
  when "TypeDeclaration"
399
405
  src.write_ln "# @type var #{visit_node(node[:var])}: #{visit_node(node[:declared_type])}"
400
- src.write_ln "#{visit_node(node[:var])} = #{visit_node(node[:value])}"
406
+ value = node[:value] ? " = #{visit_node node[:value]}" : nil
407
+ src.write_ln "#{visit_node(node[:var])}#{value}"
401
408
  when "ExceptionHandler"
402
409
  src.write_ln "begin"
403
410
  indented src do
@@ -431,7 +438,7 @@ module Gloss
431
438
  name: method(:TypeName).call(name),
432
439
  args: Array.new,
433
440
  annotations: Array.new,
434
- location: node[:location],
441
+ location: build_location(node),
435
442
  comment: node[:comment]
436
443
  )
437
444
  if @current_scope
@@ -447,7 +454,7 @@ module Gloss
447
454
  name: method(:TypeName).call(name),
448
455
  args: Array.new,
449
456
  annotations: Array.new,
450
- location: node[:location],
457
+ location: build_location(node),
451
458
  comment: node[:comment]
452
459
  )
453
460
  if @current_scope
@@ -466,6 +473,9 @@ module Gloss
466
473
  types.map { |t| visit_node(t) }.join(" | ")
467
474
  end
468
475
  src.write output
476
+ when "Next"
477
+ val = " #{node[:value]}" if node[:value]
478
+ src.write "next#{val}"
469
479
  when "EmptyNode"
470
480
  # pass
471
481
  else
@@ -497,7 +507,8 @@ module Gloss
497
507
  src.decrement_indent
498
508
  end
499
509
 
500
- private def render_args(node)
510
+ # TODO: allow NamedTuple as return type
511
+ private def render_args(node)# : { representation: String, types: Hash[Symbol, Any] }
501
512
  rp : Array[Hash[Symbol, Any]] = node.fetch(:positional_args) { EMPTY_ARRAY }.filter { |a| !a[:value] }
502
513
  op : Array[Hash[Symbol, Any]] = node.fetch(:positional_args) { EMPTY_ARRAY }.filter { |a| a[:value] }
503
514
  rkw : Hash[Symbol, Any] = node.fetch(:req_kw_args) { EMPTY_HASH }
@@ -514,7 +525,51 @@ module Gloss
514
525
  rest_p ? "*#{rest_p}" : "",
515
526
  rest_kw ? "**#{visit_node(rest_kw)}" : ""
516
527
  ].reject(&:empty?).flatten.join(", ")
517
- "(#{contents})"
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
+ # )
518
573
  end
519
574
  end
520
575
  end
@@ -1,30 +1,64 @@
1
- # frozen_string_literal: true
2
-
3
1
  require "listen"
4
2
 
5
3
  module Gloss
6
4
  class Watcher
7
- def initialize
8
- @paths = %w[src/]
5
+ def initialize(@paths : Array[String])
6
+ if @paths.empty?
7
+ @paths = [File.join(Dir.pwd, Config.src_dir)]
8
+ # either any filepath with .gl extension, or executable with extension
9
+ @only = /(?:(\.gl|(?:(?<=\/)[^\.\/]+))\z|\A[^\.\/]+\z)/
10
+ else
11
+ file_names = Array.new
12
+ paths = Array.new
13
+ @paths.each do |pa|
14
+ pn = Pathname.new(pa)
15
+ paths << pn.parent.to_s
16
+ file_names << (pn.file? ? pn.basename.to_s : pa)
17
+ end
18
+ @paths = paths.uniq
19
+ @only = /#{Regexp.union(file_names)}/
20
+ end
9
21
  end
10
22
 
11
23
  def watch
12
- puts "=====> Now listening for changes in #{@paths.join(', ')}"
13
- listener = Listen.to(*@paths, latency: 2) do |modified, added, removed|
24
+ Gloss.logger.info "Now listening for changes in #{@paths.join(', ')}"
25
+ listener = Listen.to(
26
+ *@paths,
27
+ latency: 2,
28
+ only: @only
29
+ ) do |modified, added, removed|
14
30
  (modified + added).each do |f|
31
+ Gloss.logger.info "Rewriting #{f}"
15
32
  content = File.read(f)
16
- Writer.new(Builder.new(content).run, f).run
33
+ err = catch :error do
34
+ Writer.new(
35
+ Visitor.new(
36
+ Parser.new(
37
+ content
38
+ ).run
39
+ ).run, f
40
+ ).run
41
+ nil
42
+ end
43
+ if err
44
+ Gloss.logger.error err
45
+ else
46
+ Gloss.logger.info "Done"
47
+ end
17
48
  end
18
49
  removed.each do |f|
19
50
  out_path = Utils.src_path_to_output_path(f)
51
+ Gloss.logger.info "Removing #{out_path}"
20
52
  File.delete out_path if File.exist? out_path
53
+
54
+ Gloss.logger.info "Done"
21
55
  end
22
56
  end
23
- listener.start
24
57
  begin
25
- loop { sleep 10 }
58
+ listener.start
59
+ sleep
26
60
  rescue Interrupt
27
- puts "=====> Interrupt signal received, shutting down"
61
+ Gloss.logger.info "Interrupt signal received, shutting down"
28
62
  exit 0
29
63
  end
30
64
  end