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
@@ -10,6 +10,7 @@ require_relative "block_parameter_binder"
10
10
  require_relative "body_fixpoint"
11
11
  require_relative "budget_trace"
12
12
  require_relative "dynamic_origin"
13
+ require_relative "origin_lookup"
13
14
  require_relative "fallback"
14
15
  require_relative "flow_tracer"
15
16
  require_relative "indexed_narrowing"
@@ -20,52 +21,41 @@ require_relative "struct_fold_safety"
20
21
 
21
22
  module Rigor
22
23
  module Inference
23
- # Translates AST nodes into Rigor::Type values, consulting the surrounding
24
- # Rigor::Scope for local-variable bindings and the environment registry
25
- # for nominal-type resolution. Pure: never mutates the receiver scope.
24
+ # Translates AST nodes into Rigor::Type values, consulting the surrounding Rigor::Scope for local-variable
25
+ # bindings and the environment registry for nominal-type resolution. Pure: never mutates the receiver
26
+ # scope.
26
27
  #
27
- # Accepts both real Prism nodes and synthetic Rigor::AST::Node
28
- # instances; the synthetic family lets callers and plugins ask
29
- # "what would the analyzer infer if a value of type T appeared here?"
30
- # without building a real Prism expression.
28
+ # Accepts both real Prism nodes and synthetic Rigor::AST::Node instances; the synthetic family lets
29
+ # callers and plugins ask "what would the analyzer infer if a value of type T appeared here?" without
30
+ # building a real Prism expression.
31
31
  #
32
- # Slice 1 recognises literal expressions, local-variable reads/writes,
33
- # shallow Array literals, and Rigor::AST::TypeNode. Slice 2 adds
34
- # Prism::CallNode (routed through Rigor::Inference::MethodDispatcher),
35
- # Prism::ArgumentsNode (a non-value position whose children are typed
36
- # individually by the CallNode handler), constant references resolved
37
- # through Rigor::Environment::ClassRegistry, hash and interpolated
38
- # string/symbol literals, definition expressions (def/class/module),
39
- # and explicit handlers for parameter, block, splat, instance/class/
40
- # global-variable, and self positions. Many of those handlers return
41
- # Dynamic[Top] silently because they are non-value or out-of-scope
42
- # positions for Slice 2; later slices refine them in place.
32
+ # Slice 1 recognises literal expressions, local-variable reads/writes, shallow Array literals, and
33
+ # Rigor::AST::TypeNode. Slice 2 adds Prism::CallNode (routed through Rigor::Inference::MethodDispatcher),
34
+ # Prism::ArgumentsNode (a non-value position whose children are typed individually by the CallNode
35
+ # handler), constant references resolved through Rigor::Environment::ClassRegistry, hash and interpolated
36
+ # string/symbol literals, definition expressions (def/class/module), and explicit handlers for parameter,
37
+ # block, splat, instance/class/global-variable, and self positions. Many of those handlers return
38
+ # Dynamic[Top] silently because they are non-value or out-of-scope positions for Slice 2; later slices
39
+ # refine them in place.
43
40
  #
44
- # Slice 4 phase 2b types bare-constant references (`Foo`, `Foo::Bar`)
45
- # as `Singleton[Foo]` rather than `Nominal[Foo]`, so that method
46
- # dispatch on the constant correctly looks up *class* methods. The
47
- # corresponding instance type is reachable through `Foo.new` and the
48
- # value-lattice projections.
41
+ # Slice 4 phase 2b types bare-constant references (`Foo`, `Foo::Bar`) as `Singleton[Foo]` rather than
42
+ # `Nominal[Foo]`, so that method dispatch on the constant correctly looks up *class* methods. The
43
+ # corresponding instance type is reachable through `Foo.new` and the value-lattice projections.
49
44
  #
50
- # Every other node falls back to Dynamic[Top] per the fail-soft
51
- # policy in docs/internal-spec/inference-engine.md. The optional
52
- # tracer is a Rigor::Inference::FallbackTracer (or any object
53
- # answering #record_fallback) that receives a Fallback event for
54
- # each fallback; the tracer MUST NOT change the return value of
55
- # type_of.
45
+ # Every other node falls back to Dynamic[Top] per the fail-soft policy in
46
+ # docs/internal-spec/inference-engine.md. The optional tracer is a Rigor::Inference::FallbackTracer (or
47
+ # any object answering #record_fallback) that receives a Fallback event for each fallback; the tracer
48
+ # MUST NOT change the return value of type_of.
56
49
  # rubocop:disable Metrics/ClassLength
57
50
  class ExpressionTyper
58
- # Hash-based dispatch keeps `type_of` linear and lets future slices add
59
- # node kinds without growing a single case statement past RuboCop's
60
- # cyclomatic budget. Anonymous Prism subclasses are not expected.
51
+ # Hash-based dispatch keeps `type_of` linear and lets future slices add node kinds without growing a
52
+ # single case statement past RuboCop's cyclomatic budget. Anonymous Prism subclasses are not expected.
61
53
  PRISM_DISPATCH = {
62
54
  # Literals
63
55
  Prism::IntegerNode => :type_of_literal_value,
64
56
  Prism::FloatNode => :type_of_literal_value,
65
- # `1i` / `2.5ri` lift via `node.value` which is already a
66
- # `Complex` Ruby value; same for `1r` / `1.5r` whose
67
- # value is a `Rational`. `Type::Constant` accepts both
68
- # via `SCALAR_CLASSES`.
57
+ # `1i` / `2.5ri` lift via `node.value` which is already a `Complex` Ruby value; same for `1r` / `1.5r`
58
+ # whose value is a `Rational`. `Type::Constant` accepts both via `SCALAR_CLASSES`.
69
59
  Prism::ImaginaryNode => :type_of_literal_value,
70
60
  Prism::RationalNode => :type_of_literal_value,
71
61
  Prism::SymbolNode => :symbol_type_for,
@@ -125,12 +115,10 @@ module Rigor
125
115
  Prism::IndexOrWriteNode => :type_of_assignment_write,
126
116
  Prism::IndexAndWriteNode => :type_of_assignment_write,
127
117
  Prism::MultiWriteNode => :type_of_assignment_write,
128
- # LHS-only target nodes (destructuring assignment, pattern matching,
129
- # `for x in xs`, block parameter `|a, (b, c)|`). They have no value
130
- # to extract the type-of pass acknowledges the node class so the
131
- # coverage scanner stops flagging it; binding the inner names back
132
- # into the scope is the StatementEvaluator / MultiTargetBinder /
133
- # BlockParameterBinder side's concern.
118
+ # LHS-only target nodes (destructuring assignment, pattern matching, `for x in xs`, block parameter
119
+ # `|a, (b, c)|`). They have no value to extract — the type-of pass acknowledges the node class so the
120
+ # coverage scanner stops flagging it; binding the inner names back into the scope is the
121
+ # StatementEvaluator / MultiTargetBinder / BlockParameterBinder side's concern.
134
122
  Prism::LocalVariableTargetNode => :type_of_non_value,
135
123
  Prism::MultiTargetNode => :type_of_non_value,
136
124
  Prism::InstanceVariableTargetNode => :type_of_non_value,
@@ -203,9 +191,8 @@ module Rigor
203
191
  Prism::ForwardingArgumentsNode => :type_of_non_value,
204
192
  Prism::WhileNode => :type_of_loop,
205
193
  Prism::UntilNode => :type_of_loop,
206
- # `for` matches `eval_for`'s statement-path policy: the loop
207
- # expression types `Constant[nil]` (no `break VALUE` observed),
208
- # same as `while` / `until` — annotating a `for`'s `end` line as
194
+ # `for` matches `eval_for`'s statement-path policy: the loop expression types `Constant[nil]` (no
195
+ # `break VALUE` observed), same as `while` / `until` — annotating a `for`'s `end` line as
209
196
  # `Dynamic[top]` was a display artifact of the old mapping.
210
197
  Prism::ForNode => :type_of_loop,
211
198
  Prism::DefinedNode => :type_of_defined,
@@ -242,13 +229,10 @@ module Rigor
242
229
  end
243
230
 
244
231
  def untraced_type_of(node)
245
- # Slice A-declarations. ScopeIndexer pre-fills
246
- # `scope.declared_types` for declaration-position nodes
247
- # (`module Foo` / `class Bar` headers) with the qualified
248
- # `Singleton` type so the header itself does not fall
249
- # through to `Dynamic[Top]`. The override is consulted
250
- # before any other dispatch and bypasses fail-soft
251
- # tracing on a recognised match.
232
+ # Slice A-declarations. ScopeIndexer pre-fills `scope.declared_types` for declaration-position nodes
233
+ # (`module Foo` / `class Bar` headers) with the qualified `Singleton` type so the header itself does
234
+ # not fall through to `Dynamic[Top]`. The override is consulted before any other dispatch and
235
+ # bypasses fail-soft tracing on a recognised match.
252
236
  declared = scope.declared_types[node]
253
237
  return declared if declared
254
238
 
@@ -284,26 +268,29 @@ module Rigor
284
268
  Type::Combinator.constant_of(nil)
285
269
  end
286
270
 
287
- # All `*WriteNode` flavours expose a `.value` rvalue child. Their type
288
- # is the type of that rvalue. Binding the result back into the scope
289
- # is the responsibility of the statement-level evaluator (Slice 3),
290
- # never of `type_of` itself.
271
+ # All `*WriteNode` flavours expose a `.value` rvalue child. Their type is the type of that rvalue.
272
+ # Binding the result back into the scope is the responsibility of the statement-level evaluator
273
+ # (Slice 3), never of `type_of` itself.
291
274
  def type_of_assignment_write(node)
292
275
  type_of(node.value)
293
276
  end
294
277
 
295
- # Slice 7 phase 1 — instance/class/global variable reads.
296
- # Each lookup returns the type currently bound in the
297
- # surrounding scope's per-kind binding map (populated by
298
- # `StatementEvaluator` write handlers within the same
299
- # method body), falling through to `Dynamic[Top]` when no
300
- # binding is recorded. Cross-method ivar/cvar inference is
301
- # a follow-up slice; the read handlers MUST NOT raise on a
302
- # missing binding and MUST NOT record a fallback event in
303
- # either branch — the absence of a binding is a recognised
304
- # semantic outcome, not a fail-soft compromise.
278
+ # Slice 7 phase 1 — instance/class/global variable reads. Each lookup returns the type currently bound
279
+ # in the surrounding scope's per-kind binding map (populated by `StatementEvaluator` write handlers
280
+ # within the same method body), falling through to `Dynamic[Top]` when no binding is recorded.
281
+ # Cross-method ivar/cvar inference is a follow-up slice; the read handlers MUST NOT raise on a missing
282
+ # binding and MUST NOT record a fallback event in either branch — the absence of a binding is a
283
+ # recognised semantic outcome, not a fail-soft compromise.
305
284
  def type_of_instance_variable_read(node)
306
- scope.ivar(node.name) || dynamic_top
285
+ bound = scope.ivar(node.name)
286
+ return bound if bound
287
+
288
+ # ADR-82 — an unbound instance-variable read is dynamic because the engine does not track this
289
+ # field's type (it is assigned in another method, or never seen). Route it to ivar-field typing
290
+ # (ADR-58) rather than reporting no cause, and let WD6 carry it through a `@x.foo.bar` chain. This
291
+ # records provenance only (still `dynamic_top`), never a fallback / tracer event.
292
+ scope.record_dynamic_origin(node, DynamicOrigin::INFERRED_RETURN_UNTYPED)
293
+ dynamic_top
307
294
  end
308
295
 
309
296
  def type_of_class_variable_read(node)
@@ -322,22 +309,18 @@ module Rigor
322
309
  statements_type_for(node.statements)
323
310
  end
324
311
 
325
- # Recognised position that does not produce a value: parameter lists
326
- # and individual parameter declarations, splats inside argument
327
- # lists, key-value pairs in hashes, and the implicit-rest token
328
- # inside destructuring. Returning Dynamic[Top] silently keeps these
329
- # off the unrecognised list without faking a value type.
312
+ # Recognised position that does not produce a value: parameter lists and individual parameter
313
+ # declarations, splats inside argument lists, key-value pairs in hashes, and the implicit-rest token
314
+ # inside destructuring. Returning Dynamic[Top] silently keeps these off the unrecognised list without
315
+ # faking a value type.
330
316
  def type_of_non_value(_node)
331
317
  dynamic_top
332
318
  end
333
319
 
334
- # `Prism::SelfNode` resolves to the scope's
335
- # `self_type` when one has been injected (by
336
- # `StatementEvaluator` at class-body and method-body
337
- # boundaries) or `Dynamic[Top]` at the top level. Class-body
338
- # `self` is `Singleton[<class>]`; instance-method `self` is
339
- # `Nominal[<class>]`; singleton-method `self` is
340
- # `Singleton[<class>]`.
320
+ # `Prism::SelfNode` resolves to the scope's `self_type` when one has been injected (by
321
+ # `StatementEvaluator` at class-body and method-body boundaries) or `Dynamic[Top]` at the top level.
322
+ # Class-body `self` is `Singleton[<class>]`; instance-method `self` is `Nominal[<class>]`;
323
+ # singleton-method `self` is `Singleton[<class>]`.
341
324
  def type_of_self_node(_node)
342
325
  scope.self_type || dynamic_top
343
326
  end
@@ -346,11 +329,9 @@ module Rigor
346
329
  dynamic_top
347
330
  end
348
331
 
349
- # `defined?(expr)` returns `String | nil` per Ruby semantics —
350
- # a description of the expression's category (`"local-variable"`,
351
- # `"method"`, ...) when defined, or `nil` when not. The argument
352
- # is not evaluated (it is statically inspected by the runtime),
353
- # so the typer does not recurse into it.
332
+ # `defined?(expr)` returns `String | nil` per Ruby semantics — a description of the expression's
333
+ # category (`"local-variable"`, `"method"`, ...) when defined, or `nil` when not. The argument is not
334
+ # evaluated (it is statically inspected by the runtime), so the typer does not recurse into it.
354
335
  def type_of_defined(_node)
355
336
  Type::Combinator.union(
356
337
  Type::Combinator.nominal_of("String"),
@@ -358,10 +339,9 @@ module Rigor
358
339
  )
359
340
  end
360
341
 
361
- # `$1`, `$&`, `$'`, `$+`, `$\`` — the regex back-reference and
362
- # numbered-capture globals each carry `String | nil`. They share
363
- # the typer because the typing rule is identical regardless of
364
- # which back-reference shape Prism emitted.
342
+ # `$1`, `$&`, `$'`, `$+`, `$\`` — the regex back-reference and numbered-capture globals each carry
343
+ # `String | nil`. They share the typer because the typing rule is identical regardless of which
344
+ # back-reference shape Prism emitted.
365
345
  def type_of_string_or_nil(_node)
366
346
  Type::Combinator.union(
367
347
  Type::Combinator.nominal_of("String"),
@@ -369,23 +349,20 @@ module Rigor
369
349
  )
370
350
  end
371
351
 
372
- # `$1` / `$2` / ... — numbered match-data globals. When the
373
- # narrowing tier has bound a tighter type for this number
374
- # (typically `String` after a `=~`-success guard like `unless
375
- # /(\d+)/ =~ s; raise; end`), prefer the scope-bound type.
376
- # Falls back to the default `String | nil`.
352
+ # `$1` / `$2` / ... — numbered match-data globals. When the narrowing tier has bound a tighter type for
353
+ # this number (typically `String` after a `=~`-success guard like `unless /(\d+)/ =~ s; raise; end`),
354
+ # prefer the scope-bound type. Falls back to the default `String | nil`.
377
355
  def type_of_numbered_reference(node)
378
356
  scope.global(:"$#{node.number}") || type_of_string_or_nil(node)
379
357
  end
380
358
 
381
- # `$&` / `$'` / `$\`` / `$+` — symbolic back-references. Same
382
- # narrowing model as numbered references.
359
+ # `$&` / `$'` / `$\`` / `$+` — symbolic back-references. Same narrowing model as numbered references.
383
360
  def type_of_back_reference(node)
384
361
  scope.global(node.name) || type_of_string_or_nil(node)
385
362
  end
386
363
 
387
- # `expr in pattern` — pattern-match predicate. Returns `true`
388
- # when the pattern matches, `false` otherwise.
364
+ # `expr in pattern` — pattern-match predicate. Returns `true` when the pattern matches, `false`
365
+ # otherwise.
389
366
  def type_of_match_predicate(_node)
390
367
  Type::Combinator.union(
391
368
  Type::Combinator.constant_of(true),
@@ -393,23 +370,20 @@ module Rigor
393
370
  )
394
371
  end
395
372
 
396
- # `expr => pattern` — one-line pattern-match assertion. Raises
397
- # `NoMatchingPatternError` on mismatch; on success the expression
398
- # itself evaluates to `nil`.
373
+ # `expr => pattern` — one-line pattern-match assertion. Raises `NoMatchingPatternError` on mismatch; on
374
+ # success the expression itself evaluates to `nil`.
399
375
  def type_of_match_required(_node)
400
376
  Type::Combinator.constant_of(nil)
401
377
  end
402
378
 
403
- # The expression `Foo` evaluates to the *class object* `Foo`, not
404
- # an instance. From Slice 4 phase 2b on we therefore type a
405
- # bare-constant reference as `Singleton[Foo]`; method dispatch on
406
- # that receiver looks up class methods (`Foo.new`, `Foo.bar`, ...).
379
+ # The expression `Foo` evaluates to the *class object* `Foo`, not an instance. From Slice 4 phase 2b on
380
+ # we therefore type a bare-constant reference as `Singleton[Foo]`; method dispatch on that receiver
381
+ # looks up class methods (`Foo.new`, `Foo.bar`, ...).
407
382
  #
408
- # Slice A constant-walk: when the literal name does not resolve,
409
- # we try a lexical walk based on the surrounding class context
410
- # exposed through `scope.self_type` so a reference like
411
- # `Inference::FallbackTracer` from inside `Rigor::CLI::Foo`
412
- # resolves to `Rigor::Inference::FallbackTracer`.
383
+ # Slice A constant-walk: when the literal name does not resolve, we try a lexical walk based on the
384
+ # surrounding class context exposed through `scope.self_type` so a reference like
385
+ # `Inference::FallbackTracer` from inside `Rigor::CLI::Foo` resolves to
386
+ # `Rigor::Inference::FallbackTracer`.
413
387
  def type_of_constant_read(node)
414
388
  resolve_constant_name(node.name.to_s) || fallback_for(node, family: :prism)
415
389
  end
@@ -421,14 +395,11 @@ module Rigor
421
395
  resolve_constant_name(full_name) || fallback_for(node, family: :prism)
422
396
  end
423
397
 
424
- # Try the literal name first, then walk Ruby's lexical lookup by
425
- # progressively prefixing the surrounding class path (peeled
426
- # one `::segment` at a time). For each candidate the lookup
427
- # consults `Environment#singleton_for_name` (a class object)
428
- # and then `Environment#constant_for_name` (a non-class
429
- # constant value such as `BUCKETS: Array[Symbol]`).
430
- # Returns the matched `Rigor::Type` or nil; the caller decides
431
- # whether to fall back.
398
+ # Try the literal name first, then walk Ruby's lexical lookup by progressively prefixing the surrounding
399
+ # class path (peeled one `::segment` at a time). For each candidate the lookup consults
400
+ # `Environment#singleton_for_name` (a class object) and then `Environment#constant_for_name` (a
401
+ # non-class constant value such as `BUCKETS: Array[Symbol]`). Returns the matched `Rigor::Type` or nil;
402
+ # the caller decides whether to fall back.
432
403
  def resolve_constant_name(name)
433
404
  env = scope.environment
434
405
  discovered = scope.discovered_classes
@@ -440,9 +411,8 @@ module Rigor
440
411
  in_source_class = discovered[candidate]
441
412
  return in_source_class if in_source_class
442
413
 
443
- # In-source value-bearing constants take precedence
444
- # over RBS constant decls because user code is the
445
- # authoritative source for its own constants.
414
+ # In-source value-bearing constants take precedence over RBS constant decls because user code is
415
+ # the authoritative source for its own constants.
446
416
  in_source_value = in_source[candidate]
447
417
  return in_source_value if in_source_value
448
418
 
@@ -452,20 +422,17 @@ module Rigor
452
422
  nil
453
423
  end
454
424
 
455
- # The candidate qualified names to try, in Ruby's lexical
456
- # order: most-qualified first (the surrounding class path
457
- # joined to `name`), then progressively less-qualified, then
458
- # the bare `name`. Top-level scopes (no `self_type`) yield
459
- # only `[name]`, preserving the pre-walk behaviour.
425
+ # The candidate qualified names to try, in Ruby's lexical order: most-qualified first (the surrounding
426
+ # class path joined to `name`), then progressively less-qualified, then the bare `name`. Top-level
427
+ # scopes (no `self_type`) yield only `[name]`, preserving the pre-walk behaviour.
460
428
  def lexical_constant_candidates(name)
461
429
  prefix = enclosing_class_path
462
430
  candidates = []
463
431
  while prefix && !prefix.empty?
464
432
  candidates << "#{prefix}::#{name}"
465
- # Strip the last `::` segment without `rpartition`'s throwaway
466
- # 3-element array + extra substrings (this loop is the sole
467
- # caller of the `String#rpartition` allocation seen in the
468
- # profile): `rindex` + slice gives the same prefix, or nil.
433
+ # Strip the last `::` segment without `rpartition`'s throwaway 3-element array + extra substrings
434
+ # (this loop is the sole caller of the `String#rpartition` allocation seen in the profile): `rindex`
435
+ # + slice gives the same prefix, or nil.
469
436
  idx = prefix.rindex("::")
470
437
  prefix = idx ? prefix[0, idx] : nil
471
438
  end
@@ -473,10 +440,8 @@ module Rigor
473
440
  candidates
474
441
  end
475
442
 
476
- # Pulls the enclosing qualified class name out of
477
- # `scope.self_type` when one is set. `Nominal[T]` and
478
- # `Singleton[T]` both expose `class_name`. Returns nil when
479
- # no class context is available (top-level).
443
+ # Pulls the enclosing qualified class name out of `scope.self_type` when one is set. `Nominal[T]` and
444
+ # `Singleton[T]` both expose `class_name`. Returns nil when no class context is available (top-level).
480
445
  def enclosing_class_path
481
446
  st = scope.self_type
482
447
  case st
@@ -484,23 +449,16 @@ module Rigor
484
449
  end
485
450
  end
486
451
 
487
- # Slice 5 phase 1 upgrades hash literals to `HashShape{...}`
488
- # when every entry is a static `AssocNode` whose key is a
489
- # `SymbolNode` or `StringNode` with a known value (covering the
490
- # `{ a: 1, "b" => 2 }` pattern and falling back to the generic
491
- # `Hash[K, V]` form otherwise). Splatted entries
492
- # (`{ **other }`) and dynamic keys widen to the underlying
493
- # `Hash[K, V]` form by unioning the types each entry exposes;
494
- # when no concrete pair survives we fall back to the raw `Hash`
495
- # so callers stay backward compatible.
452
+ # Slice 5 phase 1 upgrades hash literals to `HashShape{...}` when every entry is a static `AssocNode`
453
+ # whose key is a `SymbolNode` or `StringNode` with a known value (covering the `{ a: 1, "b" => 2 }`
454
+ # pattern and falling back to the generic `Hash[K, V]` form otherwise). Splatted entries (`{ **other }`)
455
+ # and dynamic keys widen to the underlying `Hash[K, V]` form by unioning the types each entry exposes;
456
+ # when no concrete pair survives we fall back to the raw `Hash` so callers stay backward compatible.
496
457
  def type_of_hash(node)
497
458
  elements = node.respond_to?(:elements) ? node.elements : []
498
- # v0.0.7 — `{}` resolves to the empty `HashShape{}` carrier
499
- # rather than `Nominal[Hash]`, mirroring the v0.0.6 empty-
500
- # array literal change. Both forms erase to plain `Hash`,
501
- # but `HashShape{}` pins the literal's known size (zero)
502
- # so HashShape projections (`empty?`, `first`, `count`,
503
- # …) fold against it.
459
+ # v0.0.7 — `{}` resolves to the empty `HashShape{}` carrier rather than `Nominal[Hash]`, mirroring the
460
+ # v0.0.6 empty-array literal change. Both forms erase to plain `Hash`, but `HashShape{}` pins the
461
+ # literal's known size (zero) so HashShape projections (`empty?`, `first`, `count`, …) fold against it.
504
462
  return Type::Combinator.hash_shape_of({}) if elements.empty?
505
463
 
506
464
  shape = static_hash_shape_for(elements)
@@ -515,9 +473,8 @@ module Rigor
515
473
  )
