rbs 3.9.4 → 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 (348) hide show
  1. checksums.yaml +4 -4
  2. data/.clang-format +75 -0
  3. data/.clangd +2 -0
  4. data/.dockerignore +37 -0
  5. data/.github/dependabot.yml +16 -14
  6. data/.github/workflows/bundle-update.yml +63 -0
  7. data/.github/workflows/c-check.yml +66 -0
  8. data/.github/workflows/comments.yml +5 -3
  9. data/.github/workflows/dependabot.yml +2 -2
  10. data/.github/workflows/jruby.yml +79 -0
  11. data/.github/workflows/release-gems.yml +164 -0
  12. data/.github/workflows/ruby.yml +84 -30
  13. data/.github/workflows/rust.yml +186 -0
  14. data/.github/workflows/truffleruby.yml +54 -0
  15. data/.github/workflows/typecheck.yml +5 -2
  16. data/.github/workflows/wasm.yml +55 -0
  17. data/.github/workflows/windows.yml +9 -3
  18. data/.gitignore +16 -0
  19. data/.rubocop.yml +2 -2
  20. data/CHANGELOG.md +430 -0
  21. data/Dockerfile.jruby +53 -0
  22. data/README.md +41 -4
  23. data/Rakefile +777 -60
  24. data/Steepfile +11 -0
  25. data/config.yml +660 -62
  26. data/core/array.rbs +541 -398
  27. data/core/basic_object.rbs +9 -8
  28. data/core/binding.rbs +0 -2
  29. data/core/builtin.rbs +9 -8
  30. data/core/class.rbs +11 -8
  31. data/core/comparable.rbs +55 -34
  32. data/core/complex.rbs +104 -78
  33. data/core/dir.rbs +61 -49
  34. data/core/encoding.rbs +12 -15
  35. data/core/enumerable.rbs +297 -196
  36. data/core/enumerator/arithmetic_sequence.rbs +70 -0
  37. data/core/enumerator/product.rbs +5 -5
  38. data/core/enumerator.rbs +91 -28
  39. data/core/errno.rbs +11 -2
  40. data/core/errors.rbs +58 -29
  41. data/core/exception.rbs +13 -13
  42. data/core/fiber.rbs +74 -54
  43. data/core/file.rbs +260 -1151
  44. data/core/file_constants.rbs +463 -0
  45. data/core/file_stat.rbs +534 -0
  46. data/core/file_test.rbs +3 -3
  47. data/core/float.rbs +257 -116
  48. data/core/gc.rbs +425 -281
  49. data/core/hash.rbs +1151 -829
  50. data/core/integer.rbs +156 -195
  51. data/core/io/buffer.rbs +53 -42
  52. data/core/io/wait.rbs +13 -35
  53. data/core/io.rbs +220 -154
  54. data/core/kernel.rbs +239 -163
  55. data/core/marshal.rbs +4 -4
  56. data/core/match_data.rbs +16 -14
  57. data/core/math.rbs +107 -66
  58. data/core/method.rbs +69 -33
  59. data/core/module.rbs +302 -150
  60. data/core/nil_class.rbs +7 -6
  61. data/core/numeric.rbs +77 -63
  62. data/core/object.rbs +9 -11
  63. data/core/object_space/weak_key_map.rbs +7 -7
  64. data/core/object_space.rbs +30 -23
  65. data/core/pathname.rbs +1312 -0
  66. data/core/proc.rbs +95 -58
  67. data/core/process.rbs +222 -202
  68. data/core/ractor.rbs +364 -518
  69. data/core/random.rbs +21 -3
  70. data/core/range.rbs +181 -79
  71. data/core/rational.rbs +60 -89
  72. data/core/rbs/ops.rbs +154 -0
  73. data/core/rbs/unnamed/argf.rbs +63 -56
  74. data/core/rbs/unnamed/env_class.rbs +19 -14
  75. data/core/rbs/unnamed/main_class.rbs +123 -0
  76. data/core/rbs/unnamed/random.rbs +11 -118
  77. data/core/regexp.rbs +258 -214
  78. data/core/ruby.rbs +53 -0
  79. data/core/ruby_vm.rbs +78 -34
  80. data/core/rubygems/config_file.rbs +5 -5
  81. data/core/rubygems/errors.rbs +6 -70
  82. data/core/rubygems/requirement.rbs +5 -15
  83. data/core/rubygems/rubygems.rbs +18 -81
  84. data/core/rubygems/specification.rbs +8 -0
  85. data/core/rubygems/version.rbs +2 -163
  86. data/core/set.rbs +493 -363
  87. data/core/signal.rbs +26 -16
  88. data/core/string.rbs +3234 -1285
  89. data/core/struct.rbs +43 -42
  90. data/core/symbol.rbs +41 -34
  91. data/core/thread.rbs +139 -83
  92. data/core/time.rbs +81 -50
  93. data/core/trace_point.rbs +41 -35
  94. data/core/true_class.rbs +2 -2
  95. data/core/unbound_method.rbs +24 -16
  96. data/core/warning.rbs +7 -7
  97. data/docs/CONTRIBUTING.md +2 -1
  98. data/docs/aliases.md +79 -0
  99. data/docs/collection.md +3 -3
  100. data/docs/config.md +171 -0
  101. data/docs/encoding.md +56 -0
  102. data/docs/gem.md +0 -1
  103. data/docs/inline.md +634 -0
  104. data/docs/rbs_by_example.md +20 -20
  105. data/docs/release.md +151 -0
  106. data/docs/rust.md +96 -0
  107. data/docs/sigs.md +3 -3
  108. data/docs/syntax.md +48 -18
  109. data/docs/type_fingerprint.md +21 -0
  110. data/docs/wasm_serialization.md +80 -0
  111. data/exe/rbs +1 -1
  112. data/ext/rbs_extension/ast_translation.c +1855 -0
  113. data/ext/rbs_extension/ast_translation.h +41 -0
  114. data/ext/rbs_extension/class_constants.c +187 -0
  115. data/{include/rbs/constants.h → ext/rbs_extension/class_constants.h} +23 -1
  116. data/ext/rbs_extension/compat.h +10 -0
  117. data/ext/rbs_extension/extconf.rb +26 -1
  118. data/ext/rbs_extension/legacy_location.c +299 -0
  119. data/ext/rbs_extension/legacy_location.h +82 -0
  120. data/ext/rbs_extension/main.c +603 -23
  121. data/ext/rbs_extension/rbs_extension.h +6 -21
  122. data/ext/rbs_extension/rbs_string_bridging.c +9 -0
  123. data/ext/rbs_extension/rbs_string_bridging.h +24 -0
  124. data/include/rbs/ast.h +1047 -0
  125. data/include/rbs/defines.h +99 -0
  126. data/include/rbs/lexer.h +207 -0
  127. data/include/rbs/location.h +40 -0
  128. data/include/rbs/parser.h +153 -0
  129. data/include/rbs/serialize.h +39 -0
  130. data/include/rbs/string.h +47 -0
  131. data/include/rbs/util/rbs_allocator.h +59 -0
  132. data/include/rbs/util/rbs_assert.h +20 -0
  133. data/include/rbs/util/rbs_buffer.h +83 -0
  134. data/include/rbs/util/rbs_constant_pool.h +6 -70
  135. data/include/rbs/util/rbs_encoding.h +282 -0
  136. data/include/rbs/util/rbs_unescape.h +24 -0
  137. data/include/rbs.h +9 -2
  138. data/lib/rbs/annotate/formatter.rb +3 -13
  139. data/lib/rbs/annotate/rdoc_annotator.rb +30 -32
  140. data/lib/rbs/annotate/rdoc_source.rb +1 -1
  141. data/lib/rbs/ast/annotation.rb +1 -1
  142. data/lib/rbs/ast/comment.rb +1 -1
  143. data/lib/rbs/ast/declarations.rb +10 -10
  144. data/lib/rbs/ast/members.rb +14 -14
  145. data/lib/rbs/ast/ruby/annotations.rb +451 -0
  146. data/lib/rbs/ast/ruby/comment_block.rb +247 -0
  147. data/lib/rbs/ast/ruby/declarations.rb +291 -0
  148. data/lib/rbs/ast/ruby/helpers/constant_helper.rb +28 -0
  149. data/lib/rbs/ast/ruby/helpers/location_helper.rb +15 -0
  150. data/lib/rbs/ast/ruby/members.rb +762 -0
  151. data/lib/rbs/ast/type_param.rb +24 -4
  152. data/lib/rbs/buffer.rb +142 -20
  153. data/lib/rbs/cli/diff.rb +16 -15
  154. data/lib/rbs/cli/validate.rb +63 -126
  155. data/lib/rbs/cli.rb +59 -29
  156. data/lib/rbs/collection/config/lockfile_generator.rb +28 -3
  157. data/lib/rbs/collection/sources/git.rb +7 -0
  158. data/lib/rbs/definition.rb +6 -1
  159. data/lib/rbs/definition_builder/ancestor_builder.rb +121 -65
  160. data/lib/rbs/definition_builder/method_builder.rb +74 -33
  161. data/lib/rbs/definition_builder.rb +177 -20
  162. data/lib/rbs/diff.rb +7 -1
  163. data/lib/rbs/environment/class_entry.rb +69 -0
  164. data/lib/rbs/environment/module_entry.rb +66 -0
  165. data/lib/rbs/environment.rb +410 -215
  166. data/lib/rbs/environment_loader.rb +2 -8
  167. data/lib/rbs/errors.rb +31 -21
  168. data/lib/rbs/inline_parser/comment_association.rb +117 -0
  169. data/lib/rbs/inline_parser.rb +568 -0
  170. data/lib/rbs/location_aux.rb +36 -4
  171. data/lib/rbs/locator.rb +5 -1
  172. data/lib/rbs/method_type.rb +5 -3
  173. data/lib/rbs/namespace.rb +47 -18
  174. data/lib/rbs/parser_aux.rb +37 -7
  175. data/lib/rbs/prototype/helpers.rb +57 -0
  176. data/lib/rbs/prototype/rb.rb +3 -28
  177. data/lib/rbs/prototype/rbi.rb +196 -45
  178. data/lib/rbs/prototype/runtime.rb +12 -2
  179. data/lib/rbs/resolver/constant_resolver.rb +2 -2
  180. data/lib/rbs/resolver/type_name_resolver.rb +120 -44
  181. data/lib/rbs/rewriter.rb +70 -0
  182. data/lib/rbs/source.rb +99 -0
  183. data/lib/rbs/subtractor.rb +7 -4
  184. data/lib/rbs/test/type_check.rb +25 -3
  185. data/lib/rbs/type_name.rb +34 -21
  186. data/lib/rbs/types.rb +91 -79
  187. data/lib/rbs/unit_test/convertibles.rb +1 -0
  188. data/lib/rbs/unit_test/type_assertions.rb +44 -8
  189. data/lib/rbs/validator.rb +2 -2
  190. data/lib/rbs/version.rb +1 -1
  191. data/lib/rbs/wasm/deserializer.rb +213 -0
  192. data/lib/rbs/wasm/location.rb +61 -0
  193. data/lib/rbs/wasm/parser.rb +137 -0
  194. data/lib/rbs/wasm/runtime.rb +196 -0
  195. data/lib/rbs/wasm/serialization_schema.rb +110 -0
  196. data/lib/rbs.rb +25 -2
  197. data/lib/rbs_jars.rb +39 -0
  198. data/lib/rdoc/discover.rb +1 -1
  199. data/lib/rdoc_plugin/parser.rb +8 -3
  200. data/rbs.gemspec +37 -5
  201. data/schema/typeParam.json +17 -1
  202. data/sig/ancestor_builder.rbs +1 -1
  203. data/sig/annotate/formatter.rbs +2 -2
  204. data/sig/annotate/rdoc_annotater.rbs +13 -10
  205. data/sig/ast/ruby/annotations.rbs +470 -0
  206. data/sig/ast/ruby/comment_block.rbs +127 -0
  207. data/sig/ast/ruby/declarations.rbs +158 -0
  208. data/sig/ast/ruby/helpers/constant_helper.rbs +11 -0
  209. data/sig/ast/ruby/helpers/location_helper.rbs +15 -0
  210. data/sig/ast/ruby/members.rbs +198 -0
  211. data/sig/buffer.rbs +81 -5
  212. data/sig/cli/diff.rbs +5 -11
  213. data/sig/cli/validate.rbs +12 -8
  214. data/sig/cli.rbs +18 -18
  215. data/sig/collection/config/lockfile_generator.rbs +2 -0
  216. data/sig/definition.rbs +6 -0
  217. data/sig/definition_builder.rbs +3 -1
  218. data/sig/environment/class_entry.rbs +50 -0
  219. data/sig/environment/module_entry.rbs +50 -0
  220. data/sig/environment.rbs +94 -87
  221. data/sig/errors.rbs +26 -20
  222. data/sig/inline_parser/comment_association.rbs +71 -0
  223. data/sig/inline_parser.rbs +126 -0
  224. data/sig/location.rbs +32 -7
  225. data/sig/locator.rbs +0 -2
  226. data/sig/manifest.yaml +0 -2
  227. data/sig/method_builder.rbs +9 -4
  228. data/sig/namespace.rbs +20 -5
  229. data/sig/parser.rbs +77 -13
  230. data/sig/prototype/helpers.rbs +2 -0
  231. data/sig/prototype/rbi.rbs +33 -4
  232. data/sig/resolver/type_name_resolver.rbs +36 -10
  233. data/sig/rewriter.rbs +45 -0
  234. data/sig/source.rbs +48 -0
  235. data/sig/type_param.rbs +13 -8
  236. data/sig/typename.rbs +15 -5
  237. data/sig/types.rbs +10 -8
  238. data/sig/unit_test/spy.rbs +0 -8
  239. data/sig/unit_test/type_assertions.rbs +15 -0
  240. data/sig/wasm/deserializer.rbs +66 -0
  241. data/sig/wasm/serialization_schema.rbs +13 -0
  242. data/src/ast.c +1628 -0
  243. data/src/lexer.c +3221 -0
  244. data/src/lexer.re +155 -0
  245. data/src/lexstate.c +221 -0
  246. data/src/location.c +31 -0
  247. data/src/parser.c +4258 -0
  248. data/src/serialize.c +958 -0
  249. data/src/string.c +41 -0
  250. data/src/util/rbs_allocator.c +171 -0
  251. data/src/util/rbs_assert.c +19 -0
  252. data/src/util/rbs_buffer.c +54 -0
  253. data/src/util/rbs_constant_pool.c +18 -92
  254. data/src/util/rbs_encoding.c +21308 -0
  255. data/src/util/rbs_unescape.c +167 -0
  256. data/stdlib/abbrev/0/array.rbs +1 -1
  257. data/stdlib/bigdecimal/0/big_decimal.rbs +116 -98
  258. data/stdlib/bigdecimal-math/0/big_math.rbs +169 -8
  259. data/stdlib/cgi/0/core.rbs +9 -393
  260. data/stdlib/cgi/0/manifest.yaml +1 -0
  261. data/stdlib/cgi-escape/0/escape.rbs +171 -0
  262. data/stdlib/coverage/0/coverage.rbs +7 -4
  263. data/stdlib/csv/0/csv.rbs +5 -5
  264. data/stdlib/date/0/date.rbs +92 -79
  265. data/stdlib/date/0/date_time.rbs +25 -24
  266. data/stdlib/delegate/0/delegator.rbs +11 -7
  267. data/stdlib/did_you_mean/0/did_you_mean.rbs +17 -16
  268. data/stdlib/digest/0/digest.rbs +117 -1
  269. data/stdlib/erb/0/erb.rbs +748 -347
  270. data/stdlib/etc/0/etc.rbs +73 -54
  271. data/stdlib/fileutils/0/fileutils.rbs +179 -160
  272. data/stdlib/forwardable/0/forwardable.rbs +13 -10
  273. data/stdlib/io-console/0/io-console.rbs +2 -2
  274. data/stdlib/ipaddr/0/ipaddr.rbs +0 -5
  275. data/stdlib/json/0/json.rbs +232 -185
  276. data/stdlib/monitor/0/monitor.rbs +5 -5
  277. data/stdlib/net-http/0/net-http.rbs +162 -134
  278. data/stdlib/objspace/0/objspace.rbs +17 -34
  279. data/stdlib/open-uri/0/open-uri.rbs +48 -8
  280. data/stdlib/open3/0/open3.rbs +469 -10
  281. data/stdlib/openssl/0/openssl.rbs +521 -397
  282. data/stdlib/optparse/0/optparse.rbs +26 -17
  283. data/stdlib/pathname/0/pathname.rbs +11 -1381
  284. data/stdlib/pp/0/pp.rbs +9 -8
  285. data/stdlib/prettyprint/0/prettyprint.rbs +7 -7
  286. data/stdlib/pstore/0/pstore.rbs +35 -30
  287. data/stdlib/psych/0/psych.rbs +65 -12
  288. data/stdlib/psych/0/store.rbs +2 -4
  289. data/stdlib/pty/0/pty.rbs +9 -6
  290. data/stdlib/random-formatter/0/random-formatter.rbs +277 -0
  291. data/stdlib/rdoc/0/code_object.rbs +4 -3
  292. data/stdlib/rdoc/0/comment.rbs +2 -0
  293. data/stdlib/rdoc/0/options.rbs +76 -0
  294. data/stdlib/rdoc/0/parser.rbs +1 -1
  295. data/stdlib/rdoc/0/rdoc.rbs +7 -5
  296. data/stdlib/rdoc/0/store.rbs +2 -2
  297. data/stdlib/resolv/0/resolv.rbs +26 -69
  298. data/stdlib/ripper/0/ripper.rbs +25 -19
  299. data/stdlib/securerandom/0/manifest.yaml +2 -0
  300. data/stdlib/securerandom/0/securerandom.rbs +7 -20
  301. data/stdlib/shellwords/0/shellwords.rbs +3 -3
  302. data/stdlib/singleton/0/singleton.rbs +3 -0
  303. data/stdlib/socket/0/addrinfo.rbs +9 -9
  304. data/stdlib/socket/0/basic_socket.rbs +3 -3
  305. data/stdlib/socket/0/ip_socket.rbs +10 -8
  306. data/stdlib/socket/0/socket.rbs +23 -10
  307. data/stdlib/socket/0/tcp_server.rbs +1 -1
  308. data/stdlib/socket/0/tcp_socket.rbs +11 -3
  309. data/stdlib/socket/0/udp_socket.rbs +1 -1
  310. data/stdlib/socket/0/unix_server.rbs +1 -1
  311. data/stdlib/stringio/0/stringio.rbs +1209 -95
  312. data/stdlib/strscan/0/string_scanner.rbs +101 -80
  313. data/stdlib/tempfile/0/manifest.yaml +3 -0
  314. data/stdlib/tempfile/0/tempfile.rbs +25 -21
  315. data/stdlib/time/0/time.rbs +8 -6
  316. data/stdlib/timeout/0/timeout.rbs +58 -7
  317. data/stdlib/tsort/0/cyclic.rbs +4 -1
  318. data/stdlib/tsort/0/interfaces.rbs +8 -8
  319. data/stdlib/tsort/0/tsort.rbs +16 -15
  320. data/stdlib/uri/0/common.rbs +42 -20
  321. data/stdlib/uri/0/file.rbs +3 -3
  322. data/stdlib/uri/0/generic.rbs +21 -18
  323. data/stdlib/uri/0/http.rbs +2 -2
  324. data/stdlib/uri/0/ldap.rbs +2 -2
  325. data/stdlib/uri/0/mailto.rbs +3 -3
  326. data/stdlib/uri/0/rfc2396_parser.rbs +12 -12
  327. data/stdlib/zlib/0/deflate.rbs +4 -3
  328. data/stdlib/zlib/0/gzip_reader.rbs +8 -8
  329. data/stdlib/zlib/0/gzip_writer.rbs +14 -12
  330. data/stdlib/zlib/0/inflate.rbs +1 -1
  331. data/stdlib/zlib/0/need_dict.rbs +1 -1
  332. data/wasm/README.md +60 -0
  333. data/wasm/rbs_wasm.c +423 -0
  334. metadata +131 -19
  335. data/ext/rbs_extension/lexer.c +0 -2728
  336. data/ext/rbs_extension/lexer.h +0 -179
  337. data/ext/rbs_extension/lexer.re +0 -147
  338. data/ext/rbs_extension/lexstate.c +0 -175
  339. data/ext/rbs_extension/location.c +0 -325
  340. data/ext/rbs_extension/location.h +0 -85
  341. data/ext/rbs_extension/parser.c +0 -2982
  342. data/ext/rbs_extension/parser.h +0 -18
  343. data/ext/rbs_extension/parserstate.c +0 -411
  344. data/ext/rbs_extension/parserstate.h +0 -163
  345. data/ext/rbs_extension/unescape.c +0 -32
  346. data/include/rbs/ruby_objs.h +0 -72
  347. data/src/constants.c +0 -153
  348. data/src/ruby_objs.c +0 -799
