rbs 4.0.3 → 4.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 (155) hide show
  1. checksums.yaml +4 -4
  2. data/.dockerignore +37 -0
  3. data/.github/dependabot.yml +1 -1
  4. data/.github/workflows/bundle-update.yml +2 -2
  5. data/.github/workflows/c-check.yml +14 -6
  6. data/.github/workflows/comments.yml +2 -2
  7. data/.github/workflows/dependabot.yml +2 -2
  8. data/.github/workflows/jruby.yml +79 -0
  9. data/.github/workflows/release-gems.yml +164 -0
  10. data/.github/workflows/ruby.yml +43 -3
  11. data/.github/workflows/rust.yml +13 -10
  12. data/.github/workflows/truffleruby.yml +54 -0
  13. data/.github/workflows/typecheck.yml +5 -2
  14. data/.github/workflows/wasm.yml +55 -0
  15. data/.github/workflows/windows.yml +5 -2
  16. data/.gitignore +8 -0
  17. data/CHANGELOG.md +95 -4
  18. data/Dockerfile.jruby +53 -0
  19. data/README.md +3 -3
  20. data/Rakefile +429 -34
  21. data/Steepfile +9 -0
  22. data/config.yml +26 -0
  23. data/core/array.rbs +243 -180
  24. data/core/builtin.rbs +7 -6
  25. data/core/class.rbs +5 -3
  26. data/core/enumerable.rbs +109 -109
  27. data/core/enumerator/product.rbs +5 -5
  28. data/core/enumerator.rbs +28 -28
  29. data/core/file.rbs +24 -1018
  30. data/core/file_constants.rbs +463 -0
  31. data/core/file_stat.rbs +534 -0
  32. data/core/float.rbs +0 -24
  33. data/core/hash.rbs +117 -101
  34. data/core/integer.rbs +21 -58
  35. data/core/io.rbs +25 -7
  36. data/core/kernel.rbs +26 -11
  37. data/core/match_data.rbs +1 -1
  38. data/core/module.rbs +88 -74
  39. data/core/numeric.rbs +3 -0
  40. data/core/object_space/weak_key_map.rbs +7 -7
  41. data/core/pathname.rbs +0 -10
  42. data/core/ractor.rbs +0 -10
  43. data/core/range.rbs +23 -23
  44. data/core/rbs/ops.rbs +154 -0
  45. data/core/rbs/unnamed/argf.rbs +3 -3
  46. data/core/ruby_vm.rbs +40 -0
  47. data/core/rubygems/errors.rbs +4 -1
  48. data/core/rubygems/requirement.rbs +0 -10
  49. data/core/rubygems/rubygems.rbs +4 -1
  50. data/core/rubygems/specification.rbs +8 -0
  51. data/core/rubygems/version.rbs +0 -160
  52. data/core/set.rbs +3 -3
  53. data/core/struct.rbs +16 -16
  54. data/core/thread.rbs +9 -14
  55. data/docs/CONTRIBUTING.md +2 -1
  56. data/docs/inline.md +65 -7
  57. data/docs/rbs_by_example.md +20 -20
  58. data/docs/release.md +151 -0
  59. data/docs/syntax.md +2 -2
  60. data/docs/wasm_serialization.md +80 -0
  61. data/ext/rbs_extension/ast_translation.c +1298 -956
  62. data/ext/rbs_extension/ast_translation.h +4 -0
  63. data/ext/rbs_extension/class_constants.c +2 -0
  64. data/ext/rbs_extension/class_constants.h +1 -0
  65. data/ext/rbs_extension/extconf.rb +1 -0
  66. data/ext/rbs_extension/legacy_location.c +11 -6
  67. data/ext/rbs_extension/main.c +139 -4
  68. data/include/rbs/ast.h +323 -298
  69. data/include/rbs/defines.h +13 -0
  70. data/include/rbs/lexer.h +1 -0
  71. data/include/rbs/serialize.h +39 -0
  72. data/lib/rbs/annotate/rdoc_annotator.rb +27 -31
  73. data/lib/rbs/ast/ruby/annotations.rb +42 -0
  74. data/lib/rbs/ast/ruby/comment_block.rb +6 -4
  75. data/lib/rbs/ast/ruby/declarations.rb +11 -1
  76. data/lib/rbs/ast/ruby/helpers/location_helper.rb +1 -1
  77. data/lib/rbs/ast/ruby/members.rb +40 -1
  78. data/lib/rbs/buffer.rb +48 -11
  79. data/lib/rbs/cli.rb +3 -5
  80. data/lib/rbs/collection/config/lockfile_generator.rb +14 -1
  81. data/lib/rbs/collection/sources/git.rb +6 -0
  82. data/lib/rbs/definition_builder/method_builder.rb +12 -6
  83. data/lib/rbs/environment.rb +10 -3
  84. data/lib/rbs/inline_parser.rb +54 -28
  85. data/lib/rbs/namespace.rb +47 -11
  86. data/lib/rbs/parser_aux.rb +4 -2
  87. data/lib/rbs/prototype/rbi.rb +193 -25
  88. data/lib/rbs/prototype/runtime.rb +2 -0
  89. data/lib/rbs/resolver/type_name_resolver.rb +12 -14
  90. data/lib/rbs/rewriter.rb +70 -0
  91. data/lib/rbs/test/type_check.rb +6 -1
  92. data/lib/rbs/type_name.rb +33 -13
  93. data/lib/rbs/unit_test/type_assertions.rb +9 -0
  94. data/lib/rbs/version.rb +1 -1
  95. data/lib/rbs/wasm/deserializer.rb +213 -0
  96. data/lib/rbs/wasm/location.rb +61 -0
  97. data/lib/rbs/wasm/parser.rb +137 -0
  98. data/lib/rbs/wasm/runtime.rb +196 -0
  99. data/lib/rbs/wasm/serialization_schema.rb +110 -0
  100. data/lib/rbs.rb +14 -2
  101. data/lib/rbs_jars.rb +39 -0
  102. data/lib/rdoc_plugin/parser.rb +5 -0
  103. data/rbs.gemspec +32 -2
  104. data/sig/annotate/rdoc_annotater.rbs +12 -9
  105. data/sig/ast/ruby/annotations.rbs +49 -0
  106. data/sig/ast/ruby/members.rbs +21 -1
  107. data/sig/buffer.rbs +19 -1
  108. data/sig/collection/config/lockfile_generator.rbs +2 -0
  109. data/sig/inline_parser.rbs +2 -0
  110. data/sig/manifest.yaml +0 -1
  111. data/sig/namespace.rbs +20 -0
  112. data/sig/parser.rbs +10 -0
  113. data/sig/prototype/rbi.rbs +33 -4
  114. data/sig/resolver/type_name_resolver.rbs +2 -4
  115. data/sig/rewriter.rbs +45 -0
  116. data/sig/typename.rbs +15 -0
  117. data/sig/unit_test/type_assertions.rbs +4 -0
  118. data/sig/wasm/deserializer.rbs +66 -0
  119. data/sig/wasm/serialization_schema.rbs +13 -0
  120. data/src/ast.c +186 -162
  121. data/src/lexer.c +220 -193
  122. data/src/lexer.re +2 -1
  123. data/src/lexstate.c +10 -1
  124. data/src/parser.c +61 -8
  125. data/src/serialize.c +958 -0
  126. data/src/util/rbs_allocator.c +14 -8
  127. data/stdlib/abbrev/0/array.rbs +1 -1
  128. data/stdlib/csv/0/csv.rbs +5 -5
  129. data/stdlib/delegate/0/delegator.rbs +2 -1
  130. data/stdlib/digest/0/digest.rbs +11 -5
  131. data/stdlib/erb/0/erb.rbs +1 -1
  132. data/stdlib/etc/0/etc.rbs +18 -4
  133. data/stdlib/fileutils/0/fileutils.rbs +21 -21
  134. data/stdlib/ipaddr/0/ipaddr.rbs +0 -5
  135. data/stdlib/json/0/json.rbs +6 -6
  136. data/stdlib/monitor/0/monitor.rbs +2 -2
  137. data/stdlib/openssl/0/openssl.rbs +46 -40
  138. data/stdlib/resolv/0/resolv.rbs +1 -1
  139. data/stdlib/shellwords/0/shellwords.rbs +1 -1
  140. data/stdlib/stringio/0/stringio.rbs +32 -10
  141. data/stdlib/strscan/0/string_scanner.rbs +74 -55
  142. data/stdlib/tempfile/0/manifest.yaml +3 -0
  143. data/stdlib/timeout/0/timeout.rbs +0 -5
  144. data/stdlib/tsort/0/cyclic.rbs +1 -1
  145. data/stdlib/tsort/0/interfaces.rbs +8 -8
  146. data/stdlib/tsort/0/tsort.rbs +9 -9
  147. data/stdlib/uri/0/generic.rbs +0 -5
  148. data/stdlib/zlib/0/gzip_reader.rbs +2 -2
  149. data/stdlib/zlib/0/zstream.rbs +0 -1
  150. data/wasm/README.md +60 -0
  151. data/wasm/rbs_wasm.c +423 -0
  152. metadata +29 -6
  153. data/.github/workflows/milestone.yml +0 -80
  154. data/.vscode/extensions.json +0 -5
  155. data/.vscode/settings.json +0 -19