516
474
  end
517
475
 
518
- # Builds `HashShape{...}` when every entry is an `AssocNode`
519
- # whose key is a static Symbol or String literal. Returns nil
520
- # otherwise so the caller falls back to the generic shape.
476
+ # Builds `HashShape{...}` when every entry is an `AssocNode` whose key is a static Symbol or String
477
+ # literal. Returns nil otherwise so the caller falls back to the generic shape.
521
478
  def static_hash_shape_for(elements)
522
479
  pairs = {}
523
480
  elements.each do |entry|
@@ -534,10 +491,9 @@ module Rigor
534
491
  Type::Combinator.hash_shape_of(pairs)
535
492
  end
536
493
 
537
- # Returns the static (Symbol|String) literal carried by a hash
538
- # key node, or nil when the key is dynamic. We only treat
539
- # SymbolNode#value and StringNode#unescaped as static when they
540
- # are non-nil (interpolation produces a nil unescaped).
494
+ # Returns the static (Symbol|String) literal carried by a hash key node, or nil when the key is
495
+ # dynamic. We only treat SymbolNode#value and StringNode#unescaped as static when they are non-nil
496
+ # (interpolation produces a nil unescaped).
541
497
  def static_hash_key(node)
542
498
  case node
543
499
  when Prism::SymbolNode
@@ -560,13 +516,10 @@ module Rigor
560
516
  [keys, values]
561
517
  end
562
518
 
563
- # An interpolated string `"#{a}b#{c}"` is `literal-string`
564
- # when every part contributes literal-bearing material:
565
- # plain text segments are literal by construction, embedded
566
- # expressions count when their type is itself literal-string-
567
- # compatible (a `Constant<String>`, the `literal-string`
568
- # carrier, an `Intersection` containing it, or a `Union`
569
- # whose members all qualify). Otherwise the result widens to
519
+ # An interpolated string `"#{a}b#{c}"` is `literal-string` when every part contributes literal-bearing
520
+ # material: plain text segments are literal by construction, embedded expressions count when their type
521
+ # is itself literal-string-compatible (a `Constant<String>`, the `literal-string` carrier, an
522
+ # `Intersection` containing it, or a `Union` whose members all qualify). Otherwise the result widens to
570
523
  # plain `Nominal[String]` as before.
571
524
  def type_of_interpolated_string(node)
572
525
  return Type::Combinator.literal_string if interpolation_parts_literal?(node.parts)
@@ -601,11 +554,9 @@ module Rigor
601
554
  Type::Combinator.constant_of(node.name)
602
555
  end
603
556
 
604
- # `class Foo; body; end`, `module Foo; body; end`, and `class << x;
605
- # body; end` evaluate to the value of the body's last expression,
606
- # or `nil` when the body is empty. We do not track class/module
607
- # scope yet, so the body is typed in the surrounding scope and
608
- # that result is returned.
557
+ # `class Foo; body; end`, `module Foo; body; end`, and `class << x; body; end` evaluate to the value of
558
+ # the body's last expression, or `nil` when the body is empty. We do not track class/module scope yet,
559
+ # so the body is typed in the surrounding scope and that result is returned.
609
560
  def type_of_class_or_module(node)
610
561
  body = node.body
611
562
  return Type::Combinator.constant_of(nil) if body.nil?
@@ -613,35 +564,29 @@ module Rigor
613
564
  type_of(body)
614
565
  end
615
566
 
616
- # `alias x y`, `alias $x $y`, and `undef foo` all evaluate to nil at
617
- # runtime; the constant carrier captures that exactly.
567
+ # `alias x y`, `alias $x $y`, and `undef foo` all evaluate to nil at runtime; the constant carrier
568
+ # captures that exactly.
618
569
  def type_of_nil_value(_node)
619
570
  Type::Combinator.constant_of(nil)
620
571
  end
621
572
 
622
- # `if c; t; (elsif c2; ...; )* else; e; end`. Prism nests `elsif`
623
- # branches as `IfNode#subsequent`. Slice 3 phase 1 types both
624
- # branches in the receiver scope and returns their union; scope
625
- # rebinding is the StatementEvaluator's job (Slice 3 phase 2).
626
- # Without an else clause the branch's implicit value is nil, which
627
- # is included in the union.
573
+ # `if c; t; (elsif c2; ...; )* else; e; end`. Prism nests `elsif` branches as `IfNode#subsequent`. Slice
574
+ # 3 phase 1 types both branches in the receiver scope and returns their union; scope rebinding is the
575
+ # StatementEvaluator's job (Slice 3 phase 2). Without an else clause the branch's implicit value is nil,
576
+ # which is included in the union.
628
577
  #
629
- # v0.0.6 — when the predicate folds to a `Type::Constant` whose
630
- # value is Ruby-truthy (resp. Ruby-falsey), the unreachable
631
- # branch is elided so the if-expression's type is the live
632
- # branch alone. Statement-level branch elision lives in
633
- # `StatementEvaluator#eval_if`; this handler covers the
634
- # expression-position ternary form (`a ? b : c`) and any
635
- # `if`/`unless` reached through `type_of`.
578
+ # v0.0.6 — when the predicate folds to a `Type::Constant` whose value is Ruby-truthy (resp.
579
+ # Ruby-falsey), the unreachable branch is elided so the if-expression's type is the live branch alone.
580
+ # Statement-level branch elision lives in `StatementEvaluator#eval_if`; this handler covers the
581
+ # expression-position ternary form (`a ? b : c`) and any `if`/`unless` reached through `type_of`.
636
582
  def type_of_if(node)
637
583
  then_type = statements_or_nil(node.statements)
638
584
  else_type = if_else_type(node.subsequent)
639
585
  elide_or_union(node.predicate, then_type, else_type)
640
586
  end
641
587
 
642
- # `unless c; t; else; e; end`. Prism uses `else_clause` here (no
643
- # `elsif` chain). Branch-elision logic mirrors `type_of_if`,
644
- # inverted: a truthy predicate selects the else branch.
588
+ # `unless c; t; else; e; end`. Prism uses `else_clause` here (no `elsif` chain). Branch-elision logic
589
+ # mirrors `type_of_if`, inverted: a truthy predicate selects the else branch.
645
590
  def type_of_unless(node)
646
591
  then_type = statements_or_nil(node.statements)
647
592
  else_type = if_else_type(node.else_clause)
@@ -654,11 +599,9 @@ module Rigor
654
599
  type_of(subsequent)
655
600
  end
656
601
 
657
- # Routes the predicate's typed value through branch elision.
658
- # `live_when_truthy` and `live_when_falsey` are the branch
659
- # types selected by the predicate's polarity; the names
660
- # match `IfNode` semantics directly and invert at the
661
- # `type_of_unless` call site.
602
+ # Routes the predicate's typed value through branch elision. `live_when_truthy` and `live_when_falsey`
603
+ # are the branch types selected by the predicate's polarity; the names match `IfNode` semantics
604
+ # directly and invert at the `type_of_unless` call site.
662
605
  def elide_or_union(predicate, live_when_truthy, live_when_falsey)
663
606
  case constant_predicate_polarity(predicate)
664
607
  when :truthy then live_when_truthy
@@ -667,14 +610,10 @@ module Rigor
667
610
  end
668
611
  end
669
612
 
670
- # Returns `:truthy`, `:falsey`, or `nil` for an arbitrary
671
- # predicate expression under three-valued logic.
672
- # {Narrowing.predicate_certainty} owns the judgment (the same
673
- # one `StatementEvaluator#live_branch_for_if` reads on the
674
- # scope side): `Nominal[Integer]` (always truthy in Ruby),
675
- # `Constant[nil]`, and `Constant[false]` fold one branch;
676
- # `Union[true, false]`, `Dynamic[T]`, and `Top` keep both
677
- # branches live.
613
+ # Returns `:truthy`, `:falsey`, or `nil` for an arbitrary predicate expression under three-valued logic.
614
+ # {Narrowing.predicate_certainty} owns the judgment (the same one `StatementEvaluator#live_branch_for_if`
615
+ # reads on the scope side): `Nominal[Integer]` (always truthy in Ruby), `Constant[nil]`, and
616
+ # `Constant[false]` fold one branch; `Union[true, false]`, `Dynamic[T]`, and `Top` keep both branches live.
678
617
  def constant_predicate_polarity(predicate)
679
618
  return nil if predicate.nil?
680
619
 
@@ -685,27 +624,22 @@ module Rigor
685
624
  statements_or_nil(node.statements)
686
625
  end
687
626
 
688
- # `a && b` and `a || b` short-circuit at the value level:
689
- # `a && b` returns `a` when `a` is falsey, else `b`.
690
- # `a || b` returns `a` when `a` is truthy, else `b`.
627
+ # `a && b` and `a || b` short-circuit at the value level: `a && b` returns `a` when `a` is falsey, else
628
+ # `b`. `a || b` returns `a` when `a` is truthy, else `b`.
691
629
  #
692
- # v0.0.6 — when the left operand folds to a `Type::Constant`,
693
- # we know which side actually flows through, so the result
694
- # is one operand's type instead of a union. Otherwise the
695
- # union-of-both-operands fallback is preserved.
630
+ # v0.0.6 — when the left operand folds to a `Type::Constant`, we know which side actually flows
631
+ # through, so the result is one operand's type instead of a union. Otherwise the union-of-both-operands
632
+ # fallback is preserved.
696
633
  def type_of_and_or(node)
697
634
  left_type = type_of(node.left)
698
635
  polarity = constant_value_polarity(left_type)
699
636
  return short_circuit_for(node, left_type, polarity) if polarity
700
637
 
701
- # The left operand only flows through on the edge that short-
702
- # circuits: `a || b` yields `a` solely when `a` is truthy, so its
703
- # falsey constituents (`nil` / `false`) can never be the value of
704
- # the OrNode (they hand off to `b`); `a && b` yields `a` solely
705
- # when `a` is falsey. Narrow the surviving left edge before the
706
- # union so `s || full` (with `s : String?`) types `String |
707
- # <full>` rather than re-admitting the stripped `nil`. Mirrors
708
- # `StatementEvaluator#eval_and_or`'s `skipped_type`.
638
+ # The left operand only flows through on the edge that short-circuits: `a || b` yields `a` solely
639
+ # when `a` is truthy, so its falsey constituents (`nil` / `false`) can never be the value of the
640
+ # OrNode (they hand off to `b`); `a && b` yields `a` solely when `a` is falsey. Narrow the surviving
641
+ # left edge before the union so `s || full` (with `s : String?`) types `String | <full>` rather than
642
+ # re-admitting the stripped `nil`. Mirrors `StatementEvaluator#eval_and_or`'s `skipped_type`.
709
643
  surviving_left =
710
644
  if node.is_a?(Prism::AndNode)
711
645
  Narrowing.narrow_falsey(left_type)
@@ -724,37 +658,30 @@ module Rigor
724
658
  end
725
659
  end
726
660
 
727
- # Returns `:truthy` / `:falsey` for a `Type::Constant`,
728
- # nil otherwise. Mirrors `constant_predicate_polarity` but
729
- # operates on a typed value (already-type-of'd) rather
730
- # than a Prism node, so the same predicate analysis can
731
- # be reused in both contexts.
661
+ # Returns `:truthy` / `:falsey` for a `Type::Constant`, nil otherwise. Mirrors
662
+ # `constant_predicate_polarity` but operates on a typed value (already-type-of'd) rather than a Prism
663
+ # node, so the same predicate analysis can be reused in both contexts.
732
664
  def constant_value_polarity(type)
733
665
  return nil unless type.is_a?(Type::Constant)
734
666
 
735
667
  type.value ? :truthy : :falsey
736
668
  end
737
669
 
738
- # Three-valued evaluation of `case predicate when pattern`
739
- # dispatch. For each `when` clause we ask: under static types,
740
- # does `pattern === predicate` definitely match (`:yes`),
741
- # definitely not match (`:no`), or possibly match (`:maybe`)?
742
- # Walking in source order:
670
+ # Three-valued evaluation of `case predicate when pattern` dispatch. For each `when` clause we ask:
671
+ # under static types, does `pattern === predicate` definitely match (`:yes`), definitely not match
672
+ # (`:no`), or possibly match (`:maybe`)? Walking in source order:
743
673
  #
744
- # - `:yes` — this branch fires, subsequent branches are
745
- # unreachable. Result = union(prior `:maybe` branches, this
746
- # `:yes` branch).
674
+ # - `:yes` — this branch fires, subsequent branches are unreachable. Result = union(prior `:maybe`
675
+ # branches, this `:yes` branch).
747
676
  # - `:no` — branch dropped.
748
677
  # - `:maybe` — branch is a candidate, continue.
749
678
  #
750
- # If no `:yes` was reached, the else clause (or `Constant[nil]`
751
- # when absent) is added to the candidate set.
679
+ # If no `:yes` was reached, the else clause (or `Constant[nil]` when absent) is added to the candidate
680
+ # set.
752
681
  #
753
- # The `case ... in` pattern-matching form (`CaseMatchNode`) and
754
- # the predicate-less form (`case; when c1; ...`) bypass the
755
- # `===` analysis: pattern matching has richer semantics, and a
756
- # predicate-less `case` reduces to a `if c1; ...; elsif c2`
757
- # chain that statement-level narrowing already handles.
682
+ # The `case ... in` pattern-matching form (`CaseMatchNode`) and the predicate-less form (`case; when
683
+ # c1; ...`) bypass the `===` analysis: pattern matching has richer semantics, and a predicate-less
684
+ # `case` reduces to a `if c1; ...; elsif c2` chain that statement-level narrowing already handles.
758
685
  def type_of_case(node)
759
686
  return type_of_case_simple_union(node) if node.is_a?(Prism::CaseMatchNode) || node.predicate.nil?
760
687
 
@@ -789,10 +716,8 @@ module Rigor
789
716
  type_of(node.else_clause)
790
717
  end
791
718
 
792
- # Combines per-pattern certainty across a `when` clause's
793
- # conditions (`when a, b, c` `a === s || b === s || c === s`).
794
- # `:yes` if any pattern is `:yes`; `:no` if all are `:no`;
795
- # `:maybe` otherwise.
719
+ # Combines per-pattern certainty across a `when` clause's conditions (`when a, b, c` ≡ `a === s || b
720
+ # === s || c === s`). `:yes` if any pattern is `:yes`; `:no` if all are `:no`; `:maybe` otherwise.
796
721
  def case_when_branch_certainty(subject_type, when_node)
797
722
  return :maybe unless when_node.respond_to?(:conditions)
798
723
 
@@ -804,23 +729,17 @@ module Rigor
804
729
  :maybe
805
730
  end
806
731
 
807
- # Static three-valued certainty for `pattern === subject`.
808
- # Specialises two pattern shapes:
732
+ # Static three-valued certainty for `pattern === subject`. Specialises two pattern shapes:
809
733
  #
810
- # - **Class / Module reference** (`Integer`, `Foo::Bar`):
811
- # reduce to `subject.is_a?(class)` via
812
- # `Narrowing.narrow_class` / `narrow_not_class`. A Bot
813
- # truthy fragment means no inhabitant matches (`:no`); a
814
- # Bot falsey fragment means every inhabitant matches
815
- # (`:yes`).
816
- # - **Value-equality literal** (numeric / String / Symbol /
817
- # true / false / nil) against a `Constant[c]` subject:
818
- # the static comparison `pattern_value === c` is exact.
819
- # Other subject carriers stay `:maybe` because the
820
- # runtime value isn't pinned.
734
+ # - **Class / Module reference** (`Integer`, `Foo::Bar`): reduce to `subject.is_a?(class)` via
735
+ # `Narrowing.narrow_class` / `narrow_not_class`. A Bot truthy fragment means no inhabitant matches
736
+ # (`:no`); a Bot falsey fragment means every inhabitant matches (`:yes`).
737
+ # - **Value-equality literal** (numeric / String / Symbol / true / false / nil) against a
738
+ # `Constant[c]` subject: the static comparison `pattern_value === c` is exact. Other subject
739
+ # carriers stay `:maybe` because the runtime value isn't pinned.
821
740
  #
822
- # Other pattern shapes (Range, Regexp, custom `===`) stay
823
- # `:maybe` — the existing union fallback handles them.
741
+ # Other pattern shapes (Range, Regexp, custom `===`) stay `:maybe` — the existing union fallback
742
+ # handles them.
824
743
  def case_when_pattern_certainty(subject_type, pattern_node)
825
744
  class_name = Source::ConstantPath.qualified_name_or_nil(pattern_node)
826
745
  return Narrowing.class_pattern_certainty(subject_type, class_name, environment: scope.environment) if class_name
@@ -831,11 +750,9 @@ module Rigor
831
750
  :maybe
832
751
  end
833
752
 
834
- # Returns `{ value: v }` when `pattern_node` types to a
835
- # `Constant[v]` of a value-equality-safe class (so `===`
836
- # reduces to `==`), else nil. Wrapped in a hash so a literal
837
- # `nil` / `false` value doesn't collide with the "no literal"
838
- # signal.
753
+ # Returns `{ value: v }` when `pattern_node` types to a `Constant[v]` of a value-equality-safe class
754
+ # (so `===` reduces to `==`), else nil. Wrapped in a hash so a literal `nil` / `false` value doesn't
755
+ # collide with the "no literal" signal.
839
756
  def literal_pattern_value(pattern_node)
840
757
  type = type_of(pattern_node)
841
758
  return nil unless type.is_a?(Type::Constant)
@@ -844,19 +761,16 @@ module Rigor
844
761
  { value: type.value }
845
762
  end
846
763
 
847
- # `when` clauses for `case` and `in` clauses for `case ... in` have
848
- # the same body shape; we reuse one handler for both Prism node
849
- # classes.
764
+ # `when` clauses for `case` and `in` clauses for `case ... in` have the same body shape; we reuse one
765
+ # handler for both Prism node classes.
850
766
  def type_of_when_or_in(node)
851
767
  statements_or_nil(node.statements)
852
768
  end
853
769
 
854
- # `begin; body; rescue R => e; r1; rescue; r2; else; e; ensure; f; end`.
855
- # The result is the union of every value-producing branch: the body
856
- # (or the else-clause when present, since it replaces the body's
857
- # value when no exception fires), plus each rescue body in the
858
- # rescue chain. The ensure clause runs but does not contribute to
859
- # the begin's value.
770
+ # `begin; body; rescue R => e; r1; rescue; r2; else; e; ensure; f; end`. The result is the union of
771
+ # every value-producing branch: the body (or the else-clause when present, since it replaces the body's
772
+ # value when no exception fires), plus each rescue body in the rescue chain. The ensure clause runs but
773
+ # does not contribute to the begin's value.
860
774
  def type_of_begin(node)
861
775
  rescue_clause = node.rescue_clause
862
776
  else_clause = node.else_clause
@@ -888,10 +802,8 @@ module Rigor
888
802
  statements_or_nil(node.statements)
889
803
  end
890
804
 
891
- # `expr rescue fallback` is RescueModifierNode in Prism. The result
892
- # is `expr`'s type when no exception is raised and `fallback`'s
893
- # type otherwise; both paths are reachable, so the result is their
894
- # union.
805
+ # `expr rescue fallback` is RescueModifierNode in Prism. The result is `expr`'s type when no exception
806
+ # is raised and `fallback`'s type otherwise; both paths are reachable, so the result is their union.
895
807
  def type_of_rescue_modifier(node)
896
808
  Type::Combinator.union(type_of(node.expression), type_of(node.rescue_expression))
897
809
  end
@@ -900,20 +812,16 @@ module Rigor
900
812
  statements_or_nil(node.statements)
901
813
  end
902
814
 
903
- # `return`, `break`, `next`, `retry`, and `redo` all transfer
904
- # control instead of producing a value. Their type is Bot, the
905
- # empty type that absorbs cleanly under union (e.g.
906
- # `Constant[1] | Bot == Constant[1]`), so the surrounding
907
- # control-flow handlers collapse correctly when one branch jumps.
815
+ # `return`, `break`, `next`, `retry`, and `redo` all transfer control instead of producing a value.
816
+ # Their type is Bot, the empty type that absorbs cleanly under union (e.g. `Constant[1] | Bot ==
817
+ # Constant[1]`), so the surrounding control-flow handlers collapse correctly when one branch jumps.
908
818
  def type_of_jump(_node)
909
819
  Type::Combinator.bot
910
820
  end
911
821
 
912
- # `while` and `until` loops produce nil unless interrupted by
913
- # `break VALUE`; the expression value of `break VALUE` is not yet
914
- # modeled (scope break-path propagation landed in `eval_loop`).
915
- # Returning Constant[nil] is safe and matches Ruby semantics for
916
- # the common case.
822
+ # `while` and `until` loops produce nil unless interrupted by `break VALUE`; the expression value of
823
+ # `break VALUE` is not yet modeled (scope break-path propagation landed in `eval_loop`). Returning
824
+ # Constant[nil] is safe and matches Ruby semantics for the common case.
917
825
  def type_of_loop(_node)
918
826
  Type::Combinator.constant_of(nil)
919
827
  end
@@ -930,12 +838,10 @@ module Rigor
930
838
  nominal_range_for_endpoints(node.left, node.right)
931
839
  end
932
840
 
933
- # Derives `Nominal[Range, [T]]` from the endpoint expression
934
- # types when at least one endpoint is statically typeable. The
935
- # element parameter is the union of the endpoint types (lifted
936
- # from `Constant<v>` to `Nominal<v.class>` so the carrier matches
937
- # what `Range#each` would yield). Falls back to bare
938
- # `Nominal[Range]` when no endpoint contributes a typable shape.
841
+ # Derives `Nominal[Range, [T]]` from the endpoint expression types when at least one endpoint is
842
+ # statically typeable. The element parameter is the union of the endpoint types (lifted from
843
+ # `Constant<v>` to `Nominal<v.class>` so the carrier matches what `Range#each` would yield). Falls back
844
+ # to bare `Nominal[Range]` when no endpoint contributes a typable shape.
939
845
  def nominal_range_for_endpoints(left_node, right_node)
940
846
  endpoints = [left_node, right_node].compact.map { |n| range_endpoint_element_type(n) }
941
847
  endpoints.reject! { |t| t.equal?(Type::Combinator.untyped) }
@@ -959,12 +865,10 @@ module Rigor
959
865
  end
960
866
  end
961
867
 