data/Rakefile CHANGED
@@ -11,42 +11,137 @@ bin = File.join(__dir__, "bin")
11
11
 
12
12
  Rake::ExtensionTask.new("rbs_extension")
13
13
 
14
+ compile_task = Rake::Task[:compile]
15
+
16
+ task :setup_extconf_compile_commands_json do
17
+ ENV["COMPILE_COMMANDS_JSON"] = "1"
18
+ end
19
+
20
+ compile_task.prerequisites.unshift(:setup_extconf_compile_commands_json)
21
+
14
22
  test_config = lambda do |t|
15
23
  t.libs << "test"
16
24
  t.libs << "lib"
17
25
  t.test_files = FileList["test/**/*_test.rb"].reject do |path|
18
26
  path =~ %r{test/stdlib/}
19
27
  end
20
- end
21
-
22
- Rake::TestTask.new(test: :compile, &test_config)
23
-
24
- unless Gem.win_platform?
25
- begin
26
- require "ruby_memcheck"
27
-
28
- namespace :test do
29
- RubyMemcheck::TestTask.new(valgrind: :compile, &test_config)
28
+ if defined?(RubyMemcheck)
29
+ if t.is_a?(RubyMemcheck::TestTask)
30
+ t.verbose = true
31
+ t.options = '-v'
30
32
  end