data/Rakefile CHANGED
@@ -33,7 +33,14 @@ test_config = lambda do |t|
33
33
  end
34
34
  end
35
35
 
36
- Rake::TestTask.new(test: :compile, &test_config)
36
+ if RUBY_ENGINE == "jruby"
37
+ # JRuby runs the parser in WebAssembly instead of the C extension, so there is
38
+ # nothing to compile. The wasm runtime must be assembled first with
39
+ # `rake wasm:jruby_setup` (which needs CRuby + the WASI SDK).
40
+ Rake::TestTask.new(:test, &test_config)
41
+ else
42
+ Rake::TestTask.new(test: :compile, &test_config)
43
+ end
37
44
 
38
45
  multitask :default => [:test, :stdlib_test, :typecheck_test, :rubocop, :validate, :test_doc]
39
46
 
@@ -48,8 +55,15 @@ task :confirm_lexer => :lexer do
48
55
  end
49
56
 
50
57
  task :confirm_templates => :templates do
51
- puts "Testing if generated code under include and src is updated with respect to templates"
52
- sh "git diff --exit-code -- include src"
58
+ puts "Testing if generated code is updated with respect to templates"
59
+
60
+ # Every template generates the file it is named after: `templates/<path>.erb` generates `<path>`.
61
+ generated = Dir.glob("templates/**/*.erb").sort.map { _1.delete_prefix("templates/").delete_suffix(".erb") }
62
+
63
+ missing = generated.reject { File.exist?(_1) }
64
+ raise "Templates without a generated file: #{missing.join(", ")}. Is the `templates` task missing an entry?" unless missing.empty?
65
+
66
+ sh "git diff --exit-code -- #{generated.join(" ")}"
53
67
  end
