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
@@ -24,27 +24,21 @@ require_relative "../builtins/exception_catalog"
24
24
  module Rigor
25
25
  module Inference
26
26
  module MethodDispatcher
27
- # Slice 2 rule book that folds method calls on `Rigor::Type::Constant`
28
- # receivers (and unions of them) into another `Constant` (or a small
29
- # `Union[Constant, …]`) whenever:
27
+ # Slice 2 rule book that folds method calls on `Rigor::Type::Constant` receivers (and unions of them)
28
+ # into another `Constant` (or a small `Union[Constant, …]`) whenever:
30
29
  #
31
- # * the receiver is a recognised scalar literal, OR a `Union` whose
32
- # members are all `Constant`,
30
+ # * the receiver is a recognised scalar literal, OR a `Union` whose members are all `Constant`,
33
31
  # * arguments (zero or one) are likewise `Constant` or `Union[Constant…]`,
34
32
  # * the method name is in the curated whitelist for the receiver's class,
35
- # * the operation cannot accidentally explode the analyzer (we cap
36
- # string-fold output at `STRING_FOLD_BYTE_LIMIT` bytes, the input
37
- # cartesian product at `UNION_FOLD_INPUT_LIMIT`, and the deduped
38
- # output union at `UNION_FOLD_OUTPUT_LIMIT`), and
39
- # * the actual Ruby invocation does not raise on at least one
40
- # receiver/argument combination.
33
+ # * the operation cannot accidentally explode the analyzer (we cap string-fold output at
34
+ # `STRING_FOLD_BYTE_LIMIT` bytes, the input cartesian product at `UNION_FOLD_INPUT_LIMIT`, and the
35
+ # deduped output union at `UNION_FOLD_OUTPUT_LIMIT`), and
36
+ # * the actual Ruby invocation does not raise on at least one receiver/argument combination.
41
37
  #
42
- # Anything else returns `nil`, signalling "no rule matched" so the
43
- # caller (`MethodDispatcher`) falls back to `Dynamic[Top]` and records a
44
- # fail-soft event. Slice 4 (RBS-backed) layers another dispatch tier
45
- # behind this rule book, but the constant-folding semantics defined
46
- # here MUST NOT regress: any value reachable by literal arithmetic at
47
- # parse time is meant to be foldable independent of RBS data.
38
+ # Anything else returns `nil`, signalling "no rule matched" so the caller (`MethodDispatcher`) falls
39
+ # back to `Dynamic[Top]` and records a fail-soft event. Slice 4 (RBS-backed) layers another dispatch
40
+ # tier behind this rule book, but the constant-folding semantics defined here MUST NOT regress: any
41
+ # value reachable by literal arithmetic at parse time is meant to be foldable independent of RBS data.
48
42
  module ConstantFolding # rubocop:disable Metrics/ModuleLength
49
43
  module_function
50
44
 
@@ -52,12 +46,10 @@ module Rigor
52
46
  :+, :-, :*, :/, :%, :**, :&, :|, :^, :<<, :>>,
53
47
  :<, :<=, :>, :>=, :==, :!=, :<=>,
54
48
  :gcd, :lcm, :fdiv, :quo, :ceildiv, :[],
55
- # Integer bit-test predicates (`(self & mask) <=> mask|0`). The
56
- # catalog marks them `:dispatch` only because a non-Integer mask
57
- # would route through `to_int`; a concrete Integer literal never
58
- # does, so the fold is pure here the sibling of the already-folded
59
- # bit-reference `:[]`. Integer-only, but Float-safe to list: a Float
60
- # receiver has no such method, so `invoke_binary` rescues the
49
+ # Integer bit-test predicates (`(self & mask) <=> mask|0`). The catalog marks them `:dispatch` only
50
+ # because a non-Integer mask would route through `to_int`; a concrete Integer literal never does,
51
+ # so the fold is pure here the sibling of the already-folded bit-reference `:[]`. Integer-only,
52
+ # but Float-safe to list: a Float receiver has no such method, so `invoke_binary` rescues the
61
53
  # `NoMethodError` to nil and the RBS tier answers.
62
54
  :allbits?, :anybits?, :nobits?
63
55
  ].freeze
@@ -66,76 +58,60 @@ module Rigor
66
58
  :start_with?, :end_with?, :include?,
67
59
  :delete_prefix, :delete_suffix,
68
60
  :match?, :index, :rindex, :center, :ljust, :rjust,
69
- # 1-arg pure transforms/queries whose output never exceeds the
70
- # input: `delete`/`squeeze` shrink the string, `count` → Integer.
61
+ # 1-arg pure transforms/queries whose output never exceeds the input: `delete`/`squeeze` shrink
62
+ # the string, `count` → Integer.
71
63
  :delete, :count, :squeeze,
72
- # ASCII / Unicode-case-fold comparison — deterministic, no
73
- # locale read: `casecmp` → -1/0/1, `casecmp?` → bool/nil.
64
+ # ASCII / Unicode-case-fold comparison — deterministic, no locale read: `casecmp` → -1/0/1,
65
+ # `casecmp?` → bool/nil.
74
66
  :casecmp, :casecmp?
75
67
  ].freeze
76
68
  SYMBOL_BINARY = Set[:==, :!=, :<=>, :<, :<=, :>, :>=].freeze
77
69
  BOOL_BINARY = Set[:&, :|, :^, :==, :!=, :===].freeze
78
70
  NIL_BINARY = Set[:==, :!=].freeze
79
- # Rational arithmetic / ordering are exact and pure. Division
80
- # (`/`) and `**` may return a `Float`/`Complex` for some operands,
81
- # all of which are foldable `Constant` value classes. `==` / `!=`
82
- # are deliberately EXCLUDED: `Rational#==` (`nurat_eqeq_p`) routes
83
- # through `rb_funcall(:==)` on the operands — user-redefinable —
84
- # so the catalog classifies it `:dispatch` and the equality stays
71
+ # Rational arithmetic / ordering are exact and pure. Division (`/`) and `**` may return a
72
+ # `Float`/`Complex` for some operands, all of which are foldable `Constant` value classes. `==` /
73
+ # `!=` are deliberately EXCLUDED: `Rational#==` (`nurat_eqeq_p`) routes through `rb_funcall(:==)` on
74
+ # the operands user-redefinable — so the catalog classifies it `:dispatch` and the equality stays
85
75
  # the RBS `bool`. (The set would otherwise bypass that gate.)
86
76
  RATIONAL_BINARY = Set[
87
77
  :+, :-, :*, :/, :**, :<=>, :<, :<=, :>, :>=,
88
78
  :div, :modulo, :%, :remainder, :fdiv, :quo
89
79
  ].freeze
90
- # Complex arithmetic. `ops_for` gains a `Complex` branch so these
91
- # reach the binary fold path (Complex was previously unary-only).
92
- # `/` and `**` stay foldable (Complex result). `==` / `!=` are
93
- # excluded for the same reason as Rational (`nucomp_eqeq_p`
94
- # delegates to operand `==`); ordering is undefined for Complex.
80
+ # Complex arithmetic. `ops_for` gains a `Complex` branch so these reach the binary fold path
81
+ # (Complex was previously unary-only). `/` and `**` stay foldable (Complex result). `==` / `!=` are
82
+ # excluded for the same reason as Rational (`nucomp_eqeq_p` delegates to operand `==`); ordering is
83
+ # undefined for Complex.
95
84
  COMPLEX_BINARY = Set[:+, :-, :*, :/, :**].freeze
96
85
 
97
- # `Set#&` and its alias `Set#intersection` are leaf-pure for a
98
- # concrete Set operand exactly like their siblings `|` / `-` / `^`
99
- # (all `:leaf` in the catalog), but the catalog flags
100
- # `set_i_intersection`'s C body `block_dependent` it drives Set's
101
- # own internal iterator so the catalog tier declines and the
102
- # intersection alone fails to fold. A concrete Set argument's `each`
103
- # is the pure core method, so the fold is sound; the hand-rolled
104
- # allow-list is the right tool, mirroring the bit-test predicates.
105
- # (The other binary set ops keep folding through the catalog.)
86
+ # `Set#&` and its alias `Set#intersection` are leaf-pure for a concrete Set operand exactly like
87
+ # their siblings `|` / `-` / `^` (all `:leaf` in the catalog), but the catalog flags
88
+ # `set_i_intersection`'s C body `block_dependent` it drives Set's own internal iterator — so the
89
+ # catalog tier declines and the intersection alone fails to fold. A concrete Set argument's `each`
90
+ # is the pure core method, so the fold is sound; the hand-rolled allow-list is the right tool,
91
+ # mirroring the bit-test predicates. (The other binary set ops keep folding through the catalog.)
106
92
  SET_BINARY = Set[:&, :intersection].freeze
107
93
 
108
94
  # v0.0.3 C — pure unary catalogue. Each method must:
109
95
  # - take zero arguments,
110
96
  # - have no side effects,
111
- # - never raise on the type's full domain (or be
112
- # guarded by `safe?` below),
113
- # - return a value safe to materialise as a
114
- # `Constant` (no large strings, no host objects).
97
+ # - never raise on the type's full domain (or be guarded by `safe?` below),
98
+ # - return a value safe to materialise as a `Constant` (no large strings, no host objects).
115
99
  #
116
- # The catalogue is the prerequisite for aggressive
117
- # constant folding through user methods: once
118
- # `Constant[3].odd?` folds to `Constant[true]`, the
119
- # inter-procedural inference path landed in v0.0.2
120
- # #5 carries the constant through the body of a
121
- # user-defined `def is_odd(n) = n.odd?` so
122
- # `Parity.new.is_odd(3)` types as `Constant[true]`
123
- # rather than the RBS-widened `bool`.
124
- # NOTE: `:hash` is deliberately NOT in any of these sets.
125
- # `Object#hash` (and the `String`/`Symbol`/`Integer`/`Float`
126
- # overrides) is salted with a per-process SipHash seed, so
127
- # `"abc".hash` returns a different Integer in every Ruby
128
- # process. Folding it to a `Constant` would bake one process's
129
- # value into the type (and the on-disk cache), making the
130
- # result non-deterministic across runs — a violation of the
131
- # purity contract this catalogue rests on. A literal's `.hash`
132
- # therefore stays the RBS-widened `Integer`. The deterministic
133
- # siblings `:inspect` / `:to_s` remain folded.
100
+ # The catalogue is the prerequisite for aggressive constant folding through user methods: once
101
+ # `Constant[3].odd?` folds to `Constant[true]`, the inter-procedural inference path landed in
102
+ # v0.0.2 #5 carries the constant through the body of a user-defined `def is_odd(n) = n.odd?` so
103
+ # `Parity.new.is_odd(3)` types as `Constant[true]` rather than the RBS-widened `bool`.
104
+ # NOTE: `:hash` is deliberately NOT in any of these sets. `Object#hash` (and the
105
+ # `String`/`Symbol`/`Integer`/`Float` overrides) is salted with a per-process SipHash seed, so
106
+ # `"abc".hash` returns a different Integer in every Ruby process. Folding it to a `Constant` would
107
+ # bake one process's value into the type (and the on-disk cache), making the result
108
+ # non-deterministic across runs a violation of the purity contract this catalogue rests on. A
109
+ # literal's `.hash` therefore stays the RBS-widened `Integer`. The deterministic siblings
110
+ # `:inspect` / `:to_s` remain folded.
134
111
  INTEGER_UNARY = Set[
135
112
  :odd?, :even?, :zero?, :positive?, :negative?,
136
- # `finite?` / `infinite?` are total on Integer (`true` / `nil`
137
- # always) and round out the numeric predicate family the Float
138
- # sibling already folds them. `nonzero?` returns `self` (non-zero)
113
+ # `finite?` / `infinite?` are total on Integer (`true` / `nil` always) and round out the numeric
114
+ # predicate family the Float sibling already folds them. `nonzero?` returns `self` (non-zero)
139
115
  # or `nil`, both foldable Constants.
140
116
  :finite?, :infinite?, :nonzero?,
141
117
  :succ, :pred, :next, :abs, :magnitude,
@@ -149,18 +125,15 @@ module Rigor
149
125
  :abs, :magnitude, :floor, :ceil, :round, :truncate,
150
126
  :next_float, :prev_float,
151
127
  :to_s, :to_i, :to_int, :to_f, :to_r, :rationalize,
152
- # `numerator` / `denominator` expose the rational
153
- # decomposition of the float (`2.5.numerator 5`,
154
- # `.denominator 2`) pure arithmetic, the Float siblings
155
- # of the already-folded Rational accessors. The non-finite
156
- # edges stay sound: `Infinity.numerator → Infinity` /
157
- # `.denominator → 1` fold to the same value Ruby returns, and
158
- # `NaN.numerator → NaN` is declined by `foldable_constant_value?`.
128
+ # `numerator` / `denominator` expose the rational decomposition of the float (`2.5.numerator → 5`,
129
+ # `.denominator 2`) — pure arithmetic, the Float siblings of the already-folded Rational
130
+ # accessors. The non-finite edges stay sound: `Infinity.numerator Infinity` /
131
+ # `.denominator → 1` fold to the same value Ruby returns, and `NaN.numerator NaN` is declined by
132
+ # `foldable_constant_value?`.
159
133
  :numerator, :denominator,
160
- # `arg` / `angle` / `phase` (aliases) return the complex
161
- # argument of the real number: `0` for `self >= 0`, `Math::PI`
162
- # for `self < 0`. Pure sign test, deterministic; a NaN
163
- # receiver yields NaN which `foldable_constant_value?` declines.
134
+ # `arg` / `angle` / `phase` (aliases) return the complex argument of the real number: `0` for
135
+ # `self >= 0`, `Math::PI` for `self < 0`. Pure sign test, deterministic; a NaN receiver yields NaN
136
+ # which `foldable_constant_value?` declines.
164
137
  :arg, :angle, :phase,
165
138
  :inspect, :-@, :+@
166
139
  ].freeze
@@ -171,19 +144,17 @@ module Rigor
171
144
  :to_s, :to_str, :to_sym, :intern,
172
145
  :to_i, :to_f, :ord, :chr, :hex, :oct, :succ, :next,
173
146
  :sum, :inspect,
174
- # `shellescape` is the String-receiver twin of the already-folded
175
- # `Shellwords.escape` — deterministic shell-quoting, no global
176
- # state. The `shellwords` library is loaded process-wide via
177
- # `shellwords_folding`, so the method is always defined here.
147
+ # `shellescape` is the String-receiver twin of the already-folded `Shellwords.escape` —
148
+ # deterministic shell-quoting, no global state. The `shellwords` library is loaded process-wide
149
+ # via `shellwords_folding`, so the method is always defined here.
178
150
  :shellescape
179
151
  ].freeze
180
152
  SYMBOL_UNARY = Set[
181
153
  :to_s, :to_sym, :to_proc, :length, :size,
182
154
  :empty?, :upcase, :downcase, :capitalize,
183
155
  :swapcase, :succ, :next, :inspect,
184
- # `name` (the frozen-string accessor), `id2name` (alias of
185
- # `to_s`), and `intern` (alias of `to_sym`) are pure reads of the
186
- # symbol's text — siblings of the already-folded `to_s` / `to_sym`.
156
+ # `name` (the frozen-string accessor), `id2name` (alias of `to_s`), and `intern` (alias of
157
+ # `to_sym`) are pure reads of the symbol's text siblings of the already-folded `to_s` / `to_sym`.
187
158
  :name, :id2name, :intern
188
159
  ].freeze
189
160
  BOOL_UNARY = Set[:!, :to_s, :inspect, :&, :|, :^].freeze
@@ -205,28 +176,23 @@ module Rigor
205
176
 
206
177
  STRING_FOLD_BYTE_LIMIT = 4096
207
178
 
208
- # Input cartesian product hard cap. Keeps fold cost bounded even
209
- # when the receiver and argument are both `Union[Constant…]`.
210
- # 5 × 5 = 25 inputs is permitted; 6 × 6 = 36 is not. The user-
211
- # facing payoff (a precise small enum) drops off fast past this
212
- # range and CRuby method invocation cost adds up.
179
+ # Input cartesian product hard cap. Keeps fold cost bounded even when the receiver and argument are
180
+ # both `Union[Constant…]`. 5 × 5 = 25 inputs is permitted; 6 × 6 = 36 is not. The user-facing payoff
181
+ # (a precise small enum) drops off fast past this range and CRuby method invocation cost adds up.
213
182
  UNION_FOLD_INPUT_LIMIT = 32
214
183
 
215
- # Output cardinality cap on the deduped result union. A single
216
- # binary op on a small range can collapse: `[1,2,3] + [2,4,6]`
217
- # produces 9 raw pairs but only 7 distinct sums. The output cap
218
- # is what ultimately limits how wide an inferred type gets.
184
+ # Output cardinality cap on the deduped result union. A single binary op on a small range can
185
+ # collapse: `[1,2,3] + [2,4,6]` produces 9 raw pairs but only 7 distinct sums. The output cap is
186
+ # what ultimately limits how wide an inferred type gets.
219
187
  UNION_FOLD_OUTPUT_LIMIT = 8
220
188
 
221
189
  # ADR-78 — reflexive over-fold guard.
222
- # Reflective dispatch (`public_send` / `send` / `__send__`) must
223
- # NOT constant-fold unless the method-name argument is itself a
224
- # value-pinned literal `Constant[Symbol]`. With a runtime-variable
225
- # method name the dispatched method is not statically determined,
226
- # so the call degrades to the RBS result (`untyped`) exactly as
227
- # it does without the guard, but explicit so a later shape-carrier
228
- # preservation tier (ADR-76 WD2) cannot surface an over-fold as a
229
- # spurious `flow.always-truthy-condition`.
190
+ # Reflective dispatch (`public_send` / `send` / `__send__`) must NOT constant-fold unless the
191
+ # method-name argument is itself a value-pinned literal `Constant[Symbol]`. With a runtime-variable
192
+ # method name the dispatched method is not statically determined, so the call degrades to the RBS
193
+ # result (`untyped`) — exactly as it does without the guard, but explicit so a later shape-carrier
194
+ # preservation tier (ADR-76 WD2) cannot surface an over-fold as a spurious
195
+ # `flow.always-truthy-condition`.
230
196
  REFLECTIVE_SEND_METHODS = %i[public_send send __send__].to_set.freeze
231
197
 
232
198
  # @return [Rigor::Type::Constant, Rigor::Type::Union, Rigor::Type::IntegerRange, nil]
@@ -239,13 +205,10 @@ module Rigor
239
205
  first_arg = args.first
240
206
  return nil unless first_arg.is_a?(Type::Constant) && first_arg.value.is_a?(Symbol)
241
207
  end
242
- # v0.0.7 — `String#%` against a `Tuple` / `HashShape`
243
- # argument runs Ruby's format-string engine when both
244
- # sides are statically constant. The standard
245
- # `numeric_set_of` path bails on Tuple / HashShape
246
- # arguments because they are not scalar-Constant
247
- # carriers, so the special-case sits ahead of the
248
- # numeric path.
208
+ # v0.0.7 — `String#%` against a `Tuple` / `HashShape` argument runs Ruby's format-string engine
209
+ # when both sides are statically constant. The standard `numeric_set_of` path bails on Tuple /
210
+ # HashShape arguments because they are not scalar-Constant carriers, so the special-case sits
211
+ # ahead of the numeric path.
249
212
  format_lift = try_fold_string_format(receiver, method_name, args)
250
213
  return format_lift if format_lift
251
214
 
@@ -258,20 +221,14 @@ module Rigor
258
221
  dispatch_by_arity(receiver_set, method_name, arg_sets)
259
222
  end
260
223
 
261
- # `Constant<String> % …` — runs the actual `String#%`
262
- # operation when both sides are statically known. The
263
- # argument may be:
264
- # - A `Type::Constant` whose value is a scalar (Integer
265
- # / Float / String / Symbol). Already handled by the
266
- # numeric path; this method declines so the standard
267
- # binary path picks it up.
268
- # - A `Type::Tuple` whose elements are all `Constant`.
269
- # Materialises the elements as a Ruby Array and runs
270
- # the format.
271
- # - A `Type::HashShape` with no optional keys whose
272
- # values are all `Constant`. Materialises a Ruby Hash
273
- # and runs the format. Symbol keys are kept as
274
- # Symbols (matching Ruby's `%{key}` resolution).
224
+ # `Constant<String> % …` — runs the actual `String#%` operation when both sides are statically
225
+ # known. The argument may be:
226
+ # - A `Type::Constant` whose value is a scalar (Integer / Float / String / Symbol). Already handled
227
+ # by the numeric path; this method declines so the standard binary path picks it up.
228
+ # - A `Type::Tuple` whose elements are all `Constant`. Materialises the elements as a Ruby Array
229
+ # and runs the format.
230
+ # - A `Type::HashShape` with no optional keys whose values are all `Constant`. Materialises a Ruby
231
+ # Hash and runs the format. Symbol keys are kept as Symbols (matching Ruby's `%{key}` resolution).
275
232
  # Anything else declines so the RBS tier widens.
