rbs 3.9.4 → 4.2.0

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 (354) 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/.gitattributes +1 -0
  6. data/.github/dependabot.yml +16 -14
  7. data/.github/workflows/bundle-update.yml +63 -0
  8. data/.github/workflows/c-check.yml +66 -0
  9. data/.github/workflows/changelog.yml +121 -0
  10. data/.github/workflows/comments.yml +6 -4
  11. data/.github/workflows/dependabot.yml +2 -2
  12. data/.github/workflows/jruby.yml +74 -0
  13. data/.github/workflows/release-gems.yml +234 -0
  14. data/.github/workflows/ruby.yml +86 -32
  15. data/.github/workflows/rust.yml +186 -0
  16. data/.github/workflows/truffleruby.yml +54 -0
  17. data/.github/workflows/typecheck.yml +6 -3
  18. data/.github/workflows/wasm.yml +55 -0
  19. data/.github/workflows/windows.yml +10 -4
  20. data/.gitignore +16 -0
  21. data/.rubocop.yml +1 -2
  22. data/CHANGELOG.md +502 -0
  23. data/Dockerfile.jruby +53 -0
  24. data/README.md +42 -5
  25. data/Rakefile +892 -111
  26. data/Steepfile +11 -0
  27. data/config.yml +665 -62
  28. data/core/array.rbs +541 -398
  29. data/core/basic_object.rbs +9 -8
  30. data/core/binding.rbs +0 -2
  31. data/core/builtin.rbs +9 -8
  32. data/core/class.rbs +11 -8
  33. data/core/comparable.rbs +55 -34
  34. data/core/complex.rbs +104 -78
  35. data/core/dir.rbs +61 -49
  36. data/core/encoding.rbs +12 -15
  37. data/core/enumerable.rbs +297 -196
  38. data/core/enumerator/arithmetic_sequence.rbs +70 -0
  39. data/core/enumerator/product.rbs +5 -5
  40. data/core/enumerator.rbs +91 -28
  41. data/core/errno.rbs +11 -2
  42. data/core/errors.rbs +58 -29
  43. data/core/exception.rbs +13 -13
  44. data/core/fiber.rbs +74 -54
  45. data/core/file.rbs +260 -1151
  46. data/core/file_constants.rbs +463 -0
  47. data/core/file_stat.rbs +534 -0
  48. data/core/file_test.rbs +3 -3
  49. data/core/float.rbs +257 -116
  50. data/core/gc.rbs +425 -281
  51. data/core/hash.rbs +1151 -829
  52. data/core/integer.rbs +156 -195
  53. data/core/io/buffer.rbs +53 -42
  54. data/core/io/wait.rbs +13 -35
  55. data/core/io.rbs +222 -155
  56. data/core/kernel.rbs +239 -163
  57. data/core/marshal.rbs +4 -4
  58. data/core/match_data.rbs +16 -14
  59. data/core/math.rbs +107 -66
  60. data/core/method.rbs +69 -33
  61. data/core/module.rbs +302 -150
  62. data/core/nil_class.rbs +7 -6
  63. data/core/numeric.rbs +77 -63
  64. data/core/object.rbs +9 -11
  65. data/core/object_space/weak_key_map.rbs +7 -7
  66. data/core/object_space.rbs +30 -23
  67. data/core/pathname.rbs +1312 -0
  68. data/core/proc.rbs +95 -58
  69. data/core/process.rbs +222 -202
  70. data/core/ractor.rbs +364 -518
  71. data/core/random.rbs +21 -3
  72. data/core/range.rbs +181 -79
  73. data/core/rational.rbs +60 -89
  74. data/core/rbs/ops.rbs +154 -0
  75. data/core/rbs/unnamed/argf.rbs +63 -56
  76. data/core/rbs/unnamed/env_class.rbs +19 -14
  77. data/core/rbs/unnamed/main_class.rbs +123 -0
  78. data/core/rbs/unnamed/random.rbs +11 -118
  79. data/core/regexp.rbs +258 -214
  80. data/core/ruby.rbs +53 -0
  81. data/core/ruby_vm.rbs +78 -34
  82. data/core/rubygems/config_file.rbs +5 -5
  83. data/core/rubygems/errors.rbs +6 -70
  84. data/core/rubygems/requirement.rbs +5 -15
  85. data/core/rubygems/rubygems.rbs +18 -81
  86. data/core/rubygems/specification.rbs +8 -0
  87. data/core/rubygems/version.rbs +2 -163
  88. data/core/set.rbs +493 -363
  89. data/core/signal.rbs +26 -16
  90. data/core/string.rbs +3234 -1285
  91. data/core/struct.rbs +43 -42
  92. data/core/symbol.rbs +41 -34
  93. data/core/thread.rbs +139 -83
  94. data/core/time.rbs +81 -50
  95. data/core/trace_point.rbs +41 -35
  96. data/core/true_class.rbs +2 -2
  97. data/core/unbound_method.rbs +24 -16
  98. data/core/warning.rbs +7 -7
  99. data/docs/CONTRIBUTING.md +3 -2
  100. data/docs/aliases.md +79 -0
  101. data/docs/collection.md +3 -3
  102. data/docs/config.md +171 -0
  103. data/docs/encoding.md +56 -0
  104. data/docs/gem.md +0 -1
  105. data/docs/inline.md +634 -0
  106. data/docs/rbs_by_example.md +20 -20
  107. data/docs/release.md +303 -0
  108. data/docs/rust.md +96 -0
  109. data/docs/sigs.md +3 -3
  110. data/docs/stdlib.md +8 -0
  111. data/docs/syntax.md +60 -18
  112. data/docs/type_fingerprint.md +21 -0
  113. data/docs/wasm_serialization.md +80 -0
  114. data/exe/rbs +1 -1
  115. data/ext/rbs_extension/ast_translation.c +1870 -0
  116. data/ext/rbs_extension/ast_translation.h +41 -0
  117. data/ext/rbs_extension/class_constants.c +189 -0
  118. data/{include/rbs/constants.h → ext/rbs_extension/class_constants.h} +24 -1
  119. data/ext/rbs_extension/compat.h +10 -0
  120. data/ext/rbs_extension/extconf.rb +26 -1
  121. data/ext/rbs_extension/legacy_location.c +299 -0
  122. data/ext/rbs_extension/legacy_location.h +82 -0
  123. data/ext/rbs_extension/main.c +639 -23
  124. data/ext/rbs_extension/rbs_extension.h +6 -21
  125. data/ext/rbs_extension/rbs_string_bridging.c +9 -0
  126. data/ext/rbs_extension/rbs_string_bridging.h +24 -0
  127. data/include/rbs/ast.h +1055 -0
  128. data/include/rbs/defines.h +104 -0
  129. data/include/rbs/lexer.h +208 -0
  130. data/include/rbs/location.h +40 -0
  131. data/include/rbs/parser.h +187 -0
  132. data/include/rbs/serialize.h +39 -0
  133. data/include/rbs/string.h +47 -0
  134. data/include/rbs/util/rbs_allocator.h +59 -0
  135. data/include/rbs/util/rbs_assert.h +20 -0
  136. data/include/rbs/util/rbs_buffer.h +83 -0
  137. data/include/rbs/util/rbs_constant_pool.h +6 -70
  138. data/include/rbs/util/rbs_encoding.h +282 -0
  139. data/include/rbs/util/rbs_unescape.h +24 -0
  140. data/include/rbs.h +9 -2
  141. data/lib/rbs/annotate/formatter.rb +3 -13
  142. data/lib/rbs/annotate/rdoc_annotator.rb +30 -32
  143. data/lib/rbs/annotate/rdoc_source.rb +1 -1
  144. data/lib/rbs/ast/annotation.rb +1 -1
  145. data/lib/rbs/ast/comment.rb +1 -1
  146. data/lib/rbs/ast/declarations.rb +11 -11
  147. data/lib/rbs/ast/members.rb +14 -14
  148. data/lib/rbs/ast/ruby/annotations.rb +451 -0
  149. data/lib/rbs/ast/ruby/comment_block.rb +247 -0
  150. data/lib/rbs/ast/ruby/declarations.rb +291 -0
  151. data/lib/rbs/ast/ruby/helpers/constant_helper.rb +28 -0
  152. data/lib/rbs/ast/ruby/helpers/location_helper.rb +15 -0
  153. data/lib/rbs/ast/ruby/members.rb +762 -0
  154. data/lib/rbs/ast/type_param.rb +25 -5
  155. data/lib/rbs/buffer.rb +142 -20
  156. data/lib/rbs/cli/diff.rb +16 -15
  157. data/lib/rbs/cli/validate.rb +63 -126
  158. data/lib/rbs/cli.rb +59 -29
  159. data/lib/rbs/collection/config/lockfile_generator.rb +28 -3
  160. data/lib/rbs/collection/sources/git.rb +7 -0
  161. data/lib/rbs/definition.rb +6 -1
  162. data/lib/rbs/definition_builder/ancestor_builder.rb +129 -70
  163. data/lib/rbs/definition_builder/method_builder.rb +74 -33
  164. data/lib/rbs/definition_builder.rb +181 -21
  165. data/lib/rbs/diff.rb +7 -1
  166. data/lib/rbs/environment/class_entry.rb +81 -0
  167. data/lib/rbs/environment/module_entry.rb +91 -0
  168. data/lib/rbs/environment.rb +410 -215
  169. data/lib/rbs/environment_loader.rb +2 -8
  170. data/lib/rbs/errors.rb +31 -21
  171. data/lib/rbs/inline_parser/comment_association.rb +117 -0
  172. data/lib/rbs/inline_parser.rb +568 -0
  173. data/lib/rbs/location_aux.rb +36 -4
  174. data/lib/rbs/locator.rb +5 -1
  175. data/lib/rbs/method_type.rb +5 -3
  176. data/lib/rbs/namespace.rb +47 -18
  177. data/lib/rbs/parser_aux.rb +37 -7
  178. data/lib/rbs/prototype/helpers.rb +24 -0
  179. data/lib/rbs/prototype/rb.rb +3 -28
  180. data/lib/rbs/prototype/rbi.rb +196 -45
  181. data/lib/rbs/prototype/runtime/value_object_generator.rb +0 -1
  182. data/lib/rbs/prototype/runtime.rb +13 -3
  183. data/lib/rbs/resolver/constant_resolver.rb +2 -2
  184. data/lib/rbs/resolver/type_name_resolver.rb +120 -44
  185. data/lib/rbs/rewriter.rb +70 -0
  186. data/lib/rbs/source.rb +99 -0
  187. data/lib/rbs/subtractor.rb +7 -4
  188. data/lib/rbs/test/type_check.rb +25 -3
  189. data/lib/rbs/type_name.rb +34 -21
  190. data/lib/rbs/types.rb +135 -81
  191. data/lib/rbs/unit_test/convertibles.rb +1 -0
  192. data/lib/rbs/unit_test/type_assertions.rb +47 -11
  193. data/lib/rbs/validator.rb +2 -2
  194. data/lib/rbs/version.rb +1 -1
  195. data/lib/rbs/wasm/deserializer.rb +213 -0
  196. data/lib/rbs/wasm/location.rb +61 -0
  197. data/lib/rbs/wasm/parser.rb +174 -0
  198. data/lib/rbs/wasm/runtime.rb +211 -0
  199. data/lib/rbs/wasm/serialization_schema.rb +111 -0
  200. data/lib/rbs.rb +25 -2
  201. data/lib/rbs_jars.rb +39 -0
  202. data/lib/rdoc/discover.rb +1 -1
  203. data/lib/rdoc_plugin/parser.rb +8 -3
  204. data/rbs.gemspec +34 -6
  205. data/schema/function.json +12 -1
  206. data/schema/typeParam.json +17 -1
  207. data/sig/ancestor_builder.rbs +1 -1
  208. data/sig/annotate/formatter.rbs +2 -2
  209. data/sig/annotate/rdoc_annotater.rbs +13 -10
  210. data/sig/ast/ruby/annotations.rbs +470 -0
  211. data/sig/ast/ruby/comment_block.rbs +127 -0
  212. data/sig/ast/ruby/declarations.rbs +158 -0
  213. data/sig/ast/ruby/helpers/constant_helper.rbs +11 -0
  214. data/sig/ast/ruby/helpers/location_helper.rbs +15 -0
  215. data/sig/ast/ruby/members.rbs +198 -0
  216. data/sig/buffer.rbs +81 -5
  217. data/sig/cli/diff.rbs +5 -11
  218. data/sig/cli/validate.rbs +12 -8
  219. data/sig/cli.rbs +18 -18
  220. data/sig/collection/config/lockfile_generator.rbs +2 -0
  221. data/sig/definition.rbs +6 -0
  222. data/sig/definition_builder.rbs +3 -1
  223. data/sig/environment/class_entry.rbs +56 -0
  224. data/sig/environment/module_entry.rbs +65 -0
  225. data/sig/environment.rbs +94 -87
  226. data/sig/errors.rbs +26 -20
  227. data/sig/inline_parser/comment_association.rbs +71 -0
  228. data/sig/inline_parser.rbs +126 -0
  229. data/sig/location.rbs +32 -7
  230. data/sig/locator.rbs +0 -2
  231. data/sig/manifest.yaml +0 -2
  232. data/sig/method_builder.rbs +9 -4
  233. data/sig/namespace.rbs +20 -5
  234. data/sig/parser.rbs +79 -15
  235. data/sig/prototype/helpers.rbs +2 -0
  236. data/sig/prototype/rbi.rbs +33 -4
  237. data/sig/resolver/type_name_resolver.rbs +36 -10
  238. data/sig/rewriter.rbs +45 -0
  239. data/sig/source.rbs +48 -0
  240. data/sig/type_param.rbs +13 -8
  241. data/sig/typename.rbs +15 -5
  242. data/sig/types.rbs +21 -9
  243. data/sig/unit_test/spy.rbs +0 -8
  244. data/sig/unit_test/type_assertions.rbs +17 -2
  245. data/sig/wasm/deserializer.rbs +66 -0
  246. data/sig/wasm/serialization_schema.rbs +13 -0
  247. data/src/ast.c +1644 -0
  248. data/src/lexer.c +3223 -0
  249. data/src/lexer.re +187 -0
  250. data/src/lexstate.c +222 -0
  251. data/src/location.c +31 -0
  252. data/src/parser.c +4318 -0
  253. data/src/serialize.c +965 -0
  254. data/src/string.c +41 -0
  255. data/src/util/rbs_allocator.c +171 -0
  256. data/src/util/rbs_assert.c +19 -0
  257. data/src/util/rbs_buffer.c +54 -0
  258. data/src/util/rbs_constant_pool.c +18 -92
  259. data/src/util/rbs_encoding.c +21454 -0
  260. data/src/util/rbs_unescape.c +167 -0
  261. data/stdlib/abbrev/0/array.rbs +1 -1
  262. data/stdlib/bigdecimal/0/big_decimal.rbs +116 -98
  263. data/stdlib/bigdecimal-math/0/big_math.rbs +169 -8
  264. data/stdlib/cgi/0/core.rbs +9 -393
  265. data/stdlib/cgi/0/manifest.yaml +1 -0
  266. data/stdlib/cgi-escape/0/escape.rbs +171 -0
  267. data/stdlib/coverage/0/coverage.rbs +7 -4
  268. data/stdlib/csv/0/csv.rbs +5 -5
  269. data/stdlib/date/0/date.rbs +92 -79
  270. data/stdlib/date/0/date_time.rbs +25 -24
  271. data/stdlib/delegate/0/delegator.rbs +11 -7
  272. data/stdlib/did_you_mean/0/did_you_mean.rbs +17 -16
  273. data/stdlib/digest/0/digest.rbs +117 -1
  274. data/stdlib/erb/0/erb.rbs +754 -353
  275. data/stdlib/etc/0/etc.rbs +73 -54
  276. data/stdlib/fileutils/0/fileutils.rbs +179 -160
  277. data/stdlib/forwardable/0/forwardable.rbs +13 -10
  278. data/stdlib/io-console/0/io-console.rbs +2 -2
  279. data/stdlib/ipaddr/0/ipaddr.rbs +0 -5
  280. data/stdlib/json/0/json.rbs +232 -185
  281. data/stdlib/monitor/0/monitor.rbs +9 -9
  282. data/stdlib/net-http/0/net-http.rbs +162 -134
  283. data/stdlib/objspace/0/objspace.rbs +17 -34
  284. data/stdlib/open-uri/0/open-uri.rbs +48 -8
  285. data/stdlib/open3/0/open3.rbs +469 -10
  286. data/stdlib/openssl/0/openssl.rbs +521 -397
  287. data/stdlib/optparse/0/optparse.rbs +26 -17
  288. data/stdlib/pathname/0/pathname.rbs +11 -1381
  289. data/stdlib/pp/0/pp.rbs +9 -8
  290. data/stdlib/prettyprint/0/prettyprint.rbs +7 -7
  291. data/stdlib/pstore/0/pstore.rbs +35 -30
  292. data/stdlib/psych/0/psych.rbs +65 -12
  293. data/stdlib/psych/0/store.rbs +2 -4
  294. data/stdlib/pty/0/pty.rbs +9 -6
  295. data/stdlib/random-formatter/0/random-formatter.rbs +277 -0
  296. data/stdlib/rdoc/0/code_object.rbs +4 -3
  297. data/stdlib/rdoc/0/comment.rbs +2 -0
  298. data/stdlib/rdoc/0/options.rbs +76 -0
  299. data/stdlib/rdoc/0/parser.rbs +1 -1
  300. data/stdlib/rdoc/0/rdoc.rbs +7 -5
  301. data/stdlib/rdoc/0/store.rbs +2 -2
  302. data/stdlib/resolv/0/resolv.rbs +26 -69
  303. data/stdlib/ripper/0/ripper.rbs +25 -19
  304. data/stdlib/securerandom/0/manifest.yaml +2 -0
  305. data/stdlib/securerandom/0/securerandom.rbs +7 -20
  306. data/stdlib/shellwords/0/shellwords.rbs +3 -3
  307. data/stdlib/singleton/0/singleton.rbs +6 -0
  308. data/stdlib/socket/0/addrinfo.rbs +9 -9
  309. data/stdlib/socket/0/basic_socket.rbs +3 -3
  310. data/stdlib/socket/0/ip_socket.rbs +10 -8
  311. data/stdlib/socket/0/socket.rbs +23 -10
  312. data/stdlib/socket/0/tcp_server.rbs +1 -1
  313. data/stdlib/socket/0/tcp_socket.rbs +11 -3
  314. data/stdlib/socket/0/udp_socket.rbs +1 -1
  315. data/stdlib/socket/0/unix_server.rbs +1 -1
  316. data/stdlib/stringio/0/stringio.rbs +1211 -96
  317. data/stdlib/strscan/0/string_scanner.rbs +101 -80
  318. data/stdlib/tempfile/0/manifest.yaml +3 -0
  319. data/stdlib/tempfile/0/tempfile.rbs +25 -21
  320. data/stdlib/time/0/time.rbs +8 -6
  321. data/stdlib/timeout/0/timeout.rbs +58 -7
  322. data/stdlib/tsort/0/cyclic.rbs +4 -1
  323. data/stdlib/tsort/0/interfaces.rbs +8 -8
  324. data/stdlib/tsort/0/tsort.rbs +16 -15
  325. data/stdlib/uri/0/common.rbs +42 -20
  326. data/stdlib/uri/0/file.rbs +3 -3
  327. data/stdlib/uri/0/generic.rbs +21 -18
  328. data/stdlib/uri/0/http.rbs +2 -2
  329. data/stdlib/uri/0/ldap.rbs +2 -2
  330. data/stdlib/uri/0/mailto.rbs +3 -3
  331. data/stdlib/uri/0/rfc2396_parser.rbs +12 -12
  332. data/stdlib/zlib/0/deflate.rbs +4 -3
  333. data/stdlib/zlib/0/gzip_file.rbs +1 -1
  334. data/stdlib/zlib/0/gzip_reader.rbs +8 -8
  335. data/stdlib/zlib/0/gzip_writer.rbs +16 -13
  336. data/stdlib/zlib/0/inflate.rbs +1 -1
  337. data/stdlib/zlib/0/need_dict.rbs +1 -1
  338. data/wasm/README.md +109 -0
  339. data/wasm/rbs_wasm.c +479 -0
  340. metadata +133 -19
  341. data/ext/rbs_extension/lexer.c +0 -2728
  342. data/ext/rbs_extension/lexer.h +0 -179
  343. data/ext/rbs_extension/lexer.re +0 -147
  344. data/ext/rbs_extension/lexstate.c +0 -175
  345. data/ext/rbs_extension/location.c +0 -325
  346. data/ext/rbs_extension/location.h +0 -85
  347. data/ext/rbs_extension/parser.c +0 -2982
  348. data/ext/rbs_extension/parser.h +0 -18
  349. data/ext/rbs_extension/parserstate.c +0 -411
  350. data/ext/rbs_extension/parserstate.h +0 -163
  351. data/ext/rbs_extension/unescape.c +0 -32
  352. data/include/rbs/ruby_objs.h +0 -72
  353. data/src/constants.c +0 -153
  354. 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