54
68
 
55
69
  # Task to format C code using clang-format
@@ -140,6 +154,9 @@ end
140
154
  rule %r{^include/(.*)\.c} => 'templates/%X.c.erb' do |t|
141
155
  puts "⚠️⚠️⚠️ #{t.name} is older than #{t.source}. You may need to run `rake templates` ⚠️⚠️⚠️"
142
156
  end
157
+ rule %r{^ext/(.*)\.c} => 'templates/%X.c.erb' do |t|
158
+ puts "⚠️⚠️⚠️ #{t.name} is older than #{t.source}. You may need to run `rake templates` ⚠️⚠️⚠️"
159
+ end
143
160
 
144
161
  task :annotate do
145
162
  sh "bin/generate_docs.sh"
@@ -160,6 +177,10 @@ task :templates do
160
177
  sh "#{ruby} templates/template.rb include/rbs/ast.h"
161
178
  sh "#{ruby} templates/template.rb src/ast.c"
162
179
 
180
+ sh "#{ruby} templates/template.rb include/rbs/serialize.h"
181
+ sh "#{ruby} templates/template.rb src/serialize.c"
182
+ sh "#{ruby} templates/template.rb lib/rbs/wasm/serialization_schema.rb"
183
+
163
184
  # Format the generated files
164
185
  Rake::Task["format:c"].invoke
165
186
  end
@@ -206,8 +227,8 @@ task :validate => :compile do
206
227
  args = ["-r", lib]
207
228
 
208
229
  if lib == "rbs"
209
- args << "-r"
210
- args << "prism"
230
+ args << "-r" << "prism"
231
+ args << "-r" << "logger"
211
232
  end
212
233
 
213
234
  sh "#{ruby} #{rbs} #{args.join(' ')} validate"
@@ -484,46 +505,329 @@ NOTES
484
505
  end
485
506
 
486
507
 
487
- desc "Generate changelog template from GH pull requests"
488
- task :changelog do
489
- major, minor, patch, _pre = RBS::VERSION.split(".", 4)
490
- major = major.to_i
491
- minor = minor.to_i
492
- patch = patch.to_i
508
+ # Pull requests with one of these labels are omitted from the changelog.
509
+ CHANGELOG_SKIP_LABELS = ["skip-changelog"]
493
510
 