31
- rescue LoadError => exn
32
- STDERR.puts "🚨🚨🚨🚨 Skipping RubyMemcheck: #{exn.inspect} 🚨🚨🚨🚨"
33
33
  end
34
34
  end
35
35
 
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
44
+
36
45
  multitask :default => [:test, :stdlib_test, :typecheck_test, :rubocop, :validate, :test_doc]
37
46
 
38
47
  task :lexer do
39
- sh "re2c -W --no-generation-date -o ext/rbs_extension/lexer.c ext/rbs_extension/lexer.re"
48
+ sh "re2c -W --no-generation-date -o src/lexer.c src/lexer.re"
49
+ sh "clang-format -i -style=file src/lexer.c"
40
50
  end
41
51
 
42
52
  task :confirm_lexer => :lexer do
43
53
  puts "Testing if lexer.c is updated with respect to lexer.re"
44
- sh "git diff --exit-code ext/rbs_extension/lexer.c"
54
+ sh "git diff --exit-code src/lexer.c"
45
55
  end
46
56
 
47
57
  task :confirm_templates => :templates do
48
- puts "Testing if generated code under include and src is updated with respect to templates"
49
- 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(" ")}"
67
+ end
68
+
69
+ # Task to format C code using clang-format
70
+ namespace :format do
71
+ dirs = ["src", "ext", "include"]
72
+
73
+ # Find all C source and header files
74
+ files = `find #{dirs.join(" ")} -type f \\( -name "*.c" -o -name "*.h" \\)`.split("\n")
75
+
76
+ desc "Format C source files using clang-format"
77
+ task :c do
78
+ puts "Formatting C files..."
79
+
80
+ # Check if clang-format is installed
81
+ unless system("which clang-format > /dev/null 2>&1")
82
+ abort "Error: clang-format not found. Please install clang-format first."
83
+ end
84
+
85
+ if files.empty?
86
+ puts "No C files found to format"
87
+ next
88
+ end
89
+
90
+ puts "Found #{files.length} files to format (excluding generated files)"
91
+
92
+ exit_status = 0
93
+ files.each do |file|
94
+ puts "Formatting #{file}"
95
+ unless system("clang-format -i -style=file #{file}")
96
+ puts "❌ Error formatting #{file}"
97
+ exit_status = 1
98
+ end
99
+ end
100
+
101
+ exit exit_status unless exit_status == 0
102
+ puts "✅ All files formatted successfully"
103
+ end
104
+
105
+ desc "Check if C source files are properly formatted"
106
+ task :c_check do
107
+ puts "Checking C file formatting..."
108
+
109
+ # Check if clang-format is installed
110
+ unless system("which clang-format > /dev/null 2>&1")
111
+ abort "Error: clang-format not found. Please install clang-format first."
112
+ end
113
+
114
+ if files.empty?
115
+ puts "No C files found to check"
116
+ next
117
+ end
118
+
119
+ puts "Found #{files.length} files to check (excluding generated files)"
120
+
121
+ needs_format = false
122
+ files.each do |file|
123
+ formatted = `clang-format -style=file #{file}`
124
+ original = File.read(file)
125
+
126
+ if formatted != original
127
+ puts "❌ #{file} needs formatting"
128
+ puts "Diff:"
129
+ # Save formatted version to temp file and run diff
130
+ temp_file = "#{file}.formatted"
131
+ File.write(temp_file, formatted)
132
+ system("diff -u #{file} #{temp_file}")
133
+ File.unlink(temp_file)
134
+ needs_format = true
135
+ end
136
+ end
137
+
138
+ if needs_format
139
+ warn "Some files need formatting. Run 'rake format:c' to format them."
140
+ exit 1
141
+ else
142
+ puts "✅ All files are properly formatted"
143
+ end
144
+ end
50
145
  end