@@ -208,15 +325,11 @@ namespace :generate do
208
325
  class TestTemplateBuilder
209
326
  attr_reader :target, :env
210
327
 
211
- def initialize(target)
328
+ def initialize(target, paths:)
212
329
  @target = target
213
330
 
214
331
  loader = RBS::EnvironmentLoader.new
215
- Dir['stdlib/*'].each do |lib|
216
- next if lib.end_with?('builtin')
217
-
218
- loader.add(library: File.basename(lib))
219
- end
332
+ paths.each { loader.add(path: Pathname(_1)) }
220
333
  @env = RBS::Environment.from_loader(loader).resolve_type_names
221
334
  end
222
335
 
@@ -228,7 +341,7 @@ namespace :generate do
228
341
  class <%= target %>SingletonTest < Test::Unit::TestCase
229
342
  include TestHelper
230
343
 
231
- # library "pathname", "securerandom" # Declare library signatures to load
344
+ # library "logger", "securerandom" # Declare library signatures to load
232
345
  testing "singleton(::<%= target %>)"
233
346
 
234
347
  <%- class_methods.each do |method_name, definition| -%>
@@ -247,7 +360,7 @@ namespace :generate do
247
360
  class <%= target %>Test < Test::Unit::TestCase
248
361
  include TestHelper
