rigortype 0.4.5 → 0.4.7

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 (452) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/gem_overlay/activesupport/core_ext.rbs +16 -13
  4. data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
  5. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
  6. data/docs/handbook/01-getting-started.md +5 -2
  7. data/docs/handbook/11-sig-gen.md +61 -5
  8. data/docs/handbook/appendix-phpstan.md +4 -4
  9. data/docs/manual/02-cli-reference.md +192 -27
  10. data/docs/manual/03-configuration.md +38 -4
  11. data/docs/manual/04-diagnostics.md +1 -1
  12. data/docs/manual/06-baseline.md +6 -4
  13. data/docs/manual/09-editor-integration.md +13 -2
  14. data/docs/manual/10-mcp-server.md +47 -3
  15. data/docs/manual/12-caching.md +9 -1
  16. data/docs/manual/plugins/rigor-actionmailer.md +30 -0
  17. data/docs/manual/plugins/rigor-actionpack.md +49 -11
  18. data/docs/manual/plugins/rigor-activesupport-core-ext.md +20 -1
  19. data/docs/manual/plugins/rigor-devise.md +15 -0
  20. data/docs/manual/plugins/rigor-rspec.md +2 -1
  21. data/lib/rigor/analysis/baseline.rb +24 -27
  22. data/lib/rigor/analysis/buffer_binding.rb +14 -0
  23. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  24. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  25. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +4 -4
  26. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  27. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +105 -12
  28. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +143 -15
  29. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +4 -1
  30. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  31. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +1 -1
  32. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +66 -3
  33. data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
  34. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
  35. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  36. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
  37. data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
  38. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  39. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  40. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  41. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +53 -2
  42. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  43. data/lib/rigor/analysis/check_rules.rb +217 -52
  44. data/lib/rigor/analysis/crash_signature.rb +2 -2
  45. data/lib/rigor/analysis/dependency_recorder.rb +15 -4
  46. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  47. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  48. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  49. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  50. data/lib/rigor/analysis/edit_replay.rb +154 -0
  51. data/lib/rigor/analysis/fact_store.rb +3 -3
  52. data/lib/rigor/analysis/incremental.rb +37 -0
  53. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -2
  54. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  55. data/lib/rigor/analysis/incremental_session.rb +202 -43
  56. data/lib/rigor/analysis/path_expansion.rb +41 -0
  57. data/lib/rigor/analysis/require_relative_closure.rb +100 -0
  58. data/lib/rigor/analysis/rule_catalog.rb +7 -3
  59. data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
  60. data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
  61. data/lib/rigor/analysis/run_stats.rb +2 -2
  62. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +8 -4
  63. data/lib/rigor/analysis/runner/declaration_position.rb +1 -1
  64. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +204 -96
  65. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +1 -1
  66. data/lib/rigor/analysis/runner/pool_coordinator.rb +157 -320
  67. data/lib/rigor/analysis/runner/project_pre_passes.rb +48 -6
  68. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  69. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  70. data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
  71. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  72. data/lib/rigor/analysis/runner.rb +233 -120
  73. data/lib/rigor/analysis/self_call_resolution_recorder.rb +1 -1
  74. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  75. data/lib/rigor/analysis/template_units.rb +9 -1
  76. data/lib/rigor/analysis/worker_session.rb +38 -33
  77. data/lib/rigor/bleeding_edge.rb +3 -6
  78. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  79. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  80. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  81. data/lib/rigor/cache/descriptor.rb +6 -5
  82. data/lib/rigor/cache/incremental_snapshot.rb +60 -18
  83. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  84. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  85. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  86. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  87. data/lib/rigor/cache/rbs_descriptor.rb +54 -3
  88. data/lib/rigor/cache/rbs_environment.rb +4 -1
  89. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  90. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  91. data/lib/rigor/cache/store.rb +11 -10
  92. data/lib/rigor/ci_detector.rb +1 -1
  93. data/lib/rigor/cli/annotate_command.rb +24 -19
  94. data/lib/rigor/cli/baseline_command.rb +145 -55
  95. data/lib/rigor/cli/catalogue.rb +256 -0
  96. data/lib/rigor/cli/check_command.rb +188 -93
  97. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  98. data/lib/rigor/cli/command.rb +15 -1
  99. data/lib/rigor/cli/coverage_command.rb +21 -8
  100. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  101. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  102. data/lib/rigor/cli/coverage_scan.rb +3 -5
  103. data/lib/rigor/cli/diff_command.rb +27 -21
  104. data/lib/rigor/cli/docs_command.rb +1 -2
  105. data/lib/rigor/cli/doctor_command.rb +12 -12
  106. data/lib/rigor/cli/effects_command.rb +50 -40
  107. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  108. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  109. data/lib/rigor/cli/effects_renderer.rb +2 -1
  110. data/lib/rigor/cli/effects_snapshot_command.rb +18 -17
  111. data/lib/rigor/cli/explain_command.rb +18 -8
  112. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  113. data/lib/rigor/cli/json_document.rb +41 -0
  114. data/lib/rigor/cli/lens_command.rb +17 -13
  115. data/lib/rigor/cli/lsp_command.rb +250 -23
  116. data/lib/rigor/cli/mcp_command.rb +14 -8
  117. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  118. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  119. data/lib/rigor/cli/options.rb +60 -4
  120. data/lib/rigor/cli/plugin_command.rb +93 -46
  121. data/lib/rigor/cli/plugins_command.rb +22 -18
  122. data/lib/rigor/cli/plugins_renderer.rb +34 -8
  123. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  124. data/lib/rigor/cli/protection_renderer.rb +2 -1
  125. data/lib/rigor/cli/show_bleedingedge_command.rb +23 -16
  126. data/lib/rigor/cli/sig_gen_command.rb +130 -34
  127. data/lib/rigor/cli/skill_command.rb +32 -3
  128. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  129. data/lib/rigor/cli/skill_describe.rb +1 -1
  130. data/lib/rigor/cli/trace_command.rb +14 -9
  131. data/lib/rigor/cli/triage_command.rb +10 -11
  132. data/lib/rigor/cli/triage_renderer.rb +2 -1
  133. data/lib/rigor/cli/type_of_command.rb +18 -12
  134. data/lib/rigor/cli/type_of_project_scope.rb +24 -0
  135. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  136. data/lib/rigor/cli/type_scan_command.rb +14 -9
  137. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  138. data/lib/rigor/cli/unused_command.rb +27 -17
  139. data/lib/rigor/cli/upgrade_command.rb +5 -0
  140. data/lib/rigor/cli.rb +209 -54
  141. data/lib/rigor/config_audit.rb +17 -6
  142. data/lib/rigor/configuration/severity_profile.rb +5 -5
  143. data/lib/rigor/configuration.rb +86 -34
  144. data/lib/rigor/effects/definition_context.rb +0 -2
  145. data/lib/rigor/effects/envelope.rb +1 -1
  146. data/lib/rigor/effects/framework_units.rb +1 -1
  147. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  148. data/lib/rigor/effects/propagator.rb +1 -1
  149. data/lib/rigor/effects/registry.rb +2 -2
  150. data/lib/rigor/effects/snapshot.rb +1 -1
  151. data/lib/rigor/effects/taint_cause.rb +0 -2
  152. data/lib/rigor/effects/unit_scan.rb +9 -13
  153. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -9
  154. data/lib/rigor/environment/class_registry.rb +3 -3
  155. data/lib/rigor/environment/core_root.rb +77 -0
  156. data/lib/rigor/environment/default_libraries.rb +1 -1
  157. data/lib/rigor/environment/deferred_signature_plan.rb +283 -0
  158. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  159. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  160. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  161. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  162. data/lib/rigor/environment/member_consistency.rb +56 -7
  163. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  164. data/lib/rigor/environment/project_path_gems.rb +124 -0
  165. data/lib/rigor/environment/rbs_hierarchy.rb +6 -15
  166. data/lib/rigor/environment/rbs_loader.rb +592 -70
  167. data/lib/rigor/environment/reflection.rb +7 -7
  168. data/lib/rigor/environment/required_features.rb +136 -32
  169. data/lib/rigor/environment/ruby_library_roots.rb +122 -0
  170. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  171. data/lib/rigor/environment.rb +44 -11
  172. data/lib/rigor/flow_contribution/merger.rb +4 -4
  173. data/lib/rigor/inference/acceptance.rb +93 -32
  174. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  175. data/lib/rigor/inference/block_run_count.rb +178 -0
  176. data/lib/rigor/inference/budget_trace.rb +8 -2
  177. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  178. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  179. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  180. data/lib/rigor/inference/callee_mutation_chain.rb +126 -0
  181. data/lib/rigor/inference/callee_write_census.rb +815 -0
  182. data/lib/rigor/inference/construction_escape.rb +217 -0
  183. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  184. data/lib/rigor/inference/deep_erasure.rb +170 -0
  185. data/lib/rigor/inference/def_handle.rb +1 -1
  186. data/lib/rigor/inference/def_node_resolver.rb +1 -1
  187. data/lib/rigor/inference/def_return_typer.rb +204 -12
  188. data/lib/rigor/inference/definer_resolution.rb +21 -5
  189. data/lib/rigor/inference/definers_index.rb +106 -0
  190. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  191. data/lib/rigor/inference/element_read_widening.rb +49 -3
  192. data/lib/rigor/inference/error_info.rb +17 -3
  193. data/lib/rigor/inference/expression_typer.rb +173 -57
  194. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  195. data/lib/rigor/inference/global_write_census.rb +110 -1
  196. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  197. data/lib/rigor/inference/hkt_body.rb +1 -2
  198. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  199. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  200. data/lib/rigor/inference/hkt_registry.rb +3 -5
  201. data/lib/rigor/inference/indexed_narrowing.rb +4 -7
  202. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  203. data/lib/rigor/inference/ivar_writer_census.rb +246 -0
  204. data/lib/rigor/inference/key_presence_guard.rb +2 -1
  205. data/lib/rigor/inference/loop_value_record.rb +57 -0
  206. data/lib/rigor/inference/match_rebinding/frame.rb +65 -0
  207. data/lib/rigor/inference/match_rebinding/yielding_match.rb +45 -0
  208. data/lib/rigor/inference/match_rebinding.rb +11 -1
  209. data/lib/rigor/inference/memo_audit.rb +55 -0
  210. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  211. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  212. data/lib/rigor/inference/method_dispatcher/block_folding.rb +1 -1
  213. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +113 -12
  214. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  215. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  216. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -3
  217. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  218. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +39 -27
  219. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  220. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +203 -17
  221. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  222. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  223. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +2 -2
  224. data/lib/rigor/inference/method_dispatcher/set_folding.rb +59 -24
  225. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +83 -20
  226. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  227. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  228. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  229. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  230. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  231. data/lib/rigor/inference/method_dispatcher.rb +109 -29
  232. data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
  233. data/lib/rigor/inference/module_function_state.rb +23 -1
  234. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  235. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  236. data/lib/rigor/inference/mutation_widening.rb +162 -29
  237. data/lib/rigor/inference/narrowing.rb +113 -38
  238. data/lib/rigor/inference/object_mixins.rb +13 -1
  239. data/lib/rigor/inference/optimistic_origin.rb +1 -1
  240. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  241. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  242. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  243. data/lib/rigor/inference/project_method_ownership.rb +6 -3
  244. data/lib/rigor/inference/project_seed_tables.rb +2 -1
  245. data/lib/rigor/inference/project_yielder.rb +122 -0
  246. data/lib/rigor/inference/receiver_alias.rb +20 -0
  247. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +3 -1
  248. data/lib/rigor/inference/refined_dispatch/own_method.rb +2 -1
  249. data/lib/rigor/inference/remover_holder_scan.rb +233 -0
  250. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  251. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  252. data/lib/rigor/inference/scope_indexer/certainty.rb +9 -1
  253. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  254. data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
  255. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +205 -0
  256. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  257. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  258. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +135 -0
  259. data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
  260. data/lib/rigor/inference/scope_indexer.rb +614 -146
  261. data/lib/rigor/inference/self_call_ivar_writes.rb +101 -17
  262. data/lib/rigor/inference/set_mutation.rb +189 -0
  263. data/lib/rigor/inference/singleton_hook_decline.rb +1 -2
  264. data/lib/rigor/inference/statement_evaluator.rb +3036 -278
  265. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  266. data/lib/rigor/inference/synthetic_method_index.rb +23 -0
  267. data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
  268. data/lib/rigor/inference/synthetic_method_scanner.rb +73 -14
  269. data/lib/rigor/inference/toplevel_def_census.rb +59 -0
  270. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  271. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  272. data/lib/rigor/inference/void_tail_summary.rb +2 -1
  273. data/lib/rigor/language_server/buffer_table.rb +9 -3
  274. data/lib/rigor/language_server/completion_provider.rb +26 -20
  275. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  276. data/lib/rigor/language_server/diagnostic_publisher.rb +28 -9
  277. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  278. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  279. data/lib/rigor/language_server/hover_provider.rb +1 -7
  280. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  281. data/lib/rigor/language_server/project_context.rb +98 -4
  282. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  283. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  284. data/lib/rigor/language_server/server.rb +12 -6
  285. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  286. data/lib/rigor/lens/plugins.rb +1 -1
  287. data/lib/rigor/mcp/server.rb +82 -3
  288. data/lib/rigor/mcp.rb +1 -1
  289. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  290. data/lib/rigor/plugin/base.rb +26 -11
  291. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  292. data/lib/rigor/plugin/io_boundary.rb +1 -1
  293. data/lib/rigor/plugin/loader.rb +25 -8
  294. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  295. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  296. data/lib/rigor/plugin/manifest.rb +38 -16
  297. data/lib/rigor/plugin/registry.rb +41 -3
  298. data/lib/rigor/plugin/services.rb +2 -3
  299. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  300. data/lib/rigor/plugin/trust_policy.rb +2 -2
  301. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  302. data/lib/rigor/plugin.rb +6 -8
  303. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  304. data/lib/rigor/project_environment.rb +16 -2
  305. data/lib/rigor/protection/analysis_guard.rb +1 -1
  306. data/lib/rigor/protection/discovery_seed.rb +96 -2
  307. data/lib/rigor/protection/mutator.rb +1 -1
  308. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  309. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  310. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  311. data/lib/rigor/rbs_extended.rb +2 -3
  312. data/lib/rigor/reflection/constant_ancestors.rb +5 -3
  313. data/lib/rigor/rooted_glob.rb +25 -2
  314. data/lib/rigor/run_scoped_slots.rb +38 -0
  315. data/lib/rigor/runtime/jit.rb +1 -1
  316. data/lib/rigor/scope/discovery_index.rb +19 -6
  317. data/lib/rigor/scope/resolution_chain.rb +3 -1
  318. data/lib/rigor/scope.rb +379 -36
  319. data/lib/rigor/sig_gen/classification.rb +4 -0
  320. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  321. data/lib/rigor/sig_gen/generator.rb +668 -84
  322. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  323. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  324. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  325. data/lib/rigor/sig_gen/method_candidate.rb +23 -4
  326. data/lib/rigor/sig_gen/observation_collector.rb +7 -6
  327. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  328. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  329. data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
  330. data/lib/rigor/sig_gen/renderer.rb +21 -12
  331. data/lib/rigor/sig_gen/skip_reason_catalog.rb +15 -2
  332. data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
  333. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  334. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  335. data/lib/rigor/sig_gen/write_result.rb +23 -12
  336. data/lib/rigor/sig_gen/writer.rb +217 -37
  337. data/lib/rigor/type/acceptance_router.rb +2 -2
  338. data/lib/rigor/type/accepts_result.rb +1 -1
  339. data/lib/rigor/type/app.rb +4 -6
  340. data/lib/rigor/type/combinator.rb +13 -16
  341. data/lib/rigor/type/constant.rb +48 -7
  342. data/lib/rigor/type/hash_shape.rb +26 -40
  343. data/lib/rigor/type/struct_instance.rb +3 -2
  344. data/lib/rigor/type/union.rb +1 -1
  345. data/lib/rigor/type_node/identifier.rb +1 -2
  346. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  347. data/lib/rigor/type_node/string_literal.rb +1 -2
  348. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  349. data/lib/rigor/type_node/union.rb +3 -6
  350. data/lib/rigor/value_semantics.rb +1 -10
  351. data/lib/rigor/version.rb +1 -1
  352. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  353. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  354. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  355. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +89 -70
  356. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  357. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +25 -4
  358. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  359. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  360. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  361. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  362. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  363. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +79 -19
  364. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  365. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +6 -3
  366. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  367. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  368. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  369. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  370. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +2 -3
  371. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +0 -1
  372. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +5 -4
  373. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  374. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  375. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +7 -12
  376. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  377. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  378. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  379. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +19 -6
  380. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +26 -13
  381. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  382. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
  383. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  384. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  385. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  386. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +7 -6
  387. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  388. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  389. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  390. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  391. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  392. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  393. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  394. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -5
  395. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  396. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  397. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  398. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  399. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -2
  400. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  401. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  402. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  403. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  404. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  405. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +1 -3
  406. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  407. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  408. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  409. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  410. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  411. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
  412. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  413. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  414. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  415. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +5 -2
  416. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  417. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  418. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +1 -1
  419. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  420. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +17 -21
  421. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  422. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +3 -2
  423. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  424. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
  425. data/sig/rigor/analysis/baseline.rbs +0 -2
  426. data/sig/rigor/analysis/fact_store.rbs +1 -0
  427. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  428. data/sig/rigor/cli/diff_command.rbs +1 -1
  429. data/sig/rigor/cli/explain_command.rbs +1 -1
  430. data/sig/rigor/effects/file_collection.rbs +5 -6
  431. data/sig/rigor/effects/registry.rbs +8 -6
  432. data/sig/rigor/effects/summary.rbs +6 -5
  433. data/sig/rigor/environment.rbs +5 -3
  434. data/sig/rigor/inference/optimistic_origin.rbs +11 -0
  435. data/sig/rigor/inference.rbs +10 -1
  436. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  437. data/sig/rigor/plugin/base.rbs +1 -0
  438. data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
  439. data/sig/rigor/plugin/effect_attribution.rbs +25 -10
  440. data/sig/rigor/plugin/effect_edge.rbs +12 -5
  441. data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
  442. data/sig/rigor/plugin/io_boundary.rbs +6 -5
  443. data/sig/rigor/plugin/manifest.rbs +16 -18
  444. data/sig/rigor/plugin/protocol_contract.rbs +6 -9
  445. data/sig/rigor/plugin/registry.rbs +1 -1
  446. data/sig/rigor/scope.rbs +51 -50
  447. data/sig/rigor/type.rbs +0 -6
  448. data/skills/rigor-plugin-author/SKILL.md +4 -4
  449. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  450. data/skills/rigor-plugin-review/SKILL.md +3 -3
  451. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +12 -1
  452. metadata +55 -3