494
- if patch == 0
495
- milestone = "RBS #{major}.#{minor}"
496
- else
497
- milestone = "RBS #{major}.#{minor}.x"
498
- end
511
+ # Resolves the commit-ish the changelog starts from.
512
+ #
513
+ # `version` is a version number, a tag, or any commit-ish. When it is omitted, the latest tag
514
+ # matching `tag_glob` is used, skipping the ones matching `exclude_globs`.
515
+ #
516
+ def resolve_changelog_base(version, tag_glob:, exclude_globs: [])
517
+ require "open3"
518
+
519
+ from =
520
+ if version
521
+ # `4.1.0` and `v4.1.0` both mean the tag `v4.1.0`, while `master` or a SHA is used as is.
522
+ version.match?(/\A\d/) ? "v#{version}" : version
523
+ else
524
+ command = ["git", "describe", "--tags", "--match", tag_glob, "--abbrev=0"]
525
+ exclude_globs.each { |glob| command.push("--exclude", glob) }
526
+
527
+ output, status = Open3.capture2(*command)
528
+ raise "🚨 Cannot detect the latest tag matching `#{tag_glob}`. Give the previous version explicitly." unless status.success?
529
+ output.chomp
530
+ end
531
+
532
+ _, status = Open3.capture2("git", "rev-parse", "--verify", "--quiet", "#{from}^{commit}")
533
+ raise "🚨 No such commit-ish: `#{from}`" unless status.success?
499
534
 
500
- puts "🔍 Finding pull requests that is associated to milestone `#{milestone}`..."
535
+ from
536
+ end
537
+
538
+ # Runs a GraphQL query against the repository of the working directory.
539
+ #
540
+ # `body` is the selection set inside `repository`, so a query can use the `$owner` and `$name`
541
+ # variables. Returns the contents of `data.repository`.
542
+ #
543
+ def changelog_graphql(body)
544
+ require "open3"
545
+ require "json"
501
546
 
502
- command = [
503
- "gh",
504
- "pr",
505
- "list",
506
- "--limit=10000",
507
- "--json",
508
- "url,title,number",
509
- "--search" ,
510
- "milestone:\"#{milestone}\" is:merged sort:updated-desc -label:Released"
511
- ]
547
+ @changelog_repository ||=
548
+ begin
549
+ output, status = Open3.capture2("gh", "repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner")
550
+ raise status.inspect unless status.success?
551
+ output.chomp.split("/", 2)
552
+ end
553
+ owner, name = @changelog_repository
512
554
 
555
+ query = <<~GRAPHQL
556
+ query($owner: String!, $name: String!) {
557
+ repository(owner: $owner, name: $name) {
558
+ #{body}
559
+ }
560
+ }
561
+ GRAPHQL
562
+
563
+ output, status = Open3.capture2(
564
+ "gh", "api", "graphql",
565
+ "-f", "query=#{query}",
566
+ "-f", "owner=#{owner}",
567
+ "-f", "name=#{name}",
568
+ binmode: true
569
+ )
570
+ raise status.inspect unless status.success?
571
+
572
+ # GitHub always answers in UTF-8, while the default external encoding follows the locale. Without
573
+ # this, a pull request body with an emoji fails to parse under `LANG=C`, as in GitHub Actions.
574
+ JSON.parse(output.force_encoding(Encoding::UTF_8), symbolize_names: true).dig(:data, :repository)
575
+ end
576
+
577
+ # Lists the commits between `from` and `HEAD`, newest first.
578
+ #
579
+ # Giving `paths` limits the commits to the ones touching the paths.
580
+ #
581
+ def changelog_commits(from, paths: [])
513
582
  require "open3"
583
+
584
+ command = ["git", "log", "--format=%H", "#{from}..HEAD"]
585
+ # `--simplify-merges` keeps the default history simplification from following only one parent of
586
+ # a merge commit, which can drop the other side. Note that `--full-history` alone is wrong here:
587
+ # it also lists merge commits that do not touch the paths, bringing back the excluded pull
588
+ # requests. The two flags produce the same commits as the default mode for this repository today.
589
+ command.push("--full-history", "--simplify-merges", "--", *paths) unless paths.empty?
590
+
514
591
  output, status = Open3.capture2(*command)
515
592
  raise status.inspect unless status.success?
516
593
 
517
- require "json"
518
- json = JSON.parse(output, symbolize_names: true)
594
+ output.lines.map(&:chomp).reject(&:empty?)
595
+ end
519
596
 