249
362
 
250
- # library "pathname", "securerandom" # Declare library signatures to load
363
+ # library "logger", "securerandom" # Declare library signatures to load
251
364
  testing "::<%= target %>"
252
365
 
253
366
  <%- instance_methods.each do |method_name, definition| -%>
@@ -308,7 +421,7 @@ namespace :generate do
308
421
  end
309
422
  end
310
423
 
311
- path.write TestTemplateBuilder.new(target).call
424
+ path.write TestTemplateBuilder.new(target, paths: args.extras).call
312
425
 
313
426
  puts "Created: #{path}"
314
427
  end
@@ -319,114 +432,782 @@ task :test_generate_stdlib do
319
432
  sh "ruby -c /tmp/Array_test.rb"
320
433
  sh "RBS_GENERATE_TEST_PATH=/tmp/Thread_Mutex_test.rb rake 'generate:stdlib_test[Thread::Mutex]'"
321
434
  sh "ruby -c /tmp/Thread_Mutex_test.rb"
435
+ sh "RBS_GENERATE_TEST_PATH=/tmp/Kconv_test.rb rake 'generate:stdlib_test[Kconv,stdlib/nkf/0,stdlib/kconv/0]'"
436
+ sh "ruby -c /tmp/Kconv_test.rb"
322
437
  end