962
- # v0.0.7 — non-interpolated regex literals lift to
963
- # `Constant<Regexp>` so `Constant<String>#scan(/regex/)`
964
- # / `#match(/regex/)` etc. can fold through the catalog
965
- # tier. Interpolated regexes (`/foo#{x}/`) reach the
966
- # second `Prism::InterpolatedRegularExpressionNode` arm
967
- # which keeps the conservative `Nominal[Regexp]` answer.
868
+ # v0.0.7 — non-interpolated regex literals lift to `Constant<Regexp>` so `Constant<String>#scan(/regex/)`
869
+ # / `#match(/regex/)` etc. can fold through the catalog tier. Interpolated regexes (`/foo#{x}/`) reach
870
+ # the second `Prism::InterpolatedRegularExpressionNode` arm which keeps the conservative
871
+ # `Nominal[Regexp]` answer.
968
872
  def type_of_regexp(node)
969
873
  return Type::Combinator.nominal_of(Regexp) unless node.is_a?(Prism::RegularExpressionNode)
970
874
 
@@ -974,15 +878,12 @@ module Rigor
974
878
  Type::Combinator.nominal_of(Regexp)
975
879
  end
976
880
 
977
- # A range endpoint folds to a static value when it is a literal
978
- # (`IntegerNode` / `StringNode`) or when its *evaluated* type is a
979
- # `Constant<v>` carrying a range-able value (Integer / Float /
980
- # String matching the literal-path value kinds). The evaluated
981
- # arm lets `(1..n)` fold to `Constant<Range>` when per-call body
982
- # inference has pinned `n` to a constant (fact2 chain). A `nil`
983
- # node is a beginless/endless boundary: keep today's static-nil
984
- # behaviour (which yields `Constant<Range>` only when the *other*
985
- # end is also static, preserving today's beginless/endless path).
881
+ # A range endpoint folds to a static value when it is a literal (`IntegerNode` / `StringNode`) or when
882
+ # its *evaluated* type is a `Constant<v>` carrying a range-able value (Integer / Float / String —
883
+ # matching the literal-path value kinds). The evaluated arm lets `(1..n)` fold to `Constant<Range>`
884
+ # when per-call body inference has pinned `n` to a constant (fact2 chain). A `nil` node is a
885
+ # beginless/endless boundary: keep today's static-nil behaviour (which yields `Constant<Range>` only
886
+ # when the *other* end is also static, preserving today's beginless/endless path).
986
887
  def static_range_endpoint(node)
987
888
  return [true, nil] if node.nil?
988
889
  return [true, node.value] if node.is_a?(Prism::IntegerNode)
@@ -997,10 +898,9 @@ module Rigor
997
898
  [false, nil]
998
899
  end
999
900
 
1000
- # Helper for the many control-flow handlers that read a body
1001
- # `Prism::StatementsNode` or treat its absence as nil. Note that
1002
- # Prism uses nil (rather than an empty `StatementsNode`) for
1003
- # missing bodies in many node kinds.
901
+ # Helper for the many control-flow handlers that read a body `Prism::StatementsNode` or treat its
902
+ # absence as nil. Note that Prism uses nil (rather than an empty `StatementsNode`) for missing bodies
903
+ # in many node kinds.
1004
904
  def statements_or_nil(statements_node)
1005
905
  return Type::Combinator.constant_of(nil) if statements_node.nil?
1006
906
 
@@ -1050,41 +950,34 @@ module Rigor
1050
950
  Type::Combinator.constant_of(unescaped)
1051
951
  end
1052
952
 
1053
- # Backtick (`cmd`) and `%x{cmd}` invoke Kernel#` and always return a
1054
- # String. Even when the content is statically known, we widen to
1055
- # Nominal[String] because the runtime value depends on the
1056
- # subprocess output, not the source text.
953
+ # Backtick (`cmd`) and `%x{cmd}` invoke Kernel#` and always return a String. Even when the content is
954
+ # statically known, we widen to Nominal[String] because the runtime value depends on the subprocess
955
+ # output, not the source text.
1057
956
  def type_of_xstring(_node)
1058
957
  Type::Combinator.nominal_of(String)
1059
958
  end
1060
959
 
1061
- # __FILE__ is the source file path. Always non-empty when
1062
- # parsing a real file (the path resolver gives the buffer
1063
- # name, which is at minimum `"(stdin)"` / `"-e"` / a real
1064
- # path — never the empty String). Widened to
1065
- # `non-empty-string` instead of `Nominal[String]` so
1066
- # downstream String-emptiness checks know the value cannot
1067
- # be `""`.
960
+ # __FILE__ is the source file path. Always non-empty when parsing a real file (the path resolver gives
961
+ # the buffer name, which is at minimum `"(stdin)"` / `"-e"` / a real path never the empty String).
962
+ # Widened to `non-empty-string` instead of `Nominal[String]` so downstream String-emptiness checks know
963
+ # the value cannot be `""`.
1068
964
  def type_of_source_file(_node)
1069
965
  Type::Combinator.non_empty_string
1070
966
  end
1071
967
 
1072
- # __LINE__ is the line of the source literal. Ruby line
1073
- # numbers are 1-indexed, so `__LINE__` is always at least
1074
- # 1 — `positive-int` (Integer in `[1, +Inf)`) is the
1075
- # canonical refinement.
968
+ # __LINE__ is the line of the source literal. Ruby line numbers are 1-indexed, so `__LINE__` is always
969
+ # at least 1 — `positive-int` (Integer in `[1, +Inf)`) is the canonical refinement.
1076
970
  def type_of_source_line(_node)
1077
971
  Type::Combinator.positive_int
1078
972
  end
1079
973
 
1080
- # `# shareable_constant_value:` magic comment wraps the next
1081
- # constant write. Type is the wrapped write's value.
974
+ # `# shareable_constant_value:` magic comment wraps the next constant write. Type is the wrapped
975
+ # write's value.
1082
976
  def type_of_shareable_constant(node)
1083
977
  type_of(node.write)
1084
978
  end
1085
979
 
1086
- # `{ x: }` shorthand hash. The implicit value is the call to
1087
- # `x` (or a local read of `x`). Delegate.
980
+ # `{ x: }` shorthand hash. The implicit value is the call to `x` (or a local read of `x`). Delegate.
1088
981
  def type_of_implicit(node)
1089
982
  type_of(node.value)
1090
983
  end
@@ -1093,25 +986,20 @@ module Rigor
1093
986
  scope.local(node.name) || dynamic_top
1094
987
  end
1095
988
 
1096
- # `it` (Ruby 3.4) — `ItLocalVariableReadNode` carries no `name`
1097
- # field; the implicit name is always `:it`, matching the binding
1098
- # `BlockParameterBinder` installs for `Prism::ItParametersNode`.
989
+ # `it` (Ruby 3.4) — `ItLocalVariableReadNode` carries no `name` field; the implicit name is always
990
+ # `:it`, matching the binding `BlockParameterBinder` installs for `Prism::ItParametersNode`.
1099
991
  def it_read(_node)
1100
992
  scope.local(:it) || dynamic_top
1101
993
  end
1102
994
 
1103
- # Slice 5 phase 1 upgrades array literals to `Tuple[T1..Tn]`
1104
- # when every element is a non-splat value. Splatted entries
1105
- # (`[*xs, 1]`) preserve the Slice 4 phase 2d behavior: we union
1106
- # the contributed element types and emit
1107
- # `Nominal[Array, [union]]`.
995
+ # Slice 5 phase 1 upgrades array literals to `Tuple[T1..Tn]` when every element is a non-splat value.
996
+ # Splatted entries (`[*xs, 1]`) preserve the Slice 4 phase 2d behavior: we union the contributed
997
+ # element types and emit `Nominal[Array, [union]]`.
1108
998
  #
1109
- # v0.0.6 — the empty literal `[]` resolves to the empty
1110
- # `Tuple[]` carrier rather than the raw `Nominal[Array]`.
1111
- # Both carriers erase to RBS `Array`, but `Tuple[]` pins
1112
- # the literal's known arity (zero), which lets the
1113
- # per-element block fold concatenate across all-empty
1114
- # positions like `[1, 2].flat_map { |_| [] }`.
999
+ # v0.0.6 — the empty literal `[]` resolves to the empty `Tuple[]` carrier rather than the raw
1000
+ # `Nominal[Array]`. Both carriers erase to RBS `Array`, but `Tuple[]` pins the literal's known arity
1001
+ # (zero), which lets the per-element block fold concatenate across all-empty positions like `[1,
1002
+ # 2].flat_map { |_| [] }`.
1115
1003
  def array_type_for(node)
1116
1004
  elements = node.elements
1117
1005
  return Type::Combinator.tuple_of if elements.empty?
@@ -1141,26 +1029,20 @@ module Rigor
1141
1029
  type_of(body.last)
1142
1030
  end
1143
1031
 
1144
- # Indexed-collection narrowing — `receiver[key]` after a
1145
- # prior `receiver[key] ||= default` reads the post-`||=`
1146
- # type when the receiver and key are stable enough to
1147
- # address. Sits ahead of `MethodDispatcher.dispatch` so
1148
- # the standard `Hash#[]` / `Array#[]` answer (which would
1149
- # fold to `Constant[nil]` for an empty `HashShape{}` or
1150
- # `Tuple[]`) does not override the narrowing. See
1032
+ # Indexed-collection narrowing — `receiver[key]` after a prior `receiver[key] ||= default` reads the
1033
+ # post-`||=` type when the receiver and key are stable enough to address. Sits ahead of
1034
+ # `MethodDispatcher.dispatch` so the standard `Hash#[]` / `Array#[]` answer (which would fold to
1035
+ # `Constant[nil]` for an empty `HashShape{}` or `Tuple[]`) does not override the narrowing. See
1151
1036
  # {Inference::IndexedNarrowing}.
1152
1037
  def indexed_narrowing_for(node)
1153
1038
  IndexedNarrowing.lookup_for_call(node, scope) || method_chain_narrowing_for(node)
1154
1039
  end
1155
1040
 
1156
- # Stable single-hop chain narrowing — `receiver.method`
1157
- # after an `is_a?` / `kind_of?` / `instance_of?` predicate
1158
- # established the narrowing on the dominated edge. The
1159
- # call MUST be no-arg + no-block + rooted at a local-var /
1160
- # ivar read; everything else falls through to the
1161
- # standard dispatcher. ROADMAP § Future cycles —
1162
- # "Method-call receiver narrowing across stable
1163
- # receivers" — Law-of-Demeter-justified single-hop scope.
1041
+ # Stable single-hop chain narrowing — `receiver.method` after an `is_a?` / `kind_of?` / `instance_of?`
1042
+ # predicate established the narrowing on the dominated edge. The call MUST be no-arg + no-block +
1043
+ # rooted at a local-var / ivar read; everything else falls through to the standard dispatcher. ROADMAP
1044
+ # § Future cycles — "Method-call receiver narrowing across stable receivers" Law-of-Demeter-justified
1045
+ # single-hop scope.
1164
1046
  def method_chain_narrowing_for(node)
1165
1047
  return nil unless node.is_a?(Prism::CallNode)
1166
1048
  return nil unless node.block.nil?
@@ -1174,14 +1056,10 @@ module Rigor
1174
1056
  end
1175
1057
  end
1176
1058
 
1177
- # v0.0.3 A — implicit-self calls prefer a same-named
1178
- # top-level `def` over RBS dispatch. Without this,
1179
- # a helper like `def select(...)` defined inside an
1180
- # `RSpec.describe ... do ... end` block mis-routes
1181
- # through `Enumerable#select` / `Object#select` and
1182
- # the caller observes `Array[Elem]` instead of the
1183
- # helper's actual return type. The check fires only
1184
- # for `node.receiver.nil?` (true implicit self), so
1059
+ # v0.0.3 A — implicit-self calls prefer a same-named top-level `def` over RBS dispatch. Without this, a
1060
+ # helper like `def select(...)` defined inside an `RSpec.describe ... do ... end` block mis-routes
1061
+ # through `Enumerable#select` / `Object#select` and the caller observes `Array[Elem]` instead of the
1062
+ # helper's actual return type. The check fires only for `node.receiver.nil?` (true implicit self), so
1185
1063
  # explicit-receiver dispatch is unaffected.
1186
1064
  def try_local_def_dispatch(node, receiver, arg_types)
1187
1065
  local_def = node.receiver.nil? ? scope.top_level_def_for(node.name) : nil
@@ -1190,24 +1068,19 @@ module Rigor
1190
1068
  local_inference = infer_top_level_user_method(local_def, receiver, arg_types)
1191
1069
  return local_inference if local_inference
1192
1070
 
1193
- # The local def matches by name but the inference was
1194
- # disqualified the parameter shape is too complex for the
1195
- # first-iteration binder (kwargs / optionals / rest), so the
1196
- # body could not be re-typed. `Dynamic[Top]` is the
1197
- # safest answer: RBS dispatch would be wrong (the method
1198
- # is user-defined and shadows whatever ancestor method the
1199
- # dispatch would find), and `Dynamic[Top]` propagates
1200
- # correctly through downstream call chains without
1201
- # surfacing misleading false-positive diagnostics.
1071
+ # The local def matches by name but the inference was disqualified — the parameter shape is too
1072
+ # complex for the first-iteration binder (kwargs / optionals / rest), so the body could not be
1073
+ # re-typed. `Dynamic[Top]` is the safest answer: RBS dispatch would be wrong (the method is
1074
+ # user-defined and shadows whatever ancestor method the dispatch would find), and `Dynamic[Top]`
1075
+ # propagates correctly through downstream call chains without surfacing misleading false-positive
1076
+ # diagnostics.
1202
1077
  dynamic_top
1203
1078
  end
1204
1079
 
1205
- # Slice 2 routes call expressions through `MethodDispatcher`. The
1206
- # receiver and every argument are typed first, then the dispatcher is
1207
- # asked for a result type. A nil result triggers the fail-soft fallback
1208
- # for the CallNode itself (the inner type_of calls already record
1209
- # their own fallbacks for unrecognised receivers/args, so the tracer
1210
- # captures both the immediate dispatch miss and the deeper cause).
1080
+ # Slice 2 routes call expressions through `MethodDispatcher`. The receiver and every argument are typed
1081
+ # first, then the dispatcher is asked for a result type. A nil result triggers the fail-soft fallback
1082
+ # for the CallNode itself (the inner type_of calls already record their own fallbacks for unrecognised
1083
+ # receivers/args, so the tracer captures both the immediate dispatch miss and the deeper cause).
1211
1084
  def call_type_for(node)
1212
1085
  narrowed = indexed_narrowing_for(node)
1213
1086
  return narrowed if narrowed
@@ -1219,15 +1092,11 @@ module Rigor
1219
1092
  local_def_result = try_local_def_dispatch(node, receiver, arg_types)
1220
1093
  return local_def_result if local_def_result
1221
1094
 
1222
- # v0.0.6 phase 2 — per-element block fold for Tuple
1223
- # receivers. When `[a, b, c].map { |x| f(x) }` and the
1224
- # receiver is a `Tuple` carrier with finite elements,
1225
- # type the block body once per position with the
1226
- # corresponding element bound to the block parameter
1227
- # and assemble the results into a `Tuple[U_1..U_n]`.
1228
- # This sits ahead of `MethodDispatcher.dispatch` so
1229
- # the RBS tier does not re-widen the answer back to
1230
- # `Array[union]`.
1095
+ # v0.0.6 phase 2 — per-element block fold for Tuple receivers. When `[a, b, c].map { |x| f(x) }` and
1096
+ # the receiver is a `Tuple` carrier with finite elements, type the block body once per position with
1097
+ # the corresponding element bound to the block parameter and assemble the results into a
1098
+ # `Tuple[U_1..U_n]`. This sits ahead of `MethodDispatcher.dispatch` so the RBS tier does not re-widen
1099
+ # the answer back to `Array[union]`.
1231
1100
  per_element = try_per_element_block_fold(node, receiver)
1232
1101
  return per_element if per_element
1233
1102
 
@@ -1248,48 +1117,50 @@ module Rigor
1248
1117
  )
1249
1118
  return result if result
1250
1119
 
1251
- # v0.0.2 #5 — inter-procedural inference for
1252
- # user-defined methods. When dispatch misses but the
1253
- # receiver is a user class with a `def` body, re-type
1254
- # the body with the call's argument types bound and
1255
- # return the body's last-expression type.
1120
+ # v0.0.2 #5 — inter-procedural inference for user-defined methods. When dispatch misses but the
1121
+ # receiver is a user class with a `def` body, re-type the body with the call's argument types bound
1122
+ # and return the body's last-expression type.
1256
1123
  user_inference = try_user_method_inference(receiver, node, arg_types)
1257
1124
  return user_inference if user_inference
1258
1125
 
1259
- # Module-singleton call resolution (ADR-57 follow-up) — when the
1260
- # receiver is `Singleton[Foo]` (a module/class constant or a
1261
- # singleton-method `self`) and `Foo` declares a user-side
1262
- # `def self.x` / `module_function` body, re-type that body
1263
- # with the call args bound. Sits after the RBS dispatch tier, so
1264
- # foreign / RBS-known singletons (`Math.sqrt`) keep their catalog
1265
- # answer; only project-defined singleton methods reach here.
1126
+ # Module-singleton call resolution (ADR-57 follow-up) — when the receiver is `Singleton[Foo]` (a
1127
+ # module/class constant or a singleton-method `self`) and `Foo` declares a user-side `def self.x` /
1128
+ # `module_function` body, re-type that body with the call args bound. Sits after the RBS dispatch
1129
+ # tier, so foreign / RBS-known singletons (`Math.sqrt`) keep their catalog answer; only
1130
+ # project-defined singleton methods reach here.
1266
1131
  singleton_inference = try_singleton_method_inference(receiver, node, arg_types)
1267
1132
  return singleton_inference if singleton_inference
1268
1133
 
1269
- # Dynamic-origin propagation: when the receiver is Dynamic[T] and
1270
- # no positive rule resolves the call, the result inherits the
1271
- # dynamic origin. Per the value-lattice algebra, this is a
1272
- # recognised semantic outcome, not a fail-soft compromise, so it
1273
- # MUST NOT record a tracer event.
1274
- return dynamic_top if receiver.is_a?(Type::Dynamic)
1275
-
1276
- # ADR-24 slice 4a this is the engine choke-point where an
1277
- # implicit-self call has exhausted every resolution tier (RBS
1278
- # dispatch + user-class ancestor walk) and falls through to
1279
- # `Dynamic[top]`. When the slice-4 recorder is active, capture the
1280
- # miss so a later slice's closed-class gate can flag it. Off by
1281
- # default: `active?` is a plain integer read.
1134
+ # Dynamic-origin propagation: when the receiver is Dynamic[T] and no positive rule resolves the call,
1135
+ # the result inherits the dynamic origin. Per the value-lattice algebra, this is a recognised
1136
+ # semantic outcome, not a fail-soft compromise, so it MUST NOT record a tracer event.
1137
+ return inherit_receiver_origin(node) if receiver.is_a?(Type::Dynamic)
1138
+
1139
+ # ADR-24 slice 4a — this is the engine choke-point where an implicit-self call has exhausted every
1140
+ # resolution tier (RBS dispatch + user-class ancestor walk) and falls through to `Dynamic[top]`. When
1141
+ # the slice-4 recorder is active, capture the miss so a later slice's closed-class gate can flag it.
1142
+ # Off by default: `active?` is a plain integer read.
1282
1143
  record_unresolved_self_call(node, receiver) if Analysis::SelfCallResolutionRecorder.active?
1283
1144
 
1284
1145
  fallback_for(node, family: :prism)
1285
1146
  end
1286
1147
 
1287
- # ADR-24 slice 4a records an unresolved *implicit-self* call (no
1288
- # explicit receiver) whose `self` types to a concrete user class.
1289
- # Explicit-receiver misses are out of scope (the existing
1290
- # `call.undefined-method` rule already owns receiver-typed dispatch);
1291
- # a non-`Nominal` self (top-level / DSL-block `self`, or a `Dynamic`
1292
- # self) is skipped so the gradual guarantee is never touched here.
1148
+ # ADR-82 WD6carry the receiver's provenance onto the call it produces (returning the unchanged
1149
+ # `dynamic_top` result), so a specific cause survives a method chain (`x.foo.bar`): without this,
1150
+ # `.foo` on a Dynamic `x` records nothing and `.bar`'s receiver looks causeless. Side-channel only
1151
+ # (the result type is untouched); a nil cause is skipped. This is the lever for the
1152
+ # intermediate-expression / chain receivers that dominate a real app's `coverage --protection`
1153
+ # catch-all.
1154
+ def inherit_receiver_origin(node)
1155
+ inherited = OriginLookup.origin_for(scope, node.receiver)
1156
+ scope.record_dynamic_origin(node, inherited) if inherited
1157
+ dynamic_top
1158
+ end
1159
+
1160
+ # ADR-24 slice 4a — records an unresolved *implicit-self* call (no explicit receiver) whose `self`
1161
+ # types to a concrete user class. Explicit-receiver misses are out of scope (the existing
1162
+ # `call.undefined-method` rule already owns receiver-typed dispatch); a non-`Nominal` self (top-level /
1163
+ # DSL-block `self`, or a `Dynamic` self) is skipped so the gradual guarantee is never touched here.
1293
1164
  def record_unresolved_self_call(node, receiver)
1294
1165
  return unless node.receiver.nil?
1295
1166
  return unless receiver.is_a?(Type::Nominal)
@@ -1306,24 +1177,18 @@ module Rigor
1306
1177
  )
1307
1178
  end
1308
1179
 
1309
- # The recorder must capture *existence* misses, not type misses.
1310
- # Reaching the choke-point means RBS dispatch produced no result, but
1311
- # a project method can still EXIST without an inferable return type
1312
- # a `module_function` sibling whose body the engine can't fully type,
1313
- # an `attr_reader` / `define_method` / `Data.define` member. Recording
1314
- # those would reproduce the 135 false positives of slice-4 attempt 1.
1315
- # So skip any name the engine's own existence signals already know:
1316
- # a `def` resolvable through the ancestor walk, or an own-class entry
1317
- # in the discovered-methods table (`def` / `attr_*` / `define_method`
1318
- # / alias). This reuses the engine's real resolution the
1319
- # "collect, don't recompute" lesson so only a name that exists
1320
- # nowhere a project signal can see reaches the recorder.
1321
- # `module_function` records its defs as `:singleton` (an implicit-self
1322
- # call inside such a method dispatches to the module's singleton
1323
- # method), while ordinary instance methods record `:instance`. The
1324
- # recorder cannot tell the two contexts apart from the call node, so
1325
- # existence under EITHER kind suppresses recording — the FP-safe
1326
- # choice, since either means the method genuinely exists.
1180
+ # The recorder must capture *existence* misses, not type misses. Reaching the choke-point means RBS
1181
+ # dispatch produced no result, but a project method can still EXIST without an inferable return type —
1182
+ # a `module_function` sibling whose body the engine can't fully type, an `attr_reader` /
1183
+ # `define_method` / `Data.define` member. Recording those would reproduce the 135 false positives of
1184
+ # slice-4 attempt 1. So skip any name the engine's own existence signals already know: a `def`
1185
+ # resolvable through the ancestor walk, or an own-class entry in the discovered-methods table (`def` /
1186
+ # `attr_*` / `define_method` / alias). This reuses the engine's real resolution the "collect, don't
1187
+ # recompute" lesson — so only a name that exists nowhere a project signal can see reaches the recorder.
1188
+ # `module_function` records its defs as `:singleton` (an implicit-self call inside such a method
1189
+ # dispatches to the module's singleton method), while ordinary instance methods record `:instance`. The
1190
+ # recorder cannot tell the two contexts apart from the call node, so existence under EITHER kind
1191
+ # suppresses recording the FP-safe choice, since either means the method genuinely exists.
1327
1192
  def self_call_method_known?(class_name, method_name)