@@ -16,6 +16,7 @@ require_relative "environment/bundle_sig_discovery"
16
16
  require_relative "environment/lockfile_resolver"
17
17
  require_relative "environment/installed_gem_set"
18
18
  require_relative "environment/rbs_collection_discovery"
19
+ require_relative "environment/project_path_gems"
19
20
  require_relative "environment/source_rbs_synthesis"
20
21
  require_relative "plugin/isolation"
21
22
  require_relative "cache/engine_source"
@@ -39,7 +40,7 @@ module Rigor
39
40
  DEFAULT_PROJECT_SIG_DIR = "sig"
40
41
  private_constant :DEFAULT_PROJECT_SIG_DIR
41
42
 
42
- # `DEFAULT_LIBRARIES` is defined in the light `environment/default_libraries.rb` (required below) so the
43
+ # `DEFAULT_LIBRARIES` is defined in the light `environment/default_libraries.rb` (required above) so the
43
44
  # ADR-87 WD4 boot-slimming probe can read it without loading the inference engine.
44
45
 
45
46
  # ADR-72 — a Gemfile.lock gem name mapped to the opt-in plugin id that ships the SAME core-ext RBS. When
@@ -146,7 +147,7 @@ module Rigor
146
147
  # code (`Plugin::Registry#hkt_overlay_registry` reads each manifest); before this it sat one line ABOVE
147
148
  # the #784 rescue, so a raise there escaped {#hkt_registry} entirely. Post-#788 that is not a per-file
148
149
  # crash storm but an uncaught abort: {HktRegistryHolder#fetch} memoises only on success, so the raise
149
- # is re-attempted at every demand, and the run-owned demands (`Runner::PoolCoordinator#hkt_scan_outcome`
150
+ # is re-attempted at every demand, and the run-owned demands (`Runner::RunLevelFacts::Collect.hkt_scan_failure`
150
151
  # after the file loop, `Analysis::WorkerSession#drain_reporters`) sit outside any rescue.
151
152
  #
152
153
  # The degradation is narrower than the scan's: only the plugin overlay is dropped, and the RBS scan
@@ -265,10 +266,12 @@ module Rigor
265
266
  # `nil` (the default), the canonical project layout `<root>/sig` is used if it exists, otherwise no
266
267
  # signature path is loaded.
267
268
  # @param cache_store — persistent cache threaded into the underlying
268
- # {Environment::RbsLoader} so constant lookups (and, in later v0.0.9 slices, other reflection
269
- # artefacts) consult the cache. Pass `nil` (the default) to skip caching for this environment.
269
+ # {Environment::RbsLoader} so constant lookups consult the cache. Pass `nil` (the default) to skip
270
+ # caching for this environment.
271
+ # @param core_root — issue #2517 — a core signature directory loaded in place of the `rbs` gem's `core/`
272
+ # ({Configuration#core_root}); nil (the default) loads the gem's core.
270
273
  # rubocop:disable-next Metrics/MethodLength, Metrics/ParameterLists
271
- def for_project(root: Dir.pwd, libraries: [], signature_paths: nil, cache_store: nil,
274
+ def for_project(root: Dir.pwd, libraries: [], signature_paths: nil, cache_store: nil, # rubocop:disable Metrics/AbcSize
272
275
  plugin_registry: nil, dependency_source_index: nil,
273
276
  rbs_extended_reporter: nil, boundary_cross_reporter: nil,
274
277
  source_rbs_synthesis_reporter: nil,
@@ -276,7 +279,8 @@ module Rigor
276
279
  bundler_lockfile: nil,
277
280
  rbs_collection_lockfile: nil, rbs_collection_auto_detect: false,
278
281
  synthetic_method_index: nil, project_patched_methods: nil,
279
- source_files: [], locked_gems: nil, required_features: nil)
282
+ source_files: [], locked_gems: nil, required_features: nil, core_root: nil,
283
+ paths: nil, exclude_patterns: nil, exclude_base: nil)
280
284
  resolved_paths = project_signature_roots(signature_paths, root: root)
281
285
  # O4 MVP — append per-gem `sig/` directories discovered under the target project's bundler install
282
286
  # root. Empty array when neither an explicit path nor auto-detection finds a bundle. Order: user
@@ -318,7 +322,11 @@ module Rigor
318
322
  # Issue #1700 — plus the gated vendored directories this run's source activates ({#feature_tokens}).
319
323
  merged_libraries = (DEFAULT_LIBRARIES + libraries.map(&:to_s) +
320
324
  feature_tokens(required_features, source_files, libraries)).uniq
321
- skip_gem_names = collection_skip_gem_names(merged_libraries)
325
+ # Issue #2441 — plus the project's own path gems ({ProjectPathGems}), whose collection copy would
326
+ # double-declare the project's `sig/`.
327
+ project_gems = ProjectPathGems.names(locked, paths: paths, root: root, exclude_patterns: exclude_patterns,
328
+ exclude_base: exclude_base || root)
329
+ skip_gem_names = collection_skip_gem_names(merged_libraries) + project_gems.to_a
322
330
  collection_paths = RbsCollectionDiscovery.discover(
323
331
  lockfile_path: rbs_collection_lockfile,
324
332
  project_root: root,
@@ -338,8 +346,10 @@ module Rigor
338
346
  # opt-in plugin twin is REACHABLE — loaded as a plugin, or wired in by `signature_paths:` (#672) —
339
347
  # so the two never both declare the methods. The paths ride in `loader_signature_paths`, so the env
340
348
  # cache descriptor digests them for free.
349
+ # Issue #2441 — the project's own path gems are neither missing RBS nor overlay-eligible: the project's
350
+ # `sig/` declares them, and an overlay (`data/gem_overlay/activesupport`) would collide with it.
341
351
  missing_gems, overlay_paths = missing_gems_and_overlay_paths(
342
- locked: locked, default_libraries: merged_libraries, bundle_sig_paths: gem_sig_paths,
352
+ locked: locked.except(*project_gems), default_libraries: merged_libraries, bundle_sig_paths: gem_sig_paths,
343
353
  rbs_collection_paths: collection_paths, plugin_registry: plugin_registry,
344
354
  signature_paths: resolved_paths, bundle_root: bundle_root
345
355
  )
@@ -364,7 +374,10 @@ module Rigor
364
374
  # Issue #610 — the plugin-contributed subset is loaded LAST and may stand down against a colliding
365
375
  # generic arity. Only the bundled plugins' own `sig/` is deferred: the user's `signature_paths:`,
366
376
  # the bundle walk and `rbs collection install` all stay authoritative.
367
- deferred_signature_paths: plugin_sig_paths
377
+ deferred_signature_paths: plugin_sig_paths,
378
+ project_signature_paths: resolved_paths,
379
+ required_features: required_features || RequiredFeatures.scan(source_files, sig_paths: resolved_paths),
380
+ core_root: core_root
368
381
  )
369
382
  # Issues #824 / #1075 — the rows ADR-112 WD5's consistency rule reports for members the project's
370
383
  # own `sig/` and an inline source both declare: an error per contradiction (or per refinement that
@@ -374,6 +387,10 @@ module Rigor
374
387
  # on every run, so the member-level outcome needs no plumbing of its own. Costs nothing for a project
375
388
  # with no inline RBS: the loader's reader short-circuits on an empty `virtual_rbs`.
376
389
  record_member_consistency(loader, source_rbs_synthesis_reporter, root)
390
+ # Issue #2517 — a selected core is built here, not lazily. The env is first demanded from inside a file's
391
+ # analysis, under rescues that turn any raise into one `internal analyzer error` row per file; building it
392
+ # now raises the {CoreRootError} once, before analysis, where the CLI reports it as a configuration error.
393
+ loader.verify_core_root!
377
394
  # ADR-20 slice 2c + 2e — seed hkt_registry with the bundled builtins. The Environment's
378
395
  # `#hkt_registry` getter then LAZILY merges in the RBS env scan on first call so fast paths that
379
396
  # don't consult HKT (e.g. `rigor check --cache-stats --no-stats`) don't pay the eager env-build cost
@@ -490,7 +507,7 @@ module Rigor
490
507
  end
491
508
 
492
509
  # ADR-72 — resolve the bundled RBS overlay directories to load for this project. A gem is eligible when
493
- # it is classified `:missing` (see {missing_rbs_gem_rows}) and its conflicting opt-in plugin twin (if
510
+ # it is classified `:missing` (see {missing_rows}) and its conflicting opt-in plugin twin (if
494
511
  # any) is NOT already reachable. Returns `[Pathname]`, deterministically ordered, or `[]` when no
495
512
  # eligible gem.
496
513
  #
@@ -642,6 +659,8 @@ module Rigor
642
659
  def record_member_undecided(reporter, record, root)
643
660
  plugin_id, path = split_virtual_source_name(record.virtual_name)
644
661
  return if path.nil?
662
+ return record_variable_undecided(reporter, record, root, plugin_id, path) if
663
+ MemberConsistency.variable_kind?(record.kind)
645
664
 
646
665
  reporter.record(
647
666
  plugin_id: plugin_id, path: path, kind: :not_honoured,
@@ -654,8 +673,22 @@ module Rigor
654
673
  )
655
674
  end
656
675
 
676
+ # Issue #2440 — an instance variable declared by both sides with different types. `sig/` binds
677
+ # (the coordinator's ruling on #2440, ADR-5): the row is `:info`, never `rbs.contradicting-signature`.
678
+ def record_variable_undecided(reporter, record, root, plugin_id, path)
679
+ reporter.record(
680
+ plugin_id: plugin_id, path: path, kind: :not_honoured,
681
+ message: "`#{member_label(record)}` is also declared in " \
682
+ "`#{path_relative_to(record.signature_path, root)}` with a different type " \
683
+ "(#{record.detail}), so the `.rbs` declaration binds and the inline one was dropped. " \
684
+ "RBS rejects two declarations of one instance variable, so only one can stay; " \
685
+ "regenerate the committed `.rbs` (`rbs-inline --output`) or align the two types."
686
+ )
687
+ end
688
+
689
+ # `Demo#m` / `Demo.m` for a method, `Demo#@x` / `Demo.@x` for an instance / class instance variable.
657
690
  def member_label(record)
658
- separator = record.kind == :singleton ? "." : "#"
691
+ separator = %i[singleton class_instance_variable].include?(record.kind) ? "." : "#"
659
692
  "#{record.class_name}#{separator}#{record.method_name}"
660
693
  end
661
694
 
@@ -26,13 +26,13 @@ module Rigor
26
26
  #
27
27
  # ## Composition rules (ADR-2)
28
28
  #
29
- # - `:return_type` — Intersect via `Type::Combinator.intersection`; collapse to bot raises
30
- # `:return_type_collapse`.
29
+ # - `:return_type` — Intersect via `Type::Combinator.intersection`; collapse to bot records a
30
+ # `:return_type_collapse` conflict.
31
31
  # - `:truthy_fact` / `:falsey_fact` / `:post_return_fact` — Edge-local; accumulate while deduping by
32
32
  # payload equality.
33
33
  # - `:mutation` / `:invalidation` / `:role` — Union; dedupe by equality.
34
- # - `:exception` — Single-valued. Two non-`nil` non-equal exceptional effects raise
35
- # `:exceptional_disagreement`.
34
+ # - `:exception` — Single-valued. Two non-`nil` non-equal exceptional effects record an
35
+ # `:exceptional_disagreement` conflict.
36
36
  #
37
37
  # ## Cross-tier contradictions
38
38
  #
@@ -75,41 +75,66 @@ module Rigor
75
75
  }.freeze
76
76
  private_constant :TYPE_HANDLERS
77
77
 
78
- UNREAD_HASH_REFUSED_KEY = :__rigor_acceptance_unread_hash_refused__
79
- private_constant :UNREAD_HASH_REFUSED_KEY
80
-
81
- # Whether a `maybe` for `param` against `arg` rests on a record that could not read a hash's entries: a
82
- # `Hash` with a gradual arm ({#gradual_hash?}) or an open `HashShape`. The record cannot prove the key set
83
- # of either, and its `maybe` climbs through whatever holds the record. `OverloadSelector`'s strict pass
84
- # reads that `maybe` as no evidence for the overload, so this re-asks with each such record answering
85
- # `no` and reports whether the pair then fails. A `maybe` from anywhere else (a subclass only the Ruby
86
- # source declares, a splat under a parameter that is no record) survives the re-ask and still counts.
87
- def maybe_rests_on_unread_hash?(param, arg)
88
- return false unless contains_type?(param) { |type| type.is_a?(Type::HashShape) }
89
- return false unless contains_type?(arg) { |type| unread_hash?(type) }
90
-
91
- refusing_unread_hashes { accepts(param, arg, mode: :gradual).no? }
78
+ UNREAD_SHAPE_REFUSED_KEY = :__rigor_acceptance_unread_shape_refused__
79
+ private_constant :UNREAD_SHAPE_REFUSED_KEY
80
+
81
+ # Whether a `maybe` for `param` against `arg` rests on a shape that could not read its argument: a record
82
+ # against a `Hash` with a gradual arm ({#gradual_hash?}) or an open `HashShape`, or a tuple against an
83
+ # `Array` of unknown arity ({#array_carrier?}). The shape cannot prove the key set or the length, and its
84
+ # `maybe` climbs through whatever holds it. `OverloadSelector`'s strict pass reads that `maybe` as no
85
+ # evidence for the overload, so this re-asks with each such shape answering `no` and reports whether the
86
+ # pair then fails. A `maybe` from anywhere else (a subclass only the Ruby source declares, a splat under a
87
+ # parameter that is no shape) survives the re-ask and still counts.
88
+ def maybe_rests_on_unread_shape?(param, arg)
89
+ return false unless rests_on_unread_hash?(param, arg) || rests_on_unread_array?(param, arg)
90
+
91
+ refusing_unread(:all) { accepts(param, arg, mode: :gradual).no? }
92
92
  end
93
93
 
94
+ # Whether any of `types` holds an `Array` a tuple cannot read ({#array_carrier?}), so a match may rest on
95
+ # the tuple's `maybe`. `OverloadSelector`'s gradual pass asks before re-matching under
96
+ # {#refusing_unread_arrays}.
97
+ def unread_array_in?(types)
98
+ types.any? { |type| contains_type?(type) { |inner| array_carrier?(inner) } }
99
+ end
100
+
101
+ # Runs the block with each tuple answering `no`, not `maybe`, for an `Array` of unknown arity. A record's
102
+ # `maybe` for an unread hash is untouched.
103
+ def refusing_unread_arrays(&) = refusing_unread(:array, &)
104
+
94
105
  # rubocop:disable-next Metrics/ClassLength
95
106
  class << self
96
107
  private
97
108
 
98
- # The flag lives on the thread, not on a parameter, because the record may sit any number of `accepts`
109
+ def rests_on_unread_hash?(param, arg)
110
+ contains_type?(param) { |type| type.is_a?(Type::HashShape) } &&
111
+ contains_type?(arg) { |type| unread_hash?(type) }
112
+ end
113
+
114
+ def rests_on_unread_array?(param, arg)
115
+ contains_type?(param) { |type| type.is_a?(Type::Tuple) } &&
116
+ contains_type?(arg) { |type| array_carrier?(type) }
117
+ end
118
+
119
+ # The flag lives on the thread, not on a parameter, because the shape may sit any number of `accepts`
99
120
  # calls below the one asked (a tuple element, a `Hash` value, a union member), and those calls go
100
- # through each carrier's `Type#accepts`. It is set only for the duration of the block.
101
- def refusing_unread_hashes
102
- previous = Thread.current[UNREAD_HASH_REFUSED_KEY]
103
- Thread.current[UNREAD_HASH_REFUSED_KEY] = true
121
+ # through each carrier's `Type#accepts`. It is set only for the duration of the block. `kind` is `:all`
122
+ # (records and tuples) or `:array` (tuples only).
123
+ def refusing_unread(kind)
124
+ previous = Thread.current[UNREAD_SHAPE_REFUSED_KEY]
125
+ Thread.current[UNREAD_SHAPE_REFUSED_KEY] = kind
104
126
  yield
105
127
  ensure
106
- Thread.current[UNREAD_HASH_REFUSED_KEY] = previous
128
+ Thread.current[UNREAD_SHAPE_REFUSED_KEY] = previous
107
129
  end
108
130
 
109
- # The `maybe` a record answers for a hash whose entries it cannot read, or `no` inside
110
- # {#refusing_unread_hashes}.
111
- def unread_hash_maybe(mode, reason)
112
- return hash_shape_no(mode, "#{reason} (refused as evidence)") if Thread.current[UNREAD_HASH_REFUSED_KEY]
131
+ # The `maybe` a shape of `kind` (`:hash` or `:array`) answers for an argument it cannot read, or `no`
132
+ # inside {#refusing_unread} for that kind.
133
+ def unread_shape_maybe(mode, reason, kind)
134
+ refused = Thread.current[UNREAD_SHAPE_REFUSED_KEY]
135
+ if refused == :all || refused == kind
136
+ return Type::AcceptsResult.no(mode: mode, reasons: "#{reason} (refused as evidence)")
137
+ end
113
138
 
114
139
  Type::AcceptsResult.maybe(mode: mode, reasons: reason)
115
140
  end
@@ -469,9 +494,10 @@ module Rigor
469
494
  end
470
495
 
471
496
  # Returns `other_type` rewritten so its type_args have the same arity as `self_type.type_args`, or
472
- # `nil` if no projection is known. Today only the Hash → Enumerable projection is hand-rolled; a
473
- # general RBS-driven implementation that consults `definition.ancestors[i].args` for arbitrary
474
- # subclass / module-include relations is the principled follow-up.
497
+ # `nil` if no projection is known. Today the Hash → Enumerable and ArithmeticSequence → Enumerator
498
+ # projections are hand-rolled; a general RBS-driven implementation that consults
499
+ # `definition.ancestors[i].args` for arbitrary subclass / module-include relations is the principled
500
+ # follow-up.
475
501
  def project_to_target_arity(self_type, other_type)
476
502
  return nil if self_type.type_args.size == other_type.type_args.size
477
503
  return nil if self_type.type_args.empty? || other_type.type_args.empty?
@@ -881,8 +907,8 @@ module Rigor
881
907
  # `Refined[base, predicate]` accepts another type X when the base accepts the *base* of X *and* X is
882
908
  # provably contained in the predicate's value set. The base check is delegated to
883
909
  # `accepts(self.base, X.base)` so handlers like `accepts_nominal` see Nominal-vs-Nominal and return
884
- # their normal answer (the inner `accepts_nominal` does not register `Refined` / `Difference` as
885
- # direct other-shapes — projecting to the base is what makes the comparison meaningful).
910
+ # their normal answer (`accepts_nominal` projects a `Refined` / `Difference` other onto its base,
911
+ # which is what makes the comparison meaningful).
886
912
  #
887
913
  # Provability rules in gradual mode (the conservative analogue of `accepts_difference`):
888
914
  #
@@ -1003,9 +1029,12 @@ module Rigor
1003
1029
 
1004
1030
  # Tuple[A1..An] accepts:
1005
1031
  # - Tuple[B1..Bn] when arities match and each Ai accepts Bi (covariant per-position).
1006
- # - Anything else: no (we cannot prove the arity from a generic nominal alone).
1032
+ # - An `Array[E]` (see {#array_element_verdict}): maybe, or no on an element contradiction.
1033
+ # - Anything else: no.
1007
1034
  def accepts_tuple(self_type, other_type, mode)
1008
1035
  unless other_type.is_a?(Type::Tuple)