323
438
 
324
- Rake::Task[:release].enhance do
325
- Rake::Task[:"release:note"].invoke
439
+ # Pull requests with one of these labels are omitted from the changelog.
440
+ CHANGELOG_SKIP_LABELS = ["skip-changelog"]
441
+
442
+ # Resolves the commit-ish the changelog starts from.
443
+ #
444
+ # `version` is a version number, a tag, or any commit-ish. When it is omitted, the latest tag
445
+ # matching `tag_glob` is used, skipping the ones matching `exclude_globs`.
446
+ #
447
+ def resolve_changelog_base(version, tag_glob:, exclude_globs: [])
448
+ require "open3"
449
+
450
+ from =
451
+ if version
452
+ # `4.1.0` and `v4.1.0` both mean the tag `v4.1.0`, while `master` or a SHA is used as is.
453
+ version.match?(/\A\d/) ? "v#{version}" : version
454
+ else
455
+ command = ["git", "describe", "--tags", "--match", tag_glob, "--abbrev=0"]
456
+ exclude_globs.each { |glob| command.push("--exclude", glob) }
457
+
458
+ output, status = Open3.capture2(*command)
459
+ raise "🚨 Cannot detect the latest tag matching `#{tag_glob}`. Give the previous version explicitly." unless status.success?
460
+ output.chomp
461
+ end
462
+
463
+ _, status = Open3.capture2("git", "rev-parse", "--verify", "--quiet", "#{from}^{commit}")
464
+ raise "🚨 No such commit-ish: `#{from}`" unless status.success?
465
+
466
+ from
326
467
  end
327
468
 