51
146
 
52
147
  rule ".c" => ".re" do |t|
@@ -59,6 +154,9 @@ end
59
154
  rule %r{^include/(.*)\.c} => 'templates/%X.c.erb' do |t|
60
155
  puts "⚠️⚠️⚠️ #{t.name} is older than #{t.source}. You may need to run `rake templates` ⚠️⚠️⚠️"
61
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
62
160
 
63
161
  task :annotate do
64
162
  sh "bin/generate_docs.sh"
@@ -70,17 +168,26 @@ task :confirm_annotation do
70
168
  end
71
169
 
72
170
  task :templates do
73
- sh "#{ruby} templates/template.rb include/rbs/constants.h"
74
- sh "#{ruby} templates/template.rb include/rbs/ruby_objs.h"
75
- sh "#{ruby} templates/template.rb src/constants.c"
76
- sh "#{ruby} templates/template.rb src/ruby_objs.c"
171
+ sh "#{ruby} templates/template.rb ext/rbs_extension/ast_translation.h"
172
+ sh "#{ruby} templates/template.rb ext/rbs_extension/ast_translation.c"
173
+
174
+ sh "#{ruby} templates/template.rb ext/rbs_extension/class_constants.h"
175
+ sh "#{ruby} templates/template.rb ext/rbs_extension/class_constants.c"
176
+
177
+ sh "#{ruby} templates/template.rb include/rbs/ast.h"
178
+ sh "#{ruby} templates/template.rb src/ast.c"
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
+
184
+ # Format the generated files
185
+ Rake::Task["format:c"].invoke
77
186
  end