520
- unless json.empty?
521
- puts
522
- json.each do |line|
523
- puts "* #{line[:title]} ([##{line[:number]}](#{line[:url]}))"
597
+ # Finds the pull requests the commits came from, keeping the order of `commits`.
598
+ #
599
+ # Returns the pull requests for the changelog and the ones omitted by `skip_labels`.
600
+ #
601
+ def changelog_pull_requests(commits, skip_labels: CHANGELOG_SKIP_LABELS)
602
+ pull_requests = {}
603
+ skipped = {}
604
+
605
+ commits.each_slice(50) do |slice|
606
+ # Ask GitHub which pull requests the commits came from, so that any merge strategy -- merge
607
+ # commit, squash, or rebase -- is handled without parsing commit messages.
608
+ aliases = slice.map.with_index do |commit, index|
609
+ <<~GRAPHQL
610
+ c#{index}: object(oid: "#{commit}") {
611
+ ... on Commit {
612
+ associatedPullRequests(first: 10) {
613
+ nodes {
614
+ number title url merged
615
+ labels(first: 100) { nodes { name } }
616
+ }
617
+ }
618
+ }
619
+ }
620
+ GRAPHQL
621
+ end
622
+
623
+ changelog_graphql(aliases.join("\n")).each_value do |commit|
624
+ next unless commit
625
+
626
+ commit.dig(:associatedPullRequests, :nodes).each do |pr|
627
+ next unless pr[:merged]
628
+
629
+ pr = { number: pr[:number], title: pr[:title], url: pr[:url], labels: pr.dig(:labels, :nodes).map { |label| label[:name] } }
630
+ if (pr[:labels] & skip_labels).empty?
631
+ pull_requests[pr[:number]] ||= pr
632
+ else
633
+ skipped[pr[:number]] ||= pr
634
+ end
635
+ end
524
636
  end
637
+ end
638
+
639
+ [pull_requests.values, skipped.values]
640
+ end
641
+
642
+ # Fetches the details that help classifying the pull requests: the changed files and the body.
643
+ #
644
+ def changelog_pull_request_details(pull_requests)
645
+ pull_requests.each_slice(50).flat_map do |slice|
646
+ aliases = slice.map do |pr|
647
+ <<~GRAPHQL
648
+ p#{pr[:number]}: pullRequest(number: #{pr[:number]}) {
649
+ body
650
+ author { login }
651
+ files(first: 100) {
652
+ nodes { path }
653
+ pageInfo { hasNextPage }
654
+ }
655
+ }
656
+ GRAPHQL
657
+ end
658
+
659
+ details = changelog_graphql(aliases.join("\n"))
660
+
661
+ slice.map do |pr|
662
+ detail = details[:"p#{pr[:number]}"] or next pr
663
+
664
+ pr.merge(
665
+ author: detail.dig(:author, :login),
666
+ # The body is a hint for writing the changelog, not a copy source. Keep it short.
667
+ body: detail[:body].to_s.strip.slice(0, 1000),
668
+ files: detail.dig(:files, :nodes).map { |file| file[:path] },
669
+ files_truncated: detail.dig(:files, :pageInfo, :hasNextPage)
670
+ )
671
+ end
672
+ end
673
+ end
674
+
675
+ # Reports the pull requests omitted by their label, so that they do not disappear silently.
676
+ #
677
+ def warn_skipped_pull_requests(skipped, skip_labels)
678
+ return if skipped.empty?
679
+
680
+ numbers = skipped.map { |pr| "##{pr[:number]}" }
681
+ numbers = numbers.take(20).push("and #{numbers.size - 20} more") if numbers.size > 20
682
+
683
+ $stderr.puts
684
+ $stderr.puts " (⏭️ Skipped #{skipped.size} pull request(s) labeled #{skip_labels.map { |label| "`#{label}`" }.join(" or ")}: #{numbers.join(", ")})"
685
+ end
686
+
687
+ # Prints the changelog template listing the pull requests merged between `from` and `HEAD`.
688
+ #
689
+ # The changelog goes to STDOUT and everything else goes to STDERR, so that the output can be
690
+ # piped to another command: `rake gem:changelog | pbcopy`
691
+ #
692
+ def print_changelog(from, paths: [], skip_labels: CHANGELOG_SKIP_LABELS)
693
+ $stderr.puts "🔍 Finding pull requests merged between `#{from}` and `HEAD`..."
694
+
695
+ commits = changelog_commits(from, paths: paths)
696
+ if commits.empty?
697
+ $stderr.puts " (🤔 There is no commit after `#{from}`.)"
698
+ return
699
+ end
700
+
701
+ pull_requests, skipped = changelog_pull_requests(commits, skip_labels: skip_labels)
702
+
703
+ if pull_requests.empty?
704
+ $stderr.puts " (🤔 No pull request is associated to the commits after `#{from}`.)"
525
705
  else