328
- namespace :release do
329
- desc "Explain the post-release steps automatically"
330
- task :note do
331
- version = Gem::Version.new(RBS::VERSION)
332
- major, minor, patch, *_ = RBS::VERSION.split(".")
333
- major = major.to_i
334
- minor = minor.to_i
335
- patch = patch.to_i
469
+ # Runs a GraphQL query against the repository of the working directory.
470
+ #
471
+ # `body` is the selection set inside `repository`, so a query can use the `$owner` and `$name`
472
+ # variables. Returns the contents of `data.repository`.
473
+ #
474
+ def changelog_graphql(body)
475
+ require "open3"
476
+ require "json"
477
+
478
+ @changelog_repository ||=
479
+ begin
480
+ output, status = Open3.capture2("gh", "repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner")
481
+ raise status.inspect unless status.success?
482
+ output.chomp.split("/", 2)
483
+ end
484
+ owner, name = @changelog_repository
485
+
486
+ query = <<~GRAPHQL
487
+ query($owner: String!, $name: String!) {
488
+ repository(owner: $owner, name: $name) {
489
+ #{body}
490
+ }
491
+ }
492
+ GRAPHQL
493
+
494
+ output, status = Open3.capture2(
495
+ "gh", "api", "graphql",
496
+ "-f", "query=#{query}",
497
+ "-f", "owner=#{owner}",
498
+ "-f", "name=#{name}",
499
+ binmode: true
500
+ )
501
+ raise status.inspect unless status.success?
502
+
503
+ # GitHub always answers in UTF-8, while the default external encoding follows the locale. Without
504
+ # this, a pull request body with an emoji fails to parse under `LANG=C`, as in GitHub Actions.
505
+ JSON.parse(output.force_encoding(Encoding::UTF_8), symbolize_names: true).dig(:data, :repository)
506
+ end
507
+
508
+ # Lists the commits between `from` and `HEAD`, newest first.
509
+ #
510
+ # Giving `paths` limits the commits to the ones touching the paths.
511
+ #
512
+ def changelog_commits(from, paths: [])
513
+ require "open3"
514
+
515
+ command = ["git", "log", "--format=%H", "#{from}..HEAD"]
516
+ # `--simplify-merges` keeps the default history simplification from following only one parent of
517
+ # a merge commit, which can drop the other side. Note that `--full-history` alone is wrong here:
518
+ # it also lists merge commits that do not touch the paths, bringing back the excluded pull
519
+ # requests. The two flags produce the same commits as the default mode for this repository today.
520
+ command.push("--full-history", "--simplify-merges", "--", *paths) unless paths.empty?
521
+
522
+ output, status = Open3.capture2(*command)
523
+ raise status.inspect unless status.success?
524
+
525
+ output.lines.map(&:chomp).reject(&:empty?)
526
+ end
527
+
528
+ # What `git cherry-pick -x` appends to the message of the commit it creates.
529
+ CHERRY_PICK_ORIGIN = /^\(cherry picked from commit ([0-9a-f]{40})\)$/
530
+
531
+ # Maps the commits that record where they were cherry-picked from to that commit.
532
+ #
533
+ # A backport is a cherry-pick, so on a release branch it is the recorded origin, not the commit
534
+ # itself, that leads to the pull request the change was written and reviewed in. Without this a
535
+ # backported change is attributed to the pull request that carried the backport, which says
536
+ # nothing about the change and is the same for every commit it brought over.
537
+ #
538
+ # A commit backported twice -- the development line, then a release branch -- carries one line
539
+ # per hop, appended in order, so the first one is where the change started.
540
+ #
541
+ def changelog_origins(commits)
542
+ return {} if commits.empty?
543
+
544
+ require "open3"
545
+
546
+ # `--no-walk` prints these commits and nothing else. NUL delimiters keep a commit message --
547
+ # which can contain anything, including what this format looks like -- from being read as the
548
+ # format itself.
549
+ output, status = Open3.capture2("git", "log", "--no-walk", "--format=%H%x00%B%x00", *commits, binmode: true)
550
+ raise status.inspect unless status.success?
551
+
552
+ # Commit messages are UTF-8, while the default external encoding follows the locale. Without
553
+ # this, splitting a message that is not ASCII fails under `LANG=C`, as in GitHub Actions.
554
+ output.force_encoding(Encoding::UTF_8)
555
+
556
+ output.split("\0").each_slice(2).each_with_object({}) do |(commit, message), origins|
557
+ commit = commit.to_s.strip
558
+ next if commit.empty?
559
+
560
+ origin = message.to_s[CHERRY_PICK_ORIGIN, 1] or next
561
+ origins[commit] = origin
562
+ end
563
+ end
564
+
565
+ # Asks GitHub which pull requests each commit came from, so that any merge strategy -- merge
566
+ # commit, squash, or rebase -- is handled without parsing commit messages.
567
+ #
568
+ # Returns `{ oid => [pull request, ...] }`, with an empty array for the commits GitHub has no
569
+ # merged pull request for, including the ones it does not know at all.
570
+ #
571
+ def changelog_associated_pull_requests(oids)
572
+ oids.uniq.each_slice(50).each_with_object({}) do |slice, found|
573
+ aliases = slice.map.with_index do |oid, index|
574
+ <<~GRAPHQL
575
+ c#{index}: object(oid: "#{oid}") {
576
+ ... on Commit {
577
+ associatedPullRequests(first: 10) {
578
+ nodes {
579
+ number title url merged
580
+ labels(first: 100) { nodes { name } }
581
+ }
582
+ }
583
+ }
584
+ }
585
+ GRAPHQL
586
+ end
587
+
588
+ response = changelog_graphql(aliases.join("\n"))
589
+
590
+ slice.each_with_index do |oid, index|
591
+ nodes = response.dig(:"c#{index}", :associatedPullRequests, :nodes) || []
592
+
593
+ found[oid] = nodes.select { |pr| pr[:merged] }.map do |pr|
594
+ { number: pr[:number], title: pr[:title], url: pr[:url], labels: pr.dig(:labels, :nodes).map { |label| label[:name] } }
595
+ end
596
+ end
597
+ end
598
+ end
599
+
600
+ # Finds the pull requests the commits came from, keeping the order of `commits`.
601
+ #
602
+ # Returns the pull requests for the changelog and the ones omitted by `skip_labels`.
603
+ #
604
+ def changelog_pull_requests(commits, skip_labels: CHANGELOG_SKIP_LABELS)
605
+ origins = changelog_origins(commits)
606
+
607
+ # Both ends of each commit, in one query: the origin a cherry-pick records, which is where the
608
+ # change was written and reviewed, and the commit as it sits in this history, whose pull request
609
+ # is the backport that brought it here. On the development line there are no origins and the
610
+ # second end is the only one.
611
+ found = changelog_associated_pull_requests(commits + origins.values)
612
+
613
+ pull_requests = {}
614
+ skipped = {}
615
+
616
+ commits.each do |commit|
617
+ carriers = found.fetch(commit, [])
618
+ prs = origins[commit] ? found.fetch(origins[commit], []) : []
619
+
620
+ # An origin that leads nowhere -- a commit cherry-picked from a fork, or one that went to the
621
+ # default branch without a pull request -- falls back to the commit in this history, which is
622
+ # at least the backport that brought it here. It is the entry itself then, not an annotation.
623
+ backports = prs.empty? ? [] : carriers
624
+ prs = carriers if prs.empty?
625
+
626
+ prs.each do |pr|
627
+ bucket = (pr[:labels] & skip_labels).empty? ? pull_requests : skipped
628
+ entry = (bucket[pr[:number]] ||= pr.merge(backports: []))
629
+ entry[:backports] |= backports.map { |backport| backport.slice(:number, :url) }
630
+ end
631
+ end
632
+
633
+ # A backport is how a change reached this line, not a change of its own, so it belongs in the
634
+ # entries it carried rather than beside them. It can still arrive as one: the merge commit of the
635
+ # backport is in the history too, and carries no `-x` trailer to resolve, so it looks like an
636
+ # ordinary commit of its own pull request. Drop the ones that annotate something.
637
+ carried = (pull_requests.each_value.to_a + skipped.each_value.to_a).flat_map { |pr| pr[:backports] }
638
+ carried = carried.map { |backport| backport[:number] }.uniq
639
+ carried.each { |number| pull_requests.delete(number) }
640
+
641
+ [pull_requests.values, skipped.values]
642
+ end
643
+
644
+ # Fetches the details that help classifying the pull requests: the changed files and the body.
645
+ #
646
+ def changelog_pull_request_details(pull_requests)
647
+ pull_requests.each_slice(50).flat_map do |slice|
648
+ aliases = slice.map do |pr|
649
+ <<~GRAPHQL
650
+ p#{pr[:number]}: pullRequest(number: #{pr[:number]}) {
651
+ body
652
+ author { login }
653
+ files(first: 100) {
654
+ nodes { path }
655
+ pageInfo { hasNextPage }
656
+ }
657
+ }
658
+ GRAPHQL
659
+ end
660
+
661
+ details = changelog_graphql(aliases.join("\n"))
662
+
663
+ slice.map do |pr|
664
+ detail = details[:"p#{pr[:number]}"] or next pr
665
+
666
+ pr.merge(
667
+ author: detail.dig(:author, :login),
668
+ # The body is a hint for writing the changelog, not a copy source. Keep it short.
669
+ body: detail[:body].to_s.strip.slice(0, 1000),
670
+ files: detail.dig(:files, :nodes).map { |file| file[:path] },
671
+ files_truncated: detail.dig(:files, :pageInfo, :hasNextPage)
672
+ )
673
+ end
674
+ end
675
+ end
676
+
677
+ # Reports the pull requests omitted by their label, so that they do not disappear silently.
678
+ #
679
+ def warn_skipped_pull_requests(skipped, skip_labels)
680
+ return if skipped.empty?
681
+
682
+ numbers = skipped.map { |pr| "##{pr[:number]}" }
683
+ numbers = numbers.take(20).push("and #{numbers.size - 20} more") if numbers.size > 20
684
+
685
+ $stderr.puts
686
+ $stderr.puts " (⏭️ Skipped #{skipped.size} pull request(s) labeled #{skip_labels.map { |label| "`#{label}`" }.join(" or ")}: #{numbers.join(", ")})"
687
+ end
688
+
689
+ # The links of one changelog entry: the pull request the change was written in, and on a release
690
+ # branch the backport that carried it onto the line.
691
+ #
692
+ # The second link is what keeps a backported entry from reading as a mistake. Its first link is a
693
+ # pull request against the development line, so the same link is listed again when that line ships,
694
+ # and nothing else tells the two occurrences apart.
695
+ #
696
+ def changelog_links(pr)
697
+ links = ["[##{pr[:number]}](#{pr[:url]})"]
698
+ links.concat(pr[:backports].to_a.map { |backport| "Backported in [##{backport[:number]}](#{backport[:url]})" })
699
+ links.join(", ")
700
+ end
701
+
702
+ # Prints the changelog template listing the pull requests merged between `from` and `HEAD`.
703
+ #
704
+ # The changelog goes to STDOUT and everything else goes to STDERR, so that the output can be
705
+ # piped to another command: `rake gem:changelog | pbcopy`
706
+ #
707
+ def print_changelog(from, paths: [], skip_labels: CHANGELOG_SKIP_LABELS)
708
+ $stderr.puts "🔍 Finding pull requests merged between `#{from}` and `HEAD`..."
709
+
710
+ commits = changelog_commits(from, paths: paths)
711
+ if commits.empty?
712
+ $stderr.puts " (🤔 There is no commit after `#{from}`.)"
713
+ return
714
+ end
715
+
716
+ pull_requests, skipped = changelog_pull_requests(commits, skip_labels: skip_labels)
717
+
718
+ if pull_requests.empty?
719
+ $stderr.puts " (🤔 No pull request is associated to the commits after `#{from}`.)"
720
+ else
721
+ $stderr.puts
722
+ pull_requests.each do |pr|
723
+ puts "* #{pr[:title]} (#{changelog_links(pr)})"
724
+ end
725
+ $stdout.flush
726
+ end
727
+
728
+ warn_skipped_pull_requests(skipped, skip_labels)
729
+ end
730
+
731
+ # Prints the same pull requests as `print_changelog` as JSON, with the details that help
732
+ # classifying them into the sections of CHANGELOG.md.
733
+ #
734
+ # This is the input for the release automation, so it always prints a valid JSON document.
735
+ #
736
+ def print_changelog_json(from, paths: [], skip_labels: CHANGELOG_SKIP_LABELS)
737
+ require "json"
738
+
739
+ $stderr.puts "🔍 Finding pull requests merged between `#{from}` and `HEAD`..."
740
+
741
+ commits = changelog_commits(from, paths: paths)
742
+ pull_requests, skipped = changelog_pull_requests(commits, skip_labels: skip_labels)
743
+ pull_requests = changelog_pull_request_details(pull_requests)
744
+
745
+ $stderr.puts " (📋 #{pull_requests.size} pull request(s))"
746
+
747
+ puts JSON.pretty_generate(
748
+ {
749
+ from: from,
750
+ to: "HEAD",
751
+ pull_requests: pull_requests,
752
+ skipped: skipped
753
+ }
754
+ )
755
+ $stdout.flush
756
+
757
+ warn_skipped_pull_requests(skipped, skip_labels)
758
+ end
759
+
760
+ namespace :gem do
761
+ # The gem is developed in the whole repository except the Rust crate, which has its own release
762
+ # cycle. Note that this is an *exclusion*, not a list of the directories shipped in the gem:
763
+ # changes in `test/` or `.github/` are part of the gem's changelog too.
764
+ # A constant defined in a `namespace` block is a top-level constant, so it needs the prefix.
765
+ GEM_CHANGELOG_PATHS = [".", ":(exclude)rust"]
766
+
767
+ # The tags a release proper starts *after*, rather than at.
768
+ GEM_PRERELEASE_TAGS = ["v*.pre*", "v*.dev*"]
769
+
770
+ # Where the changelog of the release being prepared starts, derived from `RBS::VERSION`:
771
+ #
772
+ # * `X.Y.Z.pre.N` documents what changed since `X.Y.Z.pre.N-1`, so it starts from the latest tag.
773
+ # * `X.Y.Z` documents the whole cycle, the prereleases included, so it skips the prerelease tags
774
+ # in between and starts from the previous release proper.
775
+ #
776
+ # This is the step that is easy to get wrong by hand: on a release proper the latest tag is a
777
+ # prerelease, so the obvious default would produce only the tail of the cycle. Passing a version
778
+ # explicitly overrides all of it.
779
+ #
780
+ def changelog_base(version)
781
+ excluded = Gem::Version.new(RBS::VERSION).prerelease? ? [] : GEM_PRERELEASE_TAGS
782
+ resolve_changelog_base(version, tag_glob: "v*", exclude_globs: excluded)
783
+ end
336
784
 
337
- puts "🎉🎉🎉🎉 Congratulations for **#{version}** release! 🎉🎉🎉🎉"
338
- puts
339
- puts "There are a few things left to complete the release. 💪"
340
- puts
785
+ desc "Generate changelog template from GH pull requests merged since the previous release"
786
+ task :changelog, [:version] do |_task, args|
787
+ print_changelog(changelog_base(args[:version]), paths: GEM_CHANGELOG_PATHS)
788
+ end
341
789
 
342
- if patch == 0 || version.prerelease?
343
- puts "* [ ] Update release note: https://github.com/ruby/rbs/wiki/Release-Note-#{major}.#{minor}"
790
+ namespace :changelog do
791
+ desc "Print the pull requests of `gem:changelog` as JSON, with the changed files and body of each"
792
+ task :json, [:version] do |_task, args|
793
+ print_changelog_json(changelog_base(args[:version]), paths: GEM_CHANGELOG_PATHS)
344
794
  end
795
+ end
796
+
797
+ # There are three kinds of release: `X.Y.Z`, `X.Y.Z.pre.N`, and `X.Y.Z.dev.N`. The
798
+ # `.dev.N` ones are cut from the development line for people who need a specific
799
+ # change early; they are not written up in the changelog, so there are no notes to
800
+ # publish and nothing worth announcing.
801
+ def dev_release?(version)
802
+ Gem::Version.new(version).segments.include?("dev")
803
+ end
345
804
 