78
187
 
79
- task :compile => "ext/rbs_extension/lexer.c"
80
- task :compile => "include/rbs/constants.h"
81
- task :compile => "include/rbs/ruby_objs.h"
82
- task :compile => "src/constants.c"
83
- task :compile => "src/ruby_objs.c"
188
+ task :compile => "ext/rbs_extension/class_constants.h"
189
+ task :compile => "ext/rbs_extension/class_constants.c"
190
+ task :compile => "src/lexer.c"
84
191
 
85
192
  task :test_doc do
86
193
  files = Dir.chdir(File.expand_path('..', __FILE__)) do
@@ -93,7 +200,7 @@ end
93
200
  task :validate => :compile do
94
201
  require 'yaml'
95
202
 
96
- sh "#{ruby} #{rbs} validate --exit-error-on-syntax-error"
203
+ sh "#{ruby} #{rbs} validate"
97
204
 
98
205
  libs = FileList["stdlib/*"].map {|path| File.basename(path).to_s }
99
206
 
@@ -113,8 +220,18 @@ task :validate => :compile do
113
220
  libs << "rbs"
114
221
  end
115
222
 
223
+ libs.delete("bigdecimal-math") or raise
224
+ libs.delete("bigdecimal") or raise
225
+
116
226
  libs.each do |lib|
117
- sh "#{ruby} #{rbs} -r #{lib} validate --exit-error-on-syntax-error"
227
+ args = ["-r", lib]
228
+
229
+ if lib == "rbs"
230
+ args << "-r" << "prism"
231
+ args << "-r" << "logger"
232
+ end
233
+
234
+ sh "#{ruby} #{rbs} #{args.join(' ')} validate"
118
235
  end
119
236
  end
120
237
 
@@ -126,7 +243,7 @@ end
126
243
 
127
244
  task :stdlib_test => :compile do
128
245
  test_files = FileList["test/stdlib/**/*_test.rb"].reject do |path|
129
- path =~ %r{Ractor} || path =~ %r{Encoding} || path =~ %r{CGI_test}
246
+ path =~ %r{Ractor} || path =~ %r{Encoding} || path =~ %r{CGI-escape_test}
130
247
  end
131
248
 
132
249
  if ENV["RANDOMIZE_STDLIB_TEST_ORDER"] == "true"
@@ -135,7 +252,7 @@ task :stdlib_test => :compile do
135
252
 
136
253
  sh "#{ruby} -Ilib #{bin}/test_runner.rb #{test_files.join(' ')}"
137
254
  # TODO: Ractor tests need to be run in a separate process
138
- sh "#{ruby} -Ilib #{bin}/test_runner.rb test/stdlib/CGI_test.rb"
255
+ sh "#{ruby} -Ilib #{bin}/test_runner.rb test/stdlib/CGI-escape_test.rb"
139
256
  sh "#{ruby} -Ilib #{bin}/test_runner.rb test/stdlib/Ractor_test.rb"
140
257
  sh "#{ruby} -Ilib #{bin}/test_runner.rb test/stdlib/Encoding_test.rb"
141
258
  end
@@ -228,7 +345,7 @@ namespace :generate do
228
345
  class <%= target %>SingletonTest < Test::Unit::TestCase
229
346
  include TestHelper
230
347
 
231
- # library "pathname", "securerandom" # Declare library signatures to load
348
+ # library "logger", "securerandom" # Declare library signatures to load
232
349
  testing "singleton(::<%= target %>)"
233
350
 
234
351
  <%- class_methods.each do |method_name, definition| -%>
@@ -247,7 +364,7 @@ namespace :generate do
247
364
  class <%= target %>Test < Test::Unit::TestCase
248
365
  include TestHelper
249
366
 
250
- # library "pathname", "securerandom" # Declare library signatures to load
367
+ # library "logger", "securerandom" # Declare library signatures to load
251
368
  testing "::<%= target %>"
252
369
 
253
370
  <%- instance_methods.each do |method_name, definition| -%>
@@ -388,45 +505,645 @@ NOTES
388
505
  end
389
506
 
390
507
 
391
- desc "Generate changelog template from GH pull requests"
392
- task :changelog do
393
- major, minor, patch, _pre = RBS::VERSION.split(".", 4)
394
- major = major.to_i
395
- minor = minor.to_i
396
- patch = patch.to_i
508
+ # Pull requests with one of these labels are omitted from the changelog.
509
+ CHANGELOG_SKIP_LABELS = ["skip-changelog"]
397
510
 
398
- if patch == 0
399
- milestone = "RBS #{major}.#{minor}"
400
- else
401
- milestone = "RBS #{major}.#{minor}.x"
402
- 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
403
531
 
404
- puts "🔍 Finding pull requests that is associated to milestone `#{milestone}`..."
532
+ _, status = Open3.capture2("git", "rev-parse", "--verify", "--quiet", "#{from}^{commit}")
533
+ raise "🚨 No such commit-ish: `#{from}`" unless status.success?
405
534
 
406
- command = [
407
- "gh",
408
- "pr",
409
- "list",
410
- "--limit=10000",
411
- "--json",
412
- "url,title,number",
413
- "--search" ,
414
- "milestone:\"#{milestone}\" is:merged sort:updated-desc -label:Released"
415
- ]
535
+ from
536
+ end
416
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)
417
544
  require "open3"