1328
1193
  return true if resolve_user_def_through_ancestors(class_name, method_name)
1329
1194
 
@@ -1331,94 +1196,68 @@ module Rigor
1331
1196
  scope.discovered_method?(class_name, method_name, :singleton)
1332
1197
  end
1333
1198
 
1334
- # v0.0.2 #5 — re-types the body of a user-defined
1335
- # instance method with the call site's argument types
1336
- # bound to the method's parameters. Used as a
1337
- # last-resort tier after `MethodDispatcher.dispatch`
1338
- # has exhausted its catalogue (RBS, shape, constant
1339
- # folding, user-class fallback). Returns nil when:
1199
+ # v0.0.2 #5 — re-types the body of a user-defined instance method with the call site's argument types
1200
+ # bound to the method's parameters. Used as a last-resort tier after `MethodDispatcher.dispatch` has
1201
+ # exhausted its catalogue (RBS, shape, constant folding, user-class fallback). Returns nil when:
1340
1202
  #
1341
1203
  # - the receiver is not `Nominal[T]` for some T;
1342
- # - no def_node is recorded for that class/method
1343
- # (the receiver is foreign or has only an RBS sig);
1344
- # - the def has no body, or has a parameter shape we
1345
- # cannot bind from the call's positional args;
1346
- # - the inference is already in progress for this
1347
- # (class, method, signature) tuple recursion
1348
- # safety net.
1349
- # v0.0.3 A re-types a top-level (or DSL-block-nested)
1350
- # `def` discovered by `ScopeIndexer` under the
1351
- # `TOP_LEVEL_DEF_KEY` sentinel. Mirrors the
1352
- # `infer_user_method_return` shape but uses the
1353
- # current `scope.self_type` (or implicit `Object`)
1354
- # as the receiver carrier so the body's own self is
1355
- # consistent with the call site's. Returns nil when
1356
- # the parameter shape disqualifies the def, when the
1357
- # body is empty, or when a recursion cycle is
1358
- # detected.
1204
+ # - no def_node is recorded for that class/method (the receiver is foreign or has only an RBS sig);
1205
+ # - the def has no body, or has a parameter shape we cannot bind from the call's positional args;
1206
+ # - the inference is already in progress for this (class, method, signature) tuple recursion safety
1207
+ # net.
1208
+ # v0.0.3 A — re-types a top-level (or DSL-block-nested) `def` discovered by `ScopeIndexer` under the
1209
+ # `TOP_LEVEL_DEF_KEY` sentinel. Mirrors the `infer_user_method_return` shape but uses the current
1210
+ # `scope.self_type` (or implicit `Object`) as the receiver carrier so the body's own self is
1211
+ # consistent with the call site's. Returns nil when the parameter shape disqualifies the def, when the
1212
+ # body is empty, or when a recursion cycle is detected.
1359
1213
  def infer_top_level_user_method(def_node, receiver, arg_types)
1360
1214
  infer_user_method_return(def_node, receiver, arg_types)
1361
1215
  rescue StandardError
1362
1216
  nil
1363
1217
  end
1364
1218
 
1365
- # ADR-24 slice 1 — implicit-self method-call resolution.
1366
- # `discovered_def_nodes` is now carried into method /
1367
- # class body scopes (see `StatementEvaluator#build_fresh_body_scope`),
1368
- # so a call written with no explicit receiver inside a
1369
- # method body resolves against the enclosing class's own
1370
- # definitions and the file's top-level defs. Before
1371
- # slice 1 every such call typed `Dynamic[top]`.
1219
+ # ADR-24 slice 1 — implicit-self method-call resolution. `discovered_def_nodes` is now carried into
1220
+ # method / class body scopes (see `StatementEvaluator#build_fresh_body_scope`), so a call written with
1221
+ # no explicit receiver inside a method body resolves against the enclosing class's own definitions and
1222
+ # the file's top-level defs. Before slice 1 every such call typed `Dynamic[top]`.
1372
1223
  #
1373
- # The resolved return type is adopted UNCONDITIONALLY — a resolved
1374
- # user-method call site reads the callee's inferred return, exactly as a
1375
- # toplevel call has since v0.0.3.
1224
+ # The resolved return type is adopted UNCONDITIONALLY — a resolved user-method call site reads the
1225
+ # callee's inferred return, exactly as a toplevel call has since v0.0.3.
1376
1226
  #
1377
- # ADR-24 WD3 originally gated this: inside a class / method body only a
1378
- # `Bot` return was adopted, everything else stayed `Dynamic[top]`, because
1379
- # an early unconditional-adoption experiment regressed `rigor check lib`
1380
- # by 16 diagnostics. ADR-55 / ADR-56 then chipped the gate open for the
1381
- # recursive-fixpoint summary and the value-pinned unroll envelope. ADR-57
1382
- # closed the arc: it re-ran the gate-open experiment per engine generation
1383
- # and adjudicated every firing as genuine-or-artifact, fixing the
1384
- # artifacts at their root the tail-only body evaluator dropping explicit
1385
- # `return` (slice 1), multi-value returns not contributing a Tuple
1386
- # (slice 1), escaping block-captured content mutation surviving as a
1387
- # precise seed both inline and across a method boundary (slices 2/3), two
1388
- # over-strict self-authored RBS signatures (slice 1), and an over-optional
1389
- # tuple-slot destructure (slice 3). With the residual all genuine-or-win,
1390
- # the gate opened permanently on 2026-06-12 (ADR-57 WD2): the gate-open
1391
- # `rigor check lib` + plugin self-check delta is zero, and the Mastodon /
1392
- # haml / kramdown corpora show only adjudicated wins (a more precise error
1393
- # message; FP removals).
1227
+ # ADR-24 WD3 originally gated this: inside a class / method body only a `Bot` return was adopted,
1228
+ # everything else stayed `Dynamic[top]`, because an early unconditional-adoption experiment regressed
1229
+ # `rigor check lib` by 16 diagnostics. ADR-55 / ADR-56 then chipped the gate open for the
1230
+ # recursive-fixpoint summary and the value-pinned unroll envelope. ADR-57 closed the arc: it re-ran the
1231
+ # gate-open experiment per engine generation and adjudicated every firing as genuine-or-artifact,
1232
+ # fixing the artifacts at their root — the tail-only body evaluator dropping explicit `return` (slice
1233
+ # 1), multi-value returns not contributing a Tuple (slice 1), escaping block-captured content mutation
1234
+ # surviving as a precise seed both inline and across a method boundary (slices 2/3), two over-strict
1235
+ # self-authored RBS signatures (slice 1), and an over-optional tuple-slot destructure (slice 3). With
1236
+ # the residual all genuine-or-win, the gate opened permanently on 2026-06-12 (ADR-57 WD2): the
1237
+ # gate-open `rigor check lib` + plugin self-check delta is zero, and the Mastodon / haml / kramdown
1238
+ # corpora show only adjudicated wins (a more precise error message; FP removals).
1394
1239
  #
1395
- # The historical `adoptable_self_call_result?` predicate (its
1396
- # `self_type.nil?` / `Bot` / fixpoint-summary / unroll special cases) is
1397
- # now subsumed by unconditional adoption and removed; `try_local_def_
1398
- # dispatch` / `try_user_method_inference` simply return the inferred
1399
- # return. `clamp_unroll_result` still backstops an untrustworthy unrolled
1400
- # value independently of adoption.
1401
-
1402
- # An extended (value-keyed) guard frame is `[plain_signature,
1403
- # value_key]` where `plain_signature` is itself the `[receiver,
1404
- # method]` pair; a plain frame is that pair directly.
1240
+ # The historical `adoptable_self_call_result?` predicate (its `self_type.nil?` / `Bot` /
1241
+ # fixpoint-summary / unroll special cases) is now subsumed by unconditional adoption and removed;
1242
+ # `try_local_def_dispatch` / `try_user_method_inference` simply return the inferred return.
1243
+ # `clamp_unroll_result` still backstops an untrustworthy unrolled value independently of adoption.
1244
+
1245
+ # An extended (value-keyed) guard frame is `[plain_signature, value_key]` where `plain_signature` is
1246
+ # itself the `[receiver, method]` pair; a plain frame is that pair directly.
1405
1247
  def extended_frame?(frame)
1406
1248
  frame.is_a?(Array) && frame.size == 2 && frame.first.is_a?(Array)
1407
1249
  end
1408
1250
 
1409
- # The plain `(receiver, method)` signature carried by a guard frame:
1410
- # the frame itself for a plain frame, or its first element for an
1411
- # extended (value-keyed) frame.
1251
+ # The plain `(receiver, method)` signature carried by a guard frame: the frame itself for a plain
1252
+ # frame, or its first element for an extended (value-keyed) frame.
1412
1253
  def plain_part(frame)
1413
1254
  extended_frame?(frame) ? frame.first : frame
1414
1255
  end
1415
1256
 
1416
- # True when `type` is a concrete value — a `Type::Constant` or a
1417
- # `Type::Tuple` whose elements are (recursively) all value-pinned.
1418
- # ADR-55 slice 1: a value-pinned self-call result is adopted even
1419
- # inside a class/method body (where WD3 otherwise keeps non-`Bot`
1420
- # returns as `Dynamic[top]`). A concrete value at a call site is
1421
- # strictly more precise and can never enable an undefined-method or
1257
+ # True when `type` is a concrete value — a `Type::Constant` or a `Type::Tuple` whose elements are
1258
+ # (recursively) all value-pinned. ADR-55 slice 1: a value-pinned self-call result is adopted even
1259
+ # inside a class/method body (where WD3 otherwise keeps non-`Bot` returns as `Dynamic[top]`). A
1260
+ # concrete value at a call site is strictly more precise and can never enable an undefined-method or
1422
1261
  # argument-type false positive — it is FP-neutral by construction.
1423
1262
  def fully_value_pinned?(type)
1424
1263
  case type
@@ -1442,20 +1281,16 @@ module Rigor
1442
1281
  nil
1443
1282
  end
1444
1283
 
1445
- # Module-singleton call resolution (ADR-57 follow-up) — resolves
1446
- # `Foo.<name>` on a `Singleton[Foo]` receiver against `Foo`'s
1447
- # user-side singleton defs (`def self.x`, `def Foo.x`, a
1448
- # `class << self` body, or a `module_function` method) and re-types
1449
- # the body with the call's argument types bound. The body scope's
1450
- # `self_type` is the SAME `Singleton[Foo]` carrier, so an
1451
- # implicit-self call inside (`def self.via; helper(x); end`)
1452
- # re-enters this tier and resolves against the same singleton table
1284
+ # Module-singleton call resolution (ADR-57 follow-up) — resolves `Foo.<name>` on a `Singleton[Foo]`
1285
+ # receiver against `Foo`'s user-side singleton defs (`def self.x`, `def Foo.x`, a `class << self`
1286
+ # body, or a `module_function` method) and re-types the body with the call's argument types bound.
1287
+ # The body scope's `self_type` is the SAME `Singleton[Foo]` carrier, so an implicit-self call inside
1288
+ # (`def self.via; helper(x); end`) re-enters this tier and resolves against the same singleton table
1453
1289
  # — the symmetric counterpart of the instance-side ancestor walk.
1454
1290
  #
1455
- # Resolution is OWN-class only: the singleton-ancestry chain
1456
- # (`extend`ed modules, inherited class-method dispatch) is not
1457
- # walked at this slice. A miss degrades to today's `Dynamic[top]`,
1458
- # never a false resolution (ADR-57 follow-up § module-singleton).
1291
+ # Resolution is OWN-class only: the singleton-ancestry chain (`extend`ed modules, inherited
1292
+ # class-method dispatch) is not walked at this slice. A miss degrades to today's `Dynamic[top]`, never
1293
+ # a false resolution (ADR-57 follow-up § module-singleton).
1459
1294
  def try_singleton_method_inference(receiver, call_node, arg_types)
1460
1295
  return nil unless receiver.is_a?(Type::Singleton)
1461
1296
 
@@ -1470,42 +1305,30 @@ module Rigor
1470
1305
  nil
1471
1306
  end
1472
1307
 
1473
- # ADR-24 slice 2 — resolves `method_name` against
1474
- # `class_name`'s own `def`s, then walks the user-class
1475
- # ancestor chain: included / prepended modules (transitive)
1476
- # and the superclass chain. RBS-known ancestors are NOT
1477
- # walked here the `MethodDispatcher` RBS tier runs before
1478
- # `try_user_method_inference` and already covers them; an
1479
- # ancestor name that resolves to no project-discovered
1480
- # class/module ends that branch. Cross-file: the chain is
1481
- # followed through `Scope#discovered_superclasses` /
1482
- # `#discovered_includes` / `#discovered_def_nodes`, which
1483
- # the runner seeds from the project-wide pre-pass. The walk
1484
- # is breadth-first, cycle-guarded, and node-count-capped.
1308
+ # ADR-24 slice 2 — resolves `method_name` against `class_name`'s own `def`s, then walks the user-class
1309
+ # ancestor chain: included / prepended modules (transitive) and the superclass chain. RBS-known
1310
+ # ancestors are NOT walked here the `MethodDispatcher` RBS tier runs before
1311
+ # `try_user_method_inference` and already covers them; an ancestor name that resolves to no
1312
+ # project-discovered class/module ends that branch. Cross-file: the chain is followed through
1313
+ # `Scope#discovered_superclasses` / `#discovered_includes` / `#discovered_def_nodes`, which the runner
1314
+ # seeds from the project-wide pre-pass. The walk is breadth-first, cycle-guarded, and node-count-capped.
1485
1315
  ANCESTOR_WALK_LIMIT = 100
1486
1316
  private_constant :ANCESTOR_WALK_LIMIT
1487
1317
 
1488
1318
  CLASS_GRAPH_CACHE_KEY = :__rigor_class_graph_cache__
1489
1319
  private_constant :CLASS_GRAPH_CACHE_KEY
1490
1320
 
1491
- # Run-scoped memo for the static class-graph resolvers below. They
1492
- # are pure functions of the *frozen* project index trio
1493
- # (`discovered_def_nodes` / `discovered_superclasses` /
1494
- # `discovered_includes`)`user_def_for` / `superclass_of` /
1495
- # `includes_of` read nothing else, and never touch the current
1496
- # scope's locals or narrowings so a result computed for one
1497
- # `(class, method)` is valid for every `Scope` that shares those
1498
- # tables. `ExpressionTyper` is rebuilt per `Scope#type_of`, so the
1499
- # memo lives on `Thread.current` rather than on `self`. It is keyed
1500
- # by the *identity* of the three frozen tables (nested
1501
- # `compare_by_identity` stores): a new analysis generation, or any
1502
- # `Scope` that swaps an index via `with_discovered_*`, transparently
1503
- # lands in a fresh bucket while everything sharing the tables shares
1504
- # the memo. Steady-state cost is three identity-keyed hash reads and
1505
- # zero allocation — the `||=` chains only allocate on the first miss
1506
- # of a generation. (Pool mode forks per worker, so the
1507
- # `Thread.current` store is process-local and never crosses a
1508
- # project boundary.)
1321
+ # Run-scoped memo for the static class-graph resolvers below. They are pure functions of the *frozen*
1322
+ # project index trio (`discovered_def_nodes` / `discovered_superclasses` / `discovered_includes`)
1323
+ # `user_def_for` / `superclass_of` / `includes_of` read nothing else, and never touch the current
1324
+ # scope's locals or narrowings so a result computed for one `(class, method)` is valid for every
1325
+ # `Scope` that shares those tables. `ExpressionTyper` is rebuilt per `Scope#type_of`, so the memo lives
1326
+ # on `Thread.current` rather than on `self`. It is keyed by the *identity* of the three frozen tables
1327
+ # (nested `compare_by_identity` stores): a new analysis generation, or any `Scope` that swaps an index
1328
+ # via `with_discovered_*`, transparently lands in a fresh bucket while everything sharing the tables
1329
+ # shares the memo. Steady-state cost is three identity-keyed hash reads and zero allocation — the `||=`
1330
+ # chains only allocate on the first miss of a generation. (Pool mode forks per worker, so the
1331
+ # `Thread.current` store is process-local and never crosses a project boundary.)
1509
1332
  def class_graph_buckets
1510
1333
  store = (Thread.current[CLASS_GRAPH_CACHE_KEY] ||= {}.compare_by_identity)
1511
1334
  by_def = (store[scope.discovered_def_nodes] ||= {}.compare_by_identity)
@@ -1517,14 +1340,11 @@ module Rigor
1517
1340
  resolve_user_def_with_owner(class_name, method_name).first
1518
1341
  end
1519
1342
 
1520
- # ADR-57 N5 follow-up — resolves the method's def node AND the
1521
- # ancestor that owns it (the class/module whose own `def` table
1522
- # holds the body, which may differ from `class_name` when the
1523
- # method is inherited from a superclass or included module). The
1524
- # owner is what the overridable-method adoption gate keys on. Both
1525
- # are cached together (the walk is identical to the def-only path it
1526
- # replaced) and returned as a `[def_node, owner]` pair; `owner` is
1527
- # nil exactly when `def_node` is nil.
1343
+ # ADR-57 N5 follow-up — resolves the method's def node AND the ancestor that owns it (the class/module
1344
+ # whose own `def` table holds the body, which may differ from `class_name` when the method is
1345
+ # inherited from a superclass or included module). The owner is what the overridable-method adoption
1346
+ # gate keys on. Both are cached together (the walk is identical to the def-only path it replaced) and
1347
+ # returned as a `[def_node, owner]` pair; `owner` is nil exactly when `def_node` is nil.
1528
1348
  def resolve_user_def_with_owner(class_name, method_name)
1529
1349
  cache = class_graph_buckets[:user_def]
1530
1350
  table = (cache[class_name.to_s] ||= {})
@@ -1557,12 +1377,10 @@ module Rigor
1557
1377
  [nil, nil]
1558
1378
  end
1559
1379
 
1560
- # Pushes `current`'s direct ancestors onto the BFS queue:
1561
- # included / prepended modules first (Ruby places mixins
1562
- # nearer than the superclass), then the superclass. Each
1563
- # as-written name is resolved against `current`'s lexical
1564
- # nesting; names that resolve to no project class/module
1565
- # are dropped (RBS-known / third-party ancestors).
1380
+ # Pushes `current`'s direct ancestors onto the BFS queue: included / prepended modules first (Ruby
1381
+ # places mixins nearer than the superclass), then the superclass. Each as-written name is resolved
1382
+ # against `current`'s lexical nesting; names that resolve to no project class/module are dropped
1383
+ # (RBS-known / third-party ancestors).
1566
1384
  def enqueue_ancestors(current, queue)
1567
1385
  scope.includes_of(current).each do |raw|
1568
1386
  resolved = resolve_ancestor_class_name(current, raw)
@@ -1575,13 +1393,10 @@ module Rigor
1575
1393
  queue.push(resolved_super) if resolved_super
1576
1394
  end
1577
1395
 
1578
- # Resolves a superclass name AS WRITTEN (`"Base"`, or a
1579
- # qualified `"A::B"`) to a project-discovered class,
1580
- # following Ruby's `Module.nesting` constant lookup: try
1581
- # the raw name under each enclosing namespace of the
1582
- # subclass, innermost first, then bare. Returns nil when
1583
- # no candidate names a discovered user class (e.g. the
1584
- # superclass is an RBS-known or third-party class).
1396
+ # Resolves a superclass name AS WRITTEN (`"Base"`, or a qualified `"A::B"`) to a project-discovered
1397
+ # class, following Ruby's `Module.nesting` constant lookup: try the raw name under each enclosing
1398
+ # namespace of the subclass, innermost first, then bare. Returns nil when no candidate names a
1399
+ # discovered user class (e.g. the superclass is an RBS-known or third-party class).
1585
1400
  def resolve_ancestor_class_name(subclass_qualified, raw_superclass)
1586
1401
  by_subclass = (class_graph_buckets[:name][subclass_qualified] ||= {})
1587
1402
  return by_subclass[raw_superclass] if by_subclass.key?(raw_superclass)
@@ -1605,29 +1420,22 @@ module Rigor
1605
1420
  scope.discovered_includes.key?(name)
1606
1421
  end
1607
1422
 
1608
- # ADR-57 N5 — overridable-method adoption gate. A self-call resolved
1609
- # to a project `def` whose owner has a discovered subclass / includer
1610
- # that REDEFINES the same method (same instance-vs-singleton kind) is
1611
- # a template-method site: the base body's literal return is the
1612
- # *default*, not the value every receiver sees, so adopting it as a
1613
- # flow constant is unsound (rgl `module Graph; def directed?; false`
1614
- # folds `unless directed?` always-true, ignoring `DirectedAdjacencyGraph`
1615
- # overriding it to `true` the entire rgl warning set, per the
1616
- # 2026-06-13 app/network survey N5 row). On such a hit the precise
1617
- # return degrades to `Dynamic[top]`, deliberately re-opening a Dynamic
1618
- # source ONLY for genuinely-overridden methods. A method with no
1619
- # discovered override folds exactly as before — over-conservatism must
1620
- # not re-open Dynamic for final methods.
1423
+ # ADR-57 N5 — overridable-method adoption gate. A self-call resolved to a project `def` whose owner has
1424
+ # a discovered subclass / includer that REDEFINES the same method (same instance-vs-singleton kind) is
1425
+ # a template-method site: the base body's literal return is the *default*, not the value every
1426
+ # receiver sees, so adopting it as a flow constant is unsound (rgl `module Graph; def directed?; false`
1427
+ # folds `unless directed?` always-true, ignoring `DirectedAdjacencyGraph` overriding it to `true` the
1428
+ # entire rgl warning set, per the 2026-06-13 app/network survey N5 row). On such a hit the precise
1429
+ # return degrades to `Dynamic[top]`, deliberately re-opening a Dynamic source ONLY for
1430
+ # genuinely-overridden methods. A method with no discovered override folds exactly as before —
1431
+ # over-conservatism must not re-open Dynamic for final methods.
1621
1432
  #
1622
- # The gate only inspects a *flow-constant-foldable* result (a
1623
- # `Constant`, or a `Tuple` of such): only a value-pinned return can
1624
- # mislead a downstream `if`/`unless`/`case` into an
1625
- # `always-truthy-condition` fold, which is exactly the unsoundness the
1626
- # gate exists to remove. A `Nominal` / `Dynamic` / union return cannot
1627
- # produce a flow constant, so adopting it from an overridden method is
1628
- # harmless and is left untouched — this keeps the override-relation
1629
- # walk off the hot path for the overwhelming majority of self-calls
1630
- # (whose return is not a bare constant).
1433
+ # The gate only inspects a *flow-constant-foldable* result (a `Constant`, or a `Tuple` of such): only a
1434
+ # value-pinned return can mislead a downstream `if`/`unless`/`case` into an `always-truthy-condition`
1435
+ # fold, which is exactly the unsoundness the gate exists to remove. A `Nominal` / `Dynamic` / union
1436
+ # return cannot produce a flow constant, so adopting it from an overridden method is harmless and is
1437
+ # left untouched this keeps the override-relation walk off the hot path for the overwhelming
1438
+ # majority of self-calls (whose return is not a bare constant).
1631
1439
  def degrade_if_overridable(result, owner, method_name, kind)
1632
1440
  return result if owner.nil?
1633
1441
  return result unless fully_value_pinned?(result)
@@ -1639,14 +1447,11 @@ module Rigor
1639
1447
  OVERRIDE_GATE_CACHE_KEY = :__rigor_overridable_method_gate__
1640
1448
  private_constant :OVERRIDE_GATE_CACHE_KEY
1641
1449
 