526
- puts " (🤑 There is no *unreleased* pull request associated to the milestone.)"
706
+ $stderr.puts
707
+ pull_requests.each do |pr|
708
+ puts "* #{pr[:title]} ([##{pr[:number]}](#{pr[:url]}))"
709
+ end
710
+ $stdout.flush
711
+ end
712
+
713
+ warn_skipped_pull_requests(skipped, skip_labels)
714
+ end
715
+
716
+ # Prints the same pull requests as `print_changelog` as JSON, with the details that help
717
+ # classifying them into the sections of CHANGELOG.md.
718
+ #
719
+ # This is the input for the release automation, so it always prints a valid JSON document.
720
+ #
721
+ def print_changelog_json(from, paths: [], skip_labels: CHANGELOG_SKIP_LABELS)
722
+ require "json"
723
+
724
+ $stderr.puts "🔍 Finding pull requests merged between `#{from}` and `HEAD`..."
725
+
726
+ commits = changelog_commits(from, paths: paths)
727
+ pull_requests, skipped = changelog_pull_requests(commits, skip_labels: skip_labels)
728
+ pull_requests = changelog_pull_request_details(pull_requests)
729
+
730
+ $stderr.puts " (📋 #{pull_requests.size} pull request(s))"
731
+
732
+ puts JSON.pretty_generate(
733
+ {
734
+ from: from,
735
+ to: "HEAD",
736
+ pull_requests: pull_requests,
737
+ skipped: skipped
738
+ }
739
+ )
740
+ $stdout.flush
741
+
742
+ warn_skipped_pull_requests(skipped, skip_labels)
743
+ end
744
+
745
+ namespace :gem do
746
+ # The gem is developed in the whole repository except the Rust crate, which has its own release
747
+ # cycle. Note that this is an *exclusion*, not a list of the directories shipped in the gem:
748
+ # changes in `test/` or `.github/` are part of the gem's changelog too.
749
+ # A constant defined in a `namespace` block is a top-level constant, so it needs the prefix.
750
+ GEM_CHANGELOG_PATHS = [".", ":(exclude)rust"]
751
+
752
+ # The tags a release proper starts *after*, rather than at.
753
+ GEM_PRERELEASE_TAGS = ["v*.pre*", "v*.dev*"]
754
+
755
+ # Where the changelog of the release being prepared starts, derived from `RBS::VERSION`:
756
+ #
757
+ # * `X.Y.Z.pre.N` documents what changed since `X.Y.Z.pre.N-1`, so it starts from the latest tag.
758
+ # * `X.Y.Z` documents the whole cycle, the prereleases included, so it skips the prerelease tags
759
+ # in between and starts from the previous release proper.
760
+ #
761
+ # This is the step that is easy to get wrong by hand: on a release proper the latest tag is a
762
+ # prerelease, so the obvious default would produce only the tail of the cycle. Passing a version
763
+ # explicitly overrides all of it.
764
+ #
765
+ def changelog_base(version)
766
+ excluded = Gem::Version.new(RBS::VERSION).prerelease? ? [] : GEM_PRERELEASE_TAGS
767
+ resolve_changelog_base(version, tag_glob: "v*", exclude_globs: excluded)
768
+ end
769
+
770
+ desc "Generate changelog template from GH pull requests merged since the previous release"
771
+ task :changelog, [:version] do |_task, args|
772
+ print_changelog(changelog_base(args[:version]), paths: GEM_CHANGELOG_PATHS)
773
+ end
774
+
775
+ namespace :changelog do
776
+ desc "Print the pull requests of `gem:changelog` as JSON, with the changed files and body of each"
777
+ task :json, [:version] do |_task, args|
778
+ print_changelog_json(changelog_base(args[:version]), paths: GEM_CHANGELOG_PATHS)
779
+ end
780
+ end
781
+
782
+ desc "Publish the GitHub release for RBS::VERSION, unless it is a `.dev.` version"
783
+ task :gh_release do
784
+ require "open3"
785
+
786
+ version = Gem::Version.new(RBS::VERSION)
787
+ major, minor, *_ = RBS::VERSION.split(".")
788
+ tag = "v#{RBS::VERSION}"
789
+
790
+ # There are three kinds of release: `X.Y.Z`, `X.Y.Z.pre.N`, and `X.Y.Z.dev.N`.
791
+ # The `.dev.N` ones are cut from the development line for people who need a
792
+ # specific change early; they are not written up in the changelog, so there are
793
+ # no notes to publish and nothing worth announcing.
794
+ if version.segments.include?("dev")
795
+ puts "⏭️ #{RBS::VERSION} is a dev release, so there is no GitHub release to publish."
796
+ next
797
+ end
798
+
799
+ # The release is created against an existing tag, so that the artifacts and the
800
+ # notes describe a commit that is already immutable.
801
+ _, status = Open3.capture2("git", "rev-parse", "--verify", "--quiet", "#{tag}^{commit}")
802
+ raise "🚨 No such tag: `#{tag}`. Tag the release before creating the GitHub release." unless status.success?
803
+
804
+ # The topmost section of the changelog is this release, minus its own heading.
805
+ # The encoding is explicit because the default external encoding follows the
806
+ # locale, and the changelog is not ASCII.
807
+ content = File.read(File.join(__dir__, "CHANGELOG.md"), encoding: Encoding::UTF_8)
808
+ section = content.scan(/^## \d.*?(?=^## \d)/m)[0] or raise "🚨 Cannot find a release section in CHANGELOG.md"
809
+ heading, _, body = section.partition("\n")
810
+ heading.include?(RBS::VERSION) or raise "🚨 CHANGELOG.md starts with `#{heading.strip}`, which is not #{RBS::VERSION}"
811
+
812
+ notes = <<~NOTES
813
+ [Release note](https://github.com/ruby/rbs/wiki/Release-Note-#{major}.#{minor})
814
+
815
+ #{body.strip}
816
+ NOTES
817
+
818
+ # Published rather than drafted: the notes are the changelog section that was
819
+ # already reviewed in the release pull request, so there is nothing left to edit.
820
+ command = [
821
+ "gh", "release", "create", tag,
822
+ "--title=#{RBS::VERSION}",
823
+ "--notes=#{notes}"
824
+ ]
825
+ command << "--prerelease" if version.prerelease?
826
+
827
+ output, status = Open3.capture2(*command)
828
+ raise "🚨 `gh release create` failed: #{status.inspect}" unless status.success?
829
+
830
+ puts "📝 Released #{tag}: #{output.chomp}"
527
831
  end
528
832
  end
529
833
 
@@ -549,6 +853,97 @@ task :prepare_profiling do
549
853
  Rake::Task[:"compile"].invoke
550
854
  end
551
855
 
856
+ namespace :wasm do
857
+ WASM_DIR = File.expand_path("wasm", __dir__)
858
+ WASM_OUTPUT = File.join(WASM_DIR, "rbs_parser.wasm")
859
+
860
+ # The parser under src/ is plain, self-contained C with no dependency on the
861
+ # Ruby C API, so it can be compiled to WebAssembly as-is. The only extra
862
+ # translation unit is the entry-point shim under wasm/.
863
+ def wasm_source_files
864
+ Dir.glob(File.join(__dir__, "src/**/*.c")).sort + [File.join(WASM_DIR, "rbs_wasm.c")]
865
+ end
866
+
867
+ # Locate the clang shipped with the WASI SDK.
868
+ #
869
+ # The system clang can target wasm32, but the WASI SDK additionally provides
870
+ # the wasi-libc sysroot and the wasm32 compiler-rt builtins that the link
871
+ # step needs, so we require it explicitly.
872
+ def wasi_clang
873
+ sdk = ENV["WASI_SDK_PATH"]
874
+ if sdk.nil? || sdk.empty?
875
+ raise <<~MSG
876
+ WASI_SDK_PATH is not set.
877
+
878
+ Install the WASI SDK from https://github.com/WebAssembly/wasi-sdk/releases
879
+ and point WASI_SDK_PATH at the extracted directory, for example:
880
+
881
+ export WASI_SDK_PATH=/opt/wasi-sdk
882
+ rake wasm:build
883
+ MSG
884
+ end
885
+
886
+ clang = File.join(sdk, "bin", "clang")
887
+ raise "clang not found at #{clang} (is WASI_SDK_PATH correct?)" unless File.executable?(clang)
888
+
889
+ clang
890
+ end
891
+
892
+ desc "Build the RBS parser as a WebAssembly module (requires WASI_SDK_PATH)"
893
+ task :build do
894
+ mkdir_p WASM_DIR
895
+ sh wasi_clang,
896
+ "--target=wasm32-wasip1",
897
+ # No `main`; the host calls `_initialize` and then the exported functions.
898
+ "-mexec-model=reactor",
899
+ "-std=gnu11",
900
+ "-O2",
901
+ "-Wno-unused-parameter",
902
+ "-I#{File.join(__dir__, "include")}",
903
+ "-o", WASM_OUTPUT,
904
+ *wasm_source_files
905
+ puts "Built #{WASM_OUTPUT}"
906
+ end
907
+
908
+ desc "Build and smoke-test the WebAssembly module (requires wasmtime)"
909
+ task :check => :build do
910
+ wasmtime = ENV["WASMTIME"] || "wasmtime"
911
+
912
+ # `rbs_wasm_selftest` parses a small fixed signature and returns 1 on
913
+ # success. `--invoke` prints the return value to stdout.
914
+ output = IO.popen([wasmtime, "run", "--invoke", "rbs_wasm_selftest", WASM_OUTPUT], err: File::NULL, &:read).to_s.strip
915
+
916
+ if output == "1"
917
+ puts "WebAssembly selftest passed."
918
+ else
919
+ raise "WebAssembly selftest failed: rbs_wasm_selftest returned #{output.inspect} (expected \"1\")"
920
+ end
921
+ end
922
+
923
+ # Where the runtime looks for the module by default (see RBS::WASM::Runtime).
924
+ JRUBY_WASM_DIR = File.expand_path("lib/rbs/wasm", __dir__)
925
+
926
+ desc "Download the Chicory/ASM jars into the local Maven repository (~/.m2). Run on JRuby."
927
+ task :install_jars do
928
+ # Resolves the `jar` requirements from rbs.gemspec via Maven and downloads
929
+ # them (and their transitive deps) into ~/.m2, the same way `gem install`
930
+ # does; the jars are not copied into the gem. The platform is forced to java
931
+ # because Jars::Installer skips non-java gems, and write_require_file is false
932
+ # because lib/rbs_jars.rb is hand-maintained (the generator mangles the
933
+ # `com.dylibso.chicory:runtime` artifact id).
934
+ require "jars/installer"
935
+ spec = Gem::Specification.load("rbs.gemspec")
936
+ spec.platform = "java"
937
+ Jars::Installer.new(spec).install_jars(write_require_file: false)
938
+ end
939
+
940
+ desc "Build rbs_parser.wasm and copy it next to RBS::WASM::Runtime"
941
+ task :jruby_setup => [:build] do
942
+ cp WASM_OUTPUT, File.join(JRUBY_WASM_DIR, "rbs_parser.wasm")
943
+ puts "rbs_parser.wasm is ready under #{JRUBY_WASM_DIR}"
944
+ end
945
+ end
946
+
552
947
  namespace :rust do
553
948
  namespace :rbs do
554
949
  RUST_DIR = File.expand_path("rust", __dir__)
data/Steepfile CHANGED
@@ -6,6 +6,15 @@ target :lib do
6
6
  ignore(
7
7
  "lib/rbs/test",
8
8
  # "lib/rbs/test.rb"
9
+
10
+ # JRuby-only implementations of RBS::Location and RBS::Parser. Like the C
11
+ # extension, these implement interfaces already described in sig/, and
12
+ # runtime.rb is Java interop, so they are not type-checked here.
13
+ "lib/rbs/wasm/location.rb",
14
+ "lib/rbs/wasm/runtime.rb",
15
+ "lib/rbs/wasm/parser.rb",
16
+ # jar-dependencies require_jar calls for the JRuby runtime; not type-checked.
17
+ "lib/rbs_jars.rb",
9
18
  )
10
19
 
11
20
  library "pathname", "json", "logger", "monitor", "tsort", "uri", 'dbm', 'pstore', 'singleton', 'shellwords', 'fileutils', 'find', 'digest', 'prettyprint', 'yaml', "psych", "securerandom"
data/config.yml CHANGED
@@ -769,6 +769,7 @@ nodes:
769
769
  c_type: rbs_location_range
770
770
  - name: type_name
771
771
  c_type: rbs_type_name
772
+ optional: true # NULL when the name is left out, to be inferred from the Ruby code
772
773
  - name: type_name_location
773
774
  c_type: rbs_location_range
774
775
  optional: true
@@ -781,9 +782,34 @@ nodes:
781
782
  c_type: rbs_location_range
782
783
  - name: type_name
783
784
  c_type: rbs_type_name
785
+ optional: true # NULL when the name is left out, to be inferred from the Ruby code
784
786
  - name: type_name_location
785
787
  c_type: rbs_location_range
786
788
  optional: true
789
+ - name: RBS::AST::Ruby::Annotations::ModuleSelfAnnotation
790
+ rust_name: ModuleSelfAnnotationNode
791
+ fields:
792
+ - name: prefix_location
793
+ c_type: rbs_location_range
794
+ - name: keyword_location
795
+ c_type: rbs_location_range
796
+ - name: colon_location
797
+ c_type: rbs_location_range
798
+ - name: name
799
+ c_type: rbs_type_name
800
+ - name: args
801
+ c_type: rbs_node_list
802
+ - name: open_bracket_location
803
+ c_type: rbs_location_range
804
+ optional: true
805
+ - name: close_bracket_location
806
+ c_type: rbs_location_range
807
+ optional: true
808
+ - name: args_comma_locations
809
+ c_type: rbs_location_range_list
810
+ - name: comment_location
811
+ c_type: rbs_location_range
812
+ optional: true
787
813
  - name: RBS::AST::Ruby::Annotations::ParamTypeAnnotation
788
814
  rust_name: ParamTypeAnnotationNode
789
815
  fields: