rigortype 0.2.7 → 0.2.8

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 (414) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/docs/manual/04-diagnostics.md +7 -4
  4. data/docs/manual/06-baseline.md +5 -2
  5. data/docs/manual/15-type-protection-coverage.md +6 -4
  6. data/lib/rigor/analysis/baseline.rb +56 -96
  7. data/lib/rigor/analysis/buffer_binding.rb +13 -20
  8. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +29 -48
  9. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +35 -54
  10. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +23 -37
  11. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +16 -23
  12. data/lib/rigor/analysis/check_rules/rule_walk.rb +48 -71
  13. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +19 -28
  14. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +48 -67
  15. data/lib/rigor/analysis/check_rules.rb +174 -274
  16. data/lib/rigor/analysis/dependency_recorder.rb +26 -38
  17. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +13 -25
  18. data/lib/rigor/analysis/dependency_source_inference/builder.rb +17 -30
  19. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +15 -26
  20. data/lib/rigor/analysis/dependency_source_inference/index.rb +45 -76
  21. data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +23 -40
  22. data/lib/rigor/analysis/dependency_source_inference/walker.rb +50 -83
  23. data/lib/rigor/analysis/dependency_source_inference.rb +6 -10
  24. data/lib/rigor/analysis/diagnostic.rb +48 -75
  25. data/lib/rigor/analysis/erb_template_detector.rb +13 -18
  26. data/lib/rigor/analysis/fact_store.rb +12 -22
  27. data/lib/rigor/analysis/incremental.rb +47 -63
  28. data/lib/rigor/analysis/incremental_session.rb +74 -100
  29. data/lib/rigor/analysis/project_scan.rb +12 -23
  30. data/lib/rigor/analysis/result.rb +3 -5
  31. data/lib/rigor/analysis/rule_catalog.rb +67 -95
  32. data/lib/rigor/analysis/run_stats.rb +38 -61
  33. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +101 -160
  34. data/lib/rigor/analysis/runner/pool_coordinator.rb +121 -188
  35. data/lib/rigor/analysis/runner/project_pre_passes.rb +62 -96
  36. data/lib/rigor/analysis/runner/run_snapshots.rb +12 -18
  37. data/lib/rigor/analysis/runner.rb +220 -333
  38. data/lib/rigor/analysis/self_call_resolution_recorder.rb +30 -43
  39. data/lib/rigor/analysis/worker_session.rb +64 -102
  40. data/lib/rigor/ast/type_node.rb +6 -8
  41. data/lib/rigor/ast.rb +6 -10
  42. data/lib/rigor/bleeding_edge.rb +27 -35
  43. data/lib/rigor/builtins/hkt_builtins.rb +7 -14
  44. data/lib/rigor/builtins/imported_refinements.rb +106 -149
  45. data/lib/rigor/builtins/predefined_constant_refinements.rb +51 -65
  46. data/lib/rigor/builtins/regex_refinement.rb +32 -43
  47. data/lib/rigor/builtins/static_return_refinements.rb +57 -81
  48. data/lib/rigor/cache/descriptor.rb +58 -94
  49. data/lib/rigor/cache/incremental_snapshot.rb +32 -48
  50. data/lib/rigor/cache/rbs_cache_producer.rb +10 -15
  51. data/lib/rigor/cache/rbs_class_ancestor_table.rb +10 -16
  52. data/lib/rigor/cache/rbs_class_type_param_names.rb +10 -16
  53. data/lib/rigor/cache/rbs_constant_table.rb +8 -13
  54. data/lib/rigor/cache/rbs_descriptor.rb +8 -13
  55. data/lib/rigor/cache/rbs_environment.rb +11 -19
  56. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +11 -19
  57. data/lib/rigor/cache/rbs_known_class_names.rb +8 -12
  58. data/lib/rigor/cache/store.rb +96 -151
  59. data/lib/rigor/cli/annotate_command.rb +51 -88
  60. data/lib/rigor/cli/baseline_command.rb +10 -17
  61. data/lib/rigor/cli/check_command.rb +121 -199
  62. data/lib/rigor/cli/check_runner_factory.rb +8 -14
  63. data/lib/rigor/cli/ci_detector.rb +9 -14
  64. data/lib/rigor/cli/command.rb +8 -14
  65. data/lib/rigor/cli/coverage_command.rb +19 -26
  66. data/lib/rigor/cli/coverage_mutation.rb +14 -19
  67. data/lib/rigor/cli/coverage_report.rb +0 -2
  68. data/lib/rigor/cli/coverage_scan.rb +7 -11
  69. data/lib/rigor/cli/diagnostic_formats.rb +43 -62
  70. data/lib/rigor/cli/diff_command.rb +10 -20
  71. data/lib/rigor/cli/docs_command.rb +26 -40
  72. data/lib/rigor/cli/doctor_command.rb +8 -9
  73. data/lib/rigor/cli/explain_command.rb +4 -7
  74. data/lib/rigor/cli/fused_protection_renderer.rb +4 -5
  75. data/lib/rigor/cli/fused_protection_report.rb +6 -8
  76. data/lib/rigor/cli/lsp_command.rb +11 -19
  77. data/lib/rigor/cli/mcp_command.rb +4 -6
  78. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -4
  79. data/lib/rigor/cli/mutation_protection_report.rb +6 -9
  80. data/lib/rigor/cli/options.rb +11 -19
  81. data/lib/rigor/cli/plugin_command.rb +18 -30
  82. data/lib/rigor/cli/plugins_command.rb +29 -51
  83. data/lib/rigor/cli/plugins_renderer.rb +12 -20
  84. data/lib/rigor/cli/prism_colorizer.rb +13 -19
  85. data/lib/rigor/cli/protection_renderer.rb +5 -7
  86. data/lib/rigor/cli/protection_report.rb +27 -17
  87. data/lib/rigor/cli/renderable.rb +4 -6
  88. data/lib/rigor/cli/show_bleedingedge_command.rb +5 -8
  89. data/lib/rigor/cli/sig_gen_command.rb +14 -26
  90. data/lib/rigor/cli/skill_command.rb +30 -47
  91. data/lib/rigor/cli/skill_describe.rb +40 -64
  92. data/lib/rigor/cli/trace_command.rb +9 -16
  93. data/lib/rigor/cli/trace_renderer.rb +35 -51
  94. data/lib/rigor/cli/triage_command.rb +6 -10
  95. data/lib/rigor/cli/triage_renderer.rb +1 -2
  96. data/lib/rigor/cli/type_of_command.rb +14 -23
  97. data/lib/rigor/cli/type_of_renderer.rb +3 -5
  98. data/lib/rigor/cli/type_scan_command.rb +6 -10
  99. data/lib/rigor/cli/type_scan_renderer.rb +3 -5
  100. data/lib/rigor/cli/type_scan_report.rb +2 -3
  101. data/lib/rigor/cli/upgrade_command.rb +2 -3
  102. data/lib/rigor/cli.rb +16 -28
  103. data/lib/rigor/config_audit.rb +35 -44
  104. data/lib/rigor/configuration/dependencies.rb +41 -70
  105. data/lib/rigor/configuration/severity_profile.rb +28 -42
  106. data/lib/rigor/configuration.rb +150 -241
  107. data/lib/rigor/environment/bundle_sig_discovery.rb +74 -118
  108. data/lib/rigor/environment/class_registry.rb +19 -26
  109. data/lib/rigor/environment/constant_type_cache_holder.rb +14 -21
  110. data/lib/rigor/environment/hkt_registry_holder.rb +6 -12
  111. data/lib/rigor/environment/lockfile_resolver.rb +31 -47
  112. data/lib/rigor/environment/rbs_collection_discovery.rb +39 -63
  113. data/lib/rigor/environment/rbs_coverage_report.rb +24 -42
  114. data/lib/rigor/environment/rbs_loader.rb +420 -518
  115. data/lib/rigor/environment/reflection.rb +28 -53
  116. data/lib/rigor/environment/reporters.rb +13 -24
  117. data/lib/rigor/environment.rb +176 -291
  118. data/lib/rigor/flow_contribution/conflict.rb +16 -27
  119. data/lib/rigor/flow_contribution/element.rb +7 -12
  120. data/lib/rigor/flow_contribution/fact.rb +33 -57
  121. data/lib/rigor/flow_contribution/merge_result.rb +6 -9
  122. data/lib/rigor/flow_contribution/merger.rb +32 -47
  123. data/lib/rigor/flow_contribution.rb +37 -55
  124. data/lib/rigor/inference/acceptance.rb +133 -219
  125. data/lib/rigor/inference/block_parameter_binder.rb +52 -82
  126. data/lib/rigor/inference/body_fixpoint.rb +22 -30
  127. data/lib/rigor/inference/budget_trace.rb +43 -63
  128. data/lib/rigor/inference/builtins/array_catalog.rb +4 -6
  129. data/lib/rigor/inference/builtins/comparable_catalog.rb +4 -6
  130. data/lib/rigor/inference/builtins/complex_catalog.rb +13 -19
  131. data/lib/rigor/inference/builtins/date_catalog.rb +38 -67
  132. data/lib/rigor/inference/builtins/encoding_catalog.rb +22 -36
  133. data/lib/rigor/inference/builtins/enumerable_catalog.rb +4 -6
  134. data/lib/rigor/inference/builtins/exception_catalog.rb +38 -57
  135. data/lib/rigor/inference/builtins/hash_catalog.rb +9 -14
  136. data/lib/rigor/inference/builtins/method_catalog.rb +29 -48
  137. data/lib/rigor/inference/builtins/numeric_catalog.rb +13 -18
  138. data/lib/rigor/inference/builtins/pathname_catalog.rb +8 -13
  139. data/lib/rigor/inference/builtins/proc_catalog.rb +38 -62
  140. data/lib/rigor/inference/builtins/random_catalog.rb +21 -32
  141. data/lib/rigor/inference/builtins/range_catalog.rb +12 -21
  142. data/lib/rigor/inference/builtins/rational_catalog.rb +9 -15
  143. data/lib/rigor/inference/builtins/re_catalog.rb +29 -48
  144. data/lib/rigor/inference/builtins/set_catalog.rb +18 -27
  145. data/lib/rigor/inference/builtins/string_catalog.rb +12 -19
  146. data/lib/rigor/inference/builtins/struct_catalog.rb +18 -30
  147. data/lib/rigor/inference/builtins/time_catalog.rb +28 -44
  148. data/lib/rigor/inference/closure_escape_analyzer.rb +44 -67
  149. data/lib/rigor/inference/coverage_scanner.rb +13 -18
  150. data/lib/rigor/inference/def_return_typer.rb +8 -14
  151. data/lib/rigor/inference/dynamic_origin.rb +23 -20
  152. data/lib/rigor/inference/expression_typer.rb +774 -1152
  153. data/lib/rigor/inference/fallback.rb +8 -12
  154. data/lib/rigor/inference/fallback_tracer.rb +4 -10
  155. data/lib/rigor/inference/flow_tracer.rb +25 -36
  156. data/lib/rigor/inference/hkt_body.rb +45 -68
  157. data/lib/rigor/inference/hkt_body_parser.rb +33 -52
  158. data/lib/rigor/inference/hkt_reducer.rb +38 -59
  159. data/lib/rigor/inference/hkt_registry.rb +50 -76
  160. data/lib/rigor/inference/indexed_narrowing.rb +55 -81
  161. data/lib/rigor/inference/macro_block_self_type.rb +21 -34
  162. data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +11 -18
  163. data/lib/rigor/inference/method_dispatcher/block_folding.rb +49 -83
  164. data/lib/rigor/inference/method_dispatcher/call_context.rb +16 -23
  165. data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +13 -22
  166. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +307 -454
  167. data/lib/rigor/inference/method_dispatcher/data_folding.rb +24 -34
  168. data/lib/rigor/inference/method_dispatcher/file_folding.rb +22 -37
  169. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +52 -81
  170. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +51 -78
  171. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +60 -97
  172. data/lib/rigor/inference/method_dispatcher/math_folding.rb +23 -34
  173. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +18 -26
  174. data/lib/rigor/inference/method_dispatcher/method_folding.rb +37 -71
  175. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +114 -182
  176. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +122 -193
  177. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +15 -24
  178. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +64 -93
  179. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +31 -45
  180. data/lib/rigor/inference/method_dispatcher/set_folding.rb +6 -9
  181. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +356 -511
  182. data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +22 -32
  183. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +15 -21
  184. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +66 -82
  185. data/lib/rigor/inference/method_dispatcher/time_folding.rb +10 -15
  186. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +9 -13
  187. data/lib/rigor/inference/method_dispatcher.rb +378 -524
  188. data/lib/rigor/inference/method_parameter_binder.rb +81 -114
  189. data/lib/rigor/inference/multi_target_binder.rb +51 -68
  190. data/lib/rigor/inference/mutation_widening.rb +115 -158
  191. data/lib/rigor/inference/narrowing.rb +509 -727
  192. data/lib/rigor/inference/origin_lookup.rb +38 -0
  193. data/lib/rigor/inference/parameter_inference_collector.rb +100 -100
  194. data/lib/rigor/inference/precision_scanner.rb +26 -35
  195. data/lib/rigor/inference/project_patched_methods.rb +20 -32
  196. data/lib/rigor/inference/project_patched_scanner.rb +23 -37
  197. data/lib/rigor/inference/protection_scanner.rb +16 -18
  198. data/lib/rigor/inference/rbs_type_translator.rb +49 -66
  199. data/lib/rigor/inference/scope_indexer.rb +473 -817
  200. data/lib/rigor/inference/statement_evaluator.rb +706 -1111
  201. data/lib/rigor/inference/struct_fold_safety.rb +34 -48
  202. data/lib/rigor/inference/synthetic_method.rb +9 -16
  203. data/lib/rigor/inference/synthetic_method_index.rb +20 -35
  204. data/lib/rigor/inference/synthetic_method_scanner.rb +63 -103
  205. data/lib/rigor/language_server/buffer_resolution.rb +6 -9
  206. data/lib/rigor/language_server/buffer_table.rb +11 -18
  207. data/lib/rigor/language_server/completion_provider.rb +69 -116
  208. data/lib/rigor/language_server/debouncer.rb +16 -25
  209. data/lib/rigor/language_server/diagnostic_publisher.rb +26 -40
  210. data/lib/rigor/language_server/document_symbol_provider.rb +14 -19
  211. data/lib/rigor/language_server/folding_range_provider.rb +11 -19
  212. data/lib/rigor/language_server/hover_provider.rb +8 -11
  213. data/lib/rigor/language_server/hover_renderer.rb +56 -93
  214. data/lib/rigor/language_server/loop.rb +14 -19
  215. data/lib/rigor/language_server/project_context.rb +46 -74
  216. data/lib/rigor/language_server/selection_range_provider.rb +9 -12
  217. data/lib/rigor/language_server/server.rb +55 -83
  218. data/lib/rigor/language_server/signature_help_provider.rb +29 -46
  219. data/lib/rigor/language_server/synchronized_writer.rb +4 -7
  220. data/lib/rigor/language_server/uri.rb +8 -13
  221. data/lib/rigor/language_server.rb +4 -6
  222. data/lib/rigor/mcp/loop.rb +2 -3
  223. data/lib/rigor/mcp/server.rb +4 -7
  224. data/lib/rigor/mcp.rb +3 -6
  225. data/lib/rigor/plugin/access_denied_error.rb +5 -8
  226. data/lib/rigor/plugin/additional_initializer.rb +21 -31
  227. data/lib/rigor/plugin/base.rb +335 -518
  228. data/lib/rigor/plugin/blueprint.rb +14 -23
  229. data/lib/rigor/plugin/box.rb +18 -29
  230. data/lib/rigor/plugin/fact_store.rb +16 -26
  231. data/lib/rigor/plugin/inflector.rb +37 -53
  232. data/lib/rigor/plugin/io_boundary.rb +33 -56
  233. data/lib/rigor/plugin/isolation.rb +42 -55
  234. data/lib/rigor/plugin/load_error.rb +10 -15
  235. data/lib/rigor/plugin/loader.rb +30 -49
  236. data/lib/rigor/plugin/macro/block_as_method.rb +20 -32
  237. data/lib/rigor/plugin/macro/heredoc_template.rb +35 -58
  238. data/lib/rigor/plugin/macro/nested_class_template.rb +22 -36
  239. data/lib/rigor/plugin/macro/trait_registry.rb +34 -51
  240. data/lib/rigor/plugin/macro.rb +10 -15
  241. data/lib/rigor/plugin/manifest.rb +85 -144
  242. data/lib/rigor/plugin/node_context.rb +14 -22
  243. data/lib/rigor/plugin/node_rule_walk.rb +49 -74
  244. data/lib/rigor/plugin/protocol_contract.rb +25 -39
  245. data/lib/rigor/plugin/registry.rb +132 -205
  246. data/lib/rigor/plugin/services.rb +21 -33
  247. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -19
  248. data/lib/rigor/plugin/trust_policy.rb +24 -38
  249. data/lib/rigor/plugin/type_node_resolver.rb +15 -24
  250. data/lib/rigor/protection/diagnostic_oracle.rb +11 -13
  251. data/lib/rigor/protection/mutation_scanner.rb +27 -35
  252. data/lib/rigor/protection/mutator.rb +50 -70
  253. data/lib/rigor/protection/test_suite_oracle.rb +20 -27
  254. data/lib/rigor/rbs_extended/conformance_checker.rb +48 -68
  255. data/lib/rigor/rbs_extended/hkt_directives.rb +28 -54
  256. data/lib/rigor/rbs_extended/reporter.rb +24 -40
  257. data/lib/rigor/rbs_extended.rb +107 -197
  258. data/lib/rigor/reflection.rb +68 -86
  259. data/lib/rigor/scope/discovery_index.rb +14 -19
  260. data/lib/rigor/scope.rb +255 -310
  261. data/lib/rigor/sig_gen/classification.rb +6 -10
  262. data/lib/rigor/sig_gen/generator.rb +197 -323
  263. data/lib/rigor/sig_gen/layout_index.rb +12 -20
  264. data/lib/rigor/sig_gen/method_candidate.rb +12 -17
  265. data/lib/rigor/sig_gen/observation_collector.rb +38 -70
  266. data/lib/rigor/sig_gen/observed_call.rb +13 -23
  267. data/lib/rigor/sig_gen/path_mapper.rb +17 -29
  268. data/lib/rigor/sig_gen/renderer.rb +7 -13
  269. data/lib/rigor/sig_gen/type_elaborator.rb +15 -28
  270. data/lib/rigor/sig_gen/write_result.rb +8 -16
  271. data/lib/rigor/sig_gen/writer.rb +95 -174
  272. data/lib/rigor/sig_gen.rb +3 -6
  273. data/lib/rigor/signature_path_audit.rb +24 -30
  274. data/lib/rigor/source/constant_path.rb +10 -14
  275. data/lib/rigor/source/literals.rb +31 -45
  276. data/lib/rigor/source/node_locator.rb +9 -11
  277. data/lib/rigor/source/node_walker.rb +9 -13
  278. data/lib/rigor/source.rb +3 -4
  279. data/lib/rigor/testing.rb +16 -20
  280. data/lib/rigor/triage/catalogue.rb +38 -62
  281. data/lib/rigor/triage.rb +31 -52
  282. data/lib/rigor/trinary.rb +9 -13
  283. data/lib/rigor/type/acceptance_router.rb +4 -6
  284. data/lib/rigor/type/accepts_result.rb +10 -14
  285. data/lib/rigor/type/app.rb +19 -27
  286. data/lib/rigor/type/bot.rb +4 -6
  287. data/lib/rigor/type/bound_method.rb +10 -15
  288. data/lib/rigor/type/combinator.rb +165 -257
  289. data/lib/rigor/type/constant.rb +23 -34
  290. data/lib/rigor/type/data_class.rb +10 -15
  291. data/lib/rigor/type/data_instance.rb +14 -20
  292. data/lib/rigor/type/difference.rb +21 -32
  293. data/lib/rigor/type/dynamic.rb +3 -5
  294. data/lib/rigor/type/hash_shape.rb +32 -18
  295. data/lib/rigor/type/integer_range.rb +11 -16
  296. data/lib/rigor/type/intersection.rb +27 -42
  297. data/lib/rigor/type/nominal.rb +10 -15
  298. data/lib/rigor/type/plain_lattice.rb +9 -13
  299. data/lib/rigor/type/refined.rb +67 -114
  300. data/lib/rigor/type/singleton.rb +4 -6
  301. data/lib/rigor/type/struct_class.rb +11 -16
  302. data/lib/rigor/type/struct_instance.rb +15 -21
  303. data/lib/rigor/type/tuple.rb +14 -19
  304. data/lib/rigor/type/union.rb +30 -42
  305. data/lib/rigor/type_node/generic.rb +14 -26
  306. data/lib/rigor/type_node/identifier.rb +12 -19
  307. data/lib/rigor/type_node.rb +3 -12
  308. data/lib/rigor/value_semantics.rb +16 -21
  309. data/lib/rigor/version.rb +1 -1
  310. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +18 -30
  311. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +16 -29
  312. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +12 -23
  313. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +22 -39
  314. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +26 -46
  315. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +51 -87
  316. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +10 -17
  317. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +26 -39
  318. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +118 -176
  319. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +30 -51
  320. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +57 -92
  321. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +71 -108
  322. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +13 -22
  323. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +15 -25
  324. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +8 -13
  325. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +16 -24
  326. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +30 -47
  327. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +79 -129
  328. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +36 -57
  329. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +27 -41
  330. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +16 -24
  331. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +148 -235
  332. data/plugins/rigor-activerecord/lib/rigor-activerecord.rb +3 -5
  333. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +8 -14
  334. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +9 -15
  335. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +4 -7
  336. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +23 -41
  337. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +3 -5
  338. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +6 -10
  339. data/plugins/rigor-activesupport-core-ext/lib/rigor-activesupport-core-ext.rb +8 -11
  340. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +30 -45
  341. data/plugins/rigor-devise/lib/rigor-devise.rb +3 -5
  342. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +43 -69
  343. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +26 -35
  344. data/plugins/rigor-dry-schema/lib/rigor-dry-schema.rb +3 -5
  345. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +30 -47
  346. data/plugins/rigor-dry-struct/lib/rigor-dry-struct.rb +3 -5
  347. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +67 -113
  348. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +29 -46
  349. data/plugins/rigor-dry-types/lib/rigor-dry-types.rb +3 -5
  350. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +13 -20
  351. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +14 -23
  352. data/plugins/rigor-dry-validation/lib/rigor-dry-validation.rb +2 -3
  353. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +22 -37
  354. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +28 -46
  355. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +13 -17
  356. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +30 -47
  357. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +52 -80
  358. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +21 -31
  359. data/plugins/rigor-graphql/lib/rigor-graphql.rb +3 -5
  360. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +11 -17
  361. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +20 -34
  362. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +45 -75
  363. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +22 -36
  364. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +22 -41
  365. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +17 -32
  366. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +6 -12
  367. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +4 -8
  368. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +17 -32
  369. data/plugins/rigor-rails/lib/rigor-rails.rb +11 -18
  370. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +58 -92
  371. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +20 -34
  372. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +20 -31
  373. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +49 -82
  374. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +55 -92
  375. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +51 -90
  376. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +17 -30
  377. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +38 -70
  378. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +42 -74
  379. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +280 -497
  380. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +54 -89
  381. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +32 -54
  382. data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +8 -13
  383. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +9 -16
  384. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +24 -39
  385. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +13 -22
  386. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +34 -56
  387. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +22 -42
  388. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +43 -75
  389. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +27 -39
  390. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +14 -19
  391. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +20 -34
  392. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +28 -47
  393. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +21 -37
  394. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +13 -22
  395. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +18 -31
  396. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +6 -10
  397. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +14 -29
  398. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +23 -41
  399. data/plugins/rigor-sinatra/lib/rigor-sinatra.rb +3 -5
  400. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +23 -38
  401. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +57 -96
  402. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +22 -35
  403. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +41 -69
  404. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +6 -10
  405. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +24 -41
  406. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +29 -50
  407. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +55 -88
  408. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +159 -262
  409. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +21 -29
  410. data/plugins/rigor-statesman/lib/rigor-statesman.rb +3 -5
  411. data/plugins/rigor-typescript-utility-types/lib/rigor/plugin/typescript_utility_types.rb +23 -37
  412. data/plugins/rigor-typescript-utility-types/lib/rigor-typescript-utility-types.rb +4 -6
  413. data/sig/rigor/scope.rbs +6 -0
  414. metadata +2 -1