545
+ require "json"
546
+
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
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: [])
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
+
418
591
  output, status = Open3.capture2(*command)
419
592
  raise status.inspect unless status.success?
420
593
 
421
- require "json"
422
- json = JSON.parse(output, symbolize_names: true)
594
+ output.lines.map(&:chomp).reject(&:empty?)
595
+ end
423
596
 
424
- unless json.empty?
425
- puts
426
- json.each do |line|
427
- 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
428
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
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}`.)"
429
705
  else
430
- 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}"
831
+ end
832
+ end
833
+
834
+ desc "Compile extension without C23 extensions"
835
+ task :compile_c99 do
836
+ ENV["TEST_NO_C23"] = "true"
837
+ Rake::Task[:"compile"].invoke
838
+ ensure
839
+ ENV.delete("TEST_NO_C23")
840
+ end
841
+
842
+ task :prepare_bench do
843
+ ENV.delete("DEBUG")
844
+ Rake::Task[:"clobber"].invoke
845
+ Rake::Task[:"templates"].invoke
846
+ Rake::Task[:"compile"].invoke
847
+ end
848
+
849
+ task :prepare_profiling do
850
+ ENV["DEBUG"] = "1"
851
+ Rake::Task[:"clobber"].invoke
852
+ Rake::Task[:"templates"].invoke
853
+ Rake::Task[:"compile"].invoke
854
+ end
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
+
947
+ namespace :rust do
948
+ namespace :rbs do
949
+ RUST_DIR = File.expand_path("rust", __dir__)
950
+ RBS_VERSION_FILE = File.join(RUST_DIR, "rbs_version")
951
+
952
+ VENDOR_TARGETS = {
953
+ "ruby-rbs-sys" => %w[include src],
954
+ "ruby-rbs" => %w[config.yml],
955
+ }
956
+
957
+ desc "Sync vendored RBS source from the pinned version"
958
+ task :sync do
959
+ unless File.exist?(RBS_VERSION_FILE)
960
+ raise "#{RBS_VERSION_FILE} not found. Run `rake rust:rbs:pin[VERSION]` first."
961
+ end
962
+
963
+ version = File.read(RBS_VERSION_FILE).strip
964
+ raise "#{RBS_VERSION_FILE} is empty" if version.empty?
965
+
966
+ puts "Syncing vendor/rbs/ from #{version}..."
967
+
968
+ VENDOR_TARGETS.each do |crate, entries|
969
+ vendor_dir = File.join(RUST_DIR, crate, "vendor", "rbs")
970
+
971
+ puts " Copying files for #{crate}:"
972
+ chmod_R "u+w", vendor_dir, verbose: false if File.exist?(vendor_dir)
973
+ rm_rf vendor_dir, verbose: false
974
+ mkdir_p vendor_dir, verbose: false
975
+
976
+ entries.each do |entry|
977
+ target = File.join(vendor_dir, entry)
978
+
979
+ # Extract the entry from the pinned git tag using git archive
980
+ IO.popen(["git", "archive", "--format=tar", version, "--", entry], "rb") do |tar|
981
+ IO.popen(["tar", "xf", "-", "-C", vendor_dir], "wb") do |extract|
982
+ IO.copy_stream(tar, extract)
983
+ end
984
+ end
985
+
986
+ raise "Failed to extract #{entry} from #{version}" unless File.exist?(target)
987
+ puts " #{entry}"
988
+ end
989
+
990
+ # Make files read-only to prevent accidental edits
991
+ chmod_R "a-w", vendor_dir, verbose: false
992
+ end
993
+
994
+ puts "📦 Synced vendor/rbs/ from #{version} (read-only)"
995
+ end
996
+
997
+ desc "Pin a specific RBS version for Rust crates (e.g., rake rust:rbs:pin[v4.0.3])"
998
+ task :pin, [:version] do |_t, args|
999
+ version = args[:version] or raise "Usage: rake rust:rbs:pin[VERSION]"
1000
+
1001
+ # Verify the tag exists
1002
+ unless system("git", "rev-parse", "--verify", "#{version}^{commit}", out: File::NULL, err: File::NULL)
1003
+ raise "Tag #{version} not found"
1004
+ end
1005
+
1006
+ File.write(RBS_VERSION_FILE, "#{version}\n")
1007
+ puts "📌 Pinned RBS version to #{version}"
1008
+
1009
+ Rake::Task["rust:rbs:sync"].invoke
1010
+ end
1011
+
1012
+ desc "Create symlinks from vendor/rbs/ to the repository root (for development/CI)"
1013
+ task :symlink do
1014
+ VENDOR_TARGETS.each do |crate, entries|
1015
+ vendor_dir = File.join(RUST_DIR, crate, "vendor", "rbs")
1016
+
1017
+ puts "Setting up symlinks for #{crate}..."
1018
+ entries.each do |entry|
1019
+ puts " #{entry} -> repository root"
1020
+ end
1021
+
1022
+ chmod_R "u+w", vendor_dir, verbose: false if File.exist?(vendor_dir)
1023
+ rm_rf vendor_dir, verbose: false
1024
+ mkdir_p vendor_dir, verbose: false
1025
+
1026
+ entries.each do |entry|
1027
+ ln_s File.join("..", "..", "..", "..", entry), File.join(vendor_dir, entry), verbose: false
1028
+ end
1029
+ end
1030
+
1031
+ puts "🔗 Symlinked vendor/rbs/ to repository root"
1032
+ end
1033
+ end
1034
+
1035
+ namespace :publish do
1036
+ def self.prepare_publish_branch(crate_name)
1037
+ dry_run = ENV["RBS_RUST_PUBLISH_DRY_RUN"]
1038
+
1039
+ version_file = File.join(RUST_DIR, "rbs_version")
1040
+
1041
+ unless File.exist?(version_file)
1042
+ raise "#{version_file} not found. Run `rake rust:rbs:pin[VERSION]` first."
1043
+ end
1044
+
1045
+ rbs_version = File.read(version_file).strip
1046
+ raise "#{version_file} is empty" if rbs_version.empty?
1047
+
1048
+ crate_version = File.read(File.join(RUST_DIR, crate_name, "Cargo.toml"))[/^version\s*=\s*"(.+)"/, 1]
1049
+ release_branch = "rust/release-#{crate_name}-#{Time.now.strftime('%Y%m%d%H%M%S')}"
1050
+
1051
+ puts "=" * 60
1052
+ puts "Rust crate publish: #{crate_name}#{dry_run ? " (DRY RUN)" : ""}"
1053
+ puts "=" * 60
1054
+ puts " RBS source version: #{rbs_version}"
1055
+ puts " #{crate_name}: #{crate_version} (tag: #{crate_name}-v#{crate_version})"
1056
+ puts " Release branch: #{release_branch}"
1057
+ puts "=" * 60
1058
+
1059
+ # Check that vendor dirs contain real files, not symlinks
1060
+ entries = VENDOR_TARGETS.fetch(crate_name)
1061
+ entries.each do |entry|
1062
+ path = File.join(RUST_DIR, crate_name, "vendor", "rbs", entry)
1063
+ if File.symlink?(path)
1064
+ raise "#{path} is a symlink. Run `rake rust:rbs:sync` first."
1065
+ end
1066
+ unless File.exist?(path)
1067
+ raise "#{path} does not exist. Run `rake rust:rbs:sync` first."
1068
+ end
1069
+ end
1070
+
1071
+ # Ensure working tree is clean before publishing
1072
+ unless `git status --porcelain`.strip.empty?
1073
+ raise "💢 Working tree is dirty. Please commit or stash your changes before publishing."
1074
+ end
1075
+
1076
+ # Create a release branch with vendor files committed
1077
+ original_branch = `git rev-parse --abbrev-ref HEAD`.strip
1078
+
1079
+ sh "git", "checkout", "-b", release_branch, verbose: false
1080
+ vendor_path = File.join("rust", crate_name, "vendor", "rbs")
1081
+ sh "git", "add", "-f", vendor_path, verbose: false
1082
+ sh "git", "commit", "-m", "Publish #{crate_name} (RBS #{rbs_version})", verbose: false
1083
+
1084
+ [dry_run, crate_version, original_branch]
1085
+ end
1086
+
1087
+ desc "Publish ruby-rbs-sys crate to crates.io (set RBS_RUST_PUBLISH_DRY_RUN=1 for dry-run only)"
1088
+ task :"ruby-rbs-sys" do
1089
+ crate_name = "ruby-rbs-sys"
1090
+ dry_run, crate_version, original_branch = prepare_publish_branch(crate_name)
1091
+
1092
+ begin
1093
+ puts "🔰 Dry-run publishing..."
1094
+
1095
+ Dir.chdir(File.join(RUST_DIR, crate_name)) do
1096
+ sh "cargo", "publish", "--dry-run"
1097
+ end
1098
+
1099
+ puts "✅ Dry-run succeeded!"
1100
+
1101
+ unless dry_run
1102
+ puts "💪 Publishing #{crate_name} for real..."
1103
+
1104
+ Dir.chdir(File.join(RUST_DIR, crate_name)) do
1105
+ sh "cargo", "publish"
1106
+ end
1107
+
1108
+ sh "git", "tag", "#{crate_name}-v#{crate_version}"
1109
+ sh "git", "push", "origin", "#{crate_name}-v#{crate_version}"
1110
+
1111
+ puts "🎉 Published #{crate_name} successfully!"
1112
+ end
1113
+ ensure
1114
+ sh "git", "checkout", original_branch, verbose: false
1115
+ end
1116
+ end
1117
+
1118
+ desc "Publish ruby-rbs crate to crates.io (set RBS_RUST_PUBLISH_DRY_RUN=1 for dry-run only)"
1119
+ task :"ruby-rbs" do
1120
+ crate_name = "ruby-rbs"
1121
+ dry_run, crate_version, original_branch = prepare_publish_branch(crate_name)
1122
+
1123
+ begin
1124
+ puts "🔰 Dry-run publishing..."
1125
+
1126
+ Dir.chdir(File.join(RUST_DIR, crate_name)) do
1127
+ sh "cargo", "publish", "--dry-run", "--no-verify"
1128
+ end
1129
+
1130
+ puts "✅ Dry-run succeeded!"
1131
+
1132
+ unless dry_run
1133
+ puts "💪 Publishing #{crate_name} for real..."
1134
+
1135
+ Dir.chdir(File.join(RUST_DIR, crate_name)) do
1136
+ sh "cargo", "publish"
1137
+ end
1138
+
1139
+ sh "git", "tag", "#{crate_name}-v#{crate_version}"
1140
+ sh "git", "push", "origin", "#{crate_name}-v#{crate_version}"
1141
+
1142
+ puts "🎉 Published #{crate_name} successfully!"
1143
+ end
1144
+ ensure
1145
+ sh "git", "checkout", original_branch, verbose: false
1146
+ end
1147
+ end
431
1148
  end
432
1149
  end