1642
- # Run-scoped memo for {#overridden_in_project?}, keyed (like
1643
- # `class_graph_buckets`) by the identity of the frozen discovery
1644
- # trio so a new analysis generation lands in a fresh bucket, then
1645
- # nested `kind owner method_name`. The predicate is a pure
1646
- # function of those tables. Nesting avoids allocating a composite
1647
- # cache key on the hot path (the gate runs on every adopted self-call
1648
- # return), so a steady-state hit is three identity hash reads + two
1649
- # string/symbol hash reads with zero allocation.
1450
+ # Run-scoped memo for {#overridden_in_project?}, keyed (like `class_graph_buckets`) by the identity of
1451
+ # the frozen discovery trio so a new analysis generation lands in a fresh bucket, then nested `kind →
1452
+ # owner method_name`. The predicate is a pure function of those tables. Nesting avoids allocating a
1453
+ # composite cache key on the hot path (the gate runs on every adopted self-call return), so a
1454
+ # steady-state hit is three identity hash reads + two string/symbol hash reads with zero allocation.
1650
1455
  def override_gate_buckets
1651
1456
  store = (Thread.current[OVERRIDE_GATE_CACHE_KEY] ||= {}.compare_by_identity)
1652
1457
  by_def = (store[scope.discovered_def_nodes] ||= {}.compare_by_identity)
@@ -1654,12 +1459,10 @@ module Rigor
1654
1459
  by_super[scope.discovered_includes] ||= { instance: {}, singleton: {} }
1655
1460
  end
1656
1461
 
1657
- # True when some discovered project class/module — distinct from
1658
- # `owner` — redefines `(method_name, kind)` AND is related to `owner`
1659
- # (a transitive discovered subclass of an owner class, or a
1660
- # class/module that includes/prepends extends, for singleton kind
1661
- # an owner module). A same-name reopen of `owner` itself is NOT an
1662
- # override (monkey-patch reopen shares the owner identity). Memoized
1462
+ # True when some discovered project class/module — distinct from `owner` — redefines `(method_name,
1463
+ # kind)` AND is related to `owner` (a transitive discovered subclass of an owner class, or a
1464
+ # class/module that includes/prepends extends, for singleton kind — an owner module). A same-name
1465
+ # reopen of `owner` itself is NOT an override (monkey-patch reopen shares the owner identity). Memoized
1663
1466
  # per `(owner, method_name, kind)`.
1664
1467
  def overridden_in_project?(owner, method_name, kind)
1665
1468
  by_owner = (override_gate_buckets[kind][owner] ||= {})
@@ -1676,16 +1479,13 @@ module Rigor
1676
1479
  end
1677
1480
  end
1678
1481
 
1679
- # Every discovered project class/module whose OWN def table redefines
1680
- # `(method_name, kind)`. Instance kind reads `discovered_def_nodes`,
1681
- # singleton kind reads `discovered_singleton_def_nodes` both are
1682
- # genuine project `def` bodies (not RBS / accessor synthesis), so a
1683
- # name's presence is a real redefinition. Served from a per-generation
1684
- # inverted index (`method_name [owner names]`) built once per def
1685
- # table, so the lookup is a single hash read rather than a full-table
1686
- # scan on every `(method_name, kind)` first-miss — the gate runs on
1687
- # every adopted self-call return, so the full-table `filter_map` it
1688
- # replaced was the dominant added allocation on a large `lib`.
1482
+ # Every discovered project class/module whose OWN def table redefines `(method_name, kind)`. Instance
1483
+ # kind reads `discovered_def_nodes`, singleton kind reads `discovered_singleton_def_nodes` — both are
1484
+ # genuine project `def` bodies (not RBS / accessor synthesis), so a name's presence is a real
1485
+ # redefinition. Served from a per-generation inverted index (`method_name [owner names]`) built once
1486
+ # per def table, so the lookup is a single hash read rather than a full-table scan on every
1487
+ # `(method_name, kind)` first-miss the gate runs on every adopted self-call return, so the full-table
1488
+ # `filter_map` it replaced was the dominant added allocation on a large `lib`.
1689
1489
  def redefiners_of(method_name, kind)
1690
1490
  method_definers_index(kind)[method_name] || EMPTY_REDEFINERS
1691
1491
  end
@@ -1696,11 +1496,10 @@ module Rigor
1696
1496
  METHOD_DEFINERS_INDEX_KEY = :__rigor_method_definers_index__
1697
1497
  private_constant :METHOD_DEFINERS_INDEX_KEY
1698
1498
 
1699
- # Per-generation `method_name (Symbol) → [owner names]` inverted index
1700
- # over the instance / singleton def tables, memoised by the identity
1701
- # of the def table it inverts (a new analysis generation lands in a
1702
- # fresh bucket). The toplevel sentinel is excluded — a toplevel `def`
1703
- # has no class ancestry and so can never be an override.
1499
+ # Per-generation `method_name (Symbol) → [owner names]` inverted index over the instance / singleton
1500
+ # def tables, memoised by the identity of the def table it inverts (a new analysis generation lands in
1501
+ # a fresh bucket). The toplevel sentinel is excluded — a toplevel `def` has no class ancestry and so
1502
+ # can never be an override.
1704
1503
  def method_definers_index(kind)
1705
1504
  table = kind == :singleton ? scope.discovered_singleton_def_nodes : scope.discovered_def_nodes
1706
1505
  store = (Thread.current[METHOD_DEFINERS_INDEX_KEY] ||= {}.compare_by_identity)
@@ -1717,12 +1516,10 @@ module Rigor
1717
1516
  index
1718
1517
  end
1719
1518
 
1720
- # True when `candidate`'s transitive ancestor chain (superclasses +
1721
- # included/prepended modules) reaches `owner` — i.e. `candidate` is a
1722
- # subclass of an owner class or an includer of an owner module. Reuses
1723
- # the same BFS resolver the method-resolution ancestor walk uses, so
1724
- # name resolution (lexical nesting, RBS-known-ancestor pruning) is
1725
- # identical.
1519
+ # True when `candidate`'s transitive ancestor chain (superclasses + included/prepended modules) reaches
1520
+ # `owner` — i.e. `candidate` is a subclass of an owner class or an includer of an owner module. Reuses
1521
+ # the same BFS resolver the method-resolution ancestor walk uses, so name resolution (lexical nesting,
1522
+ # RBS-known-ancestor pruning) is identical.
1726
1523
  def related_to_owner?(candidate, owner)
1727
1524
  queue = []
1728
1525
  enqueue_ancestors(candidate, queue)
@@ -1749,109 +1546,83 @@ module Rigor
1749
1546
  INFERENCE_UNROLL_FUEL_KEY = :__rigor_user_method_unroll_fuel__
1750
1547
  private_constant :INFERENCE_UNROLL_FUEL_KEY
1751
1548
 
1752
- # ADR-55 slice 2 — thread-local fixpoint return-summary table,
1753
- # keyed by the plain `(receiver, method)` signature (NOT the
1754
- # value-extended signature: extended frames from slice 1 share the
1755
- # same summary). Each entry is `{ assumption:, consulted: }` where
1756
- # `assumption` is the current Kleene iterate (seeded `bot`) and
1757
- # `consulted` flips true when an in-cycle re-entry returns it.
1549
+ # ADR-55 slice 2 — thread-local fixpoint return-summary table, keyed by the plain `(receiver, method)`
1550
+ # signature (NOT the value-extended signature: extended frames from slice 1 share the same summary).
1551
+ # Each entry is `{ assumption:, consulted: }` where `assumption` is the current Kleene iterate (seeded
1552
+ # `bot`) and `consulted` flips true when an in-cycle re-entry returns it.
1758
1553
  INFERENCE_SUMMARY_KEY = :__rigor_user_method_return_summary__
1759
1554
  private_constant :INFERENCE_SUMMARY_KEY
1760
1555
 
1761
- # Monotonic per-thread counter, bumped once each time `consult_summary`
1762
- # actually reads an in-flight fixpoint assumption (ADR-55 slice 2). A
1763
- # method return computed across an interval in which this counter does
1764
- # NOT move depended on no transient Kleene iterate, so it is FINAL and
1765
- # safe to memoise even when the `summaries` table is non-empty because
1766
- # some unrelated outermost frame merely *seeded* (but never consulted)
1767
- # its own entry. See `infer_user_method_return`'s post-hoc memo gate.
1556
+ # Monotonic per-thread counter, bumped once each time `consult_summary` actually reads an in-flight
1557
+ # fixpoint assumption (ADR-55 slice 2). A method return computed across an interval in which this
1558
+ # counter does NOT move depended on no transient Kleene iterate, so it is FINAL and safe to memoise —
1559
+ # even when the `summaries` table is non-empty because some unrelated outermost frame merely *seeded*
1560
+ # (but never consulted) its own entry. See `infer_user_method_return`'s post-hoc memo gate.
1768
1561
  SUMMARY_CONSULT_COUNTER_KEY = :__rigor_user_method_summary_consults__
1769
1562
  private_constant :SUMMARY_CONSULT_COUNTER_KEY
1770
1563
 
1771
- # Per-thread append-only log of the seed depths of every in-flight
1772
- # summary `consult_summary` read (ADR-55 slice 2 mutual-recursion
1773
- # soundness fix, 2026-06-12). Each fixpoint owner records the guard
1774
- # stack size at seed time on its entry (`depth:`); a consult appends
1775
- # the consulted entry's depth here. A fixpoint whose body evaluation
1776
- # logged a depth SHALLOWER than its own seed depth read an ancestor
1777
- # signature's transient Kleene iterate -- cross-signature mutual
1778
- # recursion (`even?`/`odd?`) -- so its computed return is entangled
1779
- # with a not-yet-converged foreign assumption and must degrade to
1780
- # `untyped` rather than fold one branch's seed into a "final"
1781
- # constant. Own-signature consults log depth == own depth, and a
1782
- # nested fixpoint that completes within the evaluation logs depths
1783
- # > own depth; neither is foreign. Cleared with the summary table
1784
- # when the guard stack drains to empty.
1564
+ # Per-thread append-only log of the seed depths of every in-flight summary `consult_summary` read
1565
+ # (ADR-55 slice 2 mutual-recursion soundness fix, 2026-06-12). Each fixpoint owner records the guard
1566
+ # stack size at seed time on its entry (`depth:`); a consult appends the consulted entry's depth here.
1567
+ # A fixpoint whose body evaluation logged a depth SHALLOWER than its own seed depth read an ancestor
1568
+ # signature's transient Kleene iterate -- cross-signature mutual recursion (`even?`/`odd?`) -- so its
1569
+ # computed return is entangled with a not-yet-converged foreign assumption and must degrade to
1570
+ # `untyped` rather than fold one branch's seed into a "final" constant. Own-signature consults log
1571
+ # depth == own depth, and a nested fixpoint that completes within the evaluation logs depths > own
1572
+ # depth; neither is foreign. Cleared with the summary table when the guard stack drains to empty.
1785
1573
  SUMMARY_CONSULT_DEPTHS_KEY = :__rigor_user_method_summary_consult_depths__
1786
1574
  private_constant :SUMMARY_CONSULT_DEPTHS_KEY
1787
1575
 
1788
- # ADR-57 follow-up — run-scoped memo for resolved user-method
1789
- # return types. The ADR-57 gate-open made every resolved in-body
1790
- # self-call adopt the callee's inferred return, which re-types the
1791
- # callee body once per call site. With a project-wide discovery
1792
- # index, file N re-types callees defined in files 1..N-1, so
1793
- # whole-`lib` cost grows superlinearly in files-per-process (the
1794
- # 2026-06-12 Rails survey's whole-`lib` scaling wall).
1576
+ # ADR-57 follow-up — run-scoped memo for resolved user-method return types. The ADR-57 gate-open made
1577
+ # every resolved in-body self-call adopt the callee's inferred return, which re-types the callee body
1578
+ # once per call site. With a project-wide discovery index, file N re-types callees defined in files
1579
+ # 1..N-1, so whole-`lib` cost grows superlinearly in files-per-process (the 2026-06-12 Rails survey's
1580
+ # whole-`lib` scaling wall).
1795
1581
  #
1796
- # `infer_user_method_return` is a pure function of
1797
- # `(def_node, receiver, arg_types)` PLUS the frozen project
1798
- # discovery index: `build_user_method_body_scope` binds the args
1799
- # to the params in a FRESH `Scope` seeded from an empty fact /
1800
- # narrowing store and inherits `scope.discovery` whole by
1801
- # reference the caller's narrowing state never enters. (This is
1802
- # what makes a signature-keyed return memo sound where the
1803
- # ADR-52 WD5 per-call-NODE contribution cache was not: that cache
1804
- # keyed scope-sensitive results on the node; this memo keys a
1805
- # scope-INSENSITIVE result on its real inputs.)
1582
+ # `infer_user_method_return` is a pure function of `(def_node, receiver, arg_types)` PLUS the frozen
1583
+ # project discovery index: `build_user_method_body_scope` binds the args to the params in a FRESH
1584
+ # `Scope` seeded from an empty fact / narrowing store and inherits `scope.discovery` whole by
1585
+ # reference the caller's narrowing state never enters. (This is what makes a signature-keyed return
1586
+ # memo sound where the ADR-52 WD5 per-call-NODE contribution cache was not: that cache keyed
1587
+ # scope-sensitive results on the node; this memo keys a scope-INSENSITIVE result on its real inputs.)
1806
1588
  #
1807
- # Two dimensions are call-site-varying and so live IN the key:
1808
- # the receiver carrier (`describe(:short)`) and the argument-type
1809
- # signature (`describe(:short)` of each arg) value-pinned args
1810
- # change folds (`factorial(5)` vs `factorial(6)`), so a coarser
1811
- # key would serve a stale fold. The third unsafe dimension
1812
- # the ADR-55 recursion machinery (unroll fuel / fixpoint Kleene
1813
- # assumption / WD1 clamp) producing a TRANSIENT result rather
1814
- # than a final return is excluded structurally: the memo is
1815
- # consulted and populated ONLY when the incoming guard stack is
1816
- # empty (a genuine top-of-stack entry, whose result is final and
1817
- # cannot be an in-progress assumption or a clamped value). Frames
1818
- # entered with a non-empty stack bypass the memo entirely and
1819
- # compute as before.
1589
+ # Two dimensions are call-site-varying and so live IN the key: the receiver carrier
1590
+ # (`describe(:short)`) and the argument-type signature (`describe(:short)` of each arg) — value-pinned
1591
+ # args change folds (`factorial(5)` vs `factorial(6)`), so a coarser key would serve a stale fold. The
1592
+ # third unsafe dimension the ADR-55 recursion machinery (unroll fuel / fixpoint Kleene
1593
+ # assumption / WD1 clamp) producing a TRANSIENT result rather than a final return is excluded
1594
+ # structurally: the memo is consulted and populated ONLY when the incoming guard stack is empty (a
1595
+ # genuine top-of-stack entry, whose result is final and cannot be an in-progress assumption or a
1596
+ # clamped value). Frames entered with a non-empty stack bypass the memo entirely and compute as before.
1820
1597
  #
1821
- # Keyed by the identity of the frozen discovery `def_nodes`
1822
- # table (a new analysis generation lands in a fresh bucket,
1823
- # mirroring `class_graph_buckets`) then by the identity of the
1824
- # `def_node` and the `[receiver, *args]` descriptor tuple.
1825
- # `ExpressionTyper` is rebuilt per `Scope#type_of`, so the store
1826
- # lives on `Thread.current`; fork-pool workers are separate
1827
- # processes, so it never crosses a project boundary.
1598
+ # Keyed by the identity of the frozen discovery `def_nodes` table (a new analysis generation lands in
1599
+ # a fresh bucket, mirroring `class_graph_buckets`) then by the identity of the `def_node` and the
1600
+ # `[receiver, *args]` descriptor tuple. `ExpressionTyper` is rebuilt per `Scope#type_of`, so the store
1601
+ # lives on `Thread.current`; fork-pool workers are separate processes, so it never crosses a project
1602
+ # boundary.
1828
1603
  RETURN_MEMO_KEY = :__rigor_user_method_return_memo__
1829
1604
  private_constant :RETURN_MEMO_KEY
1830
1605
 
1831
- # Per-inference recursion context threaded through the guard /
1832
- # fixpoint helpers (ADR-55 slice 2). Bundles the call descriptor
1833
- # (`receiver`, `arg_types`, `plain_signature`), the thread-local
1834
- # summary table, and the WD1 clamp flag so the helpers stay within the
1835
- # parameter-list budget. `def_node` is carried separately (it is the
1836
- # body owner, not call context).
1606
+ # Per-inference recursion context threaded through the guard / fixpoint helpers (ADR-55 slice 2).
1607
+ # Bundles the call descriptor (`receiver`, `arg_types`, `plain_signature`), the thread-local summary
1608
+ # table, and the WD1 clamp flag so the helpers stay within the parameter-list budget. `def_node` is
1609
+ # carried separately (it is the body owner, not call context).
1837
1610
  RecursionContext = Data.define(
1838
1611
  :receiver, :arg_types, :plain_signature, :summaries, :would_have_been_guarded
1839
1612
  )
1840
1613
  private_constant :RecursionContext
1841
1614
 
1842
- # Total body evaluations the fixpoint iteration is permitted per
1843
- # outermost entry for a signature (ADR-55 WD2). Hard, non-configurable
1844
- # — the iteration cap is part of the termination story (ADR-41 WD4).
1615
+ # Total body evaluations the fixpoint iteration is permitted per outermost entry for a signature
1616
+ # (ADR-55 WD2). Hard, non-configurable — the iteration cap is part of the termination story (ADR-41
1617
+ # WD4).
1845
1618
  RECURSION_FIXPOINT_CAP = 3
1846
1619
  private_constant :RECURSION_FIXPOINT_CAP
1847
1620
 
1848
- # Hard, non-configurable caps for the ADR-55 slice 1 constant-arg
1849
- # unroll. `RECURSION_UNROLL_FUEL` bounds the number of extended
1850
- # (value-keyed) frames per outermost inference entry;
1851
- # `RECURSION_VALUE_SIZE_CAP` disqualifies a frame whose pinned
1852
- # argument values are structurally large. Both are termination
1853
- # guards (ADR-41 WD4) — not measurement-gated precision budgets —
1854
- # so they ship default-on with no opt-in.
1621
+ # Hard, non-configurable caps for the ADR-55 slice 1 constant-arg unroll. `RECURSION_UNROLL_FUEL`
1622
+ # bounds the number of extended (value-keyed) frames per outermost inference entry;
1623
+ # `RECURSION_VALUE_SIZE_CAP` disqualifies a frame whose pinned argument values are structurally large.
1624
+ # Both are termination guards (ADR-41 WD4) — not measurement-gated precision budgets — so they ship
1625
+ # default-on with no opt-in.
1855
1626
  RECURSION_UNROLL_FUEL = 32
1856
1627
  private_constant :RECURSION_UNROLL_FUEL
1857
1628
 
@@ -1864,44 +1635,31 @@ module Rigor
1864
1635
  body_scope = build_user_method_body_scope(def_node, receiver, arg_types)
1865
1636
  return nil if body_scope.nil?
1866
1637
 
1867
- # Recursion-guard signature. Keyed on `(receiver,
1868
- # method)` only NOT the argument types. ADR-24 WD5:
1869
- # a method whose summary is still being computed
1870
- # resolves to `Dynamic[top]` for that cycle. Keying on
1871
- # arg types would let mutual recursion through a
1872
- # `module_function` module (`Acceptance#accepts`
1873
- # `accepts_one` `accepts_dynamic` `accepts`)
1874
- # recurse unboundedly whenever the carried argument
1875
- # types differ at each level — observed as a
1876
- # `SystemStackError` once implicit-self calls began
1877
- # resolving during the main walk. `describe(:short)`
1878
- # keeps non-Nominal receivers (the implicit `Object`
1879
- # carrier for top-level / DSL-block defs) printable.
1638
+ # Recursion-guard signature. Keyed on `(receiver, method)` only — NOT the argument types. ADR-24 WD5:
1639
+ # a method whose summary is still being computed resolves to `Dynamic[top]` for that cycle. Keying on
1640
+ # arg types would let mutual recursion through a `module_function` module (`Acceptance#accepts` →
1641
+ # `accepts_one` `accepts_dynamic` `accepts`) recurse unboundedly whenever the carried argument
1642
+ # types differ at each level observed as a `SystemStackError` once implicit-self calls began
1643
+ # resolving during the main walk. `describe(:short)` keeps non-Nominal receivers (the implicit
1644
+ # `Object` carrier for top-level / DSL-block defs) printable.
1880
1645
  plain_signature = [receiver.describe(:short), def_node.name]
1881
1646
  stack = (Thread.current[INFERENCE_GUARD_KEY] ||= [])
1882
1647
  summaries = (Thread.current[INFERENCE_SUMMARY_KEY] ||= {})
1883
1648
 
1884
- # ADR-57 follow-up — return memo. The inferred return is a pure
1885
- # function of `(def_node, receiver, arg_types)` and the frozen
1886
- # discovery index whenever the computation does NOT depend on a
1887
- # transient ADR-55 Kleene assumption (an in-flight fixpoint summary).
1888
- # Two structural preconditions decide whether THIS frame's result is
1889
- # even a memo candidate, both stable across the body walk: the
1890
- # signature must not already be on the recursion guard stack (else we
1891
- # are inside its own cycle) and no constant-arg unroll may be in
1892
- # flight (its value-keyed frames are transient). When both hold we
1893
- # consult the memo, and on a miss we compute, then store the result
1894
- # only if no fixpoint summary was *consulted* during the computation
1895
- # (the post-hoc consult-counter check) which is sound regardless of
1896
- # whether the `summaries` table holds inert *seeded-but-unconsulted*
1897
- # entries left by unrelated outermost frames. This is the fix for the
1898
- # whole-`lib` scaling wall: a deep DAG of non-recursive private
1899
- # readers (ActiveStorage `video_analyzer.rb`) seeded a summary on its
1900
- # first outermost method and thereafter the old `summaries.empty?`
1901
- # gate disabled the memo for every nested call, re-walking the shared
1902
- # sub-readers combinatorially (~932k body evaluations for ~20 tiny
1903
- # methods). The computation itself lives in
1904
- # `compute_user_method_return`.
1649
+ # ADR-57 follow-up — return memo. The inferred return is a pure function of `(def_node, receiver,
1650
+ # arg_types)` and the frozen discovery index whenever the computation does NOT depend on a transient
1651
+ # ADR-55 Kleene assumption (an in-flight fixpoint summary). Two structural preconditions decide
1652
+ # whether THIS frame's result is even a memo candidate, both stable across the body walk: the
1653
+ # signature must not already be on the recursion guard stack (else we are inside its own cycle) and
1654
+ # no constant-arg unroll may be in flight (its value-keyed frames are transient). When both hold we
1655
+ # consult the memo, and on a miss we compute, then store the result only if no fixpoint summary was
1656
+ # *consulted* during the computation (the post-hoc consult-counter check) which is sound
1657
+ # regardless of whether the `summaries` table holds inert *seeded-but-unconsulted* entries left by
1658
+ # unrelated outermost frames. This is the fix for the whole-`lib` scaling wall: a deep DAG of
1659
+ # non-recursive private readers (ActiveStorage `video_analyzer.rb`) seeded a summary on its first
1660
+ # outermost method and thereafter the old `summaries.empty?` gate disabled the memo for every nested
1661
+ # call, re-walking the shared sub-readers combinatorially (~932k body evaluations for ~20 tiny
1662
+ # methods). The computation itself lives in `compute_user_method_return`.
1905
1663
  unless memo_candidate?(stack, plain_signature)
1906
1664
  return compute_user_method_return(def_node, body_scope, stack, summaries,
1907
1665
  receiver, arg_types, plain_signature)