@@ -6,63 +6,46 @@ require_relative "call_context"
6
6
  module Rigor
7
7
  module Inference
8
8
  module MethodDispatcher
9
- # Slice 5 phase 2 shape-aware dispatch tier. Sits between
10
- # {ConstantFolding} (which folds Constant-on-Constant arithmetic)
11
- # and {RbsDispatch} (which projects shape carriers to their
9
+ # Slice 5 phase 2 shape-aware dispatch tier. Sits between {ConstantFolding} (which folds
10
+ # Constant-on-Constant arithmetic) and {RbsDispatch} (which projects shape carriers to their
12
11
  # underlying nominal and resolves return types through RBS).
13
12
  #
14
- # The tier resolves a curated catalogue of element-access
15
- # methods on `Rigor::Type::Tuple` and `Rigor::Type::HashShape`
16
- # receivers, returning the *precise* member type rather than the
17
- # projected `Array#[]` / `Hash#fetch` result. When the dispatch
18
- # cannot prove which element will be returned (non-static key,
19
- # out-of-range index, multi-arg `dig`, ...) the tier returns
20
- # `nil` so the surrounding pipeline falls through to
21
- # {RbsDispatch} and the projection-based answer.
13
+ # The tier resolves a curated catalogue of element-access methods on `Rigor::Type::Tuple` and
14
+ # `Rigor::Type::HashShape` receivers, returning the *precise* member type rather than the projected
15
+ # `Array#[]` / `Hash#fetch` result. When the dispatch cannot prove which element will be returned
16
+ # (non-static key, out-of-range index, multi-arg `dig`, ...) the tier returns `nil` so the
17
+ # surrounding pipeline falls through to {RbsDispatch} and the projection-based answer.
22
18
  #
23
19
  # Catalogue (Slice 5 phase 2):
24
20
  #
25
- # - Tuple#`first`, Tuple#`last`, Tuple#`size`/`length`/`count`:
26
- # no-arg, no-block.
27
- # - Tuple#`[]`, Tuple#`fetch` with a single `Constant[Integer]`
28
- # argument inside the tuple's bounds (negative indices are
29
- # normalised by length). Tuple#`[]` also handles static
30
- # Range and start-length slices, returning a sliced Tuple or
31
- # `Constant[nil]` for statically nil slices.
32
- # - Tuple#`dig` with a chain of `Constant[Integer]` /
33
- # `Constant[Symbol|String]` arguments (Slice 5 phase 2 sub-
34
- # phase 2). Each step recurses through the resolved member; a
35
- # missing key/index along the chain collapses to `Constant[nil]`
36
- # so the carrier surfaces through downstream narrowing. A
37
- # non-shape intermediate falls through to the projection
38
- # answer.
21
+ # - Tuple#`first`, Tuple#`last`, Tuple#`size`/`length`/`count`: no-arg, no-block.
22
+ # - Tuple#`[]`, Tuple#`fetch` with a single `Constant[Integer]` argument inside the tuple's bounds
23
+ # (negative indices are normalised by length). Tuple#`[]` also handles static Range and
24
+ # start-length slices, returning a sliced Tuple or `Constant[nil]` for statically nil slices.
25
+ # - Tuple#`dig` with a chain of `Constant[Integer]` / `Constant[Symbol|String]` arguments (Slice 5
26
+ # phase 2 sub-phase 2). Each step recurses through the resolved member; a missing key/index along
27
+ # the chain collapses to `Constant[nil]` so the carrier surfaces through downstream narrowing. A
28
+ # non-shape intermediate falls through to the projection answer.
39
29
  # - HashShape#`size`/`length`: no-arg.