276
233
  def try_fold_string_format(receiver, method_name, args)
277
234
  return nil unless method_name == :%
@@ -318,8 +275,8 @@ module Rigor
318
275
  end
319
276
 
320
277
  # Normalises an input type into one of:
321
- # - `Array<Object>` for a `Constant` (1-element) or
322
- # `Union[Constant…]` (n-element) — concrete values to enumerate.
278
+ # - `Array<Object>` for a `Constant` (1-element) or `Union[Constant…]` (n-element) — concrete values
279
+ # to enumerate.
323
280
  # - `Type::IntegerRange` — bounded interval.
324
281
  # - `nil` — the input shape is not foldable.
325
282
  def numeric_set_of(type)
@@ -328,20 +285,18 @@ module Rigor
328
285
  when Type::Union
329
286
  return type.members.map(&:value) if type.members.all?(Type::Constant)
330
287
 
331
- # A union that mixes `Constant<Integer>` and `IntegerRange`
332
- # members (e.g. an accumulator's running fixpoint assumption
333
- # `1 | int<1, 6>`) folds as the bounding interval. The
334
- # range-arithmetic path (`try_fold_binary_range`) then keeps
335
- # the result an `IntegerRange` instead of bailing to Dynamic.
288
+ # A union that mixes `Constant<Integer>` and `IntegerRange` members (e.g. an accumulator's
289
+ # running fixpoint assumption `1 | int<1, 6>`) folds as the bounding interval. The
290
+ # range-arithmetic path (`try_fold_binary_range`) then keeps the result an `IntegerRange`
291
+ # instead of bailing to Dynamic.
336
292
  union_integer_bounds(type)
337
293
  when Type::IntegerRange then type
338
294
  end
339
295
  end
340
296
 
341
- # Returns the bounding `IntegerRange` over a union whose members
342
- # are each an Integer `Constant` or an `IntegerRange`; `nil`
343
- # otherwise (a Float constant or any non-numeric member declines,
344
- # so precision is never silently lost).
297
+ # Returns the bounding `IntegerRange` over a union whose members are each an Integer `Constant` or
298
+ # an `IntegerRange`; `nil` otherwise (a Float constant or any non-numeric member declines, so
299
+ # precision is never silently lost).
345
300
  def union_integer_bounds(union)
346
301
  lowers = []
347
302
  uppers = []
@@ -359,9 +314,8 @@ module Rigor
359
314
  return nil
360
315
  end
361
316
  end
362
- # `IntegerRange#lower`/`#upper` surface an unbounded edge as
363
- # `±Float::INFINITY`; `integer_range` wants the `±∞` *sentinel*,
364
- # so map the extremum back.
317
+ # `IntegerRange#lower`/`#upper` surface an unbounded edge as `±Float::INFINITY`; `integer_range`
318
+ # wants the `±∞` *sentinel*, so map the extremum back.
365
319
  Type::Combinator.integer_range(infinity_to_sentinel(lowers.min),
366
320
  infinity_to_sentinel(uppers.max))
367
321
  end
@@ -391,13 +345,10 @@ module Rigor
391
345
  end
392
346
  end
393
347
 
394
- # `Integer#divmod` and `Float#divmod` return a 2-element array
395
- # `[quotient, remainder]`. We project that into
396
- # `Tuple[Constant[q], Constant[r]]` so downstream rules see
397
- # the precise element types. Union/range receivers are
398
- # widened per-position: each tuple slot carries the union of
399
- # quotients (resp. remainders) over every safe input pair.
400
- # Range inputs are not yet folded — they bail to nil.
348
+ # `Integer#divmod` and `Float#divmod` return a 2-element array `[quotient, remainder]`. We project
349
+ # that into `Tuple[Constant[q], Constant[r]]` so downstream rules see the precise element types.
350
+ # Union/range receivers are widened per-position: each tuple slot carries the union of quotients
351
+ # (resp. remainders) over every safe input pair. Range inputs are not yet folded — they bail to nil.
401
352
  def try_fold_divmod(left, right)
402
353
  pairs = collect_divmod_pairs(left, right)
403
354
  return nil unless pairs && !pairs.empty?
@@ -418,10 +369,9 @@ module Rigor
418
369
  end
419
370
  end
420
371
 
421
- # Returns `[[quotient, remainder]]` (single-element array
422
- # wrapping the tuple) on success; `nil` to signal "skip this
423
- # pair". The wrapping mirrors `invoke_binary` so we can
424
- # use `flat_map` and not lose legitimate 0/false elements.
372
+ # Returns `[[quotient, remainder]]` (single-element array wrapping the tuple) on success; `nil` to
373
+ # signal "skip this pair". The wrapping mirrors `invoke_binary` so we can use `flat_map` and not
374
+ # lose legitimate 0/false elements.
425
375
  def invoke_divmod(receiver_value, arg_value)
426
376
  return nil unless receiver_value.is_a?(Numeric) && arg_value.is_a?(Numeric)
427
377
 
@@ -442,12 +392,10 @@ module Rigor
442
392
  special = try_fold_unary_special(receiver_values, method_name)
443
393
  return special if special
444
394
 
445
- # Type-level allow check on every receiver. If one member's
446
- # type does not have the method in its allow list (e.g.
447
- # `Union[String, nil].nil?` `:nil?` is not in
448
- # `STRING_UNARY`), bail the fold so the RBS tier answers.
449
- # Silently dropping the unsafe member would lie about the
450
- # remaining receivers' behaviour.
395
+ # Type-level allow check on every receiver. If one member's type does not have the method in its
396
+ # allow list (e.g. `Union[String, nil].nil?` `:nil?` is not in `STRING_UNARY`), bail the fold so
397
+ # the RBS tier answers. Silently dropping the unsafe member would lie about the remaining
398
+ # receivers' behaviour.
451
399
  return nil unless receiver_values.all? { |rv| unary_method_allowed?(rv, method_name) }
452
400
 
453
401
  results = receiver_values.flat_map do |rv|
@@ -456,12 +404,10 @@ module Rigor
456
404
  build_constant_type(results, source: receiver_values)
457
405
  end
458
406
 
459
- # The carrier-specific unary lifts — Range-to-Tuple, the
460
- # Array-returning String / Pathname / Regexp / Set / Integer /
461
- # Numeric folds that produce a precise structural type before
462
- # the generic scalar `invoke_unary` path. The first match wins;
463
- # nil means none applied and the caller falls through to the
464
- # scalar allow-list path.
407
+ # The carrier-specific unary lifts — Range-to-Tuple, the Array-returning String / Pathname / Regexp
408
+ # / Set / Integer / Numeric folds that produce a precise structural type before the generic
409
+ # scalar `invoke_unary` path. The first match wins; nil means none applied and the caller falls
410
+ # through to the scalar allow-list path.
465
411
  def try_fold_unary_special(receiver_values, method_name)
466
412
  try_fold_range_constant_unary(receiver_values, method_name) ||
467
413
  try_fold_string_array_unary(receiver_values, method_name) ||
@@ -472,28 +418,22 @@ module Rigor
472
418
  try_fold_integer_array_unary(receiver_values, method_name) ||
473
419
  try_fold_numeric_array_unary(receiver_values, method_name)
474
420
  end
475
- # v0.0.7 — `Constant<Range>#to_a` and the no-arg
476
- # `first` / `last` / `min` / `max` short-circuit through a
477
- # Range-specific arm that catalog dispatch cannot reach:
478
- # - `to_a` returns an Array (not foldable through
479
- # `foldable_constant_value?`) lift to `Tuple[Constant…]`
480
- # when the cardinality fits within `RANGE_TO_A_LIMIT`.
481
- # - `first` / `last` / `min` / `max` are catalog-classified
482
- # `:block_dependent` because of the optional-block forms,
483
- # but the no-arg form is pure for finite integer ranges.
421
+ # v0.0.7 — `Constant<Range>#to_a` and the no-arg `first` / `last` / `min` / `max` short-circuit
422
+ # through a Range-specific arm that catalog dispatch cannot reach:
423
+ # - `to_a` returns an Array (not foldable through `foldable_constant_value?`) — lift to
424
+ # `Tuple[Constant…]` when the cardinality fits within `RANGE_TO_A_LIMIT`.
425
+ # - `first` / `last` / `min` / `max` are catalog-classified `:block_dependent` because of the
426
+ # optional-block forms, but the no-arg form is pure for finite integer ranges.
484
427
  #
485
- # Only fires on a single-receiver Range with finite integer
486
- # endpoints; mixed unions fall through so the existing
487
- # union-of-Constants path keeps the rest of the arms.
428
+ # Only fires on a single-receiver Range with finite integer endpoints; mixed unions fall through so
429
+ # the existing union-of-Constants path keeps the rest of the arms.
488
430
  RANGE_FOLD_METHODS = Set[:to_a, :first, :last, :min, :max, :count, :size, :length, :entries, :minmax,
489
431
  :sum].freeze
490
- # 1-arg head/tail projections on a `Constant<Range>`. `first(n)` /
491
- # `take(n)` return the first `n` elements, `last(n)` the final `n`,
492
- # and `min(n)` / `max(n)` the n smallest / largest (for an ascending
493
- # integer range `min(n) == first(n)` and `max(n) == last(n).reverse`)
494
- # each lifts to a per-position `Tuple[Constant[Integer]…]`. The
495
- # no-arg `first` / `last` / `min` / `max` stay on the unary path
496
- # (single Integer endpoint).
432
+ # 1-arg head/tail projections on a `Constant<Range>`. `first(n)` / `take(n)` return the first `n`
433
+ # elements, `last(n)` the final `n`, and `min(n)` / `max(n)` the n smallest / largest (for an
434
+ # ascending integer range `min(n) == first(n)` and `max(n) == last(n).reverse`) each lifts to a
435
+ # per-position `Tuple[Constant[Integer]…]`. The no-arg `first` / `last` / `min` / `max` stay on the
436
+ # unary path (single Integer endpoint).
497
437
  RANGE_FOLD_BINARY_METHODS = Set[:first, :last, :take, :min, :max].freeze