346
- if patch == 0 && !version.prerelease?
347
- puts "* [ ] Delete `RBS XYZ is the latest version of...` from release note: https://github.com/ruby/rbs/wiki/Release-Note-#{major}.#{minor}"
805
+ # The body of the topmost section of CHANGELOG.md, which is the release being
806
+ # prepared, minus its own heading.
807
+ #
808
+ # The encoding is explicit because the default external encoding follows the
809
+ # locale, and the changelog is not ASCII.
810
+ #
811
+ def changelog_section(version)
812
+ content = File.read(File.join(__dir__, "CHANGELOG.md"), encoding: Encoding::UTF_8)
813
+ section = content.scan(/^## \d.*?(?=^## \d)/m)[0] or raise "🚨 Cannot find a release section in CHANGELOG.md"
814
+ heading, _, body = section.partition("\n")
815
+ heading.include?(version) or raise "🚨 CHANGELOG.md starts with `#{heading.strip}`, which is not #{version}"
816
+ body.strip
817
+ end
818
+
819
+ desc "Check that the working tree is ready to be released as the given version"
820
+ task :check_release, [:version] do |_task, args|
821
+ version = args[:version] or raise "🚨 Pass the version being released: `rake 'gem:check_release[4.1.2]'`"
822
+ Gem::Version.correct?(version) or raise "🚨 `#{version}` is not a version number."
823
+
824
+ # The version being released and the version the commit declares are stated
825
+ # separately -- one by whoever starts the release, one by the commit itself --
826
+ # so that releasing the wrong commit, or releasing the right one under the wrong
827
+ # name, fails here rather than on RubyGems.
828
+ version == RBS::VERSION or
829
+ raise "🚨 Releasing #{version}, but this commit declares `RBS::VERSION = #{RBS::VERSION.inspect}`."
830
+
831
+ if dev_release?(version)
832
+ puts "✅ #{version} is the version of this commit. It is a dev release, so CHANGELOG.md is not checked."
833
+ else
834
+ changelog_section(version)
835
+ puts "✅ #{version} is the version of this commit, and CHANGELOG.md documents it."
348
836
  end
837
+ end
838
+
839
+ desc "Create and push the `vX.Y.Z` tag for RBS::VERSION"
840
+ task :tag do
841
+ tag = "v#{RBS::VERSION}"
842
+
843
+ # Annotated, so that the tag carries its own author and date rather than
844
+ # borrowing the tagged commit's.
845
+ sh "git", "tag", "--annotate", "--message", "RBS #{RBS::VERSION}", tag
846
+ sh "git", "push", "origin", tag
349
847
 
350
- puts "* [ ] Publish a release at GitHub"
351
- puts "* [ ] Make some announcements on Twitter/Mustdon/Slack/???"
848
+ puts "🏷️ Pushed #{tag}."
849
+ end
352
850
 
353
- puts
354
- puts
851
+ desc "Publish the GitHub release for RBS::VERSION, unless it is a `.dev.` version"
852
+ task :gh_release do
853
+ require "open3"
355
854
 
356
- puts "✏️ Making a draft release on GitHub..."
855
+ version = Gem::Version.new(RBS::VERSION)
856
+ major, minor, *_ = RBS::VERSION.split(".")
857
+ tag = "v#{RBS::VERSION}"
357
858
 
358
- content = File.read(File.join(__dir__, "CHANGELOG.md"))
359
- changelog = content.scan(/^## \d.*?(?=^## \d)/m)[0]
360
- changelog = changelog.sub(/^.*\n^.*\n/, "").rstrip
859
+ if dev_release?(RBS::VERSION)
860
+ puts "⏭️ #{RBS::VERSION} is a dev release, so there is no GitHub release to publish."
861
+ next
862
+ end
361
863
 
362
- notes = <<NOTES
363
- [Release note](https://github.com/ruby/rbs/wiki/Release-Note-#{major}.#{minor})
864
+ # The release is created against an existing tag, so that the artifacts and the
865
+ # notes describe a commit that is already immutable.
866
+ _, status = Open3.capture2("git", "rev-parse", "--verify", "--quiet", "#{tag}^{commit}")
867
+ raise "🚨 No such tag: `#{tag}`. Tag the release before creating the GitHub release." unless status.success?
364
868
 
365
- #{changelog}
366
- NOTES
869
+ notes = <<~NOTES
870
+ [Release note](https://github.com/ruby/rbs/wiki/Release-Note-#{major}.#{minor})
367
871
 
872
+ #{changelog_section(RBS::VERSION)}
873
+ NOTES
874
+
875
+ # Published rather than drafted: the notes are the changelog section that was
876
+ # already reviewed in the release pull request, so there is nothing left to edit.
368
877
  command = [
369
- "gh",
370
- "release",
371
- "create",
372
- "--draft",
373
- "v#{RBS::VERSION}",
878
+ "gh", "release", "create", tag,
374
879
  "--title=#{RBS::VERSION}",
375
880
  "--notes=#{notes}"
376
881
  ]
882
+ command << "--prerelease" if version.prerelease?
883
+
884
+ output, status = Open3.capture2(*command)
885
+ raise "🚨 `gh release create` failed: #{status.inspect}" unless status.success?
377
886
 
378
- if version.prerelease?
379
- command << "--prerelease"
887
+ puts "📝 Released #{tag}: #{output.chomp}"
888
+ end
889
+ end
890
+
891
+ desc "Compile extension without C23 extensions"
892
+ task :compile_c99 do
893
+ ENV["TEST_NO_C23"] = "true"
894
+ Rake::Task[:"compile"].invoke
895
+ ensure
896
+ ENV.delete("TEST_NO_C23")
897
+ end
898
+
899
+ task :prepare_bench do
900
+ ENV.delete("DEBUG")
901
+ Rake::Task[:"clobber"].invoke
902
+ Rake::Task[:"templates"].invoke
903
+ Rake::Task[:"compile"].invoke
904
+ end
905
+
906
+ task :prepare_profiling do
907
+ ENV["DEBUG"] = "1"
908
+ Rake::Task[:"clobber"].invoke
909
+ Rake::Task[:"templates"].invoke
910
+ Rake::Task[:"compile"].invoke
911
+ end
912
+
913
+ namespace :wasm do
914
+ WASM_DIR = File.expand_path("wasm", __dir__)
915
+ WASM_OUTPUT = File.join(WASM_DIR, "rbs_parser.wasm")
916
+
917
+ # The parser under src/ is plain, self-contained C with no dependency on the
918
+ # Ruby C API, so it can be compiled to WebAssembly as-is. The only extra
919
+ # translation unit is the entry-point shim under wasm/.
920
+ def wasm_source_files
921
+ Dir.glob(File.join(__dir__, "src/**/*.c")).sort + [File.join(WASM_DIR, "rbs_wasm.c")]
922
+ end
923
+
924
+ # Locate the clang shipped with the WASI SDK.
925
+ #
926
+ # The system clang can target wasm32, but the WASI SDK additionally provides
927
+ # the wasi-libc sysroot and the wasm32 compiler-rt builtins that the link
928
+ # step needs, so we require it explicitly.
929
+ def wasi_clang
930
+ sdk = ENV["WASI_SDK_PATH"]
931
+ if sdk.nil? || sdk.empty?
932
+ raise <<~MSG
933
+ WASI_SDK_PATH is not set.
934
+
935
+ Install the WASI SDK from https://github.com/WebAssembly/wasi-sdk/releases
936
+ and point WASI_SDK_PATH at the extracted directory, for example:
937
+
938
+ export WASI_SDK_PATH=/opt/wasi-sdk
939
+ rake wasm:build
940
+ MSG
380
941
  end
381
942
 
382
- require "open3"
383
- output, status = Open3.capture2(*command)
384
- if status.success?
385
- puts " >> Done! Open #{output.chomp} and publish the release!"
943
+ clang = File.join(sdk, "bin", "clang")
944
+ raise "clang not found at #{clang} (is WASI_SDK_PATH correct?)" unless File.executable?(clang)
945
+
946
+ clang
947
+ end
948
+
949
+ desc "Build the RBS parser as a WebAssembly module (requires WASI_SDK_PATH)"
950
+ task :build do
951
+ # `-DNDEBUG` compiles out `RBS_ASSERT`, the same way ext/rbs_extension does
952
+ # for the MRI extension. The assertions sit in the lexer and the constant
953
+ # pool, so keeping them costs about 20% of parse time; set `DEBUG=1` to keep
954
+ # them when debugging the module itself.
955
+ debug_flags = ENV["DEBUG"] ? [] : ["-DNDEBUG"]
956
+
957
+ mkdir_p WASM_DIR
958
+ sh wasi_clang,
959
+ "--target=wasm32-wasip1",
960
+ # No `main`; the host calls `_initialize` and then the exported functions.
961
+ "-mexec-model=reactor",
962
+ "-std=gnu11",
963
+ "-O2",
964
+ *debug_flags,
965
+ "-Wno-unused-parameter",
966
+ "-I#{File.join(__dir__, "include")}",
967
+ "-o", WASM_OUTPUT,
968
+ *wasm_source_files
969
+ puts "Built #{WASM_OUTPUT}"
970
+ end
971
+
972
+ desc "Build and smoke-test the WebAssembly module (requires wasmtime)"
973
+ task :check => :build do
974
+ wasmtime = ENV["WASMTIME"] || "wasmtime"
975
+
976
+ # `rbs_wasm_selftest` parses a small fixed signature and returns 1 on
977
+ # success. `--invoke` prints the return value to stdout.
978
+ output = IO.popen([wasmtime, "run", "--invoke", "rbs_wasm_selftest", WASM_OUTPUT], err: File::NULL, &:read).to_s.strip
979
+
980
+ if output == "1"
981
+ puts "WebAssembly selftest passed."
982
+ else
983
+ raise "WebAssembly selftest failed: rbs_wasm_selftest returned #{output.inspect} (expected \"1\")"
386
984
  end
387
985
  end
986
+
987
+ # Where the runtime looks for the module by default (see RBS::WASM::Runtime).
988
+ JRUBY_WASM_DIR = File.expand_path("lib/rbs/wasm", __dir__)
989
+
990
+ desc "Download the Chicory/ASM jars into the local Maven repository (~/.m2). Run on JRuby."
991
+ task :install_jars do
992
+ # Resolves the `jar` requirements from rbs.gemspec via Maven and downloads
993
+ # them (and their transitive deps) into ~/.m2, the same way `gem install`
994
+ # does; the jars are not copied into the gem. The platform is forced to java
995
+ # because Jars::Installer skips non-java gems, and write_require_file is false
996
+ # because lib/rbs_jars.rb is hand-maintained (the generator mangles the
997
+ # `com.dylibso.chicory:runtime` artifact id).
998
+ require "jars/installer"
999
+ spec = Gem::Specification.load("rbs.gemspec")
1000
+ spec.platform = "java"
1001
+ Jars::Installer.new(spec).install_jars(write_require_file: false)
1002
+ end
1003
+
1004
+ desc "Build rbs_parser.wasm and copy it next to RBS::WASM::Runtime"
1005
+ task :jruby_setup => [:build] do
1006
+ cp WASM_OUTPUT, File.join(JRUBY_WASM_DIR, "rbs_parser.wasm")
1007
+ puts "rbs_parser.wasm is ready under #{JRUBY_WASM_DIR}"
1008
+ end
388
1009
  end
389
1010
 
1011
+ namespace :rust do
1012
+ namespace :rbs do
1013
+ RUST_DIR = File.expand_path("rust", __dir__)
1014
+ RBS_VERSION_FILE = File.join(RUST_DIR, "rbs_version")
390
1015
 
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
1016
+ VENDOR_TARGETS = {
1017
+ "ruby-rbs-sys" => %w[include src],
1018
+ "ruby-rbs" => %w[config.yml],
1019
+ }
397
1020
 
398
- if patch == 0
399
- milestone = "RBS #{major}.#{minor}"
400
- else
401
- milestone = "RBS #{major}.#{minor}.x"
1021
+ desc "Sync vendored RBS source from the pinned version"
1022
+ task :sync do
1023
+ unless File.exist?(RBS_VERSION_FILE)
1024
+ raise "#{RBS_VERSION_FILE} not found. Run `rake rust:rbs:pin[VERSION]` first."
1025
+ end
1026
+
1027
+ version = File.read(RBS_VERSION_FILE).strip
1028
+ raise "#{RBS_VERSION_FILE} is empty" if version.empty?
1029
+
1030
+ puts "Syncing vendor/rbs/ from #{version}..."
1031
+
1032
+ VENDOR_TARGETS.each do |crate, entries|
1033
+ vendor_dir = File.join(RUST_DIR, crate, "vendor", "rbs")
1034
+
1035
+ puts " Copying files for #{crate}:"
1036
+ chmod_R "u+w", vendor_dir, verbose: false if File.exist?(vendor_dir)
1037
+ rm_rf vendor_dir, verbose: false
1038
+ mkdir_p vendor_dir, verbose: false
1039
+
1040
+ entries.each do |entry|
1041
+ target = File.join(vendor_dir, entry)
1042
+
1043
+ # Extract the entry from the pinned git tag using git archive
1044
+ IO.popen(["git", "archive", "--format=tar", version, "--", entry], "rb") do |tar|
1045
+ IO.popen(["tar", "xf", "-", "-C", vendor_dir], "wb") do |extract|
1046
+ IO.copy_stream(tar, extract)
1047
+ end
1048
+ end
1049
+
1050
+ raise "Failed to extract #{entry} from #{version}" unless File.exist?(target)
1051
+ puts " #{entry}"
1052
+ end
1053
+
1054
+ # Make files read-only to prevent accidental edits
1055
+ chmod_R "a-w", vendor_dir, verbose: false
1056
+ end
1057
+
1058
+ puts "📦 Synced vendor/rbs/ from #{version} (read-only)"
1059
+ end
1060
+
1061
+ desc "Pin a specific RBS version for Rust crates (e.g., rake rust:rbs:pin[v4.0.3])"
1062
+ task :pin, [:version] do |_t, args|
1063
+ version = args[:version] or raise "Usage: rake rust:rbs:pin[VERSION]"
1064
+
1065
+ # Verify the tag exists
1066
+ unless system("git", "rev-parse", "--verify", "#{version}^{commit}", out: File::NULL, err: File::NULL)
1067
+ raise "Tag #{version} not found"
1068
+ end
1069
+
1070
+ File.write(RBS_VERSION_FILE, "#{version}\n")
1071
+ puts "📌 Pinned RBS version to #{version}"
1072
+
1073
+ Rake::Task["rust:rbs:sync"].invoke
1074
+ end
1075
+
1076
+ desc "Create symlinks from vendor/rbs/ to the repository root (for development/CI)"
1077
+ task :symlink do
1078
+ VENDOR_TARGETS.each do |crate, entries|
1079
+ vendor_dir = File.join(RUST_DIR, crate, "vendor", "rbs")
1080
+
1081
+ puts "Setting up symlinks for #{crate}..."
1082
+ entries.each do |entry|
1083
+ puts " #{entry} -> repository root"
1084
+ end
1085
+
1086
+ chmod_R "u+w", vendor_dir, verbose: false if File.exist?(vendor_dir)
1087
+ rm_rf vendor_dir, verbose: false
1088
+ mkdir_p vendor_dir, verbose: false
1089
+
1090
+ entries.each do |entry|
1091
+ ln_s File.join("..", "..", "..", "..", entry), File.join(vendor_dir, entry), verbose: false
1092
+ end
1093
+ end
1094
+
1095
+ puts "🔗 Symlinked vendor/rbs/ to repository root"
1096
+ end
402
1097
  end
403
1098
 
404
- puts "🔍 Finding pull requests that is associated to milestone `#{milestone}`..."
1099
+ namespace :publish do
1100
+ def self.prepare_publish_branch(crate_name)
1101
+ dry_run = ENV["RBS_RUST_PUBLISH_DRY_RUN"]
405
1102
 
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
- ]
1103
+ version_file = File.join(RUST_DIR, "rbs_version")
416
1104
 
417
- require "open3"
418
- output, status = Open3.capture2(*command)
419
- raise status.inspect unless status.success?
1105
+ unless File.exist?(version_file)
1106
+ raise "#{version_file} not found. Run `rake rust:rbs:pin[VERSION]` first."
1107
+ end
420
1108
 
421
- require "json"
422
- json = JSON.parse(output, symbolize_names: true)
1109
+ rbs_version = File.read(version_file).strip
1110
+ raise "#{version_file} is empty" if rbs_version.empty?
1111
+
1112
+ crate_version = File.read(File.join(RUST_DIR, crate_name, "Cargo.toml"))[/^version\s*=\s*"(.+)"/, 1]
1113
+ release_branch = "rust/release-#{crate_name}-#{Time.now.strftime('%Y%m%d%H%M%S')}"
1114
+
1115
+ puts "=" * 60
1116
+ puts "Rust crate publish: #{crate_name}#{dry_run ? " (DRY RUN)" : ""}"
1117
+ puts "=" * 60
1118
+ puts " RBS source version: #{rbs_version}"
1119
+ puts " #{crate_name}: #{crate_version} (tag: #{crate_name}-v#{crate_version})"
1120
+ puts " Release branch: #{release_branch}"
1121
+ puts "=" * 60
1122
+
1123
+ # Check that vendor dirs contain real files, not symlinks
1124
+ entries = VENDOR_TARGETS.fetch(crate_name)
1125
+ entries.each do |entry|
1126
+ path = File.join(RUST_DIR, crate_name, "vendor", "rbs", entry)
1127
+ if File.symlink?(path)
1128
+ raise "#{path} is a symlink. Run `rake rust:rbs:sync` first."
1129
+ end
1130
+ unless File.exist?(path)
1131
+ raise "#{path} does not exist. Run `rake rust:rbs:sync` first."
1132
+ end
1133
+ end
423
1134
 
424
- unless json.empty?
425
- puts
426
- json.each do |line|
427
- puts "* #{line[:title]} ([##{line[:number]}](#{line[:url]}))"
1135
+ # Ensure working tree is clean before publishing
1136
+ unless `git status --porcelain`.strip.empty?
1137
+ raise "💢 Working tree is dirty. Please commit or stash your changes before publishing."
1138
+ end
1139
+
1140
+ # Create a release branch with vendor files committed
1141
+ original_branch = `git rev-parse --abbrev-ref HEAD`.strip
1142
+
1143
+ sh "git", "checkout", "-b", release_branch, verbose: false
1144
+ vendor_path = File.join("rust", crate_name, "vendor", "rbs")
1145
+ sh "git", "add", "-f", vendor_path, verbose: false
1146
+ sh "git", "commit", "-m", "Publish #{crate_name} (RBS #{rbs_version})", verbose: false
1147
+
1148
+ [dry_run, crate_version, original_branch]
1149
+ end
1150
+
1151
+ desc "Publish ruby-rbs-sys crate to crates.io (set RBS_RUST_PUBLISH_DRY_RUN=1 for dry-run only)"
1152
+ task :"ruby-rbs-sys" do
1153
+ crate_name = "ruby-rbs-sys"
1154
+ dry_run, crate_version, original_branch = prepare_publish_branch(crate_name)
1155
+
1156
+ begin
1157
+ puts "🔰 Dry-run publishing..."
1158
+
1159
+ Dir.chdir(File.join(RUST_DIR, crate_name)) do
1160
+ sh "cargo", "publish", "--dry-run"
1161
+ end
1162
+
1163
+ puts "✅ Dry-run succeeded!"
1164
+
1165
+ unless dry_run
1166
+ puts "💪 Publishing #{crate_name} for real..."
1167
+
1168
+ Dir.chdir(File.join(RUST_DIR, crate_name)) do
1169
+ sh "cargo", "publish"
1170
+ end
1171
+
1172
+ sh "git", "tag", "#{crate_name}-v#{crate_version}"
1173
+ sh "git", "push", "origin", "#{crate_name}-v#{crate_version}"
1174
+
1175
+ puts "🎉 Published #{crate_name} successfully!"
1176
+ end
1177
+ ensure
1178
+ sh "git", "checkout", original_branch, verbose: false
1179
+ end
1180
+ end
1181
+
1182
+ desc "Publish ruby-rbs crate to crates.io (set RBS_RUST_PUBLISH_DRY_RUN=1 for dry-run only)"
1183
+ task :"ruby-rbs" do
1184
+ crate_name = "ruby-rbs"
1185
+ dry_run, crate_version, original_branch = prepare_publish_branch(crate_name)
1186
+
1187
+ begin
1188
+ puts "🔰 Dry-run publishing..."
1189
+
1190
+ Dir.chdir(File.join(RUST_DIR, crate_name)) do
1191
+ sh "cargo", "publish", "--dry-run", "--no-verify"
1192
+ end
1193
+
1194
+ puts "✅ Dry-run succeeded!"
1195
+
1196
+ unless dry_run
1197
+ puts "💪 Publishing #{crate_name} for real..."
1198
+
1199
+ Dir.chdir(File.join(RUST_DIR, crate_name)) do
1200
+ sh "cargo", "publish"
1201
+ end
1202
+
1203
+ sh "git", "tag", "#{crate_name}-v#{crate_version}"
1204
+ sh "git", "push", "origin", "#{crate_name}-v#{crate_version}"
1205
+
1206
+ puts "🎉 Published #{crate_name} successfully!"
1207
+ end
1208
+ ensure
1209
+ sh "git", "checkout", original_branch, verbose: false
1210
+ end
428
1211
  end
429
- else
430
- puts " (🤑 There is no *unreleased* pull request associated to the milestone.)"
431
1212
  end
432
1213
  end