40
- # - HashShape#`[]`, HashShape#`fetch`, HashShape#`dig` with a
41
- # single `Constant[Symbol|String]` argument matching one of
42
- # the declared keys. `[]` and `dig` resolve missing keys to
43
- # `Constant[nil]`; `fetch` (no default, no block) falls through
44
- # on a miss because Ruby would raise `KeyError` and the
45
- # analyzer prefers the conservative projection answer.
46
- # - HashShape#`dig` with multi-arg chains (Slice 5 phase 2 sub-
47
- # phase 2). Same chaining semantics as Tuple#`dig`.
48
- # - HashShape#`values_at` with a list of `Constant[Symbol|String]`
49
- # arguments (Slice 5 phase 2 sub-phase 2). The result is a
50
- # `Tuple` whose elements are the per-key values
51
- # (`Constant[nil]` for missing keys, mirroring Ruby's runtime
52
- # behaviour).
30
+ # - HashShape#`[]`, HashShape#`fetch`, HashShape#`dig` with a single `Constant[Symbol|String]`
31
+ # argument matching one of the declared keys. `[]` and `dig` resolve missing keys to
32
+ # `Constant[nil]`; `fetch` (no default, no block) falls through on a miss because Ruby would raise
33
+ # `KeyError` and the analyzer prefers the conservative projection answer.
34
+ # - HashShape#`dig` with multi-arg chains (Slice 5 phase 2 sub-phase 2). Same chaining semantics as
35
+ # Tuple#`dig`.
36
+ # - HashShape#`values_at` with a list of `Constant[Symbol|String]` arguments (Slice 5 phase 2
37
+ # sub-phase 2). The result is a `Tuple` whose elements are the per-key values (`Constant[nil]` for
38
+ # missing keys, mirroring Ruby's runtime behaviour).
53
39
  #
54
40
  # Methods that this tier does NOT yet handle (they fall through):
55
41
  #
56
- # - Iteration methods that bind block parameters (`each`, `map`,
57
- # `select`, ...). Those land alongside the BlockNode-aware
58
- # scope builder.
59
- # - Tuple/HashShape mutation methods. These land with the future
60
- # effect model so read-only entries and mutation invalidation
61
- # have one place to report diagnostics.
42
+ # - Iteration methods that bind block parameters (`each`, `map`, `select`, ...). Those land alongside
43
+ # the BlockNode-aware scope builder.
44
+ # - Tuple/HashShape mutation methods. These land with the future effect model so read-only entries
45
+ # and mutation invalidation have one place to report diagnostics.
62
46
  #
63
- # See docs/internal-spec/inference-engine.md (Slice 5 phase 2)
64
- # and docs/adr/4-type-inference-engine.md for the slice
65
- # rationale.
47
+ # See docs/internal-spec/inference-engine.md (Slice 5 phase 2) and docs/adr/4-type-inference-engine.md
48
+ # for the slice rationale.
66
49
  # rubocop:disable Metrics/ClassLength, Metrics/ModuleLength
67
50
  module ShapeDispatch
68
51
  module_function
@@ -109,8 +92,8 @@ module Rigor
109
92
  itself: :shape_self
110
93
  }.freeze
111
94
 
112
- # Byte cap on a folded `tuple.join` result — a huge tuple times a
113
- # long separator must not materialise an unbounded `Constant`.
95
+ # Byte cap on a folded `tuple.join` result — a huge tuple times a long separator must not
96
+ # materialise an unbounded `Constant`.
114
97
  TUPLE_JOIN_BYTE_LIMIT = 4096
115
98
  private_constant :TUPLE_JOIN_BYTE_LIMIT
116
99
 
@@ -156,29 +139,25 @@ module Rigor
156
139
  :<= => :hash_compare,
157
140
  :> => :hash_compare,
158
141
  :>= => :hash_compare,
159
- # ADR-76 WD2 / ADR-78 WD3 — pure self-returners preserve the
160
- # `HashShape` carrier instead of degrading to the nominal `Hash`
161
- # via the RBS `() -> self` signature, so
162
- # `MESSAGES = {…}.freeze; MESSAGES[reason]` folds the value union
163
- # rather than reading `Dynamic`. `dup` / `clone` produce a fresh
164
- # object, but Rigor's shape carriers are immutable values, so
165
- # preserving the carrier is sound for reads; a later in-place
166
- # mutation routes through `MutationWidening`. (The `Tuple` table
167
- # carries the same four entries; both became safe once the
168
- # block-form over-fold guard landed in `try_dispatch` — ADR-78
169
- # WD1 — so `CONST = [...].freeze; CONST.any? { … }` no longer
170
- # folds the no-block result and fires a reflexive always-truthy.)
142
+ # ADR-76 WD2 / ADR-78 WD3 — pure self-returners preserve the `HashShape` carrier instead of
143
+ # degrading to the nominal `Hash` via the RBS `() -> self` signature, so
144
+ # `MESSAGES = {…}.freeze; MESSAGES[reason]` folds the value union rather than reading `Dynamic`.
145
+ # `dup` / `clone` produce a fresh object, but Rigor's shape carriers are immutable values, so
146
+ # preserving the carrier is sound for reads; a later in-place mutation routes through
147
+ # `MutationWidening`. (The `Tuple` table carries the same four entries; both became safe once the
148
+ # block-form over-fold guard landed in `try_dispatch` ADR-78 WD1 — so
149
+ # `CONST = [...].freeze; CONST.any? { … }` no longer folds the no-block result and fires a
150
+ # reflexive always-truthy.)
171
151
  freeze: :shape_self,
172
152
  dup: :shape_self,
173
153
  clone: :shape_self,
174
154
  itself: :shape_self
175
155
  }.freeze
176
156
 
177
- # @return [Rigor::Type, nil] the precise element/value type, or
178
- # `nil` to defer to the next dispatcher tier.
179
- # Per-carrier dispatch table. Adding a new carrier here
180
- # is a one-row change; the helper methods stay private.
181
- # Anonymous Type subclasses are not expected.
157
+ # @return [Rigor::Type, nil] the precise element/value type, or `nil` to defer to the next
158
+ # dispatcher tier.
159
+ # Per-carrier dispatch table. Adding a new carrier here is a one-row change; the helper methods
160
+ # stay private. Anonymous Type subclasses are not expected.
182
161
  RECEIVER_HANDLERS = {
183
162
  Type::Tuple => :dispatch_tuple,
184
163
  Type::HashShape => :dispatch_hash_shape,
@@ -190,13 +169,10 @@ module Rigor
190
169
  }.freeze
191
170
  private_constant :RECEIVER_HANDLERS
192
171
 