@@ -1917,28 +1675,23 @@ module Rigor
1917
1675
  receiver, arg_types, plain_signature)
1918
1676
  consults_after = Thread.current[SUMMARY_CONSULT_COUNTER_KEY] || 0
1919
1677
 
1920
- # Store only a FINAL result. If a fixpoint summary was consulted
1921
- # during the computation, `result` embeds a transient Kleene iterate
1922
- # whose value depends on the iteration in flight, so it must not be
1923
- # shared across call sites.
1678
+ # Store only a FINAL result. If a fixpoint summary was consulted during the computation, `result`
1679
+ # embeds a transient Kleene iterate whose value depends on the iteration in flight, so it must not
1680
+ # be shared across call sites.
1924
1681
  memo[memo_key] = result if consults_after == consults_before
1925
1682
  result
1926
1683
  end
1927
1684
 
1928
- # The ADR-55 recursion-guard + value-unroll + fixpoint body of
1929
- # user-method return inference, factored out so
1930
- # `infer_user_method_return` is a thin memo wrapper (the memo is
1931
- # the ADR-57 follow-up; this is unchanged from pre-memo behaviour).
1685
+ # The ADR-55 recursion-guard + value-unroll + fixpoint body of user-method return inference, factored
1686
+ # out so `infer_user_method_return` is a thin memo wrapper (the memo is the ADR-57 follow-up; this is
1687
+ # unchanged from pre-memo behaviour).
1932
1688
  def compute_user_method_return(def_node, body_scope, stack, summaries,
1933
1689
  receiver, arg_types, plain_signature)
1934
- # ADR-55 slice 1: when every bound argument is value-pinned,
1935
- # extend the guard key with a stable descriptor of the argument
1936
- # *values* so distinct constant frames may recurse (e.g.
1937
- # `factorial(5)` folds to `Constant[120]`). Distinct constant
1938
- # frames are bounded by `RECURSION_UNROLL_FUEL` per outermost
1939
- # entry; exhaustion or value blow-up falls back to the plain
1940
- # `(receiver, method)` guard — today's behaviour. Non-constant
1941
- # args never reach this path.
1690
+ # ADR-55 slice 1: when every bound argument is value-pinned, extend the guard key with a stable
1691
+ # descriptor of the argument *values* so distinct constant frames may recurse (e.g. `factorial(5)`
1692
+ # folds to `Constant[120]`). Distinct constant frames are bounded by `RECURSION_UNROLL_FUEL` per
1693
+ # outermost entry; exhaustion or value blow-up falls back to the plain `(receiver, method)` guard
1694
+ # today's behaviour. Non-constant args never reach this path.
1942
1695
  signature = plain_signature
1943
1696
  value_key = constant_argument_value_key(arg_types)
1944
1697
  extended = value_key && unroll_fuel_remaining(stack).positive?
@@ -1946,25 +1699,20 @@ module Rigor
1946
1699
 
1947
1700
  if stack.include?(signature)
1948
1701
  BudgetTrace.hit(BudgetTrace::RECURSION_GUARD)
1949
- # ADR-55 slice 2: in-cycle re-entries return the current assumed
1950
- # summary (Kleene iterate, seeded `bot`) instead of bare
1951
- # `untyped`. The fixpoint loop below seeds the entry on the
1952
- # outermost frame; if a re-entry beats it here the entry already
1953
- # exists. The WD4 composition: slice 1's clamp/fuel fallbacks
1954
- # also route here when a summary is active.
1702
+ # ADR-55 slice 2: in-cycle re-entries return the current assumed summary (Kleene iterate, seeded
1703
+ # `bot`) instead of bare `untyped`. The fixpoint loop below seeds the entry on the outermost frame;
1704
+ # if a re-entry beats it here the entry already exists. The WD4 composition: slice 1's clamp/fuel
1705
+ # fallbacks also route here when a summary is active.
1955
1706
  return consult_summary(summaries, plain_signature)
1956
1707
  end
1957
1708
 
1958
- # ADR-55 WD1 clamp (governing rule): the constant-arg unroll may
1959
- # only ever surface a fully value-pinned result; any other outcome
1960
- # must be byte-identical to the plain guard's `untyped`. A frame
1961
- # that took the extended (value-keyed) path but whose plain
1962
- # `(receiver, method)` signature is already on the stack in
1963
- # plain form or as the plain part of an extended frame would
1964
- # have been guarded before slice 1. If such a frame's body folds
1965
- # to a non-pinned type, the unroll surfaced a precise value the
1966
- # plain guard would have masked (and the body evaluator's blind
1967
- # spots can make that value wrong), so clamp it back to `untyped`.
1709
+ # ADR-55 WD1 clamp (governing rule): the constant-arg unroll may only ever surface a fully
1710
+ # value-pinned result; any other outcome must be byte-identical to the plain guard's `untyped`. A
1711
+ # frame that took the extended (value-keyed) path but whose plain `(receiver, method)` signature is
1712
+ # already on the stack — in plain form or as the plain part of an extended frame would have been
1713
+ # guarded before slice 1. If such a frame's body folds to a non-pinned type, the unroll surfaced a
1714
+ # precise value the plain guard would have masked (and the body evaluator's blind spots can make
1715
+ # that value wrong), so clamp it back to `untyped`.
1968
1716
  would_have_been_guarded =
1969
1717
  extended &&
1970
1718
  stack.any? { |frame| plain_part(frame) == plain_signature }
@@ -1976,46 +1724,39 @@ module Rigor
1976
1724
  evaluate_guarded_user_method_body(def_node, body_scope, stack, signature, context)
1977
1725
  end
1978
1726
 
1979
- # True when this frame's result is a candidate for the return memo:
1980
- # the structural preconditions, both stable across the body walk, that
1981
- # are necessary (but not sufficient) for a FINAL result. Sufficiency is
1982
- # decided post-hoc in `infer_user_method_return` by the consult-counter
1983
- # check (no transient fixpoint summary was read during the compute)
1984
- # so unlike the prior `memoisable_return?` this deliberately does NOT
1985
- # require an empty `summaries` table: inert seeded-but-unconsulted
1986
- # entries left by unrelated outermost frames do not contaminate a
1987
- # result, and gating on them disabled the memo for an entire non-
1988
- # recursive DAG (the scaling wall). The two preconditions: no constant-
1989
- # arg unroll in flight (its value-keyed frames are transient) and this
1990
- # plain signature not itself on the recursion guard stack (else we are
1991
- # inside its own cycle, returning a Kleene iterate).
1727
+ # True when this frame's result is a candidate for the return memo: the structural preconditions, both
1728
+ # stable across the body walk, that are necessary (but not sufficient) for a FINAL result. Sufficiency
1729
+ # is decided post-hoc in `infer_user_method_return` by the consult-counter check (no transient
1730
+ # fixpoint summary was read during the compute) — so unlike the prior `memoisable_return?` this
1731
+ # deliberately does NOT require an empty `summaries` table: inert seeded-but-unconsulted entries left
1732
+ # by unrelated outermost frames do not contaminate a result, and gating on them disabled the memo for
1733
+ # an entire non-recursive DAG (the scaling wall). The two preconditions: no constant-arg unroll in
1734
+ # flight (its value-keyed frames are transient) and this plain signature not itself on the recursion
1735
+ # guard stack (else we are inside its own cycle, returning a Kleene iterate).
1992
1736
  def memo_candidate?(stack, plain_signature)
1993
- # Read the unroll fuel WITHOUT the decrement side effect of
1994
- # `unroll_fuel_remaining`: a constant-arg unroll has begun iff the
1995
- # thread-local is set and the stack is non-empty (the `ensure` in
1737
+ # Read the unroll fuel WITHOUT the decrement side effect of `unroll_fuel_remaining`: a constant-arg
1738
+ # unroll has begun iff the thread-local is set and the stack is non-empty (the `ensure` in
1996
1739
  # `evaluate_guarded_user_method_body` clears it at stack-empty).
1997
1740
  unroll_idle = stack.empty? || Thread.current[INFERENCE_UNROLL_FUEL_KEY].nil?
1998
1741
  unroll_idle &&
1999
1742
  stack.none? { |frame| plain_part(frame) == plain_signature }
2000
1743
  end
2001
1744
 
2002
- # Run-scoped return-memo bucket for the current discovery
2003
- # generation. Keyed by the identity of the frozen `def_nodes`
2004
- # table so a new analysis generation (or any scope that swaps the
2005
- # index) transparently lands in a fresh bucket. See RETURN_MEMO_KEY.
1745
+ # Run-scoped return-memo bucket for the current discovery generation. Keyed by the identity of the
1746
+ # frozen `def_nodes` table so a new analysis generation (or any scope that swaps the index)
1747
+ # transparently lands in a fresh bucket. See RETURN_MEMO_KEY.
2006
1748
  def return_memo_bucket
2007
1749
  store = (Thread.current[RETURN_MEMO_KEY] ||= {}.compare_by_identity)
2008
1750
  store[scope.discovered_def_nodes] ||= {}
2009
1751
  end
2010
1752
 
2011
- # Pushes the recursion-guard frame, evaluates the body (the outermost
2012
- # frame for a plain signature runs the ADR-55 slice 2 fixpoint; nested
2013
- # extended frames evaluate once and let the owner iterate), and on the
2014
- # way out pops the frame and resets the per-outermost-entry fuel and
2015
- # summary tables when the guard stack drains to empty.
1753
+ # Pushes the recursion-guard frame, evaluates the body (the outermost frame for a plain signature runs
1754
+ # the ADR-55 slice 2 fixpoint; nested extended frames evaluate once and let the owner iterate), and on
1755
+ # the way out pops the frame and resets the per-outermost-entry fuel and summary tables when the guard
1756
+ # stack drains to empty.
2016
1757
  def evaluate_guarded_user_method_body(def_node, body_scope, stack, signature, context)
2017
- # The outermost frame for this plain signature owns the summary
2018
- # entry and runs the fixpoint loop. ADR-55 WD2.
1758
+ # The outermost frame for this plain signature owns the summary entry and runs the fixpoint loop.
1759
+ # ADR-55 WD2.
2019
1760
  outermost = stack.none? { |frame| plain_part(frame) == context.plain_signature }
2020
1761
  stack.push(signature)
2021
1762
  begin
@@ -2035,19 +1776,16 @@ module Rigor
2035
1776
  end
2036
1777
  end
2037
1778
 
2038
- # Evaluates a method body and joins the value types of every explicit
2039
- # `return value` reached during the walk with the body's tail type.
1779
+ # Evaluates a method body and joins the value types of every explicit `return value` reached during
1780
+ # the walk with the body's tail type.
2040
1781
  #
2041
- # The tail-only evaluator (`statements_type_for` → `type_of(body.last)`)
2042
- # models only the fall-through value; an early `return false` or a
2043
- # block-internal `return x` produces `Bot` at its own position and is
2044
- # otherwise invisible to method-return inference. Without this join a
2045
- # predicate helper shaped `return false unless cond; ...; true` infers
2046
- # `Constant[true]` (the early `return false` dropped), which folds
2047
- # `if helper` to always-truthy. `StatementEvaluator.with_return_sink`
2048
- # collects the returns (nested `def`/lambda are barriers; block-internal
2049
- # returns correctly bubble to the enclosing method) so the inferred
2050
- # return is `tail | return_1 | … | return_n`, matching Ruby semantics.
1782
+ # The tail-only evaluator (`statements_type_for` → `type_of(body.last)`) models only the fall-through
1783
+ # value; an early `return false` or a block-internal `return x` produces `Bot` at its own position and
1784
+ # is otherwise invisible to method-return inference. Without this join a predicate helper shaped
1785
+ # `return false unless cond; ...; true` infers `Constant[true]` (the early `return false` dropped),
1786
+ # which folds `if helper` to always-truthy. `StatementEvaluator.with_return_sink` collects the returns
1787
+ # (nested `def`/lambda are barriers; block-internal returns correctly bubble to the enclosing method)
1788
+ # so the inferred return is `tail | return_1 | … | return_n`, matching Ruby semantics.
2051
1789
  def evaluate_body_with_returns(body_scope, body)
2052
1790
  (type, post_scope), returns = StatementEvaluator.with_return_sink do
2053
1791
  body_scope.evaluate(body)
@@ -2056,16 +1794,13 @@ module Rigor
2056
1794
  [joined, post_scope]
2057
1795
  end
2058
1796
 
2059
- # ADR-55 slice 2 — Kleene fixpoint over a recursive method's return
2060
- # summary. Seeds the assumption to `bot`, evaluates the body, and (only
2061
- # if the summary was actually consulted during evaluation i.e. the
2062
- # method really recursed) iterates: if the computed return is subsumed
2063
- # by the assumption the fixpoint is reached; otherwise the assumption
2064
- # joins in the computed return and the body re-evaluates. Capped at
2065
- # `RECURSION_FIXPOINT_CAP` total evaluations; the final permitted
2066
- # iteration widens value-pinned constituents to their nominal base to
2067
- # force convergence, and any residual instability collapses to
2068
- # `untyped` (today's behaviour).
1797
+ # ADR-55 slice 2 — Kleene fixpoint over a recursive method's return summary. Seeds the assumption to
1798
+ # `bot`, evaluates the body, and (only if the summary was actually consulted during evaluation — i.e.
1799
+ # the method really recursed) iterates: if the computed return is subsumed by the assumption the
1800
+ # fixpoint is reached; otherwise the assumption joins in the computed return and the body re-evaluates.
1801
+ # Capped at `RECURSION_FIXPOINT_CAP` total evaluations; the final permitted iteration widens
1802
+ # value-pinned constituents to their nominal base to force convergence, and any residual instability
1803
+ # collapses to `untyped` (today's behaviour).
2069
1804
  def fixpoint_user_method_return(def_node, body_scope, context, widened: false)
2070
1805
  plain_signature = context.plain_signature
2071
1806
  summaries = context.summaries
@@ -2079,34 +1814,27 @@ module Rigor
2079
1814
  type, = evaluate_body_with_returns(body_scope, def_node.body)
2080
1815
  computed = clamp_unroll_result(type, context.would_have_been_guarded)
2081
1816
 
2082
- # Cross-signature mutual recursion (ADR-55 soundness fix,
2083
- # 2026-06-12): the evaluation consulted an ANCESTOR signature's
2084
- # in-flight summary (seed depth shallower than this frame's), so
2085
- # `computed` embeds a transient foreign Kleene iterate -- e.g.
2086
- # `odd?` folding `even?`'s seeded `bot` into `Constant[false]`.
2087
- # The per-signature iteration below cannot converge such an
2088
- # entangled pair (each side's iterate is conditioned on the
2089
- # other's unfinished assumption), so degrade this frame to the
1817
+ # Cross-signature mutual recursion (ADR-55 soundness fix, 2026-06-12): the evaluation consulted an
1818
+ # ANCESTOR signature's in-flight summary (seed depth shallower than this frame's), so `computed`
1819
+ # embeds a transient foreign Kleene iterate -- e.g. `odd?` folding `even?`'s seeded `bot` into
1820
+ # `Constant[false]`. The per-signature iteration below cannot converge such an entangled pair (each
1821
+ # side's iterate is conditioned on the other's unfinished assumption), so degrade this frame to the
2090
1822
  # sound `untyped` floor instead of surfacing a one-sided value.
2091
1823
  if consult_depths[consult_mark..].any? { |d| d < depth }
2092
1824
  return degrade_entangled_fixpoint(summaries, plain_signature)
2093
1825
  end
2094
1826
 
2095
- # The summary was never consulted — the method did not recurse on
2096
- # this evaluation, so there is no fixpoint to chase. Return the
2097
- # computed type directly (pre-fixpoint behaviour for non-recursive
1827
+ # The summary was never consulted — the method did not recurse on this evaluation, so there is no
1828
+ # fixpoint to chase. Return the computed type directly (pre-fixpoint behaviour for non-recursive
2098
1829
  # bodies that merely share `infer_user_method_return`).
2099
1830
  return computed unless summaries.dig(plain_signature, :consulted)
2100
1831
 
2101
- # ADR-55 slice 2 bot-collapse fix (2026-06-11). When the recursive
2102
- # method's only contribution this evaluation was the seeded `bot`
2103
- # assumption (so `computed` is `bot` even though the body recursed),
2104
- # the `joined == assumption` check below would trivially converge at
2105
- # the seed and return `bot` UNSOUND for a method with a reachable
2106
- # non-recursive exit (`passthrough` returns `:done`, `pick` returns
2107
- # `nil`). `bot` means "never returns", which feeds ADR-47
2108
- # reachability / always-falsey diagnostics, so it must be reserved
2109
- # for genuinely diverging methods (`spin`).
1832
+ # ADR-55 slice 2 bot-collapse fix (2026-06-11). When the recursive method's only contribution this
1833
+ # evaluation was the seeded `bot` assumption (so `computed` is `bot` even though the body
1834
+ # recursed), the `joined == assumption` check below would trivially converge at the seed and
1835
+ # return `bot` UNSOUND for a method with a reachable non-recursive exit (`passthrough` returns
1836
+ # `:done`, `pick` returns `nil`). `bot` means "never returns", which feeds ADR-47 reachability /
1837
+ # always-falsey diagnostics, so it must be reserved for genuinely diverging methods (`spin`).
2110
1838
  if computed.is_a?(Type::Bot)
2111
1839
  resolved = resolve_bot_collapse(def_node, context, widened: widened)
2112
1840
  return resolved unless resolved.nil?
@@ -2117,10 +1845,9 @@ module Rigor
2117
1845
  end
2118
1846
  end
2119
1847
 
2120
- # Seeds the thread-local summary entry for a fixpoint owner: the `bot`
2121
- # Kleene seed plus the guard-stack depth at seed time (the frame for
2122
- # this signature is already pushed), which `consult_summary` logs so
2123
- # nested fixpoints can detect a foreign in-flight (ancestor) consult.
1848
+ # Seeds the thread-local summary entry for a fixpoint owner: the `bot` Kleene seed plus the
1849
+ # guard-stack depth at seed time (the frame for this signature is already pushed), which
1850
+ # `consult_summary` logs so nested fixpoints can detect a foreign in-flight (ancestor) consult.
2124
1851
  # Returns the seed depth. ADR-55 slice 2.
2125
1852
  def seed_fixpoint_summary(summaries, plain_signature)
2126
1853
  depth = (Thread.current[INFERENCE_GUARD_KEY] || []).size
@@ -2130,9 +1857,8 @@ module Rigor
2130
1857
  depth
2131
1858
  end
2132
1859
 
2133
- # Degrades an entangled mutual-recursion fixpoint to the sound
2134
- # `untyped` floor (ADR-55 mutual-recursion soundness fix, 2026-06-12),
2135
- # parking `untyped` in the assumption so any consumer that still reads
1860
+ # Degrades an entangled mutual-recursion fixpoint to the sound `untyped` floor (ADR-55 mutual-recursion
1861
+ # soundness fix, 2026-06-12), parking `untyped` in the assumption so any consumer that still reads
2136
1862
  # this signature's summary sees the floor, not the stale `bot` seed.
2137
1863
  def degrade_entangled_fixpoint(summaries, plain_signature)
2138
1864
  BudgetTrace.hit(BudgetTrace::RECURSION_GUARD)
@@ -2141,12 +1867,10 @@ module Rigor
2141
1867
  Type::Combinator.untyped
2142
1868
  end
2143
1869
 
2144
- # One Kleene-iteration step of the fixpoint loop. Joins `computed` into
2145
- # the running assumption (widening value-pinned constituents on the
2146
- # final permitted iteration to force convergence) and either returns a
2147
- # final type convergence, or the capped `untyped` collapse — or
2148
- # `:continue` to request another body evaluation, having advanced the
2149
- # stored assumption. ADR-55 WD2.
1870
+ # One Kleene-iteration step of the fixpoint loop. Joins `computed` into the running assumption
1871
+ # (widening value-pinned constituents on the final permitted iteration to force convergence) and
1872
+ # either returns a final type convergence, or the capped `untyped` collapse — or `:continue` to
1873
+ # request another body evaluation, having advanced the stored assumption. ADR-55 WD2.
2150
1874
  def fixpoint_step(summaries, plain_signature, computed, iteration)
2151
1875
  assumption = summaries[plain_signature][:assumption]
2152
1876
  last_iteration = iteration == RECURSION_FIXPOINT_CAP - 1
@@ -2170,36 +1894,31 @@ module Rigor
2170
1894
  :continue
2171
1895
  end
2172
1896
 
2173
- # Rebuilds the user-method body scope with every bound positional
2174
- # parameter widened to its nominal base (`1 | 2 | 3` → `Integer`,
2175
- # `Constant[:x]` `Symbol`). Used by the bot-collapse retry in
2176
- # `fixpoint_user_method_return`: call-site argument narrowing can prune a
2177
- # recursive method's base case, and widening restores the declared-type
2178
- # view under which the base case is reachable. Returns `nil` when the
2179
- # parameter shape is not inferable (mirrors `build_user_method_body_scope`).
1897
+ # Rebuilds the user-method body scope with every bound positional parameter widened to its nominal
1898
+ # base (`1 | 2 | 3` → `Integer`, `Constant[:x]` → `Symbol`). Used by the bot-collapse retry in
1899
+ # `fixpoint_user_method_return`: call-site argument narrowing can prune a recursive method's base
1900
+ # case, and widening restores the declared-type view under which the base case is reachable. Returns
1901
+ # `nil` when the parameter shape is not inferable (mirrors `build_user_method_body_scope`).
2180
1902
  def widened_user_method_body_scope(def_node, receiver, arg_types)
2181
1903
  widened_args = arg_types.map { |arg_type| widen_value_pinned(arg_type) }
2182
1904
  build_user_method_body_scope(def_node, receiver, widened_args)
2183
1905
  end
2184
1906
 
2185
- # ADR-55 slice 2 bot-collapse resolution (2026-06-11). Called when a
2186
- # fixpoint iteration computed `bot` for a recursive body. Two escape
2187
- # hatches keep `bot` reserved for genuinely diverging methods:
1907
+ # ADR-55 slice 2 bot-collapse resolution (2026-06-11). Called when a fixpoint iteration computed `bot`
1908
+ # for a recursive body. Two escape hatches keep `bot` reserved for genuinely diverging methods:
2188
1909
  #
2189
- # 1. Re-run the fixpoint ONCE over a parameter-widened body scope
2190
- # (`1 | 2 | 3` `Integer`): call-site argument narrowing can prune
2191
- # a base-case *tail* branch (`n <= 0 ? :done : recurse` with a
2192
- # positive-only `n`), and widening un-prunes it so the base
2193
- # constituent (`:done`) surfaces. `passthrough` recovers here.
1910
+ # 1. Re-run the fixpoint ONCE over a parameter-widened body scope (`1 | 2 | 3` → `Integer`):
1911
+ # call-site argument narrowing can prune a base-case *tail* branch (`n <= 0 ? :done : recurse`
1912
+ # with a positive-only `n`), and widening un-prunes it so the base constituent (`:done`) surfaces.
1913
+ # `passthrough` recovers here.
2194
1914
  #
2195
- # 2. If the (possibly widened) body STILL computes `bot` but contains
2196
- # a reachable explicit `return` — whose value the tail-only body
2197
- # evaluator never folds into the result (`pick`'s `return nil`)
2198
- # fall to the conservative `Dynamic[top]` floor (the pre-slice-2
2199
- # observable) rather than the unsound `bot`.
1915
+ # 2. If the (possibly widened) body STILL computes `bot` but contains a reachable explicit `return`
1916
+ # — whose value the tail-only body evaluator never folds into the result (`pick`'s `return nil`)
1917
+ # fall to the conservative `Dynamic[top]` floor (the pre-slice-2 observable) rather than the
1918
+ # unsound `bot`.
2200
1919
  #
