gloss 0.0.4 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
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