193
- # v0.1.1 Track 1 slice 5b — `Integer#to_s(base)` on a
194
- # non-negative `IntegerRange` receiver. The output of
195
- # `n.to_s(b)` for `n >= 0` is digit-string-only (no
196
- # leading sign), so when the base is in this table the
197
- # result lifts to the matching imported refinement.
198
- # Bases not listed (2, 36, ...) keep the v0.1.0 baseline
199
- # since Rigor has no carrier for the resulting alphabet.
172
+ # v0.1.1 Track 1 slice 5b — `Integer#to_s(base)` on a non-negative `IntegerRange` receiver. The
173
+ # output of `n.to_s(b)` for `n >= 0` is digit-string-only (no leading sign), so when the base is in
174
+ # this table the result lifts to the matching imported refinement. Bases not listed (2, 36, ...)
175
+ # keep the v0.1.0 baseline since Rigor has no carrier for the resulting alphabet.
200
176
  TO_S_BASE_REFINEMENTS = {
201
177
  10 => :decimal_int_string,
202
178
  8 => :octal_int_string,
@@ -209,15 +185,13 @@ module Rigor
209
185
  method_name = context.method_name
210
186
  args = context.args
211
187
  args ||= []
212
- # ADR-78 WD1 — every shape handler folds *no-block* semantics; none
213
- # evaluates a passed block. So a block-form call (`tuple.any? { … }`,
214
- # `tuple.sum { }`, `tuple.count { }`) must NOT fold the no-block
215
- # result doing so ignores the block (an over-fold: `any? { false }`
216
- # would still fold `Constant[true]`). Declining defers to BlockFolding
217
- # / RBS. This is the over-fold class the Tuple shape-carrier
218
- # preservation (ADR-76 WD2) surfaced as reflexive `always-truthy` on
219
- # `CONST = [...].freeze; CONST.any? { … }`; fixing it at the fold (not
220
- # the rule) unblocks that preservation.
188
+ # ADR-78 WD1 — every shape handler folds *no-block* semantics; none evaluates a passed block. So
189
+ # a block-form call (`tuple.any? { … }`, `tuple.sum { … }`, `tuple.count { … }`) must NOT fold
190
+ # the no-block result doing so ignores the block (an over-fold: `any? { false }` would still
191
+ # fold `Constant[true]`). Declining defers to BlockFolding / RBS. This is the over-fold class the
192
+ # Tuple shape-carrier preservation (ADR-76 WD2) surfaced as reflexive `always-truthy` on
193
+ # `CONST = [...].freeze; CONST.any? { … }`; fixing it at the fold (not the rule) unblocks that
194
+ # preservation.
221
195
  return nil unless context.block_type.nil?
222
196
 
223
197
  handler = RECEIVER_HANDLERS[receiver.class]
@@ -226,11 +200,9 @@ module Rigor
226
200
  send(handler, receiver, method_name, args)
227
201
  end
228
202
 
229
- # Tightens `Array#size` / `Array#length` / `String#length` /
230
- # `String#bytesize` / `Hash#size` etc. on a `Nominal` receiver
231
- # from the RBS-declared `Integer` to `non_negative_int`. The
232
- # tier ahead of RBS sees the more precise carrier so
233
- # downstream narrowing (`if size > 0; …`) actually has a
203
+ # Tightens `Array#size` / `Array#length` / `String#length` / `String#bytesize` / `Hash#size` etc.
204
+ # on a `Nominal` receiver from the RBS-declared `Integer` to `non_negative_int`. The tier ahead of
205
+ # RBS sees the more precise carrier so downstream narrowing (`if size > 0; …`) actually has a
234
206
  # range to intersect with.
235
207
  SIZE_RETURNING_NOMINALS = Ractor.make_shareable({
236
208
  "Array" => %i[size length count],
@@ -241,12 +213,10 @@ module Rigor
241
213
  })
242
214
  private_constant :SIZE_RETURNING_NOMINALS
243
215
 
244
- # When the difference removes the empty value of the
245
- # base type (`Constant[""]`, `Constant[0]`, an empty
246
- # Tuple, an empty HashShape), `size` / `length` /
247
- # `count` MUST be `positive-int` (the base's
248
- # non-negative range minus the removed point's `0`),
249
- # and `empty?` / `zero?` MUST be `Constant[false]`.
216
+ # When the difference removes the empty value of the base type (`Constant[""]`, `Constant[0]`, an
217
+ # empty Tuple, an empty HashShape), `size` / `length` / `count` MUST be `positive-int` (the base's
218
+ # non-negative range minus the removed point's `0`), and `empty?` / `zero?` MUST be
219
+ # `Constant[false]`.
250
220
  EMPTY_REMOVAL_BASES = %w[String Array Hash Set].freeze
251
221
  private_constant :EMPTY_REMOVAL_BASES
252
222
 
@@ -267,10 +237,9 @@ module Rigor
267
237
  send(handler, shape, method_name, args)
268
238
  end
269
239
 
270
- # ADR-76 WD2 / ADR-78 WD3 — a pure self-returner
271
- # (`freeze` / `dup` / `clone` / `itself`) returns the receiver
272
- # carrier unchanged, preserving the shape that the nominal
273
- # `() -> self` RBS signature would otherwise drop.
240
+ # ADR-76 WD2 / ADR-78 WD3 — a pure self-returner (`freeze` / `dup` / `clone` / `itself`) returns
241
+ # the receiver carrier unchanged, preserving the shape that the nominal `() -> self` RBS
242
+ # signature would otherwise drop.
274
243
  def shape_self(carrier, _method_name, _args)
275
244
  carrier
276
245
  end
@@ -287,10 +256,9 @@ module Rigor
287
256
  Type::Combinator.non_negative_int
288
257
  end
289
258
 
290
- # Arg-/method-driven precision projections for a `Nominal`
291
- # receiver, consulted ahead of the no-arg size tier. Each
292
- # branch gates on the class name first so unrelated nominals
293
- # skip the work. Returns nil when no projection applies.
259
+ # Arg-/method-driven precision projections for a `Nominal` receiver, consulted ahead of the
260
+ # no-arg size tier. Each branch gates on the class name first so unrelated nominals skip the
261
+ # work. Returns nil when no projection applies.
294
262
  def nominal_projection(nominal, method_name, args)
295
263
  case nominal.class_name
296
264
  when "String"
@@ -305,14 +273,12 @@ module Rigor
305
273
  end
306
274
  end
307
275
 
308
- # `Array[T]#compact` — `compact` removes every `nil` element,
309
- # so the result element type is `T` with its `nil` constituent
310
- # stripped (`Array[Node?]#compact` `Array[Node]`). Mirrors
311
- # the `Tuple#compact` constant fold for the generic element
312
- # case. Declines when the receiver carries no type argument
313
- # (the RBS `Array[untyped]` answer is already maximal) or when
314
- # `T` has no `nil` constituent to remove (the result equals the
315
- # receiver, so the RBS tier's answer is already precise).
276
+ # `Array[T]#compact` — `compact` removes every `nil` element, so the result element type is `T`
277
+ # with its `nil` constituent stripped (`Array[Node?]#compact` `Array[Node]`). Mirrors the
278
+ # `Tuple#compact` constant fold for the generic element case. Declines when the receiver carries
279
+ # no type argument (the RBS `Array[untyped]` answer is already maximal) or when `T` has no `nil`
280
+ # constituent to remove (the result equals the receiver, so the RBS tier's answer is already
281
+ # precise).
316
282
  def array_nominal_compact(nominal, args)
317
283
  return nil unless args.empty?
318
284
 
@@ -325,10 +291,9 @@ module Rigor
325
291
  Type::Combinator.nominal_of("Array", type_args: [stripped])
326
292
  end
327
293
 
328
- # Removes the `nil` constituent from a (possibly union) type,
329
- # returning the same object when there is nothing to remove so
330
- # callers can detect the no-op cheaply. Kept local to the
331
- # dispatch tier to avoid a dependency on the narrowing module.
294
+ # Removes the `nil` constituent from a (possibly union) type, returning the same object when
295
+ # there is nothing to remove so callers can detect the no-op cheaply. Kept local to the dispatch
296
+ # tier to avoid a dependency on the narrowing module.
332
297
  def strip_nil_constituent(type)
333
298
  case type
334
299
  when Type::Constant
@@ -345,17 +310,13 @@ module Rigor
345
310
  end
346
311
  end
347
312
 
348
- # `Array[T]#flatten` (and `flatten(depth)`). When `T` is a
349
- # nested `Array[U]` nominal, one flatten level yields the
350
- # joined inner element type `Array[Array[U]]#flatten`
351
- # `Array[U]`. When `T` is non-nested the result is `Array[T]`
352
- # unchanged (Ruby returns a copy with the same element type).
353
- # Multi-level nesting is handled conservatively: each level
354
- # joins its element types, and a `depth` argument that does
355
- # not fully resolve the nesting still produces a sound
356
- # superset. Declines on an `Array` with no type argument
357
- # (the RBS `Array[untyped]` answer is already as precise as
358
- # we can be) and on a non-static depth argument.
313
+ # `Array[T]#flatten` (and `flatten(depth)`). When `T` is a nested `Array[U]` nominal, one flatten
314
+ # level yields the joined inner element type — `Array[Array[U]]#flatten` `Array[U]`. When `T`
315
+ # is non-nested the result is `Array[T]` unchanged (Ruby returns a copy with the same element
316
+ # type). Multi-level nesting is handled conservatively: each level joins its element types, and a
317
+ # `depth` argument that does not fully resolve the nesting still produces a sound superset.
318
+ # Declines on an `Array` with no type argument (the RBS `Array[untyped]` answer is already as
319
+ # precise as we can be) and on a non-static depth argument.
359
320
  def array_nominal_flatten(nominal, args)
360
321
  element = nominal.type_args&.first
361
322
  return nil if element.nil?
@@ -367,11 +328,9 @@ module Rigor
367
328
  Type::Combinator.nominal_of("Array", type_args: [flattened])
368
329
  end
369
330
 
370
- # Resolves the element type of a flattened `Array[element]`.
371
- # Each `Array[U]` nesting level contributes `U`; the per-level
372
- # element types are unioned. `depth < 0` recurses without
373
- # bound; `depth == 0` stops (Ruby's `flatten(0)` is a no-op
374
- # copy and returns the element unchanged).
331
+ # Resolves the element type of a flattened `Array[element]`. Each `Array[U]` nesting level
332
+ # contributes `U`; the per-level element types are unioned. `depth < 0` recurses without bound;
333
+ # `depth == 0` stops (Ruby's `flatten(0)` is a no-op copy and returns the element unchanged).
375
334
  def flatten_nominal_element(element, depth)
376
335
  return element if depth.zero?
377
336
  return element unless array_nominal?(element)
@@ -387,14 +346,13 @@ module Rigor
387
346
  !type.type_args.empty?
388
347
  end
389
348
 
390
- # Arg-type-driven String binary projections for any String-typed
391
- # receiver (including Nominal, Refined, and Difference fallbacks).
392
- # Called before the no-arg size guard so binary operators are seen.
349
+ # Arg-type-driven String binary projections for any String-typed receiver (including Nominal,
350
+ # Refined, and Difference fallbacks). Called before the no-arg size guard so binary operators are
351
+ # seen.
393
352
  #
394
- # - `String + non-empty-string` → `non-empty-string`
395
- # (arg guarantees the concatenation is non-empty)
396
- # - `String * Constant[0]` → `Constant[""]`
397
- # (every string repeated 0 times is the empty string)
353
+ # - `String + non-empty-string` → `non-empty-string` (arg guarantees the concatenation is
354
+ # non-empty)
355
+ # - `String * Constant[0]` → `Constant[""]` (every string repeated 0 times is the empty string)
398
356
  def dispatch_string_binary_from_arg(method_name, arg)
399
357
  case method_name
400
358
  when :+
@@ -407,11 +365,10 @@ module Rigor
407
365
  nil
408
366
  end
409
367
 
410
- # Arg-type-driven Integer binary projections for any Integer-typed
411
- # receiver (including Nominal, Refined, and Difference fallbacks).
368
+ # Arg-type-driven Integer binary projections for any Integer-typed receiver (including Nominal,
369
+ # Refined, and Difference fallbacks).
412
370
  #
413
- # - `Integer * Constant[0]` → `Constant[0]`
414
- # (any integer multiplied by 0 is 0)
371
+ # - `Integer * Constant[0]` → `Constant[0]` (any integer multiplied by 0 is 0)
415
372
  def dispatch_integer_binary_from_arg(method_name, arg)
416
373
  return nil unless method_name == :*
417
374
  return nil unless arg.is_a?(Type::Constant) && arg.value.is_a?(Integer) && arg.value.zero?
@@ -419,15 +376,12 @@ module Rigor
419
376
  Type::Combinator.constant_of(0)
420
377
  end
421
378
 
422
- # `IntegerRange#to_s` precision (v0.1.1 Track 1 slice 5b).
423
- # When the range's lower bound is `>= 0`, every member is
424
- # a non-negative integer and `to_s(base)` returns a
425
- # digit-string with no leading sign. The result lifts to
426
- # the matching imported refinement (`decimal-int-string`
427
- # for base 10, `octal-int-string` for 8, `hex-int-string`
428
- # for 16). Signed ranges fall through (the result could
429
- # carry a `-` sign that no Rigor refinement currently
430
- # captures), as do bases without a digit-only refinement.
379
+ # `IntegerRange#to_s` precision (v0.1.1 Track 1 slice 5b). When the range's lower bound is
380
+ # `>= 0`, every member is a non-negative integer and `to_s(base)` returns a digit-string with no
381
+ # leading sign. The result lifts to the matching imported refinement (`decimal-int-string` for
382
+ # base 10, `octal-int-string` for 8, `hex-int-string` for 16). Signed ranges fall through (the
383
+ # result could carry a `-` sign that no Rigor refinement currently captures), as do bases
384
+ # without a digit-only refinement.
431
385
  def dispatch_integer_range(range, method_name, args)
432
386
  return nil unless method_name == :to_s
433
387
  return nil unless range.lower >= 0
@@ -441,10 +395,9 @@ module Rigor
441
395
  Type::Combinator.public_send(refinement)
442
396
  end
443
397
 
444
- # `to_s` with no argument defaults to base 10. With one
445
- # argument, the value MUST be a `Constant<Integer>` to
446
- # be statically known. Anything else (Nominal[Integer]
447
- # arg, multi-arg, etc.) declines.
398
+ # `to_s` with no argument defaults to base 10. With one argument, the value MUST be a
399
+ # `Constant<Integer>` to be statically known. Anything else (Nominal[Integer] arg, multi-arg,
400
+ # etc.) declines.
448
401
  def base_argument(args)
449
402
  return 10 if args.empty?
450
403
  return nil unless args.size == 1
@@ -455,21 +408,18 @@ module Rigor
455
408
  arg.value
456
409
  end
457
410
 
458
- # Refinement-aware projections over a `Difference[base,
459
- # removed]` receiver. When the removed value is the
460
- # empty witness of the base (`Constant[""]` for
461
- # String, `Tuple[]` for Array, `HashShape{}` for Hash,
462
- # `Constant[0]` for Integer), the catalog tier knows:
411
+ # Refinement-aware projections over a `Difference[base, removed]` receiver. When the removed
412
+ # value is the empty witness of the base (`Constant[""]` for String, `Tuple[]` for Array,
413
+ # `HashShape{}` for Hash, `Constant[0]` for Integer), the catalog tier knows:
463
414
  #
464
415
  # ns.size # positive-int
465
416
  # ns.size == 0 # Constant[false] (via narrowing tier)
466
417
  # ns.empty? # Constant[false]
467
418
  # nzi.zero? # Constant[false]
468
419
  #
469
- # For any other base method, the difference is opaque
470
- # to ShapeDispatch we delegate to the base nominal
471
- # so the size/length tier still answers the broader
472
- # `non_negative_int` envelope where applicable.
420
+ # For any other base method, the difference is opaque to ShapeDispatch — we delegate to the base
421
+ # nominal so the size/length tier still answers the broader `non_negative_int` envelope where
422
+ # applicable.
473
423
  def dispatch_difference(difference, method_name, args)
474
424
  base = difference.base
475
425
  return nil unless base.is_a?(Type::Nominal)
@@ -499,12 +449,11 @@ module Rigor
499
449
  !!(predicate && predicate.call(difference.removed))
500
450
  end
501
451
 
502
- # Methods on a non-empty String that preserve non-emptiness
503
- # (they transform characters but never reduce the string to "").
452
+ # Methods on a non-empty String that preserve non-emptiness (they transform characters but never
453
+ # reduce the string to "").
504
454
  NON_EMPTY_STRING_PRESERVING_UNARY = Set[:upcase, :downcase, :capitalize, :swapcase, :reverse].freeze
505
- # Methods on non-zero-int that return a non-zero-int (identity ops).
506
- # Negation of a non-zero integer is non-zero; `to_i`/`to_int` are
507
- # identity operations on Integer.
455
+ # Methods on non-zero-int that return a non-zero-int (identity ops). Negation of a non-zero
456
+ # integer is non-zero; `to_i`/`to_int` are identity operations on Integer.
508
457
  NON_ZERO_INT_PRESERVING_UNARY = Set[:-@, :+@, :to_i, :to_int].freeze
509
458
  private_constant :NON_EMPTY_STRING_PRESERVING_UNARY, :NON_ZERO_INT_PRESERVING_UNARY
510
459
 
@@ -593,67 +542,64 @@ module Rigor
593
542
  Type::Combinator.positive_int
594
543
  end
595
544
 
596
- # Predicate-subset projections over a `Refined[base,
597
- # predicate]` receiver. Today the catalogue is the
598
- # String case-normalisation pair: `s.downcase` over a
599
- # `lowercase-string` receiver folds to the same
600
- # carrier (already lowercase), and `s.upcase` lifts a
601
- # `lowercase-string` to `uppercase-string`. Symmetric
602
- # rules apply with the predicates swapped. Numeric-
603
- # string idempotence over `#downcase` / `#upcase` is
604
- # also recognised because a numeric string equals its
545
+ # Predicate-subset projections over a `Refined[base, predicate]` receiver. Today the catalogue is
546
+ # the String case-normalisation pair: `s.downcase` over a `lowercase-string` receiver folds to
547
+ # the same carrier (already lowercase), and `s.upcase` lifts a `lowercase-string` to
548
+ # `uppercase-string`. Symmetric rules apply with the predicates swapped. Numeric-string
549
+ # idempotence over `#downcase` / `#upcase` is also recognised because a numeric string equals its
605
550
  # own case-normalisation.
606
551
  #
607
- # For methods this tier does not have a refinement-
608
- # specific rule for, projection delegates to
609
- # `dispatch_nominal_size` so size-returning calls on
610
- # a `Refined[String, *]` still tighten to
552
+ # For methods this tier does not have a refinement-specific rule for, projection delegates to
553
+ # `dispatch_nominal_size` so size-returning calls on a `Refined[String, *]` still tighten to
611
554
  # `non_negative_int`.
612
- # ADR-15 Phase 4b.x — `Ractor.make_shareable` (not `.freeze`)
613
- # because the keys are two-element Symbol arrays whose
614
- # inner arrays are unfrozen under shallow `.freeze`.
615
- # Surfaced on Discourse via `Ractor::IsolationError` when
616
- # the dispatch loop's `REFINED_STRING_PROJECTIONS[[id, sym]]`
617
- # lookup ran from a worker Ractor.
555
+ # ADR-15 Phase 4b.x — `Ractor.make_shareable` (not `.freeze`) because the keys are two-element
556
+ # Symbol arrays whose inner arrays are unfrozen under shallow `.freeze`. Surfaced on Discourse via
557
+ # `Ractor::IsolationError` when the dispatch loop's `REFINED_STRING_PROJECTIONS[[id, sym]]` lookup
558
+ # ran from a worker Ractor.
618
559
  REFINED_STRING_PROJECTIONS = Ractor.make_shareable({
619
560
  %i[lowercase downcase] => :refined_self,
620
561
  %i[lowercase upcase] => :uppercase_string,
621
562
  %i[uppercase upcase] => :refined_self,
622
563
  %i[uppercase downcase] => :lowercase_string,
623
- # `numeric-string` is the full Ruby numeric-literal
624
- # grammar (since the predicate delegates to the
625
- # parser). `#downcase` preserves it — lowercasing a
626
- # literal (hex digits, `0X` / `E` prefixes) yields a
627
- # valid lowercase literal but `#upcase` does NOT:
628
- # the rational / imaginary suffixes are lowercase-only
629
- # (`"1r".upcase == "1R"` is not a literal), so `upcase`
630
- # drops to the plain base `String` still sound (the
631
- # result is a String), just no longer numeric.
564
+ # `numeric-string` is the full Ruby
565
+ # numeric-literal grammar (since the predicate
566
+ # delegates to the parser). `#downcase`
567
+ # preserves it lowercasing a literal (hex
568
+ # digits, `0X` / `E` prefixes) yields a valid
569
+ # lowercase literal but `#upcase` does NOT:
570
+ # the rational / imaginary suffixes are
571
+ # lowercase-only (`"1r".upcase == "1R"` is not
572
+ # a literal), so `upcase` drops to the plain
573
+ # base `String` — still sound (the result is a
574
+ # String), just no longer numeric.
632
575
  %i[numeric downcase] => :refined_self,
633
576
  %i[numeric upcase] => :base_string,
634
- # Digit-only strings are case-invariant; the prefix
635
- # letters in `0o…` / `0x…` are accepted by the
636
- # predicate in either case so the predicate-subset
637
- # is preserved across `#downcase` / `#upcase` even
638
- # though the value-set element changes.
577
+ # Digit-only strings are case-invariant; the
578
+ # prefix letters in `0o…` / `0x…` are accepted
579
+ # by the predicate in either case so the
580
+ # predicate-subset is preserved across
581
+ # `#downcase` / `#upcase` even though the
582
+ # value-set element changes.
639
583
  %i[decimal_int downcase] => :refined_self,
640
584
  %i[decimal_int upcase] => :refined_self,
641
585
  %i[octal_int downcase] => :refined_self,
642
586
  %i[octal_int upcase] => :refined_self,
643
587
  %i[hex_int downcase] => :refined_self,
644
588
  %i[hex_int upcase] => :refined_self,
645
- # v0.1.1 Track 1 slice 2 — `to_i` / `to_int` on a
646
- # `decimal-int-string` parses to an `Integer`. The
647
- # carrier is `universal_int`, NOT `non-negative-int`:
648
- # the predicate `/\A-?\d+\z/` admits a leading sign, so
589
+ # v0.1.1 Track 1 slice 2 — `to_i` / `to_int` on
590
+ # a `decimal-int-string` parses to an
591
+ # `Integer`. The carrier is `universal_int`,
592
+ # NOT `non-negative-int`: the predicate
593
+ # `/\A-?\d+\z/` admits a leading sign, so
649
594
  # `"-7"` is a valid decimal-int-string and
650
- # `"-7".to_i == -7 < 0`. `String#to_i` is total (never
651
- # raises), so the projection is sound — just signed.
652
- # `numeric-string` is deliberately NOT projected to
653
- # `to_i` at all: it now spans the full numeric-literal
654
- # grammar, so a `"1.5"` / `"2i"` inhabitant has a
655
- # fractional or non-Integer parse it falls through to
656
- # the RBS `Integer`.
595
+ # `"-7".to_i == -7 < 0`. `String#to_i` is
596
+ # total (never raises), so the projection is
597
+ # sound — just signed. `numeric-string` is
598
+ # deliberately NOT projected to `to_i` at all:
599
+ # it now spans the full numeric-literal
600
+ # grammar, so a `"1.5"` / `"2i"` inhabitant
601
+ # has a fractional or non-Integer parse — it
602
+ # falls through to the RBS `Integer`.
657
603
  %i[decimal_int to_i] => :universal_int,
658
604
  %i[decimal_int to_int] => :universal_int
659
605
  })
@@ -685,27 +631,20 @@ module Rigor
685
631
  end
686
632
  end
687
633
 
688
- # Projects a method call over an `Intersection[M1, …]`
689
- # receiver by collecting each member's projection and
690
- # combining the results. The set-theoretic identity is
691
- # `M(A ∩ B) ⊆ M(A) ∩ M(B)`, so the meet of the per-member
692
- # projections is sound. Combining is best-effort:
634
+ # Projects a method call over an `Intersection[M1, …]` receiver by collecting each member's
635
+ # projection and combining the results. The set-theoretic identity is
636
+ # `M(A ∩ B) ⊆ M(A) ∩ M(B)`, so the meet of the per-member projections is sound. Combining is
637
+ # best-effort:
693
638
  #
694
- # - If every result is a `Type::IntegerRange`, return
695
- # their bounded-integer meet (max of lower bounds, min
696
- # of upper bounds). This catches the common
697
- # `(non_empty_string lowercase_string).size`
698
- # pattern where one member projects to `positive-int`
699
- # and the other to `non-negative-int`; the meet is
700
- # `positive-int`.
701
- # - Otherwise return the first non-nil result. A richer
702
- # meet (e.g. of Difference + Refined results when both
703
- # project) is left for a future slice; the carrier
704
- # stays sound because every member's projection is
705
- # already a superset of the true intersection.
639
+ # - If every result is a `Type::IntegerRange`, return their bounded-integer meet (max of lower
640
+ # bounds, min of upper bounds). This catches the common
641
+ # `(non_empty_string lowercase_string).size` pattern where one member projects to
642
+ # `positive-int` and the other to `non-negative-int`; the meet is `positive-int`.
643
+ # - Otherwise return the first non-nil result. A richer meet (e.g. of Difference + Refined
644
+ # results when both project) is left for a future slice; the carrier stays sound because every
645
+ # member's projection is already a superset of the true intersection.
706
646
  #
707
- # Returns nil when no member projects, so the caller
708
- # falls through to the next dispatcher tier.
647
+ # Returns nil when no member projects, so the caller falls through to the next dispatcher tier.
709
648
  def dispatch_intersection(intersection, method_name, args)
710
649
  results = intersection.members.filter_map do |member|
711
650
  ShapeDispatch.try_dispatch(
@@ -726,15 +665,11 @@ module Rigor
726
665
  results.first
727
666
  end
728
667
 
729
- # Compute the bounded-integer meet of two or more
730
- # `IntegerRange` carriers. We compare via the numeric
731
- # `lower` / `upper` accessors (`-Float::INFINITY` /
732
- # `Float::INFINITY` for the symbolic ends), then map
733
- # back to the symbolic-bound representation
734
- # `IntegerRange.new` expects. The disjoint-meet case
735
- # cannot arise from sound member-wise projections in
736
- # v0.0.4 but is guarded defensively to keep the
737
- # carrier total.
668
+ # Compute the bounded-integer meet of two or more `IntegerRange` carriers. We compare via the
669
+ # numeric `lower` / `upper` accessors (`-Float::INFINITY` / `Float::INFINITY` for the symbolic
670
+ # ends), then map back to the symbolic-bound representation `IntegerRange.new` expects. The
671
+ # disjoint-meet case cannot arise from sound member-wise projections in v0.0.4 but is guarded
672
+ # defensively to keep the carrier total.
738
673
  def narrow_integer_ranges(ranges)
739
674
  numeric_low = ranges.map(&:lower).max
740
675
  numeric_high = ranges.map(&:upper).min
@@ -745,10 +680,9 @@ module Rigor
745
680
  Type::Combinator.integer_range(min, max)
746
681
  end
747
682
 
748
- # `first` (no arg) → the first element (or `Constant[nil]` when
749
- # empty). The `first(n)` arg-form is deliberately left to RBS
750
- # overload selection (see the overload-selection specs) — folding
751
- # it here would change that documented `Array[Elem]` contract.
683
+ # `first` (no arg) → the first element (or `Constant[nil]` when empty). The `first(n)` arg-form
684
+ # is deliberately left to RBS overload selection (see the overload-selection specs) folding it
685
+ # here would change that documented `Array[Elem]` contract.
752
686
  def tuple_first(tuple, _method_name, args)
753
687
  return nil unless args.empty?
754
688
  return Type::Combinator.constant_of(nil) if tuple.elements.empty?
@@ -769,8 +703,7 @@ module Rigor
769
703
  Type::Combinator.constant_of(tuple.elements.size)
770
704
  end
771
705
 
772
- # `tuple.empty?` — folds to a precise bool from the
773
- # tuple's known arity.
706
+ # `tuple.empty?` — folds to a precise bool from the tuple's known arity.
774
707
  # rubocop:disable Style/ReturnNilInPredicateMethodDefinition
775
708
  def tuple_empty?(tuple, _method_name, args)
776
709
  return nil unless args.empty?
@@ -778,20 +711,17 @@ module Rigor
778
711
  Type::Combinator.constant_of(tuple.elements.empty?)
779
712
  end
780
713
 
781
- # `tuple.any?` (no-arg, no-block) — empty tuple → false,
782
- # non-empty true. The block / arg forms flow through
783
- # `BlockFolding` and the RBS tier.
714
+ # `tuple.any?` (no-arg, no-block) — empty tuple → false, non-empty → true. The block / arg forms
715
+ # flow through `BlockFolding` and the RBS tier.
784
716
  def tuple_any?(tuple, _method_name, args)
785
717
  return nil unless args.empty?
786
718
 
787
719
  Type::Combinator.constant_of(!tuple.elements.empty?)
788
720
  end
789
721
 
790
- # `tuple.all?` (no-arg, no-block) — true for empty
791
- # tuple (vacuous truth) AND for non-empty tuples whose
792
- # every element is provably truthy. Mixed / unknown
793
- # element truthiness declines so the RBS / BlockFolding
794
- # tiers can still answer.
722
+ # `tuple.all?` (no-arg, no-block) — true for empty tuple (vacuous truth) AND for non-empty tuples
723
+ # whose every element is provably truthy. Mixed / unknown element truthiness declines so the RBS
724
+ # / BlockFolding tiers can still answer.
795
725
  def tuple_all?(tuple, _method_name, args)
796
726
  return nil unless args.empty?
797
727
  return Type::Combinator.constant_of(true) if tuple.elements.empty?
@@ -802,9 +732,8 @@ module Rigor
802
732
  Type::Combinator.constant_of(decision)
803
733
  end
804
734
 
805
- # `tuple.none?` (no-arg, no-block) — true when every
806
- # element is provably falsey, false when any element is
807
- # provably truthy. Empty tuple folds to true (vacuous).
735
+ # `tuple.none?` (no-arg, no-block) — true when every element is provably falsey, false when any
736
+ # element is provably truthy. Empty tuple folds to true (vacuous).
808
737
  def tuple_none?(tuple, _method_name, args)
809
738
  return nil unless args.empty?
810
739
  return Type::Combinator.constant_of(true) if tuple.elements.empty?
@@ -815,13 +744,10 @@ module Rigor
815
744
  Type::Combinator.constant_of(decision)
816
745
  end
817
746
 
818
- # `tuple.include?(needle)` — folds to a precise bool when
819
- # the needle is a `Constant` and the tuple's elements
820
- # are all `Constant` (so disjointness is checkable).
821
- # If any element matches the needle's value the answer
822
- # is `Constant[true]`; if every element is a Constant
823
- # whose value is structurally distinct from the needle
824
- # the answer is `Constant[false]`.
747
+ # `tuple.include?(needle)` — folds to a precise bool when the needle is a `Constant` and the
748
+ # tuple's elements are all `Constant` (so disjointness is checkable). If any element matches the
749
+ # needle's value the answer is `Constant[true]`; if every element is a Constant whose value is
750
+ # structurally distinct from the needle the answer is `Constant[false]`.
825
751
  def tuple_include?(tuple, _method_name, args)
826
752
  return nil unless args.size == 1
827
753
 
@@ -836,9 +762,8 @@ module Rigor
836
762
  end
837
763
  # rubocop:enable Style/ReturnNilInPredicateMethodDefinition
838
764
 
839
- # `tuple.sum` — when every element is a numeric Constant,
840
- # fold to `Constant[sum]`. Mixed / non-numeric elements
841
- # decline so RBS widens.
765
+ # `tuple.sum` — when every element is a numeric Constant, fold to `Constant[sum]`. Mixed /
766
+ # non-numeric elements decline so RBS widens.
842
767
  def tuple_sum(tuple, _method_name, args)
843
768
  return nil unless args.empty?
844
769
  return Type::Combinator.constant_of(0) if tuple.elements.empty?
@@ -849,10 +774,9 @@ module Rigor
849
774
  Type::Combinator.constant_of(values.sum)
850
775
  end
851
776
 
852
- # `tuple.join(sep = "")` — fold to the joined `Constant[String]`
853
- # when every element is a `Constant` (its `to_s` is deterministic
854
- # for the scalar value classes) and the separator is absent or a
855
- # `Constant[String]`. Capped at `TUPLE_JOIN_BYTE_LIMIT`.
777
+ # `tuple.join(sep = "")` — fold to the joined `Constant[String]` when every element is a
778
+ # `Constant` (its `to_s` is deterministic for the scalar value classes) and the separator is
779
+ # absent or a `Constant[String]`. Capped at `TUPLE_JOIN_BYTE_LIMIT`.
856
780
  def tuple_join(tuple, _method_name, args)
857
781
  sep = tuple_join_separator(args)
858
782
  return nil if sep.nil?
@@ -868,8 +792,8 @@ module Rigor
868
792
  nil
869
793
  end
870
794
 
871
- # The join separator: `""` for the no-arg form, the value of a
872
- # single `Constant[String]` arg, or `nil` to decline.
795
+ # The join separator: `""` for the no-arg form, the value of a single `Constant[String]` arg, or
796
+ # `nil` to decline.
873
797
  def tuple_join_separator(args)
874
798
  return "" if args.empty?
875
799
  return nil unless args.size == 1
@@ -880,12 +804,10 @@ module Rigor
880
804
  arg.value
881
805
  end
882
806
 
883
- # `tuple.min` / `tuple.max` — fold when every element is
884
- # a `Constant` whose values share a Ruby-comparable
885
- # domain. Empty tuples fold to `Constant[nil]`. The 1-arg
886
- # `min(n)` / `max(n)` form folds to a `Tuple` of the n
887
- # edge-most values in Ruby's order (`min(n)` ascending,
888
- # `max(n)` descending) — the n-arg sibling of `first(n)`.
807
+ # `tuple.min` / `tuple.max` — fold when every element is a `Constant` whose values share a
808
+ # Ruby-comparable domain. Empty tuples fold to `Constant[nil]`. The 1-arg `min(n)` / `max(n)`
809
+ # form folds to a `Tuple` of the n edge-most values in Ruby's order (`min(n)` ascending, `max(n)`
810
+ # descending) the n-arg sibling of `first(n)`.
889
811
  def tuple_min(tuple, _method_name, args)
890
812
  tuple_minmax(tuple, args, :min)
891
813
  end
@@ -907,12 +829,10 @@ module Rigor
907
829
  nil
908
830
  end
909
831
 
910
- # `tuple.min(n)` / `tuple.max(n)` — a `Tuple` of the n
911
- # edge-most element values, delegating to Ruby's
912
- # `Array#min` / `#max` for the ordering. Declines on a
913
- # non-static / negative count or non-Constant elements.
914
- # The result is bounded by the tuple's known arity, so no
915
- # extra size cap is needed.
832
+ # `tuple.min(n)` / `tuple.max(n)` — a `Tuple` of the n edge-most element values, delegating to
833
+ # Ruby's `Array#min` / `#max` for the ordering. Declines on a non-static / negative count or
834
+ # non-Constant elements. The result is bounded by the tuple's known arity, so no extra size cap
835
+ # is needed.
916
836
  def tuple_minmax_n(tuple, arg, edge)
917
837
  return nil unless arg.is_a?(Type::Constant) && arg.value.is_a?(Integer)
918
838
  return nil if arg.value.negative?
@@ -926,11 +846,9 @@ module Rigor
926
846
  nil
927
847
  end
928
848
 
929
- # `tuple.minmax` — the `[min, max]` pair as a 2-slot
930
- # `Tuple[Constant[min], Constant[max]]`, mirroring the
931
- # `Range#minmax` fold. Every element must be a `Constant`
932
- # and the values must Ruby-compare; an empty tuple folds to
933
- # `Tuple[nil, nil]` (Ruby's `[].minmax`), incomparable
849
+ # `tuple.minmax` — the `[min, max]` pair as a 2-slot `Tuple[Constant[min], Constant[max]]`,
850
+ # mirroring the `Range#minmax` fold. Every element must be a `Constant` and the values must
851
+ # Ruby-compare; an empty tuple folds to `Tuple[nil, nil]` (Ruby's `[].minmax`), incomparable
934
852
  # mixed-class values decline.
935
853
  def tuple_minmax_pair(tuple, _method_name, args)
936
854
  return nil unless args.empty?
@@ -952,10 +870,9 @@ module Rigor
952
870
  nil
953
871
  end
954
872
 
955
- # `tuple.sort` — every element must be a `Constant` and
956
- # the values must Ruby-compare. The result is a Tuple
957
- # with the same elements in sorted order. Comparison
958
- # failures (mixed-class incomparable values) decline.
873
+ # `tuple.sort` — every element must be a `Constant` and the values must Ruby-compare. The result
874
+ # is a Tuple with the same elements in sorted order. Comparison failures (mixed-class
875
+ # incomparable values) decline.
959
876
  def tuple_sort(tuple, _method_name, args)
960
877
  return nil unless args.empty?
961
878
  return tuple if tuple.elements.size <= 1
@@ -969,32 +886,27 @@ module Rigor
969
886
  nil
970
887
  end
971
888
 
972
- # `tuple.reverse` — independent of element shape; a
973
- # tuple-precise reversed Tuple.
889
+ # `tuple.reverse` — independent of element shape; a tuple-precise reversed Tuple.
974
890
  def tuple_reverse(tuple, _method_name, args)
975
891
  return nil unless args.empty?
976
892
 
977
893
  Type::Combinator.tuple_of(*tuple.elements.reverse)
978
894
  end
979
895
 
980
- # `tuple.to_a` — Tuple is structurally identical to its
981
- # to_a (Ruby returns the receiver itself for an Array).
896
+ # `tuple.to_a` — Tuple is structurally identical to its to_a (Ruby returns the receiver itself
897
+ # for an Array).
982
898
  def tuple_to_a(tuple, _method_name, args)
983
899
  return nil unless args.empty?
984
900
 
985
901
  tuple
986
902
  end
987
903
 
988
- # `tuple.zip(other_1, other_2, …)` — pairs the receiver's
989
- # per-position elements with the per-position elements of
990
- # each other Tuple-shaped argument. The result is a Tuple
991
- # of Tuples whose arity matches the receiver: position
992
- # `i` is `Tuple[receiver[i], other_1[i], other_2[i], …]`.
993
- # Out-of-range positions in any `other_k` contribute
994
- # `Constant[nil]` (matching Ruby's runtime semantics).
995
- # Declines when any `other_k` is not a Tuple, since the
996
- # arity is then unknown and the result would be
997
- # `Array[Array[…]]` — RBS already gives that answer.
904
+ # `tuple.zip(other_1, other_2, …)` — pairs the receiver's per-position elements with the
905
+ # per-position elements of each other Tuple-shaped argument. The result is a Tuple of Tuples
906
+ # whose arity matches the receiver: position `i` is `Tuple[receiver[i], other_1[i], other_2[i],
907
+ # …]`. Out-of-range positions in any `other_k` contribute `Constant[nil]` (matching Ruby's
908
+ # runtime semantics). Declines when any `other_k` is not a Tuple, since the arity is then unknown
909
+ # and the result would be `Array[Array[…]]` — RBS already gives that answer.
998
910
  def tuple_zip(tuple, _method_name, args)
999
911
  return nil if args.empty? || args.size > MAX_ZIP_ARITY
1000
912
  return nil unless args.all?(Type::Tuple)
@@ -1009,11 +921,9 @@ module Rigor
1009
921
  MAX_ZIP_ARITY = 8
1010
922
  private_constant :MAX_ZIP_ARITY
1011
923
 
1012
- # `tuple.to_h` — folds when every Tuple element is itself
1013
- # a 2-element Tuple whose first element is a `Constant`
1014
- # (so it can serve as a Hash key). Produces a closed
1015
- # `HashShape` whose entries mirror the per-position
1016
- # pairs. Empty Tuples fold to the empty HashShape.
924
+ # `tuple.to_h` — folds when every Tuple element is itself a 2-element Tuple whose first element
925
+ # is a `Constant` (so it can serve as a Hash key). Produces a closed `HashShape` whose entries
926
+ # mirror the per-position pairs. Empty Tuples fold to the empty HashShape.
1017
927
  def tuple_to_h(tuple, _method_name, args)
1018
928
  return nil unless args.empty?
1019
929
  return Type::Combinator.hash_shape_of({}) if tuple.elements.empty?
@@ -1036,11 +946,9 @@ module Rigor
1036
946
  [key.value, value]
1037
947
  end
1038
948
 
1039
- # `tuple.values_at(i1, i2, ...)` — returns a Tuple of
1040
- # per-index elements. Each argument must be a
1041
- # `Constant[Integer]`. Out-of-range indices fill with
1042
- # `Constant[nil]`, mirroring Ruby's runtime behaviour.
1043
- # Declines when any argument is non-static.
949
+ # `tuple.values_at(i1, i2, ...)` — returns a Tuple of per-index elements. Each argument must be a
950
+ # `Constant[Integer]`. Out-of-range indices fill with `Constant[nil]`, mirroring Ruby's runtime
951
+ # behaviour. Declines when any argument is non-static.
1044
952
  def tuple_values_at(tuple, _method_name, args)
1045
953
  return nil if args.empty?
1046
954
 
@@ -1055,10 +963,8 @@ module Rigor
1055
963
  Type::Combinator.tuple_of(*values)
1056
964
  end
1057
965
 
1058
- # `tuple + other` — concatenates two Tuples. Both sides
1059
- # must be `Type::Tuple`. Returns a new Tuple whose
1060
- # elements are those of the receiver followed by those
1061
- # of the argument.
966
+ # `tuple + other` — concatenates two Tuples. Both sides must be `Type::Tuple`. Returns a new
967
+ # Tuple whose elements are those of the receiver followed by those of the argument.
1062
968
  def tuple_concat(tuple, _method_name, args)
1063
969
  return nil unless args.size == 1
1064
970
 
@@ -1068,10 +974,9 @@ module Rigor
1068
974
  Type::Combinator.tuple_of(*tuple.elements, *other.elements)
1069
975
  end
1070
976
 
1071
- # `tuple.compact` — removes every element that is
1072
- # `Constant[nil]`. Folds only when every element is a
1073
- # `Constant` (so the nil set is decidable). Mixed-shape
1074
- # elements decline so the RBS tier widens.
977
+ # `tuple.compact` — removes every element that is `Constant[nil]`. Folds only when every element
978
+ # is a `Constant` (so the nil set is decidable). Mixed-shape elements decline so the RBS tier
979
+ # widens.
1075
980
  def tuple_compact(tuple, _method_name, args)
1076
981
  return nil unless args.empty?
1077
982
  return nil unless tuple.elements.all?(Type::Constant)
@@ -1080,9 +985,8 @@ module Rigor
1080
985
  Type::Combinator.tuple_of(*kept)
1081
986
  end
1082
987
 
1083
- # `uniq` (no block) → `Tuple` of the first occurrence of each
1084
- # distinct value. Folds only when every element is a `Constant`
1085
- # so value equality is decidable; the block form defers.
988
+ # `uniq` (no block) → `Tuple` of the first occurrence of each distinct value. Folds only when
989
+ # every element is a `Constant` so value equality is decidable; the block form defers.
1086
990
  def tuple_uniq(tuple, _method_name, args)
1087
991
  return nil unless args.empty?
1088
992
  return nil unless tuple.elements.all?(Type::Constant)
@@ -1097,22 +1001,18 @@ module Rigor
1097
1001
  Type::Combinator.tuple_of(*kept)
1098
1002
  end
1099
1003
 
1100
- # `index(obj)` / `find_index(obj)` → `Constant[Integer]` of the
1101
- # first element equal to `obj`, `Constant[nil]` when none match.
1102
- # Folds only for the argument form (the block form defers) when
1103
- # every element AND the argument are `Constant` (decidable
1104
- # equality).
1004
+ # `index(obj)` / `find_index(obj)` → `Constant[Integer]` of the first element equal to `obj`,
1005
+ # `Constant[nil]` when none match. Folds only for the argument form (the block form defers) when
1006
+ # every element AND the argument are `Constant` (decidable equality).
1105
1007
  def tuple_find_index(tuple, _method_name, args)
1106
1008
  constant_index(tuple, args) { |elements, value| elements.index { |e| e.value == value } }
1107
1009
  end
1108
1010
 
1109
- # `tuple.flatten` / `tuple.flatten(depth)` — recursively
1110
- # flattens nested Tuple elements into a single Tuple. With
1111
- # no argument the flatten is unbounded (matching Ruby's
1112
- # `Array#flatten`); a `Constant[Integer]` depth bounds it.
1113
- # Non-Tuple elements (scalars, `Array[T]` nominals, …) pass
1114
- # through unchanged at their level. A non-static depth
1115
- # argument (or a non-Integer one) declines so RBS answers.
1011
+ # `tuple.flatten` / `tuple.flatten(depth)` — recursively flattens nested Tuple elements into a
1012
+ # single Tuple. With no argument the flatten is unbounded (matching Ruby's `Array#flatten`); a
1013
+ # `Constant[Integer]` depth bounds it. Non-Tuple elements (scalars, `Array[T]` nominals, …) pass
1014
+ # through unchanged at their level. A non-static depth argument (or a non-Integer one) declines
1015
+ # so RBS answers.
1116
1016
  def tuple_flatten(tuple, _method_name, args)
1117
1017
  depth = tuple_flatten_depth(args)
1118
1018
  return nil if depth == :decline
@@ -1120,10 +1020,8 @@ module Rigor
1120
1020
  Type::Combinator.tuple_of(*flatten_elements(tuple.elements, depth))
1121
1021
  end
1122
1022
 
1123
- # Returns the requested flatten depth: `-1` for the no-arg
1124
- # (unbounded) form, the Integer for a `Constant[Integer]`
1125
- # argument, or `:decline` for any non-static / wrong-arity
1126
- # argument shape.
1023
+ # Returns the requested flatten depth: `-1` for the no-arg (unbounded) form, the Integer for a
1024
+ # `Constant[Integer]` argument, or `:decline` for any non-static / wrong-arity argument shape.
1127
1025
  def tuple_flatten_depth(args)
1128
1026
  return -1 if args.empty?
1129
1027
  return :decline unless args.size == 1
@@ -1134,10 +1032,9 @@ module Rigor
1134
1032
  :decline
1135
1033
  end
1136
1034
 
1137
- # Flattens a list of element types to `depth` levels.
1138
- # `depth < 0` means unbounded. A Tuple element is spliced
1139
- # in (recursing with `depth - 1`); everything else passes
1140
- # through at this level.
1035
+ # Flattens a list of element types to `depth` levels. `depth < 0` means unbounded. A Tuple
1036
+ # element is spliced in (recursing with `depth - 1`); everything else passes through at this
1037
+ # level.
1141
1038
  def flatten_elements(elements, depth)
1142
1039
  return elements if depth.zero?
1143
1040
 
@@ -1165,10 +1062,8 @@ module Rigor
1165
1062
  Type::Combinator.constant_of(yield(tuple.elements, needle.value))
1166
1063
  end
1167
1064
 
1168
- # `tuple.take(n)` — returns the first n elements as a
1169
- # new Tuple. The argument must be a `Constant[Integer]`.
1170
- # n <= 0 returns the empty Tuple; n >= size returns the
1171
- # full receiver.
1065
+ # `tuple.take(n)` — returns the first n elements as a new Tuple. The argument must be a
1066
+ # `Constant[Integer]`. n <= 0 returns the empty Tuple; n >= size returns the full receiver.
1172
1067
  def tuple_take(tuple, _method_name, args)
1173
1068
  n = non_negative_count_arg(args)
1174
1069
  return nil if n.nil?
@@ -1176,8 +1071,8 @@ module Rigor
1176
1071
  Type::Combinator.tuple_of(*tuple.elements.take(n))
1177
1072
  end
1178
1073
 
1179
- # `drop(n)` → `Tuple` of every element after the first `n`
1180
- # (mirror of `take`; `n >= size` → empty Tuple).
1074
+ # `drop(n)` → `Tuple` of every element after the first `n` (mirror of `take`; `n >= size` → empty
1075
+ # Tuple).
1181
1076
  def tuple_drop(tuple, _method_name, args)
1182
1077
  n = non_negative_count_arg(args)
1183
1078
  return nil if n.nil?
@@ -1185,9 +1080,8 @@ module Rigor
1185
1080
  Type::Combinator.tuple_of(*tuple.elements.drop(n))
1186
1081
  end
1187
1082
 
1188
- # `rotate` (no arg → 1) / `rotate(n)` → `Tuple` of the elements
1189
- # cyclically shifted left by `n` (`Array#rotate` handles negative
1190
- # and out-of-range `n` by modulo, so any Integer arg folds).
1083
+ # `rotate` (no arg → 1) / `rotate(n)` → `Tuple` of the elements cyclically shifted left by `n`
1084
+ # (`Array#rotate` handles negative and out-of-range `n` by modulo, so any Integer arg folds).
1191
1085
  def tuple_rotate(tuple, _method_name, args)
1192
1086
  count =
1193
1087
  if args.empty?
@@ -1201,10 +1095,9 @@ module Rigor
1201
1095
  Type::Combinator.tuple_of(*tuple.elements.rotate(count))
1202
1096
  end
1203
1097
 
1204
- # Unwraps a single non-negative `Constant[Integer]` count argument
1205
- # (the `take` / `drop` / `first(n)` / `last(n)` shape). Returns the
1206
- # Integer, or nil to defer (wrong arity, non-constant, non-Integer,
1207
- # or negative — `Array#take`/`#drop` raise on negative counts).
1098
+ # Unwraps a single non-negative `Constant[Integer]` count argument (the `take` / `drop` /
1099
+ # `first(n)` / `last(n)` shape). Returns the Integer, or nil to defer (wrong arity, non-constant,
1100
+ # non-Integer, or negative `Array#take`/`#drop` raise on negative counts).
1208
1101
  def non_negative_count_arg(args)
1209
1102
  return nil unless args.size == 1
1210
1103
 
@@ -1215,10 +1108,8 @@ module Rigor
1215
1108
  arg.value
1216
1109
  end
1217
1110
 
1218
- # Returns `true` / `false` if every element's truthiness
1219
- # agrees, nil for mixed-or-unknown shapes. `all: true`
1220
- # checks every element is truthy; `all: false` checks
1221
- # every element is falsey.
1111
+ # Returns `true` / `false` if every element's truthiness agrees, nil for mixed-or-unknown shapes.
1112
+ # `all: true` checks every element is truthy; `all: false` checks every element is falsey.
1222
1113
  def tuple_predicate_truthiness(tuple, all:)
1223
1114
  samples = tuple.elements.map { |e| element_truthiness(e) }
1224
1115
  return nil if samples.any?(:unknown)
@@ -1241,9 +1132,8 @@ module Rigor
1241
1132
  elements.any? { |e| e.is_a?(Type::Constant) && e.value == value }
1242
1133
  end
1243
1134
 
1244
- # Per-element Constant value extraction. Returns nil
1245
- # when any element is non-Constant, so the caller can
1246
- # decline.
1135
+ # Per-element Constant value extraction. Returns nil when any element is non-Constant, so the
1136
+ # caller can decline.
1247
1137
  def constant_values(elements)
1248
1138
  return nil unless elements.all?(Type::Constant)
1249
1139
 
@@ -1258,15 +1148,12 @@ module Rigor
1258
1148
  values
1259
1149
  end
1260
1150
 
1261
- # `tuple[i]`, `tuple[range]`, `tuple[start, length]`, and
1262
- # `tuple.fetch(i)` for static arguments. Out-of-range single
1263
- # indices still fall through because the same handler serves
1264
- # `fetch`, while statically nil slices can be represented
1265
- # precisely for `[]`.
1266
- # `[]` and its exact alias `slice` share the index / Range /
1267
- # start-length folding. `fetch` routes here too but stays
1268
- # integer-index-only: the Range and start-length branches gate
1269
- # on this selector set, which `fetch` is deliberately not in.
1151
+ # `tuple[i]`, `tuple[range]`, `tuple[start, length]`, and `tuple.fetch(i)` for static arguments.
1152
+ # Out-of-range single indices still fall through because the same handler serves `fetch`, while
1153
+ # statically nil slices can be represented precisely for `[]`.
1154
+ # `[]` and its exact alias `slice` share the index / Range / start-length folding. `fetch` routes
1155
+ # here too but stays integer-index-only: the Range and start-length branches gate on this
1156
+ # selector set, which `fetch` is deliberately not in.
1270
1157
  SLICE_SELECTORS = Set[:[], :slice].freeze
1271
1158
  private_constant :SLICE_SELECTORS
1272
1159
 
@@ -1316,14 +1203,11 @@ module Rigor
1316
1203
  [range.begin, range.end].all? { |endpoint| endpoint.nil? || endpoint.is_a?(Integer) }
1317
1204
  end
1318
1205
 
1319
- # `tuple.dig(i, ...)` with a chain of static keys/indices.
1320
- # Each step recurses through the resolved member: a Tuple
1321
- # member dispatches `dig` on the remaining args, a HashShape
1322
- # member does the same, and a `Constant[nil]` member ends
1323
- # the chain at `Constant[nil]` (matching Ruby's `Array#dig`
1324
- # short-circuit on nil). Anything else along the chain
1325
- # falls through to the projection answer so the analyzer
1326
- # never invents a value it cannot prove.
1206
+ # `tuple.dig(i, ...)` with a chain of static keys/indices. Each step recurses through the
1207
+ # resolved member: a Tuple member dispatches `dig` on the remaining args, a HashShape member
1208
+ # does the same, and a `Constant[nil]` member ends the chain at `Constant[nil]` (matching Ruby's
1209
+ # `Array#dig` short-circuit on nil). Anything else along the chain falls through to the
1210
+ # projection answer so the analyzer never invents a value it cannot prove.
1327
1211
  def tuple_dig(tuple, _method_name, args)
1328
1212
  return nil if args.empty?
1329
1213
 
@@ -1343,8 +1227,8 @@ module Rigor
1343
1227
  tuple.elements[idx]
1344
1228
  end
1345
1229
 
1346
- # Returns the in-bounds non-negative index, or nil when the
1347
- # raw index falls outside `[-size, size)`.
1230
+ # Returns the in-bounds non-negative index, or nil when the raw index falls outside
1231
+ # `[-size, size)`.
1348
1232
  def normalise_index(raw, size)
1349
1233
  adjusted = raw.negative? ? raw + size : raw
1350
1234
  return nil if adjusted.negative? || adjusted >= size
@@ -1360,13 +1244,10 @@ module Rigor
1360
1244
  Type::Combinator.constant_of(shape.pairs.size)
1361
1245
  end
1362
1246
 
1363
- # `shape.empty?` — folds to a precise bool when the
1364
- # shape's emptiness is statically known. Closed shapes
1365
- # with no optional keys have a fixed size, so empty?
1366
- # is `Constant[shape.pairs.empty?]`. The handler returns
1367
- # `Type::t | nil` (nil signals "no rule, defer to next
1368
- # tier") so the standard predicate-return rubocop rule
1369
- # does not apply.
1247
+ # `shape.empty?` — folds to a precise bool when the shape's emptiness is statically known. Closed
1248
+ # shapes with no optional keys have a fixed size, so empty? is `Constant[shape.pairs.empty?]`.
1249
+ # The handler returns `Type::t | nil` (nil signals "no rule, defer to next tier") so the standard
1250
+ # predicate-return rubocop rule does not apply.
1370
1251
  # rubocop:disable Style/ReturnNilInPredicateMethodDefinition
1371
1252
  def hash_empty?(shape, _method_name, args)
1372
1253
  return nil unless args.empty?
@@ -1376,9 +1257,8 @@ module Rigor
1376
1257
  Type::Combinator.constant_of(shape.pairs.empty?)
1377
1258
  end
1378
1259
 
1379
- # `shape.any?` (no block, no arg) — opposite of
1380
- # `empty?`. The block / arg forms are answered by the
1381
- # RBS / BlockFolding tier.
1260
+ # `shape.any?` (no block, no arg) — opposite of `empty?`. The block / arg forms are answered by
1261
+ # the RBS / BlockFolding tier.
1382
1262
  def hash_any?(shape, _method_name, args)
1383
1263
  return nil unless args.empty?
1384
1264
  return nil unless shape.closed?
@@ -1387,9 +1267,8 @@ module Rigor
1387
1267
  Type::Combinator.constant_of(!shape.pairs.empty?)
1388
1268
  end
1389
1269
 
1390
- # `shape.none?` (no block, no arg) — mirror of `any?`.
1391
- # Folds to `Constant[shape.pairs.empty?]` for closed
1392
- # shapes with no optional keys.
1270
+ # `shape.none?` (no block, no arg) — mirror of `any?`. Folds to `Constant[shape.pairs.empty?]`
1271
+ # for closed shapes with no optional keys.
1393
1272
  def hash_none?(shape, _method_name, args)
1394
1273
  return nil unless args.empty?
1395
1274
  return nil unless shape.closed?
@@ -1398,8 +1277,7 @@ module Rigor
1398
1277
  Type::Combinator.constant_of(shape.pairs.empty?)
1399
1278
  end
1400
1279
 
1401
- # `shape.one?` (no block, no arg) — folds to
1402
- # `Constant[shape.pairs.size == 1]` for a closed shape
1280
+ # `shape.one?` (no block, no arg) — folds to `Constant[shape.pairs.size == 1]` for a closed shape
1403
1281
  # with no optional keys.
1404
1282
  def hash_one?(shape, _method_name, args)
1405
1283
  return nil unless args.empty?
@@ -1409,19 +1287,17 @@ module Rigor
1409
1287
  Type::Combinator.constant_of(shape.pairs.size == 1)
1410
1288
  end
1411
1289
 
1412
- # `shape.deconstruct_keys(keys)` — Ruby's `Hash#deconstruct_keys`
1413
- # returns the receiver itself regardless of the `keys`
1414
- # argument, so the precise answer is the shape unchanged.
1290
+ # `shape.deconstruct_keys(keys)` — Ruby's `Hash#deconstruct_keys` returns the receiver itself
1291
+ # regardless of the `keys` argument, so the precise answer is the shape unchanged.
1415
1292
  def hash_deconstruct_keys(shape, _method_name, args)
1416
1293
  return nil unless args.size == 1
1417
1294
 
1418
1295
  shape
1419
1296
  end
1420
1297
 
1421
- # `shape.fetch_values(:a, :b, ...)` — like `values_at` but
1422
- # raises `KeyError` on a missing key. Folds to `Tuple[V…]`
1423
- # only when every requested key is present; a missing key
1424
- # declines so the RBS tier reflects the raise.
1298
+ # `shape.fetch_values(:a, :b, ...)` — like `values_at` but raises `KeyError` on a missing key.
1299
+ # Folds to `Tuple[V…]` only when every requested key is present; a missing key declines so the
1300
+ # RBS tier reflects the raise.
1425
1301
  def hash_fetch_values(shape, _method_name, args)
1426
1302
  return nil if args.empty?
1427
1303
  return nil unless shape.closed?
@@ -1440,8 +1316,8 @@ module Rigor
1440
1316
  Type::Combinator.tuple_of(*values)
1441
1317
  end
1442
1318
 
1443
- # `shape.assoc(key)` — returns `Tuple[Constant[k], V]` for a
1444
- # known key, `Constant[nil]` for a missing key.
1319
+ # `shape.assoc(key)` — returns `Tuple[Constant[k], V]` for a known key, `Constant[nil]` for a
1320
+ # missing key.
1445
1321
  def hash_assoc(shape, _method_name, args)
1446
1322
  return nil unless args.size == 1
1447
1323
  return nil unless shape.closed?
@@ -1457,11 +1333,9 @@ module Rigor
1457
1333
  Type::Combinator.tuple_of(Type::Combinator.constant_of(key), shape.pairs[key])
1458
1334
  end
1459
1335
 
1460
- # `shape.rassoc(value)` — reverse of `assoc`: returns
1461
- # `Tuple[Constant[k], V]` for the first key whose VALUE equals
1462
- # the argument, `Constant[nil]` when none match. Folds when every
1463
- # value is a `Constant` so equality is decidable (mirrors
1464
- # `hash_key`, which returns only the key).
1336
+ # `shape.rassoc(value)` — reverse of `assoc`: returns `Tuple[Constant[k], V]` for the first key
1337
+ # whose VALUE equals the argument, `Constant[nil]` when none match. Folds when every value is a
1338
+ # `Constant` so equality is decidable (mirrors `hash_key`, which returns only the key).
1465
1339
  def hash_rassoc(shape, _method_name, args)
1466
1340
  return nil unless args.size == 1
1467
1341
  return nil unless shape.closed?
@@ -1477,10 +1351,9 @@ module Rigor
1477
1351
  Type::Combinator.tuple_of(Type::Combinator.constant_of(pair.first), pair.last)
1478
1352
  end
1479
1353
 
1480
- # `shape.key(value)` — reverse lookup. Folds when every
1481
- # value is a `Constant` so equality is decidable: returns
1482
- # `Constant[k]` for the first matching key, `Constant[nil]`
1483
- # when no value matches.
1354
+ # `shape.key(value)` — reverse lookup. Folds when every value is a `Constant` so equality is
1355
+ # decidable: returns `Constant[k]` for the first matching key, `Constant[nil]` when no value
1356
+ # matches.
1484
1357
  def hash_key(shape, _method_name, args)
1485
1358
  return nil unless args.size == 1
1486
1359
  return nil unless shape.closed?
@@ -1494,10 +1367,8 @@ module Rigor
1494
1367
  Type::Combinator.constant_of(pair&.first)
1495
1368
  end
1496
1369
 
1497
- # `shape.has_value?(v)` / `value?(v)` — folds to
1498
- # `Constant[true/false]` when every value is a `Constant`
1499
- # (so equality is decidable) and the argument is a
1500
- # `Constant`.
1370
+ # `shape.has_value?(v)` / `value?(v)` — folds to `Constant[true/false]` when every value is a
1371
+ # `Constant` (so equality is decidable) and the argument is a `Constant`.
1501
1372
  def hash_has_value?(shape, _method_name, args)
1502
1373
  return nil unless args.size == 1
1503
1374
  return nil unless shape.closed?
@@ -1511,11 +1382,9 @@ module Rigor
1511
1382
  Type::Combinator.constant_of(found)
1512
1383
  end
1513
1384
 
1514
- # `shape.default` / `default_proc` — a literal `HashShape`
1515
- # carries no default value or proc, so both fold to
1516
- # `Constant[nil]`. `default` accepts an optional key
1517
- # argument (still returns the default), `default_proc`
1518
- # takes none — the `args.size <= 1` guard covers both.
1385
+ # `shape.default` / `default_proc` — a literal `HashShape` carries no default value or proc, so
1386
+ # both fold to `Constant[nil]`. `default` accepts an optional key argument (still returns the
1387
+ # default), `default_proc` takes none — the `args.size <= 1` guard covers both.
1519
1388
  def hash_default(shape, _method_name, args)
1520
1389
  return nil unless args.size <= 1
1521
1390
  return nil unless shape.closed?
@@ -1523,10 +1392,9 @@ module Rigor
1523
1392
  Type::Combinator.constant_of(nil)
1524
1393
  end
1525
1394
 
1526
- # `shape < other` / `<=` / `>` / `>=` — Hash containment
1527
- # comparison. Both sides must be closed `HashShape`s whose
1528
- # values are all `Constant` (so pair equality is
1529
- # decidable). `<` / `>` are proper-subset / -superset.
1395
+ # `shape < other` / `<=` / `>` / `>=` — Hash containment comparison. Both sides must be closed
1396
+ # `HashShape`s whose values are all `Constant` (so pair equality is decidable). `<` / `>` are
1397
+ # proper-subset / -superset.
1530
1398
  def hash_compare(shape, method_name, args)
1531
1399
  return nil unless args.size == 1
1532
1400
  return nil unless shape.closed? && shape.optional_keys.empty?
@@ -1542,9 +1410,8 @@ module Rigor
1542
1410
  Type::Combinator.constant_of(hash_containment(method_name, left, right))
1543
1411
  end
1544
1412
 
1545
- # Unwraps a closed shape's pairs to a plain Ruby Hash of
1546
- # `key => value` for value-equality comparison. Returns nil
1547
- # when any value is not a `Constant`.
1413
+ # Unwraps a closed shape's pairs to a plain Ruby Hash of `key => value` for value-equality
1414
+ # comparison. Returns nil when any value is not a `Constant`.
1548
1415
  def constant_pairs(shape)
1549
1416
  return nil unless shape.pairs.values.all?(Type::Constant)
1550
1417
 
@@ -1568,10 +1435,9 @@ module Rigor
1568
1435
  left.size < right.size && hash_subset?(left, right)
1569
1436
  end
1570
1437
 
1571
- # `shape.has_key?(k)` / `key?(k)` / `member?(k)` /
1572
- # `include?(k)` folds to `Constant[true/false]` when
1573
- # the argument is a `Constant[Symbol|String]` and the
1574
- # shape is closed with no optional keys.
1438
+ # `shape.has_key?(k)` / `key?(k)` / `member?(k)` / `include?(k)` — folds to `Constant[true/false]`
1439
+ # when the argument is a `Constant[Symbol|String]` and the shape is closed with no optional
1440
+ # keys.
1575
1441
  def hash_has_key?(shape, _method_name, args)
1576
1442
  return nil unless args.size == 1
1577
1443
  return nil unless shape.closed?
@@ -1587,10 +1453,9 @@ module Rigor
1587
1453
  end
1588
1454
  # rubocop:enable Style/ReturnNilInPredicateMethodDefinition
1589
1455
 
1590
- # `shape.keys` — returns a `Tuple[Constant<k>…]` for a
1591
- # closed shape with no optional keys; the Tuple's
1592
- # arity matches the shape's per-key declaration order
1593
- # so downstream `tuple[i]` projections stay precise.
1456
+ # `shape.keys` — returns a `Tuple[Constant<k>…]` for a closed shape with no optional keys; the
1457
+ # Tuple's arity matches the shape's per-key declaration order so downstream `tuple[i]`
1458
+ # projections stay precise.
1594
1459
  def hash_keys(shape, _method_name, args)
1595
1460
  return nil unless args.empty?
1596
1461
  return nil unless shape.closed?
@@ -1599,9 +1464,8 @@ module Rigor
1599
1464
  Type::Combinator.tuple_of(*shape.pairs.keys.map { |k| Type::Combinator.constant_of(k) })
1600
1465
  end
1601
1466
 
1602
- # `shape.values` — returns a `Tuple[V_1, …, V_n]` for a
1603
- # closed shape with no optional keys, the Tuple's arity
1604
- # matching the shape's per-key value order.
1467
+ # `shape.values` — returns a `Tuple[V_1, …, V_n]` for a closed shape with no optional keys, the
1468
+ # Tuple's arity matching the shape's per-key value order.
1605
1469
  def hash_values(shape, _method_name, args)
1606
1470
  return nil unless args.empty?
1607
1471
  return nil unless shape.closed?
@@ -1610,8 +1474,8 @@ module Rigor
1610
1474
  Type::Combinator.tuple_of(*shape.pairs.values)
1611
1475
  end
1612
1476
 
1613
- # `shape.to_a` — returns a per-entry `Tuple[Tuple[K, V], …]`
1614
- # for a closed shape with no optional keys.
1477
+ # `shape.to_a` — returns a per-entry `Tuple[Tuple[K, V], …]` for a closed shape with no optional
1478
+ # keys.
1615
1479
  def hash_to_a(shape, _method_name, args)
1616
1480
  return nil unless args.empty?
1617
1481
  return nil unless shape.closed?
@@ -1623,20 +1487,18 @@ module Rigor
1623
1487
  Type::Combinator.tuple_of(*entries)
1624
1488
  end
1625
1489
 
1626
- # `shape.to_h` — Hash is structurally identical to its
1627
- # to_h (Ruby returns the receiver itself for a Hash).
1490
+ # `shape.to_h` — Hash is structurally identical to its to_h (Ruby returns the receiver itself for
1491
+ # a Hash).
1628
1492
  def hash_to_h(shape, _method_name, args)
1629
1493
  return nil unless args.empty?
1630
1494
 
1631
1495
  shape
1632
1496
  end
1633
1497
 
1634
- # `shape.invert` — swaps keys and values. Folds when
1635
- # every value is a `Constant` whose value is a Symbol
1636
- # or String (the only hashable types that
1637
- # `HashShape` accepts as keys). Duplicate values would
1638
- # alias under inversion, so Rigor declines on
1639
- # collisions rather than silently dropping entries.
1498
+ # `shape.invert` — swaps keys and values. Folds when every value is a `Constant` whose value is a
1499
+ # Symbol or String (the only hashable types that `HashShape` accepts as keys). Duplicate values
1500
+ # would alias under inversion, so Rigor declines on collisions rather than silently dropping
1501
+ # entries.
1640
1502
  def hash_invert(shape, _method_name, args)
1641
1503
  return nil unless args.empty?
1642
1504
  return nil unless shape.closed?
@@ -1652,10 +1514,9 @@ module Rigor
1652
1514
  Type::Combinator.hash_shape_of(inverted)
1653
1515
  end
1654
1516
 
1655
- # `shape.first` — returns the first `[k, v]` pair as a
1656
- # 2-Tuple, or `Constant[nil]` when the shape is empty.
1657
- # Folds only on closed shapes with no optional keys
1658
- # (open shapes might contribute extra keys at runtime).
1517
+ # `shape.first` — returns the first `[k, v]` pair as a 2-Tuple, or `Constant[nil]` when the
1518
+ # shape is empty. Folds only on closed shapes with no optional keys (open shapes might
1519
+ # contribute extra keys at runtime).
1659
1520
  def hash_first(shape, _method_name, args)
1660
1521
  return nil unless args.empty?
1661
1522
  return nil unless shape.closed?
@@ -1666,9 +1527,8 @@ module Rigor
1666
1527
  Type::Combinator.tuple_of(Type::Combinator.constant_of(key), value)
1667
1528
  end
1668
1529
 
1669
- # `shape.flatten` — flattens to `[k_1, v_1, k_2, v_2, …]`
1670
- # at depth 1. Closed shapes only; element order matches
1671
- # the per-key declaration order.
1530
+ # `shape.flatten` — flattens to `[k_1, v_1, k_2, v_2, …]` at depth 1. Closed shapes only; element
1531
+ # order matches the per-key declaration order.
1672
1532
  def hash_flatten(shape, _method_name, args)
1673
1533
  return nil unless args.empty?
1674
1534
  return nil unless shape.closed?
@@ -1678,10 +1538,9 @@ module Rigor
1678
1538
  Type::Combinator.tuple_of(*elements)
1679
1539
  end
1680
1540
 
1681
- # `shape.compact` — drops every entry whose value is
1682
- # `Constant[nil]`. Folds only when every value is a
1683
- # `Constant` (so the drop set is decidable). Mixed-shape
1684
- # values decline so the RBS tier widens.
1541
+ # `shape.compact` — drops every entry whose value is `Constant[nil]`. Folds only when every
1542
+ # value is a `Constant` (so the drop set is decidable). Mixed-shape values decline so the RBS
1543
+ # tier widens.
1685
1544
  def hash_compact(shape, _method_name, args)
1686
1545
  return nil unless args.empty?
1687
1546
  return nil unless shape.closed?
@@ -1692,11 +1551,9 @@ module Rigor
1692
1551
  Type::Combinator.hash_shape_of(kept)
1693
1552
  end
1694
1553
 
1695
- # `shape.merge(other)` — when both sides are closed
1696
- # HashShape with no optional keys, fold to the merged
1697
- # HashShape. Right-hand entries override left-hand
1698
- # entries on key collision (matching Ruby's runtime
1699
- # `Hash#merge`).
1554
+ # `shape.merge(other)` — when both sides are closed HashShape with no optional keys, fold to the
1555
+ # merged HashShape. Right-hand entries override left-hand entries on key collision (matching
1556
+ # Ruby's runtime `Hash#merge`).
1700
1557
  def hash_merge(shape, _method_name, args)
1701
1558
  return nil unless args.size == 1
1702
1559
  return nil unless shape.closed? && shape.optional_keys.empty?
@@ -1708,14 +1565,13 @@ module Rigor
1708
1565
  Type::Combinator.hash_shape_of(shape.pairs.merge(other.pairs))
1709
1566
  end
1710
1567
 
1711
- # `shape[k]` and `shape.fetch(k)` for a static symbol/string
1712
- # key. Missing-key resolution depends on the method:
1568
+ # `shape[k]` and `shape.fetch(k)` for a static symbol/string key. Missing-key resolution depends
1569
+ # on the method:
1713
1570
  #
1714
- # - `[]` returns `nil` at runtime; we surface `Constant[nil]`
1715
- # so the carrier is visible to downstream narrowing.
1716
- # - `fetch` (no default, no block) raises `KeyError`; we let
1717
- # the projection answer apply because the runtime would
1718
- # not produce a value.
1571
+ # - `[]` returns `nil` at runtime; we surface `Constant[nil]` so the carrier is visible to
1572
+ # downstream narrowing.
1573
+ # - `fetch` (no default, no block) raises `KeyError`; we let the projection answer apply because
1574
+ # the runtime would not produce a value.
1719
1575
  def hash_lookup(shape, method_name, args)
1720
1576
  return nil unless args.size == 1
1721
1577
 
@@ -1729,12 +1585,10 @@ module Rigor
1729
1585
  nil
1730
1586
  end
1731
1587
 
1732
- # `shape.dig(:a, :b, ...)` with a chain of static keys.
1733
- # Same recursion semantics as Tuple#`dig`: each step looks
1734
- # up the key, then `chain_dig` continues with the
1735
- # resolved value as the new receiver. Missing keys collapse
1736
- # to `Constant[nil]` (Ruby's `Hash#dig` short-circuits on
1737
- # nil too).
1588
+ # `shape.dig(:a, :b, ...)` with a chain of static keys. Same recursion semantics as Tuple#`dig`:
1589
+ # each step looks up the key, then `chain_dig` continues with the resolved value as the new
1590
+ # receiver. Missing keys collapse to `Constant[nil]` (Ruby's `Hash#dig` short-circuits on nil
1591
+ # too).
1738
1592
  def hash_dig(shape, _method_name, args)
1739
1593
  return nil if args.empty?
1740
1594
 
@@ -1744,10 +1598,9 @@ module Rigor
1744
1598
  chain_dig(step, args.drop(1))
1745
1599
  end
1746
1600
 
1747
- # Returns the per-step value type for a HashShape lookup
1748
- # (or `Constant[nil]` for a known-missing key). Returns
1749
- # `nil` when the argument is not a static symbol/string
1750
- # so the caller can fall through to the projection answer.
1601
+ # Returns the per-step value type for a HashShape lookup (or `Constant[nil]` for a known-missing
1602
+ # key). Returns `nil` when the argument is not a static symbol/string so the caller can fall
1603
+ # through to the projection answer.
1751
1604
  def hash_dig_step(shape, arg)
1752
1605
  return nil unless arg.is_a?(Type::Constant)
1753
1606
 
@@ -1776,11 +1629,9 @@ module Rigor
1776
1629
  !shape.pairs.key?(arg.value)
1777
1630
  end
1778
1631
 
1779
- # `shape.values_at(:a, :b, ...)` with a list of static
1780
- # keys. Returns a `Tuple` whose per-position values are
1781
- # the per-key value types (`Constant[nil]` for missing
1782
- # keys, mirroring Ruby's runtime behaviour). Falls through
1783
- # when any argument is not a static symbol/string.
1632
+ # `shape.values_at(:a, :b, ...)` with a list of static keys. Returns a `Tuple` whose per-position
1633
+ # values are the per-key value types (`Constant[nil]` for missing keys, mirroring Ruby's runtime
1634
+ # behaviour). Falls through when any argument is not a static symbol/string.
1784
1635
  def hash_values_at(shape, _method_name, args)
1785
1636
  return nil if args.empty?
1786
1637
 
@@ -1795,12 +1646,10 @@ module Rigor
1795
1646
  Type::Combinator.tuple_of(*values)
1796
1647
  end
1797
1648
 
1798
- # `shape.slice(:a, :b, ...)` — returns a sub-HashShape
1799
- # containing only the specified keys. All arguments must
1800
- # be `Constant[Symbol|String]`. Keys not present in the
1801
- # shape are silently omitted (matching Ruby's runtime
1802
- # semantics — no nil padding). Declines on open shapes
1803
- # or when any argument is not a static key.
1649
+ # `shape.slice(:a, :b, ...)` — returns a sub-HashShape containing only the specified keys. All
1650
+ # arguments must be `Constant[Symbol|String]`. Keys not present in the shape are silently
1651
+ # omitted (matching Ruby's runtime semantics no nil padding). Declines on open shapes or when
1652
+ # any argument is not a static key.
1804
1653
  def hash_slice(shape, _method_name, args)
1805
1654
  return nil if args.empty?
1806
1655
  return nil unless shape.closed?
@@ -1819,11 +1668,9 @@ module Rigor
1819
1668
  Type::Combinator.hash_shape_of(shape.pairs.slice(*requested))
1820
1669
  end
1821
1670
 
1822
- # `shape.except(:a, :b, ...)` — returns a sub-HashShape
1823
- # with the specified keys removed. All arguments must be
1824
- # `Constant[Symbol|String]`. Keys not present in the shape
1825
- # are silently ignored. Declines on open shapes or when
1826
- # any argument is not a static key.
1671
+ # `shape.except(:a, :b, ...)` — returns a sub-HashShape with the specified keys removed. All
1672
+ # arguments must be `Constant[Symbol|String]`. Keys not present in the shape are silently
1673
+ # ignored. Declines on open shapes or when any argument is not a static key.
1827
1674
  def hash_except(shape, _method_name, args)
1828
1675
  return nil if args.empty?
1829
1676
  return nil unless shape.closed?
@@ -1843,11 +1690,9 @@ module Rigor
1843
1690
  Type::Combinator.hash_shape_of(kept)
1844
1691
  end
1845
1692
 
1846
- # Continues a `dig` chain after the first step. Tuple and
1847
- # HashShape members re-dispatch into the catalogue;
1848
- # `Constant[nil]` short-circuits the chain (Hash#dig and
1849
- # Array#dig do the same at runtime); anything else falls
1850
- # through so the projection answer applies.
1693
+ # Continues a `dig` chain after the first step. Tuple and HashShape members re-dispatch into the
1694
+ # catalogue; `Constant[nil]` short-circuits the chain (Hash#dig and Array#dig do the same at
1695
+ # runtime); anything else falls through so the projection answer applies.
1851
1696
  def chain_dig(receiver, args)
1852
1697
  return receiver if args.empty?
1853
1698