2201
- # Returns the resolved type, or `nil` to let the caller's normal
2202
- # fixpoint convergence proceed (genuine divergence — `spin`).
1920
+ # Returns the resolved type, or `nil` to let the caller's normal fixpoint convergence proceed (genuine
1921
+ # divergence — `spin`).
2203
1922
  def resolve_bot_collapse(def_node, context, widened:)
2204
1923
  unless widened
2205
1924
  widened_scope = widened_user_method_body_scope(def_node, context.receiver, context.arg_types)
@@ -2211,13 +1930,11 @@ module Rigor
2211
1930
  nil
2212
1931
  end
2213
1932
 
2214
- # True when `node` contains a reachable explicit `return` statement —
2215
- # one not nested inside a return barrier (`def` / lambda / block). The
2216
- # tail-only body evaluator in `infer_user_method_return` never folds an
2217
- # early-return value into the method result, so a recursive method whose
2218
- # base case is spelled as `return value` (rather than a tail branch)
2219
- # looks like it only diverges. This detector is the signal that such a
2220
- # method has a non-recursive exit, so its bot-collapse must floor to
1933
+ # True when `node` contains a reachable explicit `return` statement — one not nested inside a return
1934
+ # barrier (`def` / lambda / block). The tail-only body evaluator in `infer_user_method_return` never
1935
+ # folds an early-return value into the method result, so a recursive method whose base case is
1936
+ # spelled as `return value` (rather than a tail branch) looks like it only diverges. This detector is
1937
+ # the signal that such a method has a non-recursive exit, so its bot-collapse must floor to
2221
1938
  # `Dynamic[top]` rather than `bot` (ADR-55 slice 2, 2026-06-11).
2222
1939
  RETURN_BARRIER_NODES = [Prism::DefNode, Prism::LambdaNode, Prism::BlockNode].freeze
2223
1940
  private_constant :RETURN_BARRIER_NODES
@@ -2230,11 +1947,10 @@ module Rigor
2230
1947
  node.compact_child_nodes.any? { |child| body_has_explicit_return?(child) }
2231
1948
  end
2232
1949
 
2233
- # Returns the current assumed summary for `plain_signature`, recording
2234
- # that it was consulted (so the fixpoint owner knows the body actually
2235
- # recursed). Falls back to `untyped` when no summary is active e.g. a
2236
- # nested extended frame guarded before its plain signature seeded an
2237
- # entry, which is the pre-slice-2 observable.
1950
+ # Returns the current assumed summary for `plain_signature`, recording that it was consulted (so the
1951
+ # fixpoint owner knows the body actually recursed). Falls back to `untyped` when no summary is active
1952
+ # — e.g. a nested extended frame guarded before its plain signature seeded an entry, which is the
1953
+ # pre-slice-2 observable.
2238
1954
  def consult_summary(summaries, plain_signature)
2239
1955
  entry = summaries[plain_signature]
2240
1956
  return Type::Combinator.untyped if entry.nil?
@@ -2244,46 +1960,38 @@ module Rigor
2244
1960
  entry[:assumption]
2245
1961
  end
2246
1962
 
2247
- # ADR-55 WD1 governing-rule clamp. When the just-evaluated frame
2248
- # took the extended (value-keyed) path but its plain signature was
2249
- # already guarded (`would_have_been_guarded`), the unroll may only
2250
- # surface a fully value-pinned result; any other outcome must be
2251
- # byte-identical to the plain guard's `untyped` (and counts a
2252
- # `RECURSION_GUARD` hit, matching the pre-slice-1 observable).
1963
+ # ADR-55 WD1 governing-rule clamp. When the just-evaluated frame took the extended (value-keyed) path
1964
+ # but its plain signature was already guarded (`would_have_been_guarded`), the unroll may only surface
1965
+ # a fully value-pinned result; any other outcome must be byte-identical to the plain guard's `untyped`
1966
+ # (and counts a `RECURSION_GUARD` hit, matching the pre-slice-1 observable).
2253
1967
  def clamp_unroll_result(type, would_have_been_guarded)
2254
1968
  return type unless would_have_been_guarded && !fully_value_pinned?(type)
2255
1969
 
2256
1970
  BudgetTrace.hit(BudgetTrace::RECURSION_GUARD)
2257
1971
  scope.record_dynamic_origin(@typing_node, DynamicOrigin::ANALYZER_BUDGET_CUTOFF) if @typing_node
2258
- # ADR-55 WD1 clamp: a guarded extended frame whose body is non-pinned
2259
- # must be byte-identical to the plain guard's `untyped`. This path
2260
- # deliberately does NOT route to the in-progress fixpoint summary:
2261
- # the summary is a Kleene lower bound mid-iteration, while the clamp
2262
- # is a soundness backstop for an untrustworthy unrolled value, so it
2263
- # must stay the conservative `untyped` upper bound. (WD4's
2264
- # summary-composition applies to the in-cycle guard and fuel paths,
2265
- # which DO return the assumed summary — see `consult_summary`.)
1972
+ # ADR-55 WD1 clamp: a guarded extended frame whose body is non-pinned must be byte-identical to the
1973
+ # plain guard's `untyped`. This path deliberately does NOT route to the in-progress fixpoint summary:
1974
+ # the summary is a Kleene lower bound mid-iteration, while the clamp is a soundness backstop for an
1975
+ # untrustworthy unrolled value, so it must stay the conservative `untyped` upper bound. (WD4's
1976
+ # summary-composition applies to the in-cycle guard and fuel paths, which DO return the assumed
1977
+ # summary see `consult_summary`.)
2266
1978
  Type::Combinator.untyped
2267
1979
  end
2268
1980
 
2269
- # Widens every value-pinned constituent of `type` to its nominal base
2270
- # (`Constant[1]` → `Integer`, `Tuple[Constant…]` → its element bases),
2271
- # leaving non-pinned constituents untouched. Used on the fixpoint's
2272
- # final permitted iteration (ADR-55 WD2) to force convergence — the
2273
- # tower of distinct constant iterates collapses to one nominal type.
1981
+ # Widens every value-pinned constituent of `type` to its nominal base (`Constant[1]` → `Integer`,
1982
+ # `Tuple[Constant…]` → its element bases), leaving non-pinned constituents untouched. Used on the
1983
+ # fixpoint's final permitted iteration (ADR-55 WD2) to force convergence the tower of distinct
1984
+ # constant iterates collapses to one nominal type.
2274
1985
  def widen_value_pinned(type)
2275
1986
  Type::Combinator.widen_value_pinned(type)
2276
1987
  end
2277
1988
 
2278
- # Consumes one unit from the thread-local unroll-fuel counter and
2279
- # returns the units that were available *before* this consumption
2280
- # (so a positive return means the extended value-key may be used).
2281
- # Fuel is per-outermost inference entry: at the top level (empty
2282
- # guard stack) it seeds to `RECURSION_UNROLL_FUEL`, and the
2283
- # `ensure` in `infer_user_method_return` clears it once the stack
2284
- # drains back to empty. On exhaustion (return 0) it records a
2285
- # `RECURSION_UNROLL_FUEL` hit so the caller keeps the plain
2286
- # `(receiver, method)` signature — today's behaviour.
1989
+ # Consumes one unit from the thread-local unroll-fuel counter and returns the units that were
1990
+ # available *before* this consumption (so a positive return means the extended value-key may be used).
1991
+ # Fuel is per-outermost inference entry: at the top level (empty guard stack) it seeds to
1992
+ # `RECURSION_UNROLL_FUEL`, and the `ensure` in `infer_user_method_return` clears it once the stack
1993
+ # drains back to empty. On exhaustion (return 0) it records a `RECURSION_UNROLL_FUEL` hit so the caller
1994
+ # keeps the plain `(receiver, method)` signature today's behaviour.
2287
1995
  def unroll_fuel_remaining(stack)
2288
1996
  remaining = Thread.current[INFERENCE_UNROLL_FUEL_KEY]
2289
1997
  remaining = RECURSION_UNROLL_FUEL if remaining.nil? || stack.empty?
@@ -2295,12 +2003,11 @@ module Rigor
2295
2003
  remaining
2296
2004
  end
2297
2005
 
2298
- # A stable, hashable descriptor of the argument values when EVERY
2299
- # element of `arg_types` is value-pinned: a `Type::Constant`, or a
2300
- # `Type::Tuple` whose elements are (recursively) all value-pinned.
2301
- # Returns nil when any argument is not value-pinned (the ordinary
2302
- # type-keyed path) or when any pinned value's structural size
2303
- # exceeds `RECURSION_VALUE_SIZE_CAP` (value blow-up → fall back).
2006
+ # A stable, hashable descriptor of the argument values when EVERY element of `arg_types` is
2007
+ # value-pinned: a `Type::Constant`, or a `Type::Tuple` whose elements are (recursively) all
2008
+ # value-pinned. Returns nil when any argument is not value-pinned (the ordinary type-keyed path) or
2009
+ # when any pinned value's structural size exceeds `RECURSION_VALUE_SIZE_CAP` (value blow-up fall
2010
+ # back).
2304
2011
  def constant_argument_value_key(arg_types)
2305
2012
  return nil if arg_types.empty?
2306
2013
 
@@ -2316,10 +2023,9 @@ module Rigor
2316
2023
  keys.map(&:first)
2317
2024
  end
2318
2025
 
2319
- # Returns `[descriptor, structural_size]` for a value-pinned type,
2320
- # or nil for anything else. Strings count by a cheap length proxy
2321
- # (length > 256 ≈ 64+ nodes) so a long built string disqualifies
2322
- # the frame without a deep walk; tuples recurse.
2026
+ # Returns `[descriptor, structural_size]` for a value-pinned type, or nil for anything else. Strings
2027
+ # count by a cheap length proxy (length > 256 ≈ 64+ nodes) so a long built string disqualifies the
2028
+ # frame without a deep walk; tuples recurse.
2323
2029
  def pinned_value_descriptor(arg)
2324
2030
  case arg
2325
2031
  when Type::Constant
@@ -2340,35 +2046,27 @@ module Rigor
2340
2046
  end
2341
2047
  end
2342
2048
 
2343
- # Builds the body scope for a user-defined instance
2344
- # method call: a fresh `Scope` with `self_type` set to
2345
- # the receiver's nominal type, the project-wide
2346
- # accumulators inherited (so the body sees the same
2347
- # `discovered_classes` / `class_ivars` / etc. the
2348
- # caller does), and required positional parameters
2349
- # bound from the call's `arg_types` by index. Returns
2350
- # nil when the parameter shape is too complex for the
2351
- # first-iteration binder (rest args, keyword args,
2352
- # block params, etc.).
2049
+ # Builds the body scope for a user-defined instance method call: a fresh `Scope` with `self_type` set
2050
+ # to the receiver's nominal type, the project-wide accumulators inherited (so the body sees the same
2051
+ # `discovered_classes` / `class_ivars` / etc. the caller does), and required positional parameters
2052
+ # bound from the call's `arg_types` by index. Returns nil when the parameter shape is too complex for
2053
+ # the first-iteration binder (rest args, keyword args, block params, etc.).
2353
2054
  def build_user_method_body_scope(def_node, receiver, arg_types)
2354
2055
  params = def_node.parameters
2355
2056
  required = params&.requireds || []
2356
2057
  return nil unless params.nil? || user_method_param_shape_simple?(params)
2357
2058
  return nil unless required.size == arg_types.size
2358
2059
 
2359
- # Bind required positionals by index. The body scope starts from an
2360
- # empty fact store and narrowing set, so `with_local`'s fact /
2361
- # narrowing invalidations would be no-ops here — build the locals
2060
+ # Bind required positionals by index. The body scope starts from an empty fact store and narrowing
2061
+ # set, so `with_local`'s fact / narrowing invalidations would be no-ops here — build the locals
2362
2062
  # table directly (matching `with_local`'s `name.to_sym` key).
2363
2063
  locals = {}
2364
2064
  required.each_with_index { |param, index| locals[param.name.to_sym] = arg_types[index] }
2365
2065
 
2366
- # Construct the body scope in a SINGLE allocation — the previous
2367
- # `Scope.empty.with_*.with_*…` chain allocated a fresh frozen Scope
2368
- # per field, run per user-method-call inference (ADR-44). The
2369
- # discovery index is inherited whole by reference (ADR-53 Track A);
2370
- # the hand-copied per-field list this replaces had silently dropped
2371
- # `data_member_layouts` and `discovered_method_visibilities`.
2066
+ # Construct the body scope in a SINGLE allocation — the previous `Scope.empty.with_*.with_*…` chain
2067
+ # allocated a fresh frozen Scope per field, run per user-method-call inference (ADR-44). The
2068
+ # discovery index is inherited whole by reference (ADR-53 Track A); the hand-copied per-field list
2069
+ # this replaces had silently dropped `data_member_layouts` and `discovered_method_visibilities`.
2372
2070
  Scope.new(
2373
2071
  environment: scope.environment,
2374
2072
  locals: locals.freeze,
@@ -2379,10 +2077,9 @@ module Rigor
2379
2077
  )
2380
2078
  end
2381
2079
 
2382
- # ADR-48 Struct slice 3 — the fold-safe-local set for a method body
2383
- # (runs only on a return-memo miss, so the per-call cost is bounded —
2384
- # measured perf-neutral). Struct member layouts of constant receivers
2385
- # are resolved through the discovery side-table the body scope inherits.
2080
+ # ADR-48 Struct slice 3 — the fold-safe-local set for a method body (runs only on a return-memo miss,
2081
+ # so the per-call cost is bounded — measured perf-neutral). Struct member layouts of constant
2082
+ # receivers are resolved through the discovery side-table the body scope inherits.
2386
2083
  def struct_fold_safe_locals_for(body)
2387
2084
  StructFoldSafety.fold_safe_locals(
2388
2085
  body,
@@ -2390,10 +2087,8 @@ module Rigor
2390
2087
  )
2391
2088
  end
2392
2089
 
2393
- # First iteration accepts only required positional
2394
- # parameters: `def foo(a, b, c)`. Optionals, rest,
2395
- # keyword params, and block params disqualify the
2396
- # method from inference (the caller observes
2090
+ # First iteration accepts only required positional parameters: `def foo(a, b, c)`. Optionals, rest,
2091
+ # keyword params, and block params disqualify the method from inference (the caller observes
2397
2092
  # `Dynamic[Top]` instead).
2398
2093
  def user_method_param_shape_simple?(params)
2399
2094
  return false unless params.is_a?(Prism::ParametersNode)
@@ -2405,16 +2100,12 @@ module Rigor
2405
2100
  params.block.nil?
2406
2101
  end
2407
2102
 
2408
- # Slice A-engine. Implicit-self calls (no `node.receiver`)
2409
- # adopt the surrounding scope's `self_type` as their receiver
2410
- # so calls like `attr_reader_method_name` or
2411
- # `private_helper(...)` inside an instance method dispatch
2412
- # against the enclosing class. Slice 7 phase 10 when
2413
- # `self_type` is nil (top-level program), the receiver
2414
- # MUST default to `Nominal[Object]` so Kernel intrinsics
2415
- # like `require`, `require_relative`, `raise`, and `puts`
2416
- # dispatch through Object/Kernel rather than falling through
2417
- # to `Dynamic[Top]`.
2103
+ # Slice A-engine. Implicit-self calls (no `node.receiver`) adopt the surrounding scope's `self_type`
2104
+ # as their receiver so calls like `attr_reader_method_name` or `private_helper(...)` inside an
2105
+ # instance method dispatch against the enclosing class. Slice 7 phase 10 — when `self_type` is nil
2106
+ # (top-level program), the receiver MUST default to `Nominal[Object]` so Kernel intrinsics like
2107
+ # `require`, `require_relative`, `raise`, and `puts` dispatch through Object/Kernel rather than
2108
+ # falling through to `Dynamic[Top]`.
2418
2109
  def call_receiver_type_for(node)
2419
2110
  return type_of(node.receiver) if node.receiver
2420
2111
 
@@ -2432,28 +2123,20 @@ module Rigor
2432
2123
  arguments_node.arguments.map { |argument| type_of(argument) }
2433
2124
  end
2434
2125
 
2435
- # When the call carries a `Prism::BlockNode`, build the block's
2436
- # entry scope (outer locals plus parameter bindings driven by
2437
- # the receiving method's RBS signature), type the block body
2438
- # under that scope, and return the body's value type. The
2439
- # result feeds `MethodDispatcher.dispatch`'s `block_type:` so
2440
- # generic methods like `Array#map[U] { (Elem) -> U } -> Array[U]`
2441
- # resolve `U` to the block's return type. Returns `nil` when
2442
- # the call has no block, when the receiver is unknown, or
2443
- # when typing the body raises (defensive against malformed
2444
- # subtrees); the dispatcher then runs in its no-block-aware
2445
- # path.
2126
+ # When the call carries a `Prism::BlockNode`, build the block's entry scope (outer locals plus
2127
+ # parameter bindings driven by the receiving method's RBS signature), type the block body under that
2128
+ # scope, and return the body's value type. The result feeds `MethodDispatcher.dispatch`'s
2129
+ # `block_type:` so generic methods like `Array#map[U] { (Elem) -> U } -> Array[U]` resolve `U` to the
2130
+ # block's return type. Returns `nil` when the call has no block, when the receiver is unknown, or when
2131
+ # typing the body raises (defensive against malformed subtrees); the dispatcher then runs in its
2132
+ # no-block-aware path.
2446
2133
  #
2447
- # ADR-14 gap-#3 (d): a `Prism::BlockArgumentNode` carrying
2448
- # `&:symbol` (the Symbol#to_proc shorthand) is treated as
2449
- # a block. The block's return type is computed by
2450
- # dispatching `:symbol` on the expected block param type
2451
- # (per `Symbol#to_proc`'s `{ |x| x.symbol }` semantics).
2452
- # A precise inner dispatch produces the right return; any
2453
- # failure step falls back to `Dynamic[Top]` so the
2454
- # dispatcher still SEES a block — selecting the block-
2455
- # bearing overload of e.g. `Hash#transform_values` over
2456
- # the no-block overload that returns `Enumerator`.
2134
+ # ADR-14 gap-#3 (d): a `Prism::BlockArgumentNode` carrying `&:symbol` (the Symbol#to_proc shorthand) is
2135
+ # treated as a block. The block's return type is computed by dispatching `:symbol` on the expected
2136
+ # block param type (per `Symbol#to_proc`'s `{ |x| x.symbol }` semantics). A precise inner dispatch
2137
+ # produces the right return; any failure step falls back to `Dynamic[Top]` so the dispatcher still
2138
+ # SEES a block selecting the block-bearing overload of e.g. `Hash#transform_values` over the
2139
+ # no-block overload that returns `Enumerator`.
2457
2140
  def block_return_type_for(call_node, receiver_type, arg_types)
2458
2141
  block_arg = call_node.block
2459
2142
  return nil if block_arg.nil?
@@ -2465,11 +2148,9 @@ module Rigor
2465
2148
  arg_types: arg_types,
2466
2149
  environment: scope.environment
2467
2150
  )
2468
- # ADR-16 Tier A: when a registered plugin's `block_as_methods`
2469
- # entry matches `(receiver_type, call_node.name)`, narrow the
2470
- # block body's `self_type` to the receiver class's instance
2471
- # type. The narrowing is `nil` for unmatched calls, leaving
2472
- # the existing scope contract unchanged.
2151
+ # ADR-16 Tier A: when a registered plugin's `block_as_methods` entry matches `(receiver_type,
2152
+ # call_node.name)`, narrow the block body's `self_type` to the receiver class's instance type. The
2153
+ # narrowing is `nil` for unmatched calls, leaving the existing scope contract unchanged.
2473
2154
  narrowed_self = MacroBlockSelfType.narrow_self_type_for(
2474
2155
  scope: scope, call_node: call_node, receiver_type: receiver_type
2475
2156
  )
@@ -2490,15 +2171,11 @@ module Rigor
2490
2171
  end
2491
2172
  end
2492
2173
 
2493
- # `&:symbol` desugars to a one-arg Proc that dispatches
2494
- # `symbol` against its argument. When the param type is
2495
- # known and the resulting inner dispatch is precise,
2496
- # this returns the precise carrier; otherwise it
2497
- # returns `Dynamic[Top]` (still non-nil) so the outer
2498
- # dispatcher selects the block-bearing overload.
2499
- # `&proc_local` / `&method(:foo)` and friends — anything
2500
- # not a bare SymbolNode — still resolve to
2501
- # `Dynamic[Top]` for the same block-presence signal.
2174
+ # `&:symbol` desugars to a one-arg Proc that dispatches `symbol` against its argument. When the param
2175
+ # type is known and the resulting inner dispatch is precise, this returns the precise carrier;
2176
+ # otherwise it returns `Dynamic[Top]` (still non-nil) so the outer dispatcher selects the
2177
+ # block-bearing overload. `&proc_local` / `&method(:foo)` and friends — anything not a bare
2178
+ # SymbolNode — still resolve to `Dynamic[Top]` for the same block-presence signal.
2502
2179
  def symbol_block_return_type(block_arg, expected_param_types)
2503
2180
  expression = block_arg.expression
2504
2181
  return dynamic_top unless expression.is_a?(Prism::SymbolNode)
@@ -2525,19 +2202,14 @@ module Rigor
2525
2202
  block_scope.type_of(body)
2526
2203
  end
2527
2204
 
2528
- # v0.0.6 phase 2 — per-element block fold for Tuple
2529
- # receivers under `:map` / `:collect`. Walks every Tuple
2530
- # position, binds the block parameter to that element's
2531
- # type, and re-types the block body. The per-position
2532
- # results are assembled into `Tuple[U_1..U_n]`, strictly
2533
- # tighter than the RBS-projected `Array[union]`.
2205
+ # v0.0.6 phase 2 — per-element block fold for Tuple receivers under `:map` / `:collect`. Walks every
2206
+ # Tuple position, binds the block parameter to that element's type, and re-types the block body. The
2207
+ # per-position results are assembled into `Tuple[U_1..U_n]`, strictly tighter than the RBS-projected
2208
+ # `Array[union]`.
2534
2209
  #
2535
- # Declines (returns nil) when the receiver is not a
2536
- # `Tuple` with at least one element, when the call has
2537
- # no `Prism::BlockNode`, when the method is outside the
2538
- # supported set, when block typing raises mid-loop, or
2539
- # when the block has no body. The decline path leaves
2540
- # the dispatch chain untouched.
2210
+ # Declines (returns nil) when the receiver is not a `Tuple` with at least one element, when the call
2211
+ # has no `Prism::BlockNode`, when the method is outside the supported set, when block typing raises
2212
+ # mid-loop, or when the block has no body. The decline path leaves the dispatch chain untouched.
2541
2213
  PER_ELEMENT_TUPLE_METHODS = Set[
2542
2214
  :map, :collect, :filter_map, :flat_map,
2543
2215
  :select, :filter, :reject,
@@ -2551,12 +2223,10 @@ module Rigor
2551
2223
  ].freeze
2552
2224
  private_constant :HASH_SHAPE_TRANSFORM_METHODS
2553
2225
 
2554
- # Cardinality cap for per-element block fold over
2555
- # finite-bound `Constant<Range>` receivers. Walking
2556
- # `(1..1_000_000).map { }` element-wise would balloon
2557
- # block-typing cost and explode the resulting Tuple, so
2558
- # only short ranges expand into per-position folds.
2559
- # Larger ranges decline so the RBS tier widens.
2226
+ # Cardinality cap for per-element block fold over finite-bound `Constant<Range>` receivers. Walking
2227
+ # `(1..1_000_000).map { … }` element-wise would balloon block-typing cost and explode the resulting
2228
+ # Tuple, so only short ranges expand into per-position folds. Larger ranges decline so the RBS tier
2229
+ # widens.
2560
2230
  PER_ELEMENT_RANGE_LIMIT = 8