498
438
  RANGE_TO_A_LIMIT = 16
499
439
  private_constant :RANGE_FOLD_METHODS, :RANGE_FOLD_BINARY_METHODS, :RANGE_TO_A_LIMIT
@@ -516,10 +456,9 @@ module Rigor
516
456
  when :last, :max then range_endpoint_constant(range, :last)
517
457
  when :count, :size, :length then Type::Combinator.constant_of(range.to_a.size)
518
458
  when :minmax then range_minmax_tuple(range)
519
- # `range.sum` is closed-form (Gauss) for an integer range, so a
520
- # huge range still costs O(1) and yields a single Integer no
521
- # materialisation, no cap needed. Endless ranges are already
522
- # excluded by the Integer-endpoint guard in the caller.
459
+ # `range.sum` is closed-form (Gauss) for an integer range, so a huge range still costs O(1) and
460
+ # yields a single Integer no materialisation, no cap needed. Endless ranges are already excluded
461
+ # by the Integer-endpoint guard in the caller.
523
462
  when :sum then Type::Combinator.constant_of(range.sum)
524
463
  end
525
464
  end
@@ -554,9 +493,8 @@ module Rigor
554
493
  )
555
494
  end
556
495
 
557
- # `(1..10).first(3)` / `.take(3)` / `.last(3)` — the 1-arg head /
558
- # tail forms. `first`/`last` already fold no-arg through the unary
559
- # path; this is the n-arg sibling, mirroring the Tuple carrier's
496
+ # `(1..10).first(3)` / `.take(3)` / `.last(3)` — the 1-arg head / tail forms. `first`/`last` already
497
+ # fold no-arg through the unary path; this is the n-arg sibling, mirroring the Tuple carrier's
560
498
  # `first(n)`/`take(n)` handlers. Lifts to `Tuple[Constant…]`.
561
499
  def try_fold_range_constant_binary(receiver_values, method_name, arg_values)
562
500
  return nil unless RANGE_FOLD_BINARY_METHODS.include?(method_name)
@@ -573,11 +511,9 @@ module Rigor
573
511
 
574
512
  def range_take_tuple(range, method_name, count)
575
513
  return nil unless count.is_a?(Integer) && !count.negative?
576
- # `first(n)`/`last(n)`/`take(n)`/`min(n)`/`max(n)` materialise at
577
- # most `min(n, size)` elements; cap that count so a huge `n` (or
578
- # range) never blows up the Constant. `Range#size` and the head/
579
- # tail projections are O(n) for integer endpoints (no full
580
- # materialisation).
514
+ # `first(n)`/`last(n)`/`take(n)`/`min(n)`/`max(n)` materialise at most `min(n, size)` elements; cap
515
+ # that count so a huge `n` (or range) never blows up the Constant. `Range#size` and the head/tail
516
+ # projections are O(n) for integer endpoints (no full materialisation).
581
517
  return nil if [count, range.size].min > RANGE_TO_A_LIMIT
582
518
 
583
519
  values = range_head_tail(range, method_name, count)
@@ -586,10 +522,9 @@ module Rigor
586
522
  Type::Combinator.tuple_of(*values.map { |v| Type::Combinator.constant_of(v) })
587
523
  end
588
524
 
589
- # The n elements a head/tail projection selects, in Ruby's order.
590
- # For an ascending integer range `min(n)` is the leading `n`
591
- # (`first(n)`) and `max(n)` the trailing `n` reversed (descending),
592
- # so neither needs the full sort `Array#min`/`#max` would do.
525
+ # The n elements a head/tail projection selects, in Ruby's order. For an ascending integer range
526
+ # `min(n)` is the leading `n` (`first(n)`) and `max(n)` the trailing `n` reversed (descending), so
527
+ # neither needs the full sort `Array#min`/`#max` would do.
593
528
  def range_head_tail(range, method_name, count)
594
529
  case method_name
595
530
  when :last then range.last(count)
@@ -619,77 +554,62 @@ module Rigor
619
554
  end
620
555
  build_constant_type(results, source: receiver_values + arg_values)
621
556
  end
622
- # v0.0.7 — `Constant<String>#chars` / `bytes` / `codepoints` /
623
- # `grapheme_clusters` / `lines` / `split` (no-arg) return a Ruby
624
- # Array of foldable scalars; `foldable_constant_value?` rejects Array
625
- # results, so the standard unary path declines. Lift the
626
- # Array to a per-position `Tuple[Constant…]` directly,
627
- # capped at `STRING_ARRAY_LIFT_LIMIT` to keep the result
628
- # bounded for long strings. (`codepoints` yields per-character
629
- # Integer codepoints, the sibling of the byte-valued `bytes`;
630
- # `grapheme_clusters` is the extended-grapheme sibling of `chars`.)
631
- # `shellsplit` is the String-receiver twin of the already-folded
632
- # `Shellwords.split` — lifts the token Array to a Tuple. Raises
633
- # `ArgumentError` on unmatched quotes, which `try_fold_string_array_unary`
634
- # rescues to nil (RBS tier widens). `shellwords` is loaded process-wide
635
- # via `shellwords_folding`.
557
+ # v0.0.7 — `Constant<String>#chars` / `bytes` / `codepoints` / `grapheme_clusters` / `lines` /
558
+ # `split` (no-arg) return a Ruby Array of foldable scalars; `foldable_constant_value?` rejects
559
+ # Array results, so the standard unary path declines. Lift the Array to a per-position
560
+ # `Tuple[Constant…]` directly, capped at `STRING_ARRAY_LIFT_LIMIT` to keep the result bounded for
561
+ # long strings. (`codepoints` yields per-character Integer codepoints, the sibling of the
562
+ # byte-valued `bytes`; `grapheme_clusters` is the extended-grapheme sibling of `chars`.)
563
+ # `shellsplit` is the String-receiver twin of the already-folded `Shellwords.split` lifts the
564
+ # token Array to a Tuple. Raises `ArgumentError` on unmatched quotes, which
565
+ # `try_fold_string_array_unary` rescues to nil (RBS tier widens). `shellwords` is loaded
566
+ # process-wide via `shellwords_folding`.
636
567
  STRING_ARRAY_UNARY_METHODS = Set[:chars, :bytes, :codepoints, :grapheme_clusters,
637
568
  :lines, :split, :shellsplit].freeze
638
- # `partition` / `rpartition` always return a fixed 3-element
639
- # `[head, separator, tail]` Array whose members are substrings of
640
- # the receiver (bounded by the input), so they lift to a precise
641
- # 3-slot `Tuple[Constant…]`.
569
+ # `partition` / `rpartition` always return a fixed 3-element `[head, separator, tail]` Array whose
570
+ # members are substrings of the receiver (bounded by the input), so they lift to a precise 3-slot
571
+ # `Tuple[Constant…]`.
642
572
  STRING_ARRAY_BINARY_METHODS = Set[:split, :scan, :partition, :rpartition].freeze
643
573
  STRING_ARRAY_LIFT_LIMIT = 32
644
574
  private_constant :STRING_ARRAY_UNARY_METHODS,
645
575
  :STRING_ARRAY_BINARY_METHODS,
646
576
  :STRING_ARRAY_LIFT_LIMIT
647
577
 
648
- # `Constant<Regexp>#names` returns an Array of capture-group name
649
- # strings. Lifted to a Tuple so downstream narrowing can project
650
- # per-element types. The catalog classifies the C body as `:leaf`
578
+ # `Constant<Regexp>#names` returns an Array of capture-group name strings. Lifted to a Tuple so
579
+ # downstream narrowing can project per-element types. The catalog classifies the C body as `:leaf`
651
580
  # so it is safe to evaluate at fold time; no `$~` side effect.
652
581
  REGEXP_ARRAY_UNARY_METHODS = Set[:names].freeze
653
582
  private_constant :REGEXP_ARRAY_UNARY_METHODS
654
583
 
655
- # `Constant<Set>#to_a` returns an Array of the set's elements.
656
- # Ruby 3.2+ Set is C-implemented with a Hash as its backing store,
657
- # so element ordering is deterministic (insertion order).
658
- # The catalog marks `to_a` as `:dispatch` (it calls through to the
659
- # internal hash), so this dedicated handler bypasses the catalog gate.
584
+ # `Constant<Set>#to_a` returns an Array of the set's elements. Ruby 3.2+ Set is C-implemented with a
585
+ # Hash as its backing store, so element ordering is deterministic (insertion order). The catalog
586
+ # marks `to_a` as `:dispatch` (it calls through to the internal hash), so this dedicated handler
587
+ # bypasses the catalog gate.
660
588
  SET_ARRAY_UNARY_METHODS = Set[:to_a, :entries].freeze
661
589
  private_constant :SET_ARRAY_UNARY_METHODS
662
590
 
663
- # `Constant<Integer>#digits` returns the base-10 (or base-n with
664
- # an argument only the no-arg form is folded here) place
665
- # values as a little-endian Array of Integers. Lifted to a
666
- # Tuple so downstream rules see the precise per-position type.
667
- # `digits` raises `Math::DomainError` on a negative receiver,
668
- # so the negative case bails to the RBS tier.
591
+ # `Constant<Integer>#digits` returns the base-10 (or base-n with an argument — only the no-arg form
592
+ # is folded here) place values as a little-endian Array of Integers. Lifted to a Tuple so downstream
593
+ # rules see the precise per-position type. `digits` raises `Math::DomainError` on a negative
594
+ # receiver, so the negative case bails to the RBS tier.
669
595
  INTEGER_ARRAY_UNARY_METHODS = Set[:digits].freeze
670
596
  private_constant :INTEGER_ARRAY_UNARY_METHODS
671
597
 
672
- # 1-arg Integer methods that return an Array of foldable
673
- # Integers: `digits(base)` (base-n place values; raises on a
674
- # negative receiver or base < 2 declines) and `gcdlcm(other)`
675
- # (the fixed `[gcd, lcm]` pair). Both are pure arithmetic; the
676
- # result lifts to a `Tuple[Constant[Integer]…]`.
598
+ # 1-arg Integer methods that return an Array of foldable Integers: `digits(base)` (base-n place
599
+ # values; raises on a negative receiver or base < 2 → declines) and `gcdlcm(other)` (the fixed
600
+ # `[gcd, lcm]` pair). Both are pure arithmetic; the result lifts to a `Tuple[Constant[Integer]…]`.
677
601
  INTEGER_ARRAY_BINARY_METHODS = Set[:digits, :gcdlcm].freeze
678
602
  private_constant :INTEGER_ARRAY_BINARY_METHODS
679
603
 
680
- # v0.0.7 — `Constant<Pathname>` delegates to a curated set
681
- # of pure path-manipulation methods. Pathname is immutable
682
- # in Ruby (per its docstring) and the catalog classifies
683
- # most methods `:dispatch` because the C body delegates to
684
- # File / Dir / FileTest. The methods listed here are
685
- # filesystem-independent — they read only `@path` — so
686
- # invoking them at fold time produces a deterministic
687
- # result regardless of the host filesystem state.
604
+ # v0.0.7 — `Constant<Pathname>` delegates to a curated set of pure path-manipulation methods.
605
+ # Pathname is immutable in Ruby (per its docstring) and the catalog classifies most methods
606
+ # `:dispatch` because the C body delegates to File / Dir / FileTest. The methods listed here are
607
+ # filesystem-independent they read only `@path` so invoking them at fold time produces a
608
+ # deterministic result regardless of the host filesystem state.
688
609
  #
689
- # Filesystem-touching methods (`exist?`, `file?`, `read`,
690
- # `stat`, …) are intentionally NOT folded: their answer
691
- # depends on the analysis machine's filesystem, which is
692
- # neither stable nor relevant to the analyzed program.
610
+ # Filesystem-touching methods (`exist?`, `file?`, `read`, `stat`, …) are intentionally NOT folded:
611
+ # their answer depends on the analysis machine's filesystem, which is neither stable nor relevant to
612
+ # the analyzed program.
693
613
  PATHNAME_PURE_UNARY = Set[
694
614
  :to_s, :to_path, :to_str,
695
615
  :basename, :dirname, :extname, :cleanpath,
@@ -699,22 +619,19 @@ module Rigor
699
619
  PATHNAME_PURE_BINARY = Set[
700
620
  :+, :join, :sub_ext, :<=>, :==, :eql?, :===,
701
621
  :relative_path_from,
702
- # `/` is the exact alias of `+` (`def /(other) = self + other`),
703
- # the idiomatic path-join operator (`dir / "file"`). `basename`'s
704
- # 1-arg suffix-stripping form (`path.basename(".rb")` the stem)
705
- # is the binary sibling of the already-folded no-arg `basename`
706
- # both are pure `@path` string manipulation, no filesystem read.
622
+ # `/` is the exact alias of `+` (`def /(other) = self + other`), the idiomatic path-join operator
623
+ # (`dir / "file"`). `basename`'s 1-arg suffix-stripping form (`path.basename(".rb")` → the stem)
624
+ # is the binary sibling of the already-folded no-arg `basename` both are pure `@path` string
625
+ # manipulation, no filesystem read.
707
626
  :/, :basename
708
627
  ].freeze
709
628
  private_constant :PATHNAME_PURE_UNARY, :PATHNAME_PURE_BINARY
710
629
 
711
- # `Constant<Pathname>#split` returns the fixed 2-element
712
- # `[dirname, basename]` pair (both Pathname), the path-string
713
- # split of `File.split`. Lifted to `Tuple[Constant[Pathname],
714
- # Constant[Pathname]]`. Filesystem-independent reads only
715
- # `@path` so it is deterministic at fold time, the
716
- # Array-returning sibling of the scalar `basename` / `dirname`
717
- # folds (which `try_fold_pathname_unary` already covers).
630
+ # `Constant<Pathname>#split` returns the fixed 2-element `[dirname, basename]` pair (both Pathname),
631
+ # the path-string split of `File.split`. Lifted to `Tuple[Constant[Pathname], Constant[Pathname]]`.
632
+ # Filesystem-independent reads only `@path` so it is deterministic at fold time, the
633
+ # Array-returning sibling of the scalar `basename` / `dirname` folds (which
634
+ # `try_fold_pathname_unary` already covers).
718
635
  PATHNAME_ARRAY_UNARY_METHODS = Set[:split].freeze
719
636
  private_constant :PATHNAME_ARRAY_UNARY_METHODS
720
637
 
@@ -750,10 +667,9 @@ module Rigor
750
667
  nil
751
668
  end
752
669
 
753
- # `Constant<Pathname>#split` — lift the `[dirname, basename]`
754
- # Pathname pair to a Tuple[Constant[Pathname], Constant[Pathname]].
755
- # Pure path-string manipulation (no filesystem read); both
756
- # elements are Pathname, a foldable Constant class.
670
+ # `Constant<Pathname>#split` — lift the `[dirname, basename]` Pathname pair to a
671
+ # Tuple[Constant[Pathname], Constant[Pathname]]. Pure path-string manipulation (no filesystem
672
+ # read); both elements are Pathname, a foldable Constant class.
757
673
  def try_fold_pathname_array_unary(receiver_values, method_name)
758
674
  return nil unless PATHNAME_ARRAY_UNARY_METHODS.include?(method_name)
759
675
  return nil unless receiver_values.size == 1
@@ -778,10 +694,9 @@ module Rigor
778
694
  nil
779
695
  end
780
696
 
781
- # `Constant<Regexp>#names` — lift the Array[String] of named-capture
782
- # group names to a Tuple[Constant[String]…]. Safe to evaluate at fold
783
- # time: the C body reads only the regexp's internal names table,
784
- # writes no global state, and always returns an Array of frozen Strings.
697
+ # `Constant<Regexp>#names` — lift the Array[String] of named-capture group names to a
698
+ # Tuple[Constant[String]…]. Safe to evaluate at fold time: the C body reads only the regexp's
699
+ # internal names table, writes no global state, and always returns an Array of frozen Strings.
785
700
  def try_fold_regexp_array_unary(receiver_values, method_name)
786
701
  return nil unless REGEXP_ARRAY_UNARY_METHODS.include?(method_name)
787
702
  return nil unless receiver_values.size == 1
@@ -794,10 +709,9 @@ module Rigor
794
709
  nil
795
710
  end
796
711
 
797
- # `Constant<Set>#to_a` / `#entries` — lift the Array of set elements
798
- # to a Tuple[Constant[…]…] when every element is a foldable scalar.
799
- # Ruby 3.2+ Set is C-implemented; element order is deterministic
800
- # (insertion order), so the result is stable across invocations.
712
+ # `Constant<Set>#to_a` / `#entries` — lift the Array of set elements to a Tuple[Constant[…]…] when
713
+ # every element is a foldable scalar. Ruby 3.2+ Set is C-implemented; element order is
714
+ # deterministic (insertion order), so the result is stable across invocations.
801
715
  def try_fold_set_array_unary(receiver_values, method_name)
802
716
  return nil unless SET_ARRAY_UNARY_METHODS.include?(method_name)
803
717
  return nil unless receiver_values.size == 1
@@ -810,11 +724,10 @@ module Rigor
810
724
  nil
811
725
  end
812
726
 
813
- # `Constant<Integer>#digits` — lift the Array of base-10 place
814
- # values to a Tuple[Constant[Integer]…]. Safe to evaluate at
815
- # fold time: the C body is pure arithmetic. Negative receivers
816
- # raise `Math::DomainError`; the fold declines so the RBS tier
817
- # answers with `Array[Integer]`.
727
+ # `Constant<Integer>#digits` — lift the Array of base-10 place values to a
728
+ # Tuple[Constant[Integer]…]. Safe to evaluate at fold time: the C body is pure arithmetic. Negative
729
+ # receivers raise `Math::DomainError`; the fold declines so the RBS tier answers with
730
+ # `Array[Integer]`.
818
731
  def try_fold_integer_array_unary(receiver_values, method_name)
819
732
  return nil unless INTEGER_ARRAY_UNARY_METHODS.include?(method_name)
820
733
  return nil unless receiver_values.size == 1
@@ -828,11 +741,9 @@ module Rigor
828
741
  nil
829
742
  end
830
743
 
831
- # `Constant<Integer>#digits(base)` / `#gcdlcm(other)` — the
832
- # 1-arg Array-returning Integer methods. `digits(base)` declines
833
- # on a negative receiver (the unary path's guard); other domain
834
- # errors (base < 2) raise and are rescued. `gcdlcm` is total over
835
- # Integer args.
744
+ # `Constant<Integer>#digits(base)` / `#gcdlcm(other)` — the 1-arg Array-returning Integer methods.
745
+ # `digits(base)` declines on a negative receiver (the unary path's guard); other domain errors
746
+ # (base < 2) raise and are rescued. `gcdlcm` is total over Integer args.
836
747
  def try_fold_integer_array_binary(receiver_values, method_name, arg_values)
837
748
  return nil unless INTEGER_ARRAY_BINARY_METHODS.include?(method_name)
838
749
  return nil unless receiver_values.size == 1 && arg_values.size == 1
@@ -847,18 +758,16 @@ module Rigor
847
758
  nil
848
759
  end
849
760
 
850
- # `Constant<Complex>#rect` / `#rectangular` — lifts `[real, imaginary]`
851
- # to `Tuple[Constant[re], Constant[im]]`. Both components are always
852
- # numeric (Integer or Float for literal complexes), so they satisfy
853
- # `foldable_constant_value?`.
761
+ # `Constant<Complex>#rect` / `#rectangular` — lifts `[real, imaginary]` to
762
+ # `Tuple[Constant[re], Constant[im]]`. Both components are always numeric (Integer or Float for
763
+ # literal complexes), so they satisfy `foldable_constant_value?`.
854
764
  #
855
- # `Constant<Complex>#polar` — lifts `[abs, arg]` to
856
- # `Tuple[Constant[Float], Constant[Float]]`. Evaluated at fold time
857
- # via `Complex#polar` (which calls `Math.hypot` and `Math.atan2`).
765
+ # `Constant<Complex>#polar` — lifts `[abs, arg]` to `Tuple[Constant[Float], Constant[Float]]`.
766
+ # Evaluated at fold time via `Complex#polar` (which calls `Math.hypot` and `Math.atan2`).
858
767
  # Deterministic: reads only the receiver's real and imaginary parts.
859
768
  #
860
- # Rational receivers also support `rect` / `rectangular` / `polar`:
861
- # `Rational(r,1).rect` → `[r, 0]`, `Rational(r,1).polar` → `[abs, arg]`.
769
+ # Rational receivers also support `rect` / `rectangular` / `polar`: `Rational(r,1).rect` →
770
+ # `[r, 0]`, `Rational(r,1).polar` → `[abs, arg]`.
862
771
  NUMERIC_ARRAY_UNARY_METHODS = Set[:rect, :rectangular, :polar].freeze
863
772
  private_constant :NUMERIC_ARRAY_UNARY_METHODS
864
773
 
@@ -874,9 +783,8 @@ module Rigor
874
783
  nil
875
784
  end
876
785
 
877
- # `Constant<String>#split(arg)` / `#scan(arg)` — lift the
878
- # Array result to a Tuple when both sides are statically
879
- # known and the cardinality fits.
786
+ # `Constant<String>#split(arg)` / `#scan(arg)` — lift the Array result to a Tuple when both sides
787
+ # are statically known and the cardinality fits.
880
788
  def try_fold_string_array_binary(receiver_values, method_name, arg_values)
881
789
  return nil unless STRING_ARRAY_BINARY_METHODS.include?(method_name)
882
790
  return nil unless receiver_values.size == 1 && arg_values.size == 1
@@ -899,17 +807,14 @@ module Rigor
899
807
  Type::Combinator.tuple_of(*result.map { |v| Type::Combinator.constant_of(v) })
900
808
  end
901
809
 
902
- # 2-arg fold dispatch. Used by `Comparable#between?(min, max)`,
903
- # `Comparable#clamp(min, max)`, and `Integer#pow(exp, mod)` —
904
- # methods the catalog classifies `:leaf` but that the prior
905
- # 0/1-arg switch could not reach.
810
+ # 2-arg fold dispatch. Used by `Comparable#between?(min, max)`, `Comparable#clamp(min, max)`, and
811
+ # `Integer#pow(exp, mod)` — methods the catalog classifies `:leaf` but that the prior 0/1-arg switch
812
+ # could not reach.
906
813
  #
907
- # v0.0.6 — IntegerRange-shaped receivers participate in
908
- # `Comparable#between?` and `Comparable#clamp` folds.
909
- # `int<a,b>.between?(min, max)` decides three-valued via
910
- # the receiver's bounds against scalar args; `int<a,b>.clamp`
911
- # narrows the receiver's bounds against the bracket. Other
912
- # ternary methods over IntegerRange operands still decline.
814
+ # v0.0.6 — IntegerRange-shaped receivers participate in `Comparable#between?` and
815
+ # `Comparable#clamp` folds. `int<a,b>.between?(min, max)` decides three-valued via the receiver's
816
+ # bounds against scalar args; `int<a,b>.clamp` narrows the receiver's bounds against the bracket.
817
+ # Other ternary methods over IntegerRange operands still decline.
913
818
  def try_fold_ternary(receiver_set, method_name, arg_sets)
914
819
  return try_fold_ternary_range(receiver_set, method_name, arg_sets) if receiver_set.is_a?(Type::IntegerRange)
915
820
  return nil if arg_sets.any?(Type::IntegerRange)
@@ -917,11 +822,9 @@ module Rigor
917
822
  try_fold_ternary_set(receiver_set, method_name, arg_sets)
918
823
  end
919
824
 
920
- # Receiver IntegerRange + two scalar `Constant<Integer>`
921
- # args the only IntegerRange-aware ternary fold today.
922
- # `between?` returns Trinary truthiness over the bracket;
923
- # `clamp` returns the intersected IntegerRange (or a
924
- # collapsed Constant if the result pins a single point).
825
+ # Receiver IntegerRange + two scalar `Constant<Integer>` args — the only IntegerRange-aware ternary
826
+ # fold today. `between?` returns Trinary truthiness over the bracket; `clamp` returns the
827
+ # intersected IntegerRange (or a collapsed Constant if the result pins a single point).
925
828
  def try_fold_ternary_range(range, method_name, arg_sets)
926
829
  return nil unless arg_sets.all?(Array)
927
830
 
@@ -957,12 +860,9 @@ module Rigor
957
860
 
958
861
  # `int<a,b>.clamp(min, max)`:
959
862
  # - new_lower = max(a, min), new_upper = min(b, max).
960
- # - When new_lower > new_upper the bracket excluded the
961
- # range entirely; the call still returns one of the
962
- # bracket bounds at runtime, but Rigor is strictly less
963
- # precise here than Ruby — decline so the RBS tier
964
- # widens to plain Integer rather than the dispatcher
965
- # inventing a value.
863
+ # - When new_lower > new_upper the bracket excluded the range entirely; the call still returns one
864
+ # of the bracket bounds at runtime, but Rigor is strictly less precise here than Ruby — decline
865
+ # so the RBS tier widens to plain Integer rather than the dispatcher inventing a value.
966
866
  def range_clamp(range, min_arg, max_arg)
967
867
  new_lower = clamp_lower_bound(range.lower, min_arg)
968
868
  new_upper = clamp_upper_bound(range.upper, max_arg)
@@ -1017,15 +917,12 @@ module Rigor
1017
917
  catalog_allows?(receiver_value, method_name)
1018
918
  end
1019
919
 
1020
- # Builds a Constant or Union[Constant…] from a flat list of
1021
- # Ruby values. When the deduped set exceeds
1022
- # `UNION_FOLD_OUTPUT_LIMIT` and every result is an Integer,
1023
- # widens to the bounding `IntegerRange` instead of returning
1024
- # nil that is the graceful escape valve for additions over
1025
- # disjoint integer ranges. The `source` array is used only as
1026
- # a hint that the result set's "Integer-ness" was already
1027
- # implied by the inputs (so the widening fallback only fires
1028
- # for arithmetic over integers).
920
+ # Builds a Constant or Union[Constant…] from a flat list of Ruby values. When the deduped set
921
+ # exceeds `UNION_FOLD_OUTPUT_LIMIT` and every result is an Integer, widens to the bounding
922
+ # `IntegerRange` instead of returning nil — that is the graceful escape valve for additions over
923
+ # disjoint integer ranges. The `source` array is used only as a hint that the result set's
924
+ # "Integer-ness" was already implied by the inputs (so the widening fallback only fires for
925
+ # arithmetic over integers).
1029
926
  def build_constant_type(values, source: nil)
1030
927
  return nil if values.empty?
1031
928
 
@@ -1041,11 +938,9 @@ module Rigor
1041
938
  constants.size == 1 ? constants.first : Type::Combinator.union(*constants)
1042
939
  end
1043
940
 
1044
- # Widening fallback: when every successful result is an
1045
- # Integer, return the bounding `IntegerRange` rather than
1046
- # losing the answer entirely. The fallback is also gated on
1047
- # the input set being all-integers, so a fold whose results
1048
- # happen to land on integers but whose receivers were Floats
941
+ # Widening fallback: when every successful result is an Integer, return the bounding `IntegerRange`
942
+ # rather than losing the answer entirely. The fallback is also gated on the input set being
943
+ # all-integers, so a fold whose results happen to land on integers but whose receivers were Floats
1049
944
  # does not silently change shape.
1050
945
  def widen_to_integer_range(values, source)
1051
946
  return nil unless values.all?(Integer)
@@ -1054,9 +949,8 @@ module Rigor
1054
949
  Type::Combinator.integer_range(values.min, values.max)
1055
950
  end
1056
951
 
1057
- # Reserved hook: present so future `:strict` modes can raise
1058
- # rather than silently returning nil. Today it always returns
1059
- # nil so behaviour is unchanged.
952
+ # Reserved hook: present so future `:strict` modes can raise rather than silently returning nil.
953
+ # Today it always returns nil so behaviour is unchanged.
1060
954
  def raise_if_strict
1061
955
  nil
1062
956
  end
@@ -1068,9 +962,8 @@ module Rigor
1068
962
  RANGE_ADDITIVE = Set[:+, :-].freeze
1069
963
  RANGE_COMPARISON = Set[:<, :<=, :>, :>=, :==, :!=].freeze
1070
964
 
1071
- # Per-operator dispatch table for binary range ops. Each
1072
- # value is a method symbol on `ConstantFolding` taking
1073
- # `(left, right)` and returning a `Type` or `nil`.
965
+ # Per-operator dispatch table for binary range ops. Each value is a method symbol on
966
+ # `ConstantFolding` taking `(left, right)` and returning a `Type` or `nil`.
1074
967
  BINARY_RANGE_HANDLERS = {
1075
968
  :* => :range_multiply,
1076
969
  :/ => :range_divide,
@@ -1089,11 +982,9 @@ module Rigor
1089
982
  nil
1090
983
  end
1091
984
 
1092
- # Promotes an array-of-values input to an `IntegerRange` when
1093
- # every value is an `Integer`. Used so a mixed `Constant +
1094
- # IntegerRange` call can be reduced to range × range
1095
- # arithmetic. Returns `nil` for non-Integer arrays so a
1096
- # `Constant[Float]` does not silently degrade.
985
+ # Promotes an array-of-values input to an `IntegerRange` when every value is an `Integer`. Used so a
986
+ # mixed `Constant + IntegerRange` call can be reduced to range × range arithmetic. Returns `nil` for
987
+ # non-Integer arrays so a `Constant[Float]` does not silently degrade.
1097
988
  def ensure_integer_range(operand)
1098
989
  case operand
1099
990
  when Type::IntegerRange then operand
@@ -1113,10 +1004,9 @@ module Rigor
1113
1004
  build_integer_range(lower, upper)
1114
1005
  end
1115
1006
 
1116
- # Range × Range. Computes the four corner products with
1117
- # `safe_mul` so that `0 × ±∞` is treated as 0 rather than
1118
- # NaN that captures the algebraic truth that the actual
1119
- # range elements are integers, never literal infinity.
1007
+ # Range × Range. Computes the four corner products with `safe_mul` so that `0 × ±∞` is treated as 0
1008
+ # rather than NaN — that captures the algebraic truth that the actual range elements are integers,
1009
+ # never literal infinity.
1120
1010
  def range_multiply(left, right)
1121
1011
  corners = [
1122
1012
  safe_mul(left.lower, right.lower),
@@ -1127,21 +1017,18 @@ module Rigor
1127
1017
  build_integer_range(corners.min, corners.max)
1128
1018
  end
1129
1019
 
1130
- # 0 dominates: 0 × anything (including ±∞) is 0. Without this
1131
- # special case Ruby's `0 * Float::INFINITY` is `NaN`, which
1132
- # would corrupt `min`/`max`.
1020
+ # 0 dominates: 0 × anything (including ±∞) is 0. Without this special case Ruby's
1021
+ # `0 * Float::INFINITY` is `NaN`, which would corrupt `min`/`max`.
1133
1022
  def safe_mul(left, right)
1134
1023
  return 0 if left.zero? || right.zero?
1135
1024
 
1136
1025
  left * right
1137
1026
  end
1138
1027
 
1139
- # Range ÷ Range using Ruby's integer floor division. If the
1140
- # right range covers 0 the operation may raise
1141
- # `ZeroDivisionError`, so the fold bails (caller falls back
1142
- # to RBS-widened `Integer`). When both inputs are finite we
1143
- # compute the four corner quotients; the universal-on-one-side
1144
- # case is handled by treating ±∞ ÷ n as ±∞ and n ÷ ±∞ as 0.
1028
+ # Range ÷ Range using Ruby's integer floor division. If the right range covers 0 the operation may
1029
+ # raise `ZeroDivisionError`, so the fold bails (caller falls back to RBS-widened `Integer`). When
1030
+ # both inputs are finite we compute the four corner quotients; the universal-on-one-side case is
1031
+ # handled by treating ±∞ ÷ n as ±∞ and n ÷ ±∞ as 0.
1145
1032
  def range_divide(left, right)
1146
1033
  return nil if right.covers?(0)
1147
1034
 
@@ -1166,10 +1053,9 @@ module Rigor
1166
1053
  numer.to_i.div(denom.to_i).to_f
1167
1054
  end
1168
1055
 
1169
- # Range % Range. Only the `(any range) % (positive constant n)`
1170
- # and `(any range) % (negative constant n)` cases are folded
1171
- # precisely the former narrows to `int<0, n-1>`, the latter
1172
- # to `int<n+1, 0>`. Other shapes fall back to nil.
1056
+ # Range % Range. Only the `(any range) % (positive constant n)` and `(any range) % (negative
1057
+ # constant n)` cases are folded precisely — the former narrows to `int<0, n-1>`, the latter to
1058
+ # `int<n+1, 0>`. Other shapes fall back to nil.
1173
1059
  def range_modulo(_left, right)
1174
1060
  return nil unless right.finite? && right.min == right.max
1175
1061
 
@@ -1183,10 +1069,9 @@ module Rigor
1183
1069
  end
1184
1070
  end
1185
1071
 
1186
- # Builds an `IntegerRange` from numeric `lower`/`upper`
1187
- # endpoints. Collapses single-point finite ranges to a
1188
- # `Constant` so downstream rules (which prefer the more
1189
- # specific carrier) see the most precise result.
1072
+ # Builds an `IntegerRange` from numeric `lower`/`upper` endpoints. Collapses single-point finite
1073
+ # ranges to a `Constant` so downstream rules (which prefer the more specific carrier) see the most
1074
+ # precise result.
1190
1075
  def build_integer_range(lower, upper)
1191
1076
  min = lower == -Float::INFINITY ? Type::IntegerRange::NEG_INFINITY : Integer(lower)
1192
1077
  max = upper == Float::INFINITY ? Type::IntegerRange::POS_INFINITY : Integer(upper)
@@ -1260,10 +1145,9 @@ module Rigor
1260
1145
  RANGE_UNARY_SHIFTS = Set[:succ, :next, :pred].freeze
1261
1146
  RANGE_UNARY_PARITY = Set[:even?, :odd?].freeze
1262
1147
 
1263
- # `(method_name) -> handler symbol` for the unary range
1264
- # surface that does not need extra context. The grouped
1265
- # categories (predicates / shifts / parity) stay separate
1266
- # because they share dispatch logic.
1148
+ # `(method_name) -> handler symbol` for the unary range surface that does not need extra context.
1149
+ # The grouped categories (predicates / shifts / parity) stay separate because they share dispatch
1150
+ # logic.
1267
1151
  UNARY_RANGE_DIRECT = {
1268
1152
  abs: :range_unary_abs,
1269
1153
  magnitude: :range_unary_abs,
@@ -1295,10 +1179,8 @@ module Rigor
1295
1179
  range
1296
1180
  end
1297
1181
 
1298
- # `even?`/`odd?` on a single-point range collapses to an
1299
- # exact `Constant[bool]`. Any range spanning 2 integers
1300
- # contains both an even and an odd value, so the result is
1301
- # `Union[true, false]`.
1182
+ # `even?`/`odd?` on a single-point range collapses to an exact `Constant[bool]`. Any range spanning
1183
+ # 2 integers contains both an even and an odd value, so the result is `Union[true, false]`.
1302
1184
  def range_unary_parity(range, method_name)
1303
1185
  if range.finite? && range.min == range.max
1304
1186
  value = range.min.public_send(method_name)
@@ -1308,11 +1190,10 @@ module Rigor
1308
1190
  end
1309
1191
  end
1310
1192
 
1311
- # Integer#bit_length is non-negative and bounded by the
1312
- # bit_length of the wider endpoint. For half-open ranges the
1313
- # upper bound is unknown (any large integer is reachable), so
1314
- # we widen to non_negative_int. Negative endpoints map via
1315
- # `~n` semantics; using the magnitude is a safe upper bound.
1193
+ # Integer#bit_length is non-negative and bounded by the bit_length of the wider endpoint. For
1194
+ # half-open ranges the upper bound is unknown (any large integer is reachable), so we widen to
1195
+ # non_negative_int. Negative endpoints map via `~n` semantics; using the magnitude is a safe upper
1196
+ # bound.
1316
1197
  def range_unary_bit_length(range)
1317
1198
  return Type::Combinator.non_negative_int unless range.finite?
1318
1199
 
@@ -1377,9 +1258,8 @@ module Rigor
1377
1258
 
1378
1259
  # ----------------------------------------------------------------
1379
1260
 
1380
- # Returns `[value]` on success, `nil` to signal "skip this pair".
1381
- # The 1-element-array shape lets callers distinguish a successful
1382
- # `false`/`nil` fold from a skipped pair when chaining via
1261
+ # Returns `[value]` on success, `nil` to signal "skip this pair". The 1-element-array shape lets
1262
+ # callers distinguish a successful `false`/`nil` fold from a skipped pair when chaining via
1383
1263
  # `flat_map`.
1384
1264
  def invoke_binary(receiver_value, method_name, arg_value)
1385
1265
  return nil unless safe?(receiver_value, method_name, arg_value)
@@ -1390,10 +1270,9 @@ module Rigor
1390
1270
  nil
1391
1271
  end
1392
1272
 
1393
- # Returns `[value]` on success, `nil` to signal "skip this triple".
1394
- # Mirrors `invoke_binary` but for the 2-argument shape; the wrap
1395
- # convention lets callers `flat_map` without losing
1396
- # legitimate `false`/`nil` folds.
1273
+ # Returns `[value]` on success, `nil` to signal "skip this triple". Mirrors `invoke_binary` but for
1274
+ # the 2-argument shape; the wrap convention lets callers `flat_map` without losing legitimate
1275
+ # `false`/`nil` folds.
1397
1276
  def invoke_ternary(receiver_value, method_name, av0, av1)
1398
1277
  return nil unless ternary_method_allowed?(receiver_value, method_name)
1399
1278
 
@@ -1403,8 +1282,7 @@ module Rigor
1403
1282
  nil
1404
1283
  end
1405
1284
 
1406
- # Returns `[value]` on success, `nil` to signal "skip". See
1407
- # `invoke_binary` for why we wrap.
1285
+ # Returns `[value]` on success, `nil` to signal "skip". See `invoke_binary` for why we wrap.
1408
1286
  def invoke_unary(receiver_value, method_name)
1409
1287
  return nil unless unary_safe?(receiver_value, method_name)
1410
1288
  return nil if string_unary_blow_up?(receiver_value, method_name)
@@ -1421,29 +1299,21 @@ module Rigor
1421
1299
  catalog_allows?(receiver_value, method_name)
1422
1300
  end
1423
1301
 
1424
- # Consults the offline numeric catalog (data/builtins/ruby_core/
1425
- # numeric.yml) as a superset of the hand-rolled unary/binary
1426
- # allow lists. The catalog's `leaf` / `trivial` /
1427
- # `leaf_when_numeric` entries promise the underlying CRuby
1428
- # implementation does not call back into user-redefinable
1429
- # Ruby methods, so executing them on a literal Integer/Float
1430
- # is safe regardless of monkey-patching.
1302
+ # Consults the offline numeric catalog (data/builtins/ruby_core/numeric.yml) as a superset of the
1303
+ # hand-rolled unary/binary allow lists. The catalog's `leaf` / `trivial` / `leaf_when_numeric`
1304
+ # entries promise the underlying CRuby implementation does not call back into user-redefinable Ruby
1305
+ # methods, so executing them on a literal Integer/Float is safe regardless of monkey-patching.
1431
1306
  #
1432
1307
  # Resolution order:
1433
1308
  #
1434
- # 1. Primary class catalog (e.g. NUMERIC_CATALOG for an
1435
- # Integer receiver). When the catalog has an entry
1436
- # even one classified `:dispatch` that answer wins.
1437
- # The class's direct `rb_define_method` registration is
1438
- # authoritative; we MUST NOT fall through to a module
1439
- # catalog and risk over-folding.
1440
- # 2. Module catalogs (Comparable, Enumerable, …) that the
1441
- # receiver's class includes by ancestry. Reached only
1442
- # when the primary catalog has NO entry for the method
1443
- # — typically because the method is inherited purely
1444
- # through `include Comparable` / `include Enumerable`
1445
- # (e.g. `Integer#between?` / `Integer#clamp` are not in
1446
- # numeric.yml because the Init block does not
1309
+ # 1. Primary class catalog (e.g. NUMERIC_CATALOG for an Integer receiver). When the catalog has an
1310
+ # entry even one classified `:dispatch` that answer wins. The class's direct
1311
+ # `rb_define_method` registration is authoritative; we MUST NOT fall through to a module catalog
1312
+ # and risk over-folding.
1313
+ # 2. Module catalogs (Comparable, Enumerable, …) that the receiver's class includes by ancestry.
1314
+ # Reached only when the primary catalog has NO entry for the method — typically because the
1315
+ # method is inherited purely through `include Comparable` / `include Enumerable` (e.g.
1316
+ # `Integer#between?` / `Integer#clamp` are not in numeric.yml because the Init block does not
1447
1317
  # `rb_define_method` them on Integer).
1448
1318
  def catalog_allows?(receiver_value, method_name)
1449
1319
  catalog, class_name = catalog_for(receiver_value)
@@ -1455,11 +1325,9 @@ module Rigor
1455
1325
  end
1456
1326
  end
1457
1327
 
1458
- # `(Module, catalog, class_name)` triples consulted as a
1459
- # fallthrough when the primary class catalog has no entry.
1460
- # Each triple's Module is matched against the receiver
1461
- # class's ancestor chain at lookup time; the catalog
1462
- # corresponds to the module-mode YAML at
1328
+ # `(Module, catalog, class_name)` triples consulted as a fallthrough when the primary class catalog
1329
+ # has no entry. Each triple's Module is matched against the receiver class's ancestor chain at
1330
+ # lookup time; the catalog corresponds to the module-mode YAML at
1463
1331
  # `data/builtins/ruby_core/<topic>.yml`.
1464
1332
  MODULE_CATALOGS = Ractor.make_shareable([
1465
1333
  [Comparable, Builtins::COMPARABLE_CATALOG, "Comparable"],
@@ -1467,10 +1335,9 @@ module Rigor
1467
1335
  ])
1468
1336
  private_constant :MODULE_CATALOGS
1469
1337
 
1470
- # Returns the `(catalog, class_name)` pairs for every
1471
- # registered module that is in the receiver's ancestor
1472
- # chain. The receiver's class's `Module#ancestors` is
1473
- # cached by Ruby; the `Set` membership check is cheap.
1338
+ # Returns the `(catalog, class_name)` pairs for every registered module that is in the receiver's
1339
+ # ancestor chain. The receiver's class's `Module#ancestors` is cached by Ruby; the `Set` membership
1340
+ # check is cheap.
1474
1341
  def module_catalogs_for(receiver_value)
1475
1342
  ancestors = Set.new(receiver_value.class.ancestors)
1476
1343
  MODULE_CATALOGS.filter_map do |mod, catalog, class_name|
@@ -1478,18 +1345,13 @@ module Rigor
1478
1345
  end
1479
1346
  end
1480
1347
 
1481
- # `(catalog, class_name)` per receiver class. The class_name
1482
- # is what each catalog's RBS-rooted entries are keyed by.
1483
- # `catalog_for` walks this table in declaration order so
1484
- # subclasses (Symbol < String) hit their dedicated entry
1485
- # before any base-class fallback would, and adding a new
1486
- # class is a one-line addition rather than another `when`
1487
- # arm on a growing case statement.
1488
- # Subclass-before-superclass ordering: `DateTime < Date`,
1489
- # so the `DateTime` row MUST come before the `Date` row.
1490
- # Otherwise a `DateTime` receiver would match the `Date`
1491
- # arm first and the catalog would consult the Date entry
1492
- # in `DATE_CATALOG` for the wrong class.
1348
+ # `(catalog, class_name)` per receiver class. The class_name is what each catalog's RBS-rooted
1349
+ # entries are keyed by. `catalog_for` walks this table in declaration order so subclasses
1350
+ # (Symbol < String) hit their dedicated entry before any base-class fallback would, and adding a
1351
+ # new class is a one-line addition rather than another `when` arm on a growing case statement.
1352
+ # Subclass-before-superclass ordering: `DateTime < Date`, so the `DateTime` row MUST come before the
1353
+ # `Date` row. Otherwise a `DateTime` receiver would match the `Date` arm first and the catalog would
1354
+ # consult the Date entry in `DATE_CATALOG` for the wrong class.
1493
1355
  CATALOG_BY_CLASS = Ractor.make_shareable([
1494
1356
  [Integer, [Builtins::NUMERIC_CATALOG, "Integer"]],
1495
1357
  [Float, [Builtins::NUMERIC_CATALOG, "Float"]],
@@ -1517,8 +1379,7 @@ module Rigor
1517
1379
  ])
1518
1380
  private_constant :CATALOG_BY_CLASS
1519
1381
 
1520
- # Returns `[catalog, class_name]` for receivers we have a
1521
- # catalog for; nil otherwise.
1382
+ # Returns `[catalog, class_name]` for receivers we have a catalog for; nil otherwise.
1522
1383
  def catalog_for(receiver_value)
1523
1384
  CATALOG_BY_CLASS.each do |klass, entry|
1524
1385
  return entry if receiver_value.is_a?(klass)
@@ -1540,26 +1401,20 @@ module Rigor
1540
1401
  end
1541
1402
  end
1542
1403
 
1543
- # `String#reverse` / `#swapcase` / `#succ` etc. produce a string
1544
- # at least as large as the receiver. The binary `:+` / `:*` paths
1545
- # have their own `string_blow_up?` output guard; this is the unary
1546
- # analogue decline to fold a unary String op whose receiver is
1547
- # already at or beyond `STRING_FOLD_BYTE_LIMIT`, since the folded
1548
- # output would be just as large and constant-materialising it buys
1549
- # no precision worth the bytes. Non-String receivers never blow up
1550
- # through a unary op, so they pass.
1404
+ # `String#reverse` / `#swapcase` / `#succ` etc. produce a string at least as large as the receiver.
1405
+ # The binary `:+` / `:*` paths have their own `string_blow_up?` output guard; this is the unary
1406
+ # analogue decline to fold a unary String op whose receiver is already at or beyond
1407
+ # `STRING_FOLD_BYTE_LIMIT`, since the folded output would be just as large and constant-materialising
1408
+ # it buys no precision worth the bytes. Non-String receivers never blow up through a unary op, so
1409
+ # they pass.
1551
1410
  def string_unary_blow_up?(receiver_value, _method_name)
1552
1411
  receiver_value.is_a?(String) && receiver_value.bytesize >= STRING_FOLD_BYTE_LIMIT
1553
1412
  end
1554
1413
 
1555
- # Scalar / String / Symbol values fold; everything
1556
- # else (Array, Hash, Proc, Range, ...) is held back
1557
- # because `Type::Constant` does not model those
1558
- # carriers and surfacing one would mis-type
1559
- # downstream calls. `Range`, `Array`, and friends
1560
- # have their own shape carriers; this method picks
1561
- # the conservative envelope of "values that already
1562
- # round-trip through `Type::Combinator.constant_of`".
1414
+ # Scalar / String / Symbol values fold; everything else (Array, Hash, Proc, Range, ...) is held back
1415
+ # because `Type::Constant` does not model those carriers and surfacing one would mis-type downstream
1416
+ # calls. `Range`, `Array`, and friends have their own shape carriers; this method picks the
1417
+ # conservative envelope of "values that already round-trip through `Type::Combinator.constant_of`".
1563
1418
  FOLDABLE_CONSTANT_CLASSES = [
1564
1419
  Integer, Float, Rational, Complex, String, Symbol,
1565
1420
  Regexp, Pathname, ::Set, Date, Time,
@@ -1570,11 +1425,10 @@ module Rigor
1570
1425
  def foldable_constant_value?(value)
1571
1426
  return false unless FOLDABLE_CONSTANT_CLASSES.any? { |klass| value.is_a?(klass) }
1572
1427
 
1573
- # A NaN result (`0.0 / 0.0`, `Float::NAN`-propagating arithmetic,
1574
- # or a NaN-bearing Complex) is non-reflexive under `==`, so a
1575
- # `Constant[NaN]` would break the `==` / `eql?` / `hash` contract
1576
- # `build_constant_type` relies on for union dedup. Decline the
1577
- # fold and let the RBS tier answer with the widened class.
1428
+ # A NaN result (`0.0 / 0.0`, `Float::NAN`-propagating arithmetic, or a NaN-bearing Complex) is
1429
+ # non-reflexive under `==`, so a `Constant[NaN]` would break the `==` / `eql?` / `hash` contract
1430
+ # `build_constant_type` relies on for union dedup. Decline the fold and let the RBS tier answer
1431
+ # with the widened class.
1578
1432
  return false if value.is_a?(Float) && value.nan?
1579
1433
  return false if value.is_a?(Complex) && complex_nan?(value)
1580
1434
 
@@ -1612,8 +1466,8 @@ module Rigor
1612
1466
  end
1613
1467
  end
1614
1468
 
1615
- # Integer / 0 and Integer % 0 raise; Float / 0 and Float / 0.0 return
1616
- # Float::INFINITY or NaN, which are valid `Constant[Float]` values.
1469
+ # Integer / 0 and Integer % 0 raise; Float / 0 and Float / 0.0 return Float::INFINITY or NaN, which
1470
+ # are valid `Constant[Float]` values.
1617
1471
  def integer_division_by_zero?(receiver_value, method_name, arg_value)
1618
1472
  return false unless %i[/ %].include?(method_name)
1619
1473
  return false unless receiver_value.is_a?(Integer)
@@ -1632,10 +1486,9 @@ module Rigor
1632
1486
  end
1633
1487
  end
1634
1488
 
1635
- # `"x".center(width)` / `#ljust` / `#rjust` produce a string
1636
- # of `max(width, len)` characters. A literal `width` far
1637
- # larger than the receiver would materialise a huge Constant;
1638
- # cap it at the same byte limit the concat / repeat paths use.
1489
+ # `"x".center(width)` / `#ljust` / `#rjust` produce a string of `max(width, len)` characters. A
1490
+ # literal `width` far larger than the receiver would materialise a huge Constant; cap it at the same
1491
+ # byte limit the concat / repeat paths use.
1639
1492
  def string_pad_blow_up?(arg_value)
1640
1493
  arg_value.is_a?(Integer) && arg_value > STRING_FOLD_BYTE_LIMIT
1641
1494
  end