1036
+ return array_element_verdict(self_type, other_type, mode) if array_carrier?(other_type)
1037
+
1009
1038
  return Type::AcceptsResult.no(
1010
1039
  mode: mode,
1011
1040
  reasons: "Tuple does not accept #{other_type.class}"
@@ -1025,6 +1054,38 @@ module Rigor
1025
1054
  combine_arg_results(per_element, mode)
1026
1055
  end
1027
1056
 
1057
+ # An `Array` nominal, directly or as the base of a `Difference` (`non-empty-array[E]`). Its arity is
1058
+ # unknown, so a tuple cannot reject it on length: `URI.encode_www_form` takes
1059
+ # `Enumerable[[_ToS, _ToS]]` and is handed the `Array[Array[String]]` that `URI.decode_www_form`
1060
+ # answers. An `Array` subclass is not read, as {#gradual_hash?} does not read a `Hash` subclass.
1061
+ def array_carrier?(type)
1062
+ nominal = type.is_a?(Type::Difference) ? type.base : type
1063
+ nominal.is_a?(Type::Nominal) && nominal.class_name == "Array"
1064
+ end
1065
+
1066
+ # `maybe`, unless the element type rules the tuple out at any length: some position accepts no member
1067
+ # of `E` (`[Integer, Integer]` never takes an `Array[String]`). A raw `Array` stays `maybe`, and so does
1068
+ # an element with a gradual arm, since no position rejects a `Dynamic` member. The accepted cost (ADR-5)
1069
+ # is that a wrong-length `Array[E]` passed or returned where a tuple is declared goes unreported.
1070
+ def array_element_verdict(self_type, other_type, mode)
1071
+ nominal = other_type.is_a?(Type::Difference) ? other_type.base : other_type
1072
+ element = nominal.type_args.first
1073
+ unless element.nil?
1074
+ members = ContentJoin.union_members(element)
1075
+ refused = self_type.elements.find do |formal|
1076
+ members.all? { |member| accepts(formal, member, mode: mode).no? }
1077
+ end
1078
+ if refused
1079
+ return Type::AcceptsResult.no(
1080
+ mode: mode,
1081
+ reasons: "tuple member #{refused.describe(:short)} rejects #{element.describe(:short)}"
1082
+ )
1083
+ end
1084
+ end
1085
+
1086
+ unread_shape_maybe(mode, "tuple arity unknown for #{other_type.describe(:short)}", :array)
1087
+ end
1088
+
1028
1089
  # HashShape{k1: T1, ...} accepts another HashShape when every required key of self is required on
1029
1090
  # the other side and Ti accepts Ui (depth covariant). Optional keys may be absent on the other side;
1030
1091
  # when present, their values are checked. A closed self rejects known extra keys, and answers at most
@@ -1056,7 +1117,7 @@ module Rigor
1056
1117
  result = combine_arg_results(per_entry, mode)
1057
1118
  return result unless result.yes? && self_type.closed? && other_type.open?
1058
1119
 
1059
- unread_hash_maybe(mode, "HashShape closed target cannot check open source")
1120
+ unread_shape_maybe(mode, "HashShape closed target cannot check open source", :hash)
1060
1121
  end
1061
1122
 
1062
1123
  # A `Hash` nominal — directly or as the base of a `Difference` (`non-empty-hash[K, V]`) — that is raw or
@@ -1085,7 +1146,7 @@ module Rigor
1085
1146
  refused = refused_value_keys(self_type, value_type, mode)
1086
1147
  return hash_shape_no(mode, "Hash value type refused for required keys: #{refused.inspect}") if refused.any?
1087
1148
 
1088
- unread_hash_maybe(mode, "HashShape cannot check #{other_type.describe(:short)}")
1149
+ unread_shape_maybe(mode, "HashShape cannot check #{other_type.describe(:short)}", :hash)
1089
1150
  end
1090
1151
 
1091
1152
  def unholdable_keys(self_type, key_type, mode)
@@ -211,9 +211,8 @@ module Rigor
211
211
  cursor
212
212
  end
213
213
 
214
- # `|*rest|` binds an Array of the leftover positional arguments. The expected-types array is
215
- # per-position, not per-rest; we cannot reliably pick a single element type for rest, so we default to
216
- # `Array[Dynamic[Top]]`. Element-type precision for rest parameters is deferred (demand-gated).
214
+ # `|*rest|` binds an Array of the leftover positional arguments: `Array[Dynamic[Top]]`, unless an
215
+ # auto-splat answered a rest type ({BlockAutoSplat}). The expected types are per-position, not per-rest.
217
216
  def bind_rest(params_node, bindings)
218
217
  rest = params_node.rest
219
218
  return unless rest.respond_to?(:name) && rest&.name
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "block_call_timing"
6
+ require_relative "closure_escape_analyzer"
7
+ require_relative "project_method_ownership"
8
+
9
+ module Rigor
10
+ module Inference
11
+ # Issue #2303 — whether a project method's body may run the block it is called with more than once ON ONE PATH,
12
+ # for the expression-position block-value pass (`ExpressionTyper#block_may_repeat?` through
13
+ # {ProjectYielder.block_value_may_repeat?}). That pass lays the #587 (b) captured binding under a repeating block,
14
+ # which adds values a single run never reads, so a callee read as repeating when it is not reports diagnostics on
15
+ # correct code (`s = "a"; r = either(f) { t = s; s = nil; t }; r.upcase`). The rule here therefore errs toward
16
+ # "runs once", unlike {ProjectYielder.may_run_block_repeatedly?}, whose only asker widens a return type.
17
+ #
18
+ # The walk counts block runs per path, saturating at {MANY}:
19
+ #
20
+ # - a `yield`, or `call` / `yield` / `===` / `[]` on the `&blk` parameter, is one run;
21
+ # - a sequence sums, and a branch (`if` / `unless` / ternary / `case` / `case ... in`) takes the largest arm,
22
+ # so `f ? yield(1) : yield(2)` is one run;
23
+ # - a `return`, `break`, `next` or receiver-less `raise` / `fail` ends its path, so the `yield` after
24
+ # `return yield(1) if k` is a different path from the one before it;
25
+ # - the body of `while` / `until` / `for`, of `Kernel#loop`, or the block of a catalogued core iterator
26
+ # ({ClosureEscapeAnalyzer.iterator_name?}) is {MANY} when a run lies on a path that reaches the next
27
+ # iteration (falling through the body, or a `next`); a run only on a path that leaves it (`return`, `break`,
28
+ # `raise`) adds what that path holds, so `xs.each { |x| return yield(x) if x > 1 }` is one run;
29
+ # - a hand-off of the block (`g(&blk)`, `g(&)`, `...`) is {MANY};
30
+ # - a block under any other call runs its body once (`then { yield }`, `synchronize { yield }`,
31
+ # `File.open(f) { yield f }`), a lambda body is not run at its definition, and a nested `def` is not this
32
+ # method's body.
33
+ #
34
+ # Reading `&blk` other than calling it (`return unless blk`) is no run.
35
+ #
36
+ # A result is `[fall, total, nxt, brk]`: the most runs on a path that falls through the node, on any path
37
+ # through it, on a path that ends at a `next`, and on one that ends at a `break` (nil where no path does).
38
+ module BlockRunCount
39
+ module_function
40
+
41
+ MANY = 2
42
+ ZERO = [0, 0, nil, nil].freeze
43
+ ALWAYS = [MANY, MANY, nil, nil].freeze
44
+ BLOCK_RUN_CALLS = Set[:call, :yield, :===, :[]].freeze
45
+ LOOP_NODES = [Prism::WhileNode, Prism::UntilNode, Prism::ForNode].freeze
46
+ RAISING_CALLS = Set[:raise, :fail].freeze
47
+ private_constant :ZERO, :ALWAYS, :BLOCK_RUN_CALLS, :LOOP_NODES, :RAISING_CALLS
48
+
49
+ # Memoised per `def` node for the run ({ProjectMethodOwnership.memo}): every call site, and every pass over
50
+ # one, asks the same callee.
51
+ def may_repeat?(def_node, scope)
52
+ memo = ProjectMethodOwnership.memo(scope)[:block_runs]
53
+ return memo[def_node] if memo.key?(def_node)
54
+
55
+ memo[def_node] = runs(def_node.body, def_node.parameters&.block&.name)[1] >= MANY
56
+ end
57
+
58
+ def runs(node, block_name)
59
+ case node
60
+ when nil, Prism::DefNode, Prism::LambdaNode then ZERO
61
+ when Prism::YieldNode then run_after(sequence(node.compact_child_nodes, block_name))
62
+ when Prism::ForwardingArgumentsNode then ALWAYS
63
+ when Prism::BlockArgumentNode then block_argument_runs(node, block_name)
64
+ when Prism::IfNode then branch(runs(node.predicate, block_name), [node.statements, node.subsequent], block_name)
65
+ when Prism::UnlessNode
66
+ branch(runs(node.predicate, block_name), [node.statements, node.else_clause], block_name)
67
+ when Prism::CaseNode, Prism::CaseMatchNode then case_runs(node, block_name)
68
+ when Prism::CallNode then call_runs(node, block_name)
69
+ else other_runs(node, block_name)
70
+ end
71
+ end
72
+
73
+ def other_runs(node, block_name)
74
+ result = sequence(node.compact_child_nodes, block_name)
75
+ case node
76
+ when *LOOP_NODES then iterated(result)
77
+ when Prism::ReturnNode then [nil, result[1], result[2], result[3]]
78
+ when Prism::NextNode then [nil, result[1], larger(result[2], result[0]), result[3]]
79
+ when Prism::BreakNode then [nil, result[1], result[2], larger(result[3], result[0])]
80
+ else result
81
+ end
82
+ end
83
+
84
+ # A body run any number of times: {MANY} when a run reaches the next iteration, else the runs of the paths
85
+ # that leave it. It may run zero times or be left by a `break`, and the walk continues after it either way.
86
+ def iterated((fall, total, nxt, brk))
87
+ again = [fall, nxt].compact.max || 0
88
+ return ALWAYS if again.positive?
89
+
90
+ [[0, brk].compact.max, total, nil, nil]
91
+ end
92
+
93
+ # A body run once: a `next` or `break` in it ends the run, and the walk continues after the call.
94
+ def run_once((fall, total, nxt, brk))
95
+ [[fall, nxt, brk].compact.max, total, nil, nil]
96
+ end
97
+
98
+ def block_argument_runs(node, block_name)
99
+ expression = node.expression
100
+ forwards = expression.nil? || (expression.is_a?(Prism::LocalVariableReadNode) && expression.name == block_name)
101
+ forwards ? ALWAYS : runs(expression, block_name)
102
+ end
103
+
104
+ def sequence(nodes, block_name)
105
+ nodes.reduce(ZERO) do |acc, child|
106
+ break acc if acc[0].nil?
107
+
108
+ chain(acc, runs(child, block_name))
109
+ end
110
+ end
111
+
112
+ # `first` then `second`; nothing follows a path that does not fall through `first`.
113
+ def chain(first, second)
114
+ fall, total, nxt, brk = first
115
+ return first if fall.nil?
116
+
117
+ [second[0] && sum(fall, second[0]), [total, sum(fall, second[1])].max,
118
+ larger(nxt, second[2] && sum(fall, second[2])), larger(brk, second[3] && sum(fall, second[3]))]
119
+ end
120
+
121
+ def run_after(result)
122
+ chain(result, [1, 1, nil, nil])
123
+ end
124
+
125
+ # `head` runs first, then exactly one of `arms` (a nil arm is the path that takes none).
126
+ def branch(head, arms, block_name)
127
+ return head if head[0].nil?
128
+
129
+ arms.map { |arm| chain(head, runs(arm, block_name)) }.reduce do |a, b|
130
+ [larger(a[0], b[0]), [a[1], b[1]].max, larger(a[2], b[2]), larger(a[3], b[3])]
131
+ end
132
+ end
133
+
134
+ def case_runs(node, block_name)
135
+ clauses = node.conditions
136
+ tests = clauses.flat_map { |clause| clause.is_a?(Prism::WhenNode) ? clause.conditions : [clause.pattern] }
137
+ arms = clauses.map(&:statements) + [node.else_clause]
138
+ branch(sequence([node.predicate, *tests], block_name), arms, block_name)
139
+ end
140
+
141
+ def call_runs(node, block_name)
142
+ return run_after(runs(node.arguments, block_name)) if block_run_call?(node, block_name)
143
+
144
+ block = node.block
145
+ literal = block.is_a?(Prism::BlockNode)
146
+ head = sequence([node.receiver, node.arguments, (block unless literal)], block_name)
147
+ return [nil, head[1], head[2], head[3]] if raising_call?(node)
148
+ return head unless literal
149
+
150
+ body = runs(block.body, block_name)
151
+ chain(head, repeating_call?(node) ? iterated(body) : run_once(body))
152
+ end
153
+
154
+ def sum(left, right)
155
+ (left + right).clamp(0, MANY)
156
+ end
157
+
158
+ def larger(left, right)
159
+ [left, right].compact.max
160
+ end
161
+
162
+ def repeating_call?(node)
163
+ ClosureEscapeAnalyzer.iterator_name?(node.name) || BlockCallTiming.kernel_loop?(node)
164
+ end
165
+
166
+ def raising_call?(node)
167
+ node.receiver.nil? && RAISING_CALLS.include?(node.name)
168
+ end
169
+
170
+ # `blk.call` and its spellings.
171
+ def block_run_call?(node, block_name)
172
+ receiver = node.receiver
173
+ !block_name.nil? && receiver.is_a?(Prism::LocalVariableReadNode) && receiver.name == block_name &&
174
+ BLOCK_RUN_CALLS.include?(node.name)
175
+ end
176
+ end
177
+ end
178
+ end
@@ -16,7 +16,7 @@ module Rigor
16
16
  # non-stores) vs memo-key normalization (collapse arg-granularity thrash — a signature whose distinct-key
17
17
  # count far exceeds its body-eval savings).
18
18
  #
19
- # Three categories, one per guard site:
19
+ # The cutoff categories, one per guard site:
20
20
  #
21
21
  # - {RECURSION_GUARD} — `ExpressionTyper#infer_user_method_return` detected a `(receiver, method)` cycle
22
22
  # and returned `Dynamic[top]` (the de-facto recursion-depth budget, effective depth 1).
@@ -27,6 +27,8 @@ module Rigor
27
27
  # fuel and fell back to the plain `(receiver, method)` guard (in-cycle call → `Dynamic[top]`).
28
28
  # - {RECURSION_FIXPOINT_CAP} — the fixpoint return-summary iteration (ADR-55 slice 2) hit its 3-evaluation
29
29
  # cap without converging and collapsed the summary to `untyped` (today's behaviour).
30
+ # - {BLOCK_WRITEBACK_CAP} — the block / loop write-back fixpoint ({BodyFixpoint.converge}) collapsed a
31
+ # local that had not converged to `Dynamic[top]`.
30
32
  #
31
33
  # Enabled only when `RIGOR_BUDGET_TRACE` is set (to any non-empty value) in the environment, or via
32
34
  # {enable!} in tests. When disabled, {hit} is a single boolean check and returns immediately, so normal
@@ -52,6 +54,10 @@ module Rigor
52
54
  # `Dynamic[top]` (the escaping-block floor). Shared by slice B's loop-body fixpoint and by slice C's
53
55
  # content-evidence fixpoint, where the collapsed name is one element / key / value slot of a collection join.
54
56
  BLOCK_WRITEBACK_CAP = :block_writeback_cap
57
+ # `CalleeMutationChain` discovered more than {CalleeMutationChain::MAX_NODES} nodes from one parameter and took
58
+ # the rest as mutating (the callee floor's FP-safe cut). A label: it may vary with the order, the floor does not
59
+ # (ADR-127).
60
+ CALLEE_CHAIN_CAP = :callee_chain_cap
55
61
 
56
62
  # ADR-57 return-memo profile counters (not cutoffs — see the module doc). All bumped by
57
63
  # `ExpressionTyper#infer_user_method_return` / `#compute_user_method_return`.
@@ -82,7 +88,7 @@ module Rigor
82
88
 
83
89
  CATEGORIES = [
84
90
  RECURSION_GUARD, ANCESTOR_WALK_LIMIT, HKT_FUEL_EXHAUSTED, RECURSION_UNROLL_FUEL,
85
- RECURSION_FIXPOINT_CAP, BLOCK_WRITEBACK_CAP,
91
+ RECURSION_FIXPOINT_CAP, BLOCK_WRITEBACK_CAP, CALLEE_CHAIN_CAP,
86
92
  MEMO_ENTRIES, MEMO_HITS, MEMO_MISSES, MEMO_BODY_EVALS,
87
93
  MEMO_REFUSE_ON_STACK, MEMO_REFUSE_UNROLL, MEMO_REFUSE_TRANSIENT
88
94
  ].freeze
@@ -9,8 +9,8 @@ module Rigor
9
9
  #
10
10
  # Exception is the base of every Ruby error class (RuntimeError, StandardError, KeyError, …). The
11
11
  # Init_Exception block in `references/ruby/error.c` registers the entire hierarchy in one pass, so
12
- # the YAML carries 27 classes — but only the base `Exception` row is wired into `CATALOG_BY_CLASS`
13
- # for v0.0.5. A `RuntimeError` receiver hits the Exception arm via `is_a?(Exception)` and the
12
+ # the YAML carries 27 classes — but only the base `Exception` row is wired into `CATALOG_BY_CLASS`.
13
+ # A `RuntimeError` receiver hits the Exception arm via `is_a?(Exception)` and the
14
14
  # catalog answers with the base-class entries; subclass-specific methods (`KeyError#receiver`,
15
15
  # `NameError#name`, …) intentionally miss the lookup until a later slice routes per-subclass
16
16
  # class_names.
@@ -16,11 +16,8 @@ module Rigor
16
16
  #
17
17
  # No mutation blocklist is needed. The numeric classes expose no foldable bang or indirect-mutator
18
18
  # method that the static C classifier mis-attributes (every `:leaf` numeric method is a pure value
19
- # computation), so the generic `MethodCatalog` loader — shared with the eighteen other per-class
20
- # catalogs — covers it directly. This previously hand-rolled its own `safe_for_folding?` /
21
- # `method_entry` / `load_catalog` copy of `MethodCatalog`; folding it onto the shared loader also
22
- # picks up alias resolution (e.g. `Integer#magnitude` → `abs`, `Integer#inspect` → `to_s`), which the
23
- # old bespoke loader silently dropped.
19
+ # computation), so the generic `MethodCatalog` loader covers it directly. Using the shared loader
20
+ # rather than a bespoke copy also gives it alias resolution (e.g. `Integer#magnitude` → `abs`).
24
21
  NUMERIC_CATALOG = MethodCatalog.for_topic("numeric")
25
22
  end
26
23
  end
@@ -20,8 +20,8 @@ module Rigor
20
20
  mutating_selectors: {
21
21
  "Random" => Set[
22
22
  # `rand_random` -> `random_real` / `random_ulong_limited` advance the MT state on the receiver
23
- # (instance #rand) and on `Random::DEFAULT` (singleton .rand). The classifier misses the
24
- # indirect mutator.
23
+ # (instance #rand) and on the process-wide default generator (singleton .rand). The classifier
24
+ # misses the indirect mutator.
25
25
  :rand,
26
26
  # `random_bytes` / `random_s_bytes` consume MT output the same way #rand does — every call
27
27
  # mutates the underlying generator.