2561
2231
  private_constant :PER_ELEMENT_RANGE_LIMIT
2562
2232
 
@@ -2573,18 +2243,14 @@ module Rigor
2573
2243
  assemble_per_element_result(call_node.name, per_position, element_types)
2574
2244
  end
2575
2245
 
2576
- # Evaluates the call's block once per receiver element.
2577
- # Two block shapes are supported:
2246
+ # Evaluates the call's block once per receiver element. Two block shapes are supported:
2578
2247
  #
2579
- # - `Prism::BlockNode` — a full `do … end` / `{ … }` block;
2580
- # the body is re-typed per position with the element
2581
- # bound to the block parameter.
2582
- # - `Prism::BlockArgumentNode` wrapping a `SymbolNode`
2583
- # the `&:predicate` shorthand; the symbol is dispatched
2584
- # as a zero-arg method on each element type.
2248
+ # - `Prism::BlockNode` — a full `do … end` / `{ … }` block; the body is re-typed per position with the
2249
+ # element bound to the block parameter.
2250
+ # - `Prism::BlockArgumentNode` wrapping a `SymbolNode` — the `&:predicate` shorthand; the symbol is
2251
+ # dispatched as a zero-arg method on each element type.
2585
2252
  #
2586
- # Any other shape (`&proc_local`, `&method(:foo)`, no
2587
- # block) returns `nil` so the fold declines.
2253
+ # Any other shape (`&proc_local`, `&method(:foo)`, no block) returns `nil` so the fold declines.
2588
2254
  def per_element_block_results(block, element_types)
2589
2255
  case block
2590
2256
  when Prism::BlockNode
@@ -2613,20 +2279,17 @@ module Rigor
2613
2279
  nil
2614
2280
  end
2615
2281
 
2616
- # Returns the per-position element types for a finite,
2617
- # statically-known receiver shape or nil when the
2618
- # receiver does not pin a finite element list.
2282
+ # Returns the per-position element types for a finite, statically-known receiver shape — or nil when
2283
+ # the receiver does not pin a finite element list.
2619
2284
  #
2620
2285
  # `Tuple[A, B, …]` → [A, B, …]
2621
2286
  # `Constant<a..b>` → [Constant[a], …, Constant[b]]
2622
2287
  # everything else → nil
2623
2288
  #
2624
- # Note: `Type::IntegerRange` is the bounded-Integer
2625
- # carrier (`int<a, b>` represents "an Integer between
2626
- # a and b"), not a Range value. Calls like `.map` /
2627
- # `.find` on an `IntegerRange` receiver would resolve
2628
- # to `Integer#map` / `Integer#find` — neither exists —
2629
- # so IntegerRange does NOT participate in this fold.
2289
+ # Note: `Type::IntegerRange` is the bounded-Integer carrier (`int<a, b>` represents "an Integer between
2290
+ # a and b"), not a Range value. Calls like `.map` / `.find` on an `IntegerRange` receiver would resolve
2291
+ # to `Integer#map` / `Integer#find` neither exists so IntegerRange does NOT participate in this
2292
+ # fold.
2630
2293
  def per_element_elements_of(receiver_type)
2631
2294
  case receiver_type
2632
2295
  when Type::Tuple then receiver_type.elements
@@ -2647,47 +2310,38 @@ module Rigor
2647
2310
  INJECT_METHODS = Set[:inject, :reduce].freeze
2648
2311
  private_constant :INJECT_METHODS
2649
2312
 
2650
- # Cap on the element count for the Part 2 constant-threading
2651
- # fold — mirrors `ReduceFolding::CONSTANT_FOLD_ELEMENT_CAP`. The
2652
- # size is checked BEFORE enumeration so `(1..1_000_000)` declines
2653
- # without materialising.
2313
+ # Cap on the element count for the Part 2 constant-threading fold — mirrors
2314
+ # `ReduceFolding::CONSTANT_FOLD_ELEMENT_CAP`. The size is checked BEFORE enumeration so `(1..1_000_000)`
2315
+ # declines without materialising.
2654
2316
  INJECT_CONSTANT_ELEMENT_CAP = 64
2655
2317
  private_constant :INJECT_CONSTANT_ELEMENT_CAP
2656
2318
 
2657
- # Magnitude cap on a folded Integer accumulator — mirrors
2658
- # `ReduceFolding`'s bit cap so factorial-style blow-up declines
2659
- # to the Part 1 nominal result rather than parking a heavy
2660
- # bignum literal in the type graph.
2319
+ # Magnitude cap on a folded Integer accumulator — mirrors `ReduceFolding`'s bit cap so factorial-style
2320
+ # blow-up declines to the Part 1 nominal result rather than parking a heavy bignum literal in the type
2321
+ # graph.
2661
2322
  INJECT_CONSTANT_BIT_CAP = 256
2662
2323
  private_constant :INJECT_CONSTANT_BIT_CAP
2663
2324
 
2664
2325
  # Block-form `inject` / `reduce` return-type fold.
2665
2326
  #
2666
- # Part 1 (soundness): the accumulator of a block-form fold must
2667
- # reach a fixpoint over an unknown number of iterations the
2668
- # RBS tier's generic `(S) { (S, E) -> S } -> S` binds `S` from a
2669
- # SINGLE block pass (acc=seed, elem=element-join), so
2670
- # `(1..5).inject(1) { |a, i| a * i }` types `int<1, 5>` while the
2671
- # runtime is 120 (out of range — unsound). We iterate the
2672
- # accumulator type to a capped fixpoint (ADR-55/56 `BodyFixpoint`)
2673
- # so the multiply converges to `Integer`, never a value-bounded
2327
+ # Part 1 (soundness): the accumulator of a block-form fold must reach a fixpoint over an unknown
2328
+ # number of iterations the RBS tier's generic `(S) { (S, E) -> S } -> S` binds `S` from a SINGLE
2329
+ # block pass (acc=seed, elem=element-join), so `(1..5).inject(1) { |a, i| a * i }` types `int<1, 5>`
2330
+ # while the runtime is 120 (out of range — unsound). We iterate the accumulator type to a capped
2331
+ # fixpoint (ADR-55/56 `BodyFixpoint`) so the multiply converges to `Integer`, never a value-bounded
2674
2332
  # interval the runtime escapes.
2675
2333
  #
2676
- # Part 2 (precision): when the receiver is a fully-constant
2677
- # finite collection (`Constant[Range]` / `Tuple` of `Constant`),
2678
- # the seed is `Constant` (or the no-seed first element), and the
2679
- # block body folds to a `Constant` on EVERY iteration with the
2680
- # running accumulator + element bound, thread the accumulator
2681
- # through per-element block evaluation and return the final
2682
- # `Constant` (`(1..5).inject(1) { |a, i| a * i } -> 120`).
2334
+ # Part 2 (precision): when the receiver is a fully-constant finite collection (`Constant[Range]` /
2335
+ # `Tuple` of `Constant`), the seed is `Constant` (or the no-seed first element), and the block body
2336
+ # folds to a `Constant` on EVERY iteration with the running accumulator + element bound, thread the
2337
+ # accumulator through per-element block evaluation and return the final `Constant` (`(1..5).inject(1)
2338
+ # { |a, i| a * i } -> 120`).
2683
2339
  #
2684
- # The two are layered: Part 2 is attempted first (a constant
2685
- # answer is strictly tighter); on any decline it falls through to
2686
- # the Part 1 sound nominal fixpoint, and on a Part 1 decline to
2687
- # the RBS tier. Captured-local write-back (ADR-56) runs at the
2688
- # statement level independent of this return-type computation, so
2689
- # a block that both accumulates and mutates captured state keeps
2690
- # its write-back regardless of which arm answers here.
2340
+ # The two are layered: Part 2 is attempted first (a constant answer is strictly tighter); on any
2341
+ # decline it falls through to the Part 1 sound nominal fixpoint, and on a Part 1 decline to the RBS
2342
+ # tier. Captured-local write-back (ADR-56) runs at the statement level independent of this return-type
2343
+ # computation, so a block that both accumulates and mutates captured state keeps its write-back
2344
+ # regardless of which arm answers here.
2691
2345
  #
2692
2346
  # @return [Rigor::Type, nil]
2693
2347
  def try_block_inject_fold(call_node, receiver, arg_types)
@@ -2705,9 +2359,8 @@ module Rigor
2705
2359
  try_nominal_inject_fixpoint(receiver, block, seed, has_seed)
2706
2360
  end
2707
2361
 
2708
- # Splits the positional args into the optional seed. A Symbol
2709
- # final arg (`inject(seed, :*)`) is the no-block Symbol form and
2710
- # never reaches here (the block guard already failed for it).
2362
+ # Splits the positional args into the optional seed. A Symbol final arg (`inject(seed, :*)`) is the
2363
+ # no-block Symbol form and never reaches here (the block guard already failed for it).
2711
2364
  #
2712
2365
  # @return [Array(Rigor::Type, nil), Boolean] `[seed, has_seed]`
2713
2366
  def inject_seed(arg_types)
@@ -2717,10 +2370,9 @@ module Rigor
2717
2370
  end
2718
2371
  end
2719
2372
 
2720
- # Part 2 — thread the accumulator through per-element block
2721
- # evaluation over a fully-constant finite receiver. Declines
2722
- # (nil) on a non-constant receiver / seed, a size or magnitude
2723
- # cap, or any per-step result that is not a foldable `Constant`.
2373
+ # Part 2 — thread the accumulator through per-element block evaluation over a fully-constant finite
2374
+ # receiver. Declines (nil) on a non-constant receiver / seed, a size or magnitude cap, or any per-step
2375
+ # result that is not a foldable `Constant`.
2724
2376
  def try_constant_inject_fold(receiver, block, seed, has_seed)
2725
2377
  members = inject_constant_members(receiver)
2726
2378
  return nil if members.nil?
@@ -2735,8 +2387,7 @@ module Rigor
2735
2387
  acc
2736
2388
  end
2737
2389
 
2738
- # Extracts the receiver's foldable constant values, size-capped
2739
- # before enumeration, or nil to decline.
2390
+ # Extracts the receiver's foldable constant values, size-capped before enumeration, or nil to decline.
2740
2391
  def inject_constant_members(receiver)
2741
2392
  case receiver
2742
2393
  when Type::Constant then inject_constant_range_members(receiver.value)
@@ -2767,11 +2418,9 @@ module Rigor
2767
2418
  elements.map(&:value)
2768
2419
  end
2769
2420
 
2770
- # Seeds the constant accumulator: with a seed the memo starts at
2771
- # the (foldable) seed value and every member is folded; without a
2772
- # seed the first member seeds the memo and the rest are folded.
2773
- # The accumulator is carried as a `Constant` type (so the block
2774
- # body sees a value-pinned param).
2421
+ # Seeds the constant accumulator: with a seed the memo starts at the (foldable) seed value and every
2422
+ # member is folded; without a seed the first member seeds the memo and the rest are folded. The
2423
+ # accumulator is carried as a `Constant` type (so the block body sees a value-pinned param).
2775
2424
  #
2776
2425
  # @return [Array(Rigor::Type::Constant, nil), Array] `[acc, rest]`
2777
2426
  def inject_constant_start(members, seed, has_seed)
@@ -2786,10 +2435,9 @@ module Rigor
2786
2435
  end
2787
2436
  end
2788
2437
 
2789
- # Evaluates the block body once with the running constant
2790
- # accumulator + the next constant element bound to the block
2791
- # params, returning the result when it is a foldable `Constant`
2792
- # within the magnitude cap, else nil to decline the whole fold.
2438
+ # Evaluates the block body once with the running constant accumulator + the next constant element
2439
+ # bound to the block params, returning the result when it is a foldable `Constant` within the
2440
+ # magnitude cap, else nil to decline the whole fold.
2793
2441
  def inject_constant_step(block, acc, element_value)
2794
2442
  element = Type::Combinator.constant_of(element_value)
2795
2443
  result = type_block_body_with_param(block, [acc, element])
@@ -2811,13 +2459,10 @@ module Rigor
2811
2459
  value.is_a?(Integer) && value.bit_length > INJECT_CONSTANT_BIT_CAP
2812
2460
  end
2813
2461
 
2814
- # Part 1 — the sound nominal accumulator fixpoint. Iterates
2815
- # `acc = join(acc, block(acc, element))` to a capped fixpoint with
2816
- # final `Constant -> Nominal` widening (ADR-55/56 `BodyFixpoint`),
2817
- # seeding `acc` from the seed type (or the element type for the
2818
- # no-seed form) and binding the element-join to the element param.
2819
- # Declines (nil) when the element type is unknown so the RBS tier
2820
- # owns the call.
2462
+ # Part 1 — the sound nominal accumulator fixpoint. Iterates `acc = join(acc, block(acc, element))` to
2463
+ # a capped fixpoint with final `Constant -> Nominal` widening (ADR-55/56 `BodyFixpoint`), seeding `acc`
2464
+ # from the seed type (or the element type for the no-seed form) and binding the element-join to the
2465
+ # element param. Declines (nil) when the element type is unknown so the RBS tier owns the call.
2821
2466
  def try_nominal_inject_fixpoint(receiver, block, seed, has_seed)
2822
2467
  element = MethodDispatcher::IteratorDispatch.element_type_of(receiver)
2823
2468
  return nil if element.nil?
@@ -2838,9 +2483,8 @@ module Rigor
2838
2483
  converged[:__inject_acc__] || seed_acc
2839
2484
  end
2840
2485
 
2841
- # `index(value)` and `find_index(value)` carry a positional
2842
- # argument and search by `==` rather than running the block.
2843
- # Decline so the RBS tier owns those forms.
2486
+ # `index(value)` and `find_index(value)` carry a positional argument and search by `==` rather than
2487
+ # running the block. Decline so the RBS tier owns those forms.
2844
2488
  def find_family_with_args?(call_node)
2845
2489
  return false unless %i[find_index index].include?(call_node.name)
2846
2490
 
@@ -2862,19 +2506,14 @@ module Rigor
2862
2506
  end
2863
2507
  end
2864
2508
 
2865
- # `select` / `filter` / `reject`: keeps each receiver
2866
- # element whose per-position predicate result folds to a
2867
- # decisive `Constant` Ruby-truthy for `select` / `filter`,
2868
- # Ruby-falsey for `reject`. The surviving elements assemble
2869
- # into a `Tuple`, strictly tighter than the RBS-projected
2870
- # `Array[Elem]`.
2509
+ # `select` / `filter` / `reject`: keeps each receiver element whose per-position predicate result
2510
+ # folds to a decisive `Constant` — Ruby-truthy for `select` / `filter`, Ruby-falsey for `reject`. The
2511
+ # surviving elements assemble into a `Tuple`, strictly tighter than the RBS-projected `Array[Elem]`.
2871
2512
  #
2872
- # Folds tightly only when EVERY position is a `Constant`:
2873
- # a single non-`Constant` position leaves the result
2874
- # cardinality unknown (the element might or might not
2875
- # survive), so the dispatcher declines and the RBS tier
2876
- # widens to `Array[Elem]`. `[].select` style empty results
2877
- # are sound — an empty `Tuple` is the empty-array carrier.
2513
+ # Folds tightly only when EVERY position is a `Constant`: a single non-`Constant` position leaves the
2514
+ # result cardinality unknown (the element might or might not survive), so the dispatcher declines and
2515
+ # the RBS tier widens to `Array[Elem]`. `[].select` style empty results are sound — an empty `Tuple`
2516
+ # is the empty-array carrier.
2878
2517
  def assemble_filter_result(per_position, element_types, keep_on_truthy:)
2879
2518
  return nil unless per_position.all?(Type::Constant)
2880
2519
 
@@ -2884,12 +2523,9 @@ module Rigor
2884
2523
  Type::Combinator.tuple_of(*kept)
2885
2524
  end
2886
2525
 
2887
- # `filter_map` folds tightly only when every per-position
2888
- # result is a `Constant`: positions whose value is `nil`
2889
- # or `false` drop, the rest survive in declaration order.
2890
- # When any position is non-Constant the dispatcher
2891
- # declines (returns nil) so the RBS tier widens to
2892
- # `Array[U]`.
2526
+ # `filter_map` folds tightly only when every per-position result is a `Constant`: positions whose
2527
+ # value is `nil` or `false` drop, the rest survive in declaration order. When any position is
2528
+ # non-Constant the dispatcher declines (returns nil) so the RBS tier widens to `Array[U]`.
2893
2529
  def assemble_filter_map_result(per_position)
2894
2530
  return nil unless per_position.all?(Type::Constant)
2895
2531
 
@@ -2897,18 +2533,14 @@ module Rigor
2897
2533
  Type::Combinator.tuple_of(*kept)
2898
2534
  end
2899
2535
 
2900
- # `flat_map` flattens a single level: if the per-position
2901
- # result is a `Tuple`, its elements are concatenated; if
2902
- # it's a non-Array scalar carrier (`Constant<…>` over a
2903
- # non-Array literal) it contributes one element. We fold
2904
- # tightly only when every per-position result is one of
2905
- # those two recognisable shapes — `Nominal[Array[T]]`,
2906
- # `Union[…]`, and other opaque carriers decline so the
2907
- # RBS tier widens to `Array[U]`.
2536
+ # `flat_map` flattens a single level: if the per-position result is a `Tuple`, its elements are
2537
+ # concatenated; if it's a non-Array scalar carrier (`Constant<…>` over a non-Array literal) it
2538
+ # contributes one element. We fold tightly only when every per-position result is one of those two
2539
+ # recognisable shapes — `Nominal[Array[T]]`, `Union[…]`, and other opaque carriers decline so the RBS
2540
+ # tier widens to `Array[U]`.
2908
2541
  #
2909
- # `Type::Constant` only ever holds non-Array scalars (the
2910
- # carrier rejects Array literals), so a single `Constant`
2911
- # safely contributes itself as a single Tuple element.
2542
+ # `Type::Constant` only ever holds non-Array scalars (the carrier rejects Array literals), so a single
2543
+ # `Constant` safely contributes itself as a single Tuple element.
2912
2544
  def assemble_flat_map_result(per_position)
2913
2545
  flattened = per_position.flat_map { |type| flat_map_contribution(type) }
2914
2546
  return nil if flattened.nil? || flattened.any?(&:nil?)
@@ -2924,16 +2556,13 @@ module Rigor
2924
2556
  end
2925
2557
  end
2926
2558
 
2927
- # `find` / `detect`: returns the first receiver element
2928
- # whose block result is Ruby-truthy, or `nil` when no
2929
- # position folds to truthy.
2559
+ # `find` / `detect`: returns the first receiver element whose block result is Ruby-truthy, or `nil`
2560
+ # when no position folds to truthy.
2930
2561
  #
2931
- # Folds tightly only when every per-position block result
2932
- # is a `Type::Constant` otherwise we cannot decide which
2933
- # position (if any) is "the first matching one". When the
2934
- # first decisive truthy position is found, the answer is
2935
- # the corresponding receiver element. When every position
2936
- # folds to falsey, the answer is `Constant[nil]`.
2562
+ # Folds tightly only when every per-position block result is a `Type::Constant` — otherwise we cannot
2563
+ # decide which position (if any) is "the first matching one". When the first decisive truthy position
2564
+ # is found, the answer is the corresponding receiver element. When every position folds to falsey,
2565
+ # the answer is `Constant[nil]`.
2937
2566
  def assemble_find_result(per_position, element_types)
2938
2567
  return nil unless per_position.all?(Type::Constant)
2939
2568
 
@@ -2943,8 +2572,8 @@ module Rigor
2943
2572
  element_types[first_truthy_index]
2944
2573
  end
2945
2574
 
2946
- # `find_index` / `index`: returns the index of the first
2947
- # truthy position, or `Constant[nil]` when nothing matches.
2575
+ # `find_index` / `index`: returns the index of the first truthy position, or `Constant[nil]` when
2576
+ # nothing matches.
2948
2577
  def assemble_find_index_result(per_position)
2949
2578
  return nil unless per_position.all?(Type::Constant)
2950
2579
 
@@ -2958,28 +2587,22 @@ module Rigor
2958
2587
  type.is_a?(Type::Constant) && type.value && type.value != false
2959
2588
  end
2960
2589
 
2961
- # Per-pair block fold for `HashShape#transform_keys` and
2962
- # `HashShape#transform_values` (and their bang variants).
2590
+ # Per-pair block fold for `HashShape#transform_keys` and `HashShape#transform_values` (and their bang
2591
+ # variants).
2963
2592
  #
2964
- # When the receiver is a closed `HashShape` with no optional
2965
- # keys, applies the call's block (a `Prism::BlockNode` or
2966
- # `Prism::BlockArgumentNode`) to each key/value pair
2967
- # independently and assembles a new `HashShape`:
2593
+ # When the receiver is a closed `HashShape` with no optional keys, applies the call's block (a
2594
+ # `Prism::BlockNode` or `Prism::BlockArgumentNode`) to each key/value pair independently and
2595
+ # assembles a new `HashShape`:
2968
2596
  #
2969
- # - `transform_values` / `transform_values!`: re-types
2970
- # each VALUE by binding it to the block parameter; keys
2971
- # are preserved unchanged.
2972
- # - `transform_keys` / `transform_keys!`: re-types each
2973
- # KEY by wrapping it in `Constant[k]` and passing it to
2974
- # the block; values are preserved unchanged. The result
2975
- # key must be a `Constant[Symbol | String]` — otherwise
2976
- # the tier declines (the new key cannot be used as a
2977
- # static HashShape index). Collisions (two old keys
2978
- # mapping to the same new key) also decline.
2597
+ # - `transform_values` / `transform_values!`: re-types each VALUE by binding it to the block
2598
+ # parameter; keys are preserved unchanged.
2599
+ # - `transform_keys` / `transform_keys!`: re-types each KEY by wrapping it in `Constant[k]` and
2600
+ # passing it to the block; values are preserved unchanged. The result key must be a
2601
+ # `Constant[Symbol | String]` otherwise the tier declines (the new key cannot be used as a static
2602
+ # HashShape index). Collisions (two old keys mapping to the same new key) also decline.
2979
2603
  #
2980
- # Returns `nil` on any decline so the dispatcher falls
2981
- # through to `RbsDispatch` and gets the widened `Hash[K, V]`
2982
- # answer.
2604
+ # Returns `nil` on any decline so the dispatcher falls through to `RbsDispatch` and gets the widened
2605
+ # `Hash[K, V]` answer.
2983
2606
  def try_hash_shape_block_fold(call_node, receiver_type)
2984
2607
  return nil unless HASH_SHAPE_TRANSFORM_METHODS.include?(call_node.name)
2985
2608
  return nil unless receiver_type.is_a?(Type::HashShape)
@@ -3023,9 +2646,8 @@ module Rigor
3023
2646
  Type::Combinator.hash_shape_of(new_pairs)
3024
2647
  end
3025
2648
 
3026
- # Applies a single-argument block (either a full BlockNode
3027
- # or a `&:symbol` BlockArgumentNode) to `param_type` and
3028
- # returns the resulting type, or `nil` on failure.
2649
+ # Applies a single-argument block (either a full BlockNode or a `&:symbol` BlockArgumentNode) to
2650
+ # `param_type` and returns the resulting type, or `nil` on failure.
3029
2651
  def apply_hash_block(block_arg, param_type)
3030
2652
  case block_arg
3031
2653
  when Prism::BlockNode