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
@@ -11,39 +11,31 @@ require_relative "../builtins/regex_refinement"
11
11
 
12
12
  module Rigor
13
13
  module Inference
14
- # Control-flow predicate narrowing and type-lattice narrowing
15
- # primitives.
14
+ # Control-flow predicate narrowing and type-lattice narrowing primitives.
16
15
  #
17
16
  # `Rigor::Inference::Narrowing` answers two related questions:
18
17
  #
19
- # 1. Type-level narrowing: given a `Rigor::Type` value, what is its
20
- # truthy fragment, its falsey fragment, its nil fragment, and its
21
- # non-nil fragment? These primitives understand the value-lattice
22
- # algebra (`Constant`, `Nominal`, `Singleton`, `Tuple`, `HashShape`,
23
- # `Union`) and stay conservative on `Top` and `Dynamic[T]`.
24
- # 2. Predicate-level narrowing: given a Prism predicate node and an
25
- # entry scope, what are the truthy-edge scope and the falsey-edge
26
- # scope? The catalogue covers truthiness, `nil?`, `!`, `&&`/`||`,
27
- # class-membership (`is_a?`, `kind_of?`, `instance_of?`), trusted
28
- # equality/inequality against static literals, `case`/`when`,
29
- # regex match globals, string predicates (`start_with?` etc.),
30
- # key-presence, array emptiness, numeric comparison, and
18
+ # 1. Type-level narrowing: given a `Rigor::Type` value, what is its truthy fragment, its
19
+ # falsey fragment, its nil fragment, and its non-nil fragment? These primitives
20
+ # understand the value-lattice algebra (`Constant`, `Nominal`, `Singleton`, `Tuple`,
21
+ # `HashShape`, `Union`) and stay conservative on `Top` and `Dynamic[T]`.
22
+ # 2. Predicate-level narrowing: given a Prism predicate node and an entry scope, what are the
23
+ # truthy-edge scope and the falsey-edge scope? The catalogue covers truthiness, `nil?`,
24
+ # `!`, `&&`/`||`, class-membership (`is_a?`, `kind_of?`, `instance_of?`), trusted
25
+ # equality/inequality against static literals, `case`/`when`, regex match globals, string
26
+ # predicates (`start_with?` etc.), key-presence, array emptiness, numeric comparison, and
31
27
  # `respond_to?`.
32
28
  #
33
- # Consumed by `Rigor::Inference::StatementEvaluator` to refine
34
- # `then`/`else` scopes of `IfNode`/`UnlessNode` and
35
- # `case`/`when` branches.
29
+ # Consumed by `Rigor::Inference::StatementEvaluator` to refine `then`/`else` scopes of
30
+ # `IfNode`/`UnlessNode` and `case`/`when` branches.
36
31
  #
37
- # The module is pure: every public function returns fresh values and
38
- # MUST NOT mutate its inputs. Unrecognised predicate shapes degrade
39
- # silently to "no narrowing" by returning `nil` from the internal
40
- # analyser; the public `predicate_scopes` always returns an
41
- # `[truthy_scope, falsey_scope]` pair (the entry scope twice when no
42
- # rule matches).
32
+ # The module is pure: every public function returns fresh values and MUST NOT mutate its
33
+ # inputs. Unrecognised predicate shapes degrade silently to "no narrowing" by returning
34
+ # `nil` from the internal analyser; the public `predicate_scopes` always returns an
35
+ # `[truthy_scope, falsey_scope]` pair (the entry scope twice when no rule matches).
43
36
  #
44
37
  # See docs/internal-spec/inference-engine.md (Narrowing) and
45
- # docs/type-specification/control-flow-analysis.md for the
46
- # binding contract.
38
+ # docs/type-specification/control-flow-analysis.md for the binding contract.
47
39
  # rubocop:disable Metrics/ModuleLength
48
40
  module Narrowing
49
41
  TRUSTED_EQUALITY_LITERAL_CLASSES = [String, Symbol, Integer, TrueClass, FalseClass, NilClass].freeze
@@ -53,15 +45,13 @@ module Rigor
53
45
 
54
46
  module_function
55
47
 
56
- # Truthy fragment of `type`: the subset whose inhabitants are truthy
57
- # in Ruby's sense (anything other than `nil` and `false`).
48
+ # Truthy fragment of `type`: the subset whose inhabitants are truthy in Ruby's sense
49
+ # (anything other than `nil` and `false`).
58
50
  #
59
- # `Top`, `Dynamic[T]`, `Bot`, `Singleton[C]`, `Tuple[*]`, and
60
- # `HashShape{*}` flow through unchanged: Top/Dynamic stay
61
- # conservative because the analyzer cannot express the
62
- # difference type without a richer carrier and Dynamic must
63
- # preserve its provenance under the value-lattice algebra; the
64
- # remaining carriers are already truthy by inhabitance.
51
+ # `Top`, `Dynamic[T]`, `Bot`, `Singleton[C]`, `Tuple[*]`, and `HashShape{*}` flow through
52
+ # unchanged: Top/Dynamic stay conservative because the analyzer cannot express the
53
+ # difference type without a richer carrier and Dynamic must preserve its provenance under
54
+ # the value-lattice algebra; the remaining carriers are already truthy by inhabitance.
65
55
  def narrow_truthy(type)
66
56
  case type
67
57
  when Type::Constant
@@ -75,9 +65,8 @@ module Rigor
75
65
  end
76
66
  end
77
67
 
78
- # Falsey fragment of `type`: the subset whose inhabitants are
79
- # `nil` or `false`. Carriers that cannot inhabit a falsey value
80
- # collapse to `Bot`.
68
+ # Falsey fragment of `type`: the subset whose inhabitants are `nil` or `false`. Carriers
69
+ # that cannot inhabit a falsey value collapse to `Bot`.
81
70
  def narrow_falsey(type)
82
71
  case type
83
72
  when Type::Constant then falsey_value?(type.value) ? type : Type::Combinator.bot
@@ -87,12 +76,10 @@ module Rigor
87
76
  end
88
77
  end
89
78
 
90
- # Nil fragment of `type`: the subset whose inhabitants are `nil`.
91
- # Used by `nil?` predicate narrowing. `Top`/`Dynamic` narrow to
92
- # the canonical `Constant[nil]` so downstream dispatch resolves
93
- # through `NilClass`; carriers that never inhabit `nil`
94
- # (`Singleton`, `Tuple`, `HashShape`) collapse to `Bot`. `Bot`
95
- # is its own nil fragment.
79
+ # Nil fragment of `type`: the subset whose inhabitants are `nil`. Used by `nil?` predicate
80
+ # narrowing. `Top`/`Dynamic` narrow to the canonical `Constant[nil]` so downstream
81
+ # dispatch resolves through `NilClass`; carriers that never inhabit `nil` (`Singleton`,
82
+ # `Tuple`, `HashShape`) collapse to `Bot`. `Bot` is its own nil fragment.
96
83
  def narrow_nil(type)
97
84
  case type
98
85
  when Type::Constant then type.value.nil? ? type : Type::Combinator.bot
@@ -102,9 +89,8 @@ module Rigor
102
89
  end
103
90
  end
104
91
 
105
- # Non-nil fragment of `type`: the subset whose inhabitants are
106
- # not `nil`. Mirror of {.narrow_nil} for the falsey edge of
107
- # `x.nil?`.
92
+ # Non-nil fragment of `type`: the subset whose inhabitants are not `nil`. Mirror of
93
+ # {.narrow_nil} for the falsey edge of `x.nil?`.
108
94
  def narrow_non_nil(type)
109
95
  case type
110
96
  when Type::Constant
@@ -114,23 +100,19 @@ module Rigor
114
100
  when Type::Union
115
101
  Type::Combinator.union(*type.members.map { |m| narrow_non_nil(m) })
116
102
  else
117
- # Top, Dynamic, Singleton, Tuple, HashShape, Bot: there is
118
- # no nil contribution to remove, so the type is its own
119
- # non-nil fragment.
103
+ # Top, Dynamic, Singleton, Tuple, HashShape, Bot: there is no nil contribution to
104
+ # remove, so the type is its own non-nil fragment.
120
105
  type
121
106
  end
122
107
  end
123
108
 
124
- # Three-valued truthiness certainty of a predicate's type,
125
- # derived from the truthy / falsey fragments above: `:truthy`
126
- # when no inhabitant is falsey (the falsey fragment is `Bot`),
127
- # `:falsey` when no inhabitant is truthy, nil when both
128
- # fragments are inhabited — or when the type itself is nil /
129
- # `Bot` (dead code is not a certainty claim). This is the single
130
- # owner of the judgment both branch-elision consumers read
131
- # (`ExpressionTyper#elide_or_union` on the value side,
132
- # `StatementEvaluator#live_branch_for_if` on the scope side), so
133
- # the type a dead branch is elided from and the scope that stops
109
+ # Three-valued truthiness certainty of a predicate's type, derived from the truthy /
110
+ # falsey fragments above: `:truthy` when no inhabitant is falsey (the falsey fragment is
111
+ # `Bot`), `:falsey` when no inhabitant is truthy, nil when both fragments are inhabited —
112
+ # or when the type itself is nil / `Bot` (dead code is not a certainty claim). This is the
113
+ # single owner of the judgment both branch-elision consumers read
114
+ # (`ExpressionTyper#elide_or_union` on the value side, `StatementEvaluator#live_branch_for_if`
115
+ # on the scope side), so the type a dead branch is elided from and the scope that stops
134
116
  # flowing through it can never disagree.
135
117
  def predicate_certainty(type)
136
118
  return nil if type.nil? || type.is_a?(Type::Bot)
@@ -144,14 +126,12 @@ module Rigor
144
126
  nil
145
127
  end
146
128
 
147
- # Three-valued certainty of `C === subject` for a class / module
148
- # `when` pattern, derived from {.narrow_class} /
149
- # {.narrow_not_class}: `:no` when no inhabitant of the subject
150
- # matches, `:yes` when every inhabitant matches, `:maybe`
151
- # otherwise. The value-side counterpart of the scope narrowing
152
- # {.case_when_scopes} performs for the same condition shape, kept
153
- # here so the branch a `case` expression's type drops and the
154
- # clause whose body scope goes dead derive from one judgment.
129
+ # Three-valued certainty of `C === subject` for a class / module `when` pattern, derived
130
+ # from {.narrow_class} / {.narrow_not_class}: `:no` when no inhabitant of the subject
131
+ # matches, `:yes` when every inhabitant matches, `:maybe` otherwise. The value-side
132
+ # counterpart of the scope narrowing {.case_when_scopes} performs for the same condition
133
+ # shape, kept here so the branch a `case` expression's type drops and the clause whose
134
+ # body scope goes dead derive from one judgment.
155
135
  def class_pattern_certainty(subject_type, class_name, environment:)
156
136
  truthy_bot = narrow_class(subject_type, class_name, environment: environment).is_a?(Type::Bot)
157
137
  falsey_bot = narrow_not_class(subject_type, class_name, environment: environment).is_a?(Type::Bot)
@@ -162,18 +142,16 @@ module Rigor
162
142
  :maybe
163
143
  end
164
144
 
165
- # Classes whose `===` is plain value equality, so a literal
166
- # `when` pattern against a pinned `Constant` subject is exact in
167
- # both directions. Anything else keeps custom-`===` semantics
168
- # and stays `:maybe` in {.value_pattern_certainty}.
145
+ # Classes whose `===` is plain value equality, so a literal `when` pattern against a
146
+ # pinned `Constant` subject is exact in both directions. Anything else keeps custom-`===`
147
+ # semantics and stays `:maybe` in {.value_pattern_certainty}.
169
148
  VALUE_EQUALITY_CLASSES = [Integer, Float, Rational, Complex, String, Symbol,
170
149
  TrueClass, FalseClass, NilClass].freeze
171
150
 
172
- # Three-valued certainty of `<literal> === subject` for a
173
- # value-equality literal pattern: exact (`:yes` / `:no`) only
174
- # when the subject is itself a pinned `Constant` of a
175
- # value-equality class; `:maybe` otherwise (the runtime value
176
- # isn't pinned, or `===` may be user-defined).
151
+ # Three-valued certainty of `<literal> === subject` for a value-equality literal pattern:
152
+ # exact (`:yes` / `:no`) only when the subject is itself a pinned `Constant` of a
153
+ # value-equality class; `:maybe` otherwise (the runtime value isn't pinned, or `===` may
154
+ # be user-defined).
177
155
  def value_pattern_certainty(subject_type, pattern_value)
178
156
  return :maybe unless subject_type.is_a?(Type::Constant)
179
157
  return :maybe unless VALUE_EQUALITY_CLASSES.any? { |klass| subject_type.value.is_a?(klass) }
@@ -183,10 +161,9 @@ module Rigor
183
161
 
184
162
  # Equality fragment of `type` against a trusted literal.
185
163
  #
186
- # String/Symbol/Integer equality narrows only when the current
187
- # domain is already a finite union of trusted literals. Nil and
188
- # booleans are singleton values, so they can be extracted from a
189
- # mixed union such as `Integer | nil` without manufacturing a new
164
+ # String/Symbol/Integer equality narrows only when the current domain is already a
165
+ # finite union of trusted literals. Nil and booleans are singleton values, so they can be
166
+ # extracted from a mixed union such as `Integer | nil` without manufacturing a new
190
167
  # positive domain from the comparison alone.
191
168
  def narrow_equal(type, literal)
192
169
  return type unless trusted_equality_literal?(literal)
@@ -200,9 +177,9 @@ module Rigor
200
177
  end
201
178
  end
202
179
 
203
- # Complement of {.narrow_equal}. Negative facts are domain-relative:
204
- # they remove a literal from an already-known domain but do not create
205
- # an unbounded difference type when the domain is broad or dynamic.
180
+ # Complement of {.narrow_equal}. Negative facts are domain-relative: they remove a literal
181
+ # from an already-known domain but do not create an unbounded difference type when the
182
+ # domain is broad or dynamic.
206
183
  def narrow_not_equal(type, literal)
207
184
  return type unless trusted_equality_literal?(literal)
208
185
 
@@ -215,46 +192,41 @@ module Rigor
215
192
  end
216
193
  end
217
194
 
218
- # Integer-comparison fragment of `type` against an Integer
219
- # literal `bound`. Narrows the receiver of `x < n`, `x <= n`,
220
- # `x > n`, `x >= n` (and the reversed forms) to the subset of
221
- # the existing domain that satisfies the comparison. Hooks in:
222
- # - `Constant<Integer>` is preserved when it satisfies the
223
- # comparison, otherwise collapsed to `Bot`.
224
- # - `IntegerRange[a..b]` becomes the intersection with the
225
- # half-line implied by the comparison; an empty intersection
226
- # collapses to `Bot`, a single-point intersection collapses
227
- # to `Constant<Integer>`.
228
- # - `Nominal[Integer]` becomes the half-line itself (e.g.
229
- # `x > 0` on `Nominal[Integer]` is `positive_int`).
195
+ # Integer-comparison fragment of `type` against an Integer literal `bound`. Narrows the
196
+ # receiver of `x < n`, `x <= n`, `x > n`, `x >= n` (and the reversed forms) to the subset
197
+ # of the existing domain that satisfies the comparison. Hooks in:
198
+ # - `Constant<Integer>` is preserved when it satisfies the comparison, otherwise collapsed
199
+ # to `Bot`.
200
+ # - `IntegerRange[a..b]` becomes the intersection with the half-line implied by the
201
+ # comparison; an empty intersection collapses to `Bot`, a single-point intersection
202
+ # collapses to `Constant<Integer>`.
203
+ # - `Nominal[Integer]` becomes the half-line itself (e.g. `x > 0` on `Nominal[Integer]` is
204
+ # `positive_int`).
230
205
  # - `Union` narrows each member independently.
231
- # - Other carriers (Float, String, Top, Dynamic) flow through
232
- # unchanged: the analyzer does not have a Float-range carrier
233
- # today, and no other carrier participates in numeric ordering.
206
+ # - Other carriers (Float, String, Top, Dynamic) flow through unchanged: the analyzer does
207
+ # not have a Float-range carrier today, and no other carrier participates in numeric
208
+ # ordering.
234
209
  def narrow_integer_comparison(type, comparator, bound)
235
210
  return type unless bound.is_a?(Integer) && %i[< <= > >=].include?(comparator)
236
211
 
237
212
  narrow_integer_comparison_dispatch(type, comparator, bound)
238
213
  end
239
214
 
240
- # Equality fragment of `type` against an Integer `value`.
241
- # `Constant<Integer>` is preserved when it equals `value`,
242
- # otherwise collapses to `Bot`. `IntegerRange` covers? `value`
243
- # narrows to `Constant[value]`; an out-of-range comparison
244
- # collapses to `Bot`. `Nominal[Integer]` narrows to
245
- # `Constant[value]`. `Union` narrows each member.
215
+ # Equality fragment of `type` against an Integer `value`. `Constant<Integer>` is preserved
216
+ # when it equals `value`, otherwise collapses to `Bot`. `IntegerRange` covers? `value`
217
+ # narrows to `Constant[value]`; an out-of-range comparison collapses to `Bot`.
218
+ # `Nominal[Integer]` narrows to `Constant[value]`. `Union` narrows each member.
246
219
  def narrow_integer_equal(type, value)
247
220
  return type unless value.is_a?(Integer)
248
221
 
249
222
  narrow_integer_equal_dispatch(type, value)
250
223
  end
251
224
 
252
- # Complement of {.narrow_integer_equal}. Removes a single
253
- # integer value from the domain when one endpoint of an
254
- # `IntegerRange` is exactly that value (so the result stays a
255
- # contiguous range). Domains where the value sits strictly
256
- # between the endpoints stay unchanged: punching a hole would
257
- # require a two-piece carrier the lattice does not yet model.
225
+ # Complement of {.narrow_integer_equal}. Removes a single integer value from the domain
226
+ # when one endpoint of an `IntegerRange` is exactly that value (so the result stays a
227
+ # contiguous range). Domains where the value sits strictly between the endpoints stay
228
+ # unchanged: punching a hole would require a two-piece carrier the lattice does not yet
229
+ # model.
258
230
  def narrow_integer_not_equal(type, value)
259
231
  return type unless value.is_a?(Integer)
260
232
 
@@ -270,70 +242,54 @@ module Rigor
270
242
  end
271
243
  end
272
244
 
273
- # Class-membership fragment of `type`: the subset whose
274
- # inhabitants are instances of `class_name` (or its subclasses
275
- # when `exact: false`). `class_name` is the qualified name of
276
- # the class as it appears in source (`"Integer"`, `"Foo::Bar"`).
277
- # Slice 6 phase 2 sub-phase 1 narrows the `if x.is_a?(C)`
278
- # / `if x.kind_of?(C)` / `if x.instance_of?(C)` truthy edge.
245
+ # Class-membership fragment of `type`: the subset whose inhabitants are instances of
246
+ # `class_name` (or its subclasses when `exact: false`). `class_name` is the qualified name
247
+ # of the class as it appears in source (`"Integer"`, `"Foo::Bar"`). Slice 6 phase 2
248
+ # sub-phase 1 narrows the `if x.is_a?(C)` / `if x.kind_of?(C)` / `if x.instance_of?(C)`
249
+ # truthy edge.
279
250
  #
280
- # Nominal narrowing is hierarchy-aware through the analyzer
281
- # environment: when the bound type is a supertype of
282
- # `class_name` the result narrows DOWN to `Nominal[class_name]`
283
- # (e.g., `Numeric & Integer = Integer`); when the bound type is
284
- # already a subtype it is preserved; disjoint hierarchies
285
- # collapse to `Bot`. Classes the environment cannot resolve
286
- # fall back to the conservative answer (the type unchanged) so
287
- # the analyzer never asserts narrowing it cannot prove.
251
+ # Nominal narrowing is hierarchy-aware through the analyzer environment: when the bound
252
+ # type is a supertype of `class_name` the result narrows DOWN to `Nominal[class_name]`
253
+ # (e.g., `Numeric & Integer = Integer`); when the bound type is already a subtype it is
254
+ # preserved; disjoint hierarchies collapse to `Bot`. Classes the environment cannot
255
+ # resolve fall back to the conservative answer (the type unchanged) so the analyzer never
256
+ # asserts narrowing it cannot prove.
288
257
  def narrow_class(type, class_name, exact: false, environment: Environment.default)
289
258
  context = ClassNarrowingContext.new(exact: exact, polarity: :positive, environment: environment)
290
259
  narrow_class_dispatch(type, class_name, context)
291
260
  end
292
261
 
293
- # Mirror of {.narrow_class} for the falsey edge of
294
- # `is_a?`/`kind_of?`/`instance_of?`. Inhabitants that DO
295
- # satisfy the predicate are removed; inhabitants that do not
296
- # are preserved. Conservative on Top/Dynamic/Bot (preserved
297
- # unchanged) because the analyzer cannot prove the negative
298
- # without a richer carrier.
262
+ # Mirror of {.narrow_class} for the falsey edge of `is_a?`/`kind_of?`/`instance_of?`.
263
+ # Inhabitants that DO satisfy the predicate are removed; inhabitants that do not are
264
+ # preserved. Conservative on Top/Dynamic/Bot (preserved unchanged) because the analyzer
265
+ # cannot prove the negative without a richer carrier.
299
266
  def narrow_not_class(type, class_name, exact: false, environment: Environment.default)
300
267
  context = ClassNarrowingContext.new(exact: exact, polarity: :negative, environment: environment)
301
268
  narrow_class_dispatch(type, class_name, context)
302
269
  end
303
270
 
304
- # Negation pair for `assert_value is ~refinement` /
305
- # `predicate-if-* is ~refinement` directives. Computes
306
- # the complement of `refinement` within the current
307
- # local's domain `current_type`.
271
+ # Negation pair for `assert_value is ~refinement` / `predicate-if-* … is ~refinement`
272
+ # directives. Computes the complement of `refinement` within the current local's domain
273
+ # `current_type`.
308
274
  #
309
275
  # Carrier-by-carrier rules:
310
276
  #
311
- # - `Difference[base, Constant[v]]`. Complement of
312
- # `base \ {v}` within `current_type`. Walk the current
313
- # type's union members, keep each part disjoint from
314
- # `base`, and add the removed-value Constant once when
315
- # any current member covers it. `assert s is
316
- # ~non-empty-string` over `s: String | nil` narrows to
317
- # `Constant[""] | NilClass`.
318
- # - `IntegerRange[a, b]` (v0.0.5+ slice). Complement is
319
- # the two open halves `int<min, a-1>` and
320
- # `int<b+1, max>`, each intersected with the
321
- # integer-domain parts of `current_type`. Non-integer
322
- # parts (nil, String, …) of a Union receiver survive
323
- # unchanged. `assert n is ~int<5, 10>` over `n:
324
- # Integer | nil` narrows to `int<min, 4> | int<11,
325
- # max> | NilClass`.
326
- # - `Type::Intersection[M1, M2, …]` (v0.0.5+ slice). De
327
- # Morgan: `D \ (M1 ∩ M2) = (D \ M1) ∪ (D \ M2)`. Each
328
- # member's complement is computed independently within
329
- # `current_type` and the results are unioned. Members
330
- # the algebra cannot complement (Refined, non-Constant
331
- # Difference, …) contribute `current_type` itself, so
332
- # the union widens the answer to `current_type` —
333
- # sound but imprecise.
334
- # - `Refined[base, predicate]`. Predicate complements are
335
- # not reducible to a finite carrier without a richer
336
- # shape (e.g. `~lowercase-string` is "uppercase OR
277
+ # - `Difference[base, Constant[v]]`. Complement of `base \ {v}` within `current_type`.
278
+ # Walk the current type's union members, keep each part disjoint from `base`, and add
279
+ # the removed-value Constant once when any current member covers it. `assert s is
280
+ # ~non-empty-string` over `s: String | nil` narrows to `Constant[""] | NilClass`.
281
+ # - `IntegerRange[a, b]` (v0.0.5+ slice). Complement is the two open halves `int<min,
282
+ # a-1>` and `int<b+1, max>`, each intersected with the integer-domain parts of
283
+ # `current_type`. Non-integer parts (nil, String, …) of a Union receiver survive
284
+ # unchanged. `assert n is ~int<5, 10>` over `n: Integer | nil` narrows to `int<min, 4> |
285
+ # int<11, max> | NilClass`.
286
+ # - `Type::Intersection[M1, M2, …]` (v0.0.5+ slice). De Morgan: `D \ (M1 ∩ M2) = (D \ M1)
287
+ # (D \ M2)`. Each member's complement is computed independently within
288
+ # `current_type` and the results are unioned. Members the algebra cannot complement
289
+ # (Refined, non-Constant Difference, …) contribute `current_type` itself, so the union
290
+ # widens the answer to `current_type` sound but imprecise.
291
+ # - `Refined[base, predicate]`. Predicate complements are not reducible to a finite
292
+ # carrier without a richer shape (e.g. `~lowercase-string` is "uppercase OR
337
293
  # mixed-case"); `current_type` is returned unchanged.
338
294
  def narrow_not_refinement(current_type, refinement_type)
339
295
  case refinement_type
@@ -352,15 +308,12 @@ module Rigor
352
308
  end
353
309
  end
354
310
 
355
- # ADR-7 § "Slice 4-A" — public Fact-shaped narrowing
356
- # entry. Distinguishes a `Nominal[<class>]`-typed Fact
357
- # (uses `narrow_class` / `narrow_not_class` for
358
- # hierarchy-aware narrowing) from a refinement-shaped
359
- # Fact (refined types, IntegerRange, Difference, …).
360
- # The implementation lives next to its sibling helpers
361
- # `narrow_class` and `narrow_not_refinement`; consumers
362
- # outside `Narrowing` (today: `StatementEvaluator`'s
363
- # post-return assertion path) reach for it via
311
+ # ADR-7 § "Slice 4-A" — public Fact-shaped narrowing entry. Distinguishes a
312
+ # `Nominal[<class>]`-typed Fact (uses `narrow_class` / `narrow_not_class` for
313
+ # hierarchy-aware narrowing) from a refinement-shaped Fact (refined types, IntegerRange,
314
+ # Difference, ). The implementation lives next to its sibling helpers `narrow_class` and
315
+ # `narrow_not_refinement`; consumers outside `Narrowing` (today:
316
+ # `StatementEvaluator`'s post-return assertion path) reach for it via
364
317
  # `Rigor::Inference::Narrowing.narrow_for_fact`.
365
318
  def narrow_for_fact(current, fact, environment)
366
319
  if fact.type.is_a?(Type::Nominal) && fact.type.type_args.empty?
@@ -375,9 +328,8 @@ module Rigor
375
328
  fact.type
376
329
  end
377
330
 
378
- # Public predicate analyser. Returns `[truthy_scope, falsey_scope]`,
379
- # always; when no narrowing rule matches the predicate node both
380
- # entries are the receiver scope unchanged.
331
+ # Public predicate analyser. Returns `[truthy_scope, falsey_scope]`, always; when no
332
+ # narrowing rule matches the predicate node both entries are the receiver scope unchanged.
381
333
  #
382
334
  # @param node [Prism::Node, nil]
383
335
  # @param scope [Rigor::Scope]
@@ -391,29 +343,21 @@ module Rigor
391
343
 
392
344
  # Slice 7 phase 5 — `case`/`when` narrowing.
393
345
  #
394
- # Given the subject of a `case` (the expression after the
395
- # `case` keyword) and an array of `when`-clause condition
396
- # nodes (`when_clause.conditions`), returns a pair of
397
- # scopes:
346
+ # Given the subject of a `case` (the expression after the `case` keyword) and an array of
347
+ # `when`-clause condition nodes (`when_clause.conditions`), returns a pair of scopes:
398
348
  #
399
- # - `body_scope`: the scope under which the body of the
400
- # `when` clause MUST be evaluated. The subject local is
401
- # narrowed by the union of every condition's truthy
402
- # edge so the body sees the most specific type
403
- # compatible with "any of the conditions matched".
404
- # - `falsey_scope`: the scope under which the next branch
405
- # (the next `when` or the `else`) MUST be evaluated.
406
- # The subject is narrowed by the conjunction of every
407
- # condition's falsey edge.
349
+ # - `body_scope`: the scope under which the body of the `when` clause MUST be evaluated.
350
+ # The subject local is narrowed by the union of every condition's truthy edge so the
351
+ # body sees the most specific type compatible with "any of the conditions matched".
352
+ # - `falsey_scope`: the scope under which the next branch (the next `when` or the `else`)
353
+ # MUST be evaluated. The subject is narrowed by the conjunction of every condition's
354
+ # falsey edge.
408
355
  #
409
- # The narrowing is best-effort: if the subject is not a
410
- # `Prism::LocalVariableReadNode` or none of the condition
411
- # shapes are recognised, both returned scopes equal the
412
- # input scope. The catalogue mirrors
413
- # {.case_equality_target_class}: static class/module
414
- # constants narrow as `is_a?`; integer/float-endpoint
415
- # ranges narrow to `Numeric`; string-endpoint ranges and
416
- # regexp literals narrow to `String`.
356
+ # The narrowing is best-effort: if the subject is not a `Prism::LocalVariableReadNode` or
357
+ # none of the condition shapes are recognised, both returned scopes equal the input
358
+ # scope. The catalogue mirrors {.case_equality_target_class}: static class/module
359
+ # constants narrow as `is_a?`; integer/float-endpoint ranges narrow to `Numeric`;
360
+ # string-endpoint ranges and regexp literals narrow to `String`.
417
361
  #
418
362
  # @param subject [Prism::Node, nil] the `case` subject.
419
363
  # @param conditions [Array<Prism::Node>] the `when`
@@ -421,12 +365,10 @@ module Rigor
421
365
  # @param scope [Rigor::Scope]
422
366
  # @return [Array(Rigor::Scope, Rigor::Scope)]
423
367
  def case_when_scopes(subject, conditions, scope)
424
- # C1 — `case x when /re/` runs `/re/ === x`, which sets the
425
- # regex match-data globals exactly as a successful `=~` does.
426
- # Narrow `$~`/`$&`/`$1..$N` on the clause body (the match
427
- # edge); the falsey scope keeps the entry globals because a
428
- # later clause may match a different regex. Applied even when
429
- # the subject is not a narrowable local read.
368
+ # C1 — `case x when /re/` runs `/re/ === x`, which sets the regex match-data globals
369
+ # exactly as a successful `=~` does. Narrow `$~`/`$&`/`$1..$N` on the clause body (the
370
+ # match edge); the falsey scope keeps the entry globals because a later clause may match
371
+ # a different regex. Applied even when the subject is not a narrowable local read.
430
372
  body_scope = apply_when_regex_globals(conditions, scope)
431
373
 
432
374
  return [body_scope, scope] unless subject.is_a?(Prism::LocalVariableReadNode)
@@ -440,14 +382,12 @@ module Rigor
440
382
  [truthy, falsey]
441
383
  end
442
384
 
443
- # When the clause has exactly one `RegularExpressionNode`
444
- # literal condition, narrow the match-data globals on the body
445
- # edge (same rule as `analyse_regex_match_predicate`'s truthy
446
- # edge). With multiple regex conditions (`when /a/, /b/`) the
447
- # body is reachable through any of them, so only `$~`/`$&` are
448
- # safely non-nil; numbered groups whose presence differs per
449
- # alternative stay `String | nil`. With no regex condition the
450
- # entry scope passes through unchanged.
385
+ # When the clause has exactly one `RegularExpressionNode` literal condition, narrow the
386
+ # match-data globals on the body edge (same rule as `analyse_regex_match_predicate`'s
387
+ # truthy edge). With multiple regex conditions (`when /a/, /b/`) the body is reachable
388
+ # through any of them, so only `$~`/`$&` are safely non-nil; numbered groups whose
389
+ # presence differs per alternative stay `String | nil`. With no regex condition the entry
390
+ # scope passes through unchanged.
451
391
  def apply_when_regex_globals(conditions, scope)
452
392
  regexes = conditions.grep(Prism::RegularExpressionNode)
453
393
  return scope if regexes.empty?
@@ -462,10 +402,9 @@ module Rigor
462
402
  truthy
463
403
  end
464
404
 
465
- # Internal analyser. Returns `[truthy_scope, falsey_scope]` when
466
- # the predicate shape is recognised, or `nil` to signal "no
467
- # narrowing" so the public surface can fall back to the entry
468
- # scope.
405
+ # Internal analyser. Returns `[truthy_scope, falsey_scope]` when the predicate shape is
406
+ # recognised, or `nil` to signal "no narrowing" so the public surface can fall back to the
407
+ # entry scope.
469
408
  def analyse(node, scope)
470
409
  case node
471
410
  when Prism::ParenthesesNode
@@ -497,15 +436,12 @@ module Rigor
497
436
  class << self
498
437
  private
499
438
 
500
- # Complement of `Difference[base, Constant[v]]` within
501
- # `current_type`. Walks the current type's union members,
502
- # keeps each member disjoint from `base` (those values
503
- # were never in the refinement to begin with), and adds
504
- # the removed-value `Constant[v]` exactly once when any
505
- # current member covers it. Members that are fully
506
- # contained in the refinement (i.e. inside `base` and
507
- # NOT equal to the removed value) are dropped — they are
508
- # exactly the values the negation excludes.
439
+ # Complement of `Difference[base, Constant[v]]` within `current_type`. Walks the
440
+ # current type's union members, keeps each member disjoint from `base` (those values
441
+ # were never in the refinement to begin with), and adds the removed-value `Constant[v]`
442
+ # exactly once when any current member covers it. Members that are fully contained in
443
+ # the refinement (i.e. inside `base` and NOT equal to the removed value) are dropped —
444
+ # they are exactly the values the negation excludes.
509
445
  def complement_difference(current_type, difference)
510
446
  base = difference.base
511
447
  removed = difference.removed
@@ -536,13 +472,11 @@ module Rigor
536
472
  result.yes? || result.maybe?
537
473
  end
538
474
 
539
- # Complement of an `IntegerRange[a, b]` within
540
- # `current_type`. Splits the range complement into the
541
- # two open halves `int<min, a-1>` and `int<b+1, max>`
542
- # (skipping a half when its bound is infinity), then
543
- # intersects each half with the integer-domain parts of
544
- # `current_type`. Non-integer parts of a Union receiver
545
- # (nil, String, …) survive unchanged.
475
+ # Complement of an `IntegerRange[a, b]` within `current_type`. Splits the range
476
+ # complement into the two open halves `int<min, a-1>` and `int<b+1, max>` (skipping a
477
+ # half when its bound is infinity), then intersects each half with the integer-domain
478
+ # parts of `current_type`. Non-integer parts of a Union receiver (nil, String, …)
479
+ # survive unchanged.
546
480
  def complement_integer_range(current_type, range)
547
481
  halves = integer_range_complement_halves(range)
548
482
  parts = current_type.is_a?(Type::Union) ? current_type.members : [current_type]
@@ -564,11 +498,10 @@ module Rigor
564
498
  Type::Combinator.union(*survivors)
565
499
  end
566
500
 
567
- # Returns the two open halves of an IntegerRange's
568
- # complement: the left half `int<-∞, a-1>` (when `a` is
569
- # finite) and the right half `int<b+1, ∞>` (when `b` is
570
- # finite). Universal ranges (both bounds infinite) yield
571
- # an empty array — the complement is empty.
501
+ # Returns the two open halves of an IntegerRange's complement: the left half `int<-∞,
502
+ # a-1>` (when `a` is finite) and the right half `int<b+1, ∞>` (when `b` is finite).
503
+ # Universal ranges (both bounds infinite) yield an empty array the complement is
504
+ # empty.
572
505
  def integer_range_complement_halves(range)
573
506
  halves = []
574
507
  left_max = range.min
@@ -592,12 +525,10 @@ module Rigor
592
525
  end
593
526
  end
594
527
 
595
- # Intersect an integer-domain part with a complement
596
- # half-range. For a Nominal[Integer] receiver the meet
597
- # is the half itself; for an existing IntegerRange the
598
- # meet narrows both bounds; for a Constant[Integer] the
599
- # meet is the constant when the half covers it,
600
- # otherwise nil.
528
+ # Intersect an integer-domain part with a complement half-range. For a
529
+ # Nominal[Integer] receiver the meet is the half itself; for an existing IntegerRange
530
+ # the meet narrows both bounds; for a Constant[Integer] the meet is the constant when
531
+ # the half covers it, otherwise nil.
601
532
  def intersect_integer_part(part, half)
602
533
  case part
603
534
  when Type::Nominal
@@ -638,11 +569,9 @@ module Rigor
638
569
  low > high
639
570
  end
640
571
 
641
- # De Morgan: `D \ (M1 ∩ M2 ∩ …) = (D \ M1) ∪ (D \ M2) ∪
642
- # …`. Each member's complement is computed independently
643
- # within `current_type` and the results are unioned.
644
- # Members the algebra cannot complement contribute
645
- # `current_type` itself, so the union widens to
572
+ # De Morgan: `D \ (M1 ∩ M2 ∩ …) = (D \ M1) ∪ (D \ M2) ∪ …`. Each member's complement is
573
+ # computed independently within `current_type` and the results are unioned. Members the
574
+ # algebra cannot complement contribute `current_type` itself, so the union widens to
646
575
  # `current_type` overall — sound but imprecise.
647
576
  def complement_intersection(current_type, intersection)
648
577
  per_member = intersection.members.map do |member|
@@ -651,29 +580,22 @@ module Rigor
651
580
  Type::Combinator.union(*per_member)
652
581
  end
653
582
 
654
- # v0.0.7 — complement of a `Refined[base, predicate]`
655
- # refinement within `current_type`. Walks the current
656
- # type's union members; parts that are disjoint from
657
- # the refinement's base survive unchanged; parts equal
658
- # to the refinement itself drop entirely (they were
659
- # exactly the negated subset); parts that overlap the
660
- # base contribute `Difference[part, refined]` so
661
- # downstream narrowing knows the refinement subset is
662
- # excluded.
583
+ # v0.0.7 — complement of a `Refined[base, predicate]` refinement within `current_type`.
584
+ # Walks the current type's union members; parts that are disjoint from the refinement's
585
+ # base survive unchanged; parts equal to the refinement itself drop entirely (they were
586
+ # exactly the negated subset); parts that overlap the base contribute `Difference[part,
587
+ # refined]` so downstream narrowing knows the refinement subset is excluded.
663
588
  #
664
- # v0.0.9 — when the predicate has a registered
665
- # complement (see {Type::Refined::COMPLEMENT_PAIRS}) and
666
- # the part is exactly the refinement's base, the
667
- # narrowing returns `Refined[base, complement_predicate]`
668
- # instead of `Difference[base, refined]`. This is the
669
- # `~T` symmetry the spec promises: `~lowercase-string`
670
- # narrows `String` to `non-lowercase-string` rather than
589
+ # v0.0.9 — when the predicate has a registered complement (see
590
+ # {Type::Refined::COMPLEMENT_PAIRS}) and the part is exactly the refinement's base, the
591
+ # narrowing returns `Refined[base, complement_predicate]` instead of
592
+ # `Difference[base, refined]`. This is the `~T` symmetry the spec promises:
593
+ # `~lowercase-string` narrows `String` to `non-lowercase-string` rather than
671
594
  # `Difference[String, lowercase-string]`.
672
595
  #
673
- # Predicates without a registered complement still fall
674
- # back to the imprecise but sound `Difference[part,
675
- # refined]` carrier so behaviour is unchanged for
676
- # untouched call sites.
596
+ # Predicates without a registered complement still fall back to the imprecise but sound
597
+ # `Difference[part, refined]` carrier so behaviour is unchanged for untouched call
598
+ # sites.
677
599
  def complement_refined(current_type, refined)
678
600
  complement = registered_complement_for(refined)
679
601
  parts = current_type.is_a?(Type::Union) ? current_type.members : [current_type]
@@ -706,10 +628,9 @@ module Rigor
706
628
  %w[NilClass FalseClass].include?(nominal.class_name)
707
629
  end
708
630
 
709
- # Carriers that the {.narrow_falsey} fast path does not handle
710
- # by structural inspection. Singleton/Tuple/HashShape inhabit
711
- # truthy values, so their falsey fragment is empty; everything
712
- # else (Top, Dynamic, Bot, and any future carrier) stays
631
+ # Carriers that the {.narrow_falsey} fast path does not handle by structural
632
+ # inspection. Singleton/Tuple/HashShape inhabit truthy values, so their falsey fragment
633
+ # is empty; everything else (Top, Dynamic, Bot, and any future carrier) stays
713
634
  # conservative and is returned unchanged.
714
635
  def narrow_falsey_other(type)
715
636
  case type
@@ -718,12 +639,10 @@ module Rigor
718
639
  end
719
640
  end
720
641
 
721
- # Carriers that the {.narrow_nil} fast path does not handle by
722
- # structural inspection. Top/Dynamic narrow to `Constant[nil]`
723
- # so dispatch resolves through `NilClass`; Bot is its own nil
724
- # fragment; the remaining carriers (Singleton, Tuple,
725
- # HashShape, and any future carrier whose inhabitants exclude
726
- # nil) collapse to `Bot`.
642
+ # Carriers that the {.narrow_nil} fast path does not handle by structural inspection.
643
+ # Top/Dynamic narrow to `Constant[nil]` so dispatch resolves through `NilClass`; Bot is
644
+ # its own nil fragment; the remaining carriers (Singleton, Tuple, HashShape, and any
645
+ # future carrier whose inhabitants exclude nil) collapse to `Bot`.
727
646
  def narrow_nil_other(type)
728
647
  case type
729
648
  when Type::Dynamic, Type::Top then Type::Combinator.constant_of(nil)
@@ -811,13 +730,11 @@ module Rigor
811
730
  analyse(node.body, scope)
812
731
  end
813
732
 
814
- # The truthiness of a `StatementsNode` is determined by its
815
- # last statement (intermediate statements run for effect and
816
- # then the predicate's value is the tail's). Earlier
817
- # statements MAY have scope effects, but Slice 6 phase 1 does
818
- # NOT thread those through the analyser (the StatementEvaluator
819
- # has already produced `post_pred` for the call site, and
820
- # narrowing is layered on that scope).
733
+ # The truthiness of a `StatementsNode` is determined by its last statement
734
+ # (intermediate statements run for effect and then the predicate's value is the
735
+ # tail's). Earlier statements MAY have scope effects, but Slice 6 phase 1 does NOT
736
+ # thread those through the analyser (the StatementEvaluator has already produced
737
+ # `post_pred` for the call site, and narrowing is layered on that scope).
821
738
  def analyse_statements(node, scope)
822
739
  return nil if node.body.empty?
823
740
 
@@ -834,19 +751,15 @@ module Rigor
834
751
  ]
835
752
  end
836
753
 
837
- # Assignment-in-condition: `if name = expr` and the more
838
- # frequent `if cond && (name = expr)` / `if cond && name =
839
- # expr` Redmine-style guard. By the time narrowing runs,
840
- # `StatementEvaluator#eval_local_write` has already bound
841
- # the assigned local in `scope` to the rvalue type. The
842
- # write's own truthiness IS the assigned value's
843
- # truthiness, so the truthy edge narrows the local by
844
- # `narrow_truthy(current)` and the falsey edge by
845
- # `narrow_falsey(current)`. Mirrors `analyse_local_read`
846
- # because the only meaningful difference between
847
- # "predicate is `var`" and "predicate is `var = expr`" is
848
- # which scope holds the just-bound value; the narrowing
849
- # contract on the surrounding `if` is the same.
754
+ # Assignment-in-condition: `if name = expr` and the more frequent `if cond && (name =
755
+ # expr)` / `if cond && name = expr` Redmine-style guard. By the time narrowing runs,
756
+ # `StatementEvaluator#eval_local_write` has already bound the assigned local in `scope`
757
+ # to the rvalue type. The write's own truthiness IS the assigned value's truthiness, so
758
+ # the truthy edge narrows the local by `narrow_truthy(current)` and the falsey edge by
759
+ # `narrow_falsey(current)`. Mirrors `analyse_local_read` because the only meaningful
760
+ # difference between "predicate is `var`" and "predicate is `var = expr`" is which scope
761
+ # holds the just-bound value; the narrowing contract on the surrounding `if` is the
762
+ # same.
850
763
  def analyse_local_write(node, scope)
851
764
  current = scope.local(node.name)
852
765
  return nil if current.nil?
@@ -857,16 +770,13 @@ module Rigor
857
770
  ]
858
771
  end
859
772
 
860
- # Truthy guard on an instance variable. Covers both the bare
861
- # read `if @ivar`, the left arm of `@ivar && @ivar.foo`,
862
- # the receiver of `unless @ivar.nil?` once `!` reverses it —
863
- # and the assignment-in-condition write `if @ivar = expr`.
864
- # Mirrors `analyse_local_read` / `analyse_local_write`: the
865
- # truthy edge narrows the ivar by `narrow_truthy` (dropping
866
- # `nil` / `false`), the falsey edge by `narrow_falsey`. The
867
- # narrowing is scoped to the guarded branch only, so it is
868
- # purely additive — it never widens or re-narrows the ivar
869
- # binding seen elsewhere.
773
+ # Truthy guard on an instance variable. Covers both the bare read — `if @ivar`, the
774
+ # left arm of `@ivar && @ivar.foo`, the receiver of `unless @ivar.nil?` once `!`
775
+ # reverses it — and the assignment-in-condition write `if @ivar = expr`. Mirrors
776
+ # `analyse_local_read` / `analyse_local_write`: the truthy edge narrows the ivar by
777
+ # `narrow_truthy` (dropping `nil` / `false`), the falsey edge by `narrow_falsey`. The
778
+ # narrowing is scoped to the guarded branch only, so it is purely additive — it never
779
+ # widens or re-narrows the ivar binding seen elsewhere.
870
780
  def analyse_ivar(node, scope)
871
781
  current = scope.ivar(node.name)
872
782
  return nil if current.nil?
@@ -897,24 +807,18 @@ module Rigor
897
807
  ]
898
808
  end
899
809
 
900
- # `if /(?<x>...)/ =~ str` — Prism wraps the `=~` call in a
901
- # `MatchWriteNode` listing the named-capture targets. The
902
- # parent `eval_match_write` has already bound each target
903
- # to `String | nil`; in the truthy branch (the regex
904
- # matched) every named capture is guaranteed `String`,
905
- # and in the falsey branch (no match) every capture is
906
- # `nil`. Subtract the dead half on each edge so callers
907
- # like `year.upcase` inside the truthy branch no longer
908
- # fire `possible-nil-receiver`.
810
+ # `if /(?<x>...)/ =~ str` — Prism wraps the `=~` call in a `MatchWriteNode` listing the
811
+ # named-capture targets. The parent `eval_match_write` has already bound each target to
812
+ # `String | nil`; in the truthy branch (the regex matched) every named capture is
813
+ # guaranteed `String`, and in the falsey branch (no match) every capture is `nil`.
814
+ # Subtract the dead half on each edge so callers like `year.upcase` inside the truthy
815
+ # branch no longer fire `possible-nil-receiver`.
909
816
  #
910
- # v0.1.1 Track 1 slice 1 — when the regex source is a
911
- # statically known `RegularExpressionNode` and a named
912
- # capture's body matches one of the curated shapes in
913
- # {Rigor::Builtins::RegexRefinement::RULES}, the truthy
914
- # branch narrows further than `String` to the matching
915
- # imported refinement (e.g. `decimal-int-string` for
916
- # `\d+`). Bodies outside the table fall back to the
917
- # v0.1.0 baseline (plain `String`).
817
+ # v0.1.1 Track 1 slice 1 — when the regex source is a statically known
818
+ # `RegularExpressionNode` and a named capture's body matches one of the curated shapes
819
+ # in {Rigor::Builtins::RegexRefinement::RULES}, the truthy branch narrows further than
820
+ # `String` to the matching imported refinement (e.g. `decimal-int-string` for `\d+`).
821
+ # Bodies outside the table fall back to the v0.1.0 baseline (plain `String`).
918
822
  def analyse_match_write(node, scope)
919
823
  string_t = Type::Combinator.nominal_of("String")
920
824
  nil_t = Type::Combinator.constant_of(nil)
@@ -930,12 +834,10 @@ module Rigor
930
834
  [truthy, falsey]
931
835
  end
932
836
 
933
- # Extracts `{ capture_name => Refinement }` for every named
934
- # capture group in the `MatchWriteNode`'s wrapped `=~` call
935
- # whose body the recogniser table accepts. Bodies that
936
- # contain nested groups, anchors, alternation, or anything
937
- # else outside the curated forms drop out and the caller
938
- # falls back to plain `String`.
837
+ # Extracts `{ capture_name => Refinement }` for every named capture group in the
838
+ # `MatchWriteNode`'s wrapped `=~` call whose body the recogniser table accepts. Bodies
839
+ # that contain nested groups, anchors, alternation, or anything else outside the curated
840
+ # forms drop out and the caller falls back to plain `String`.
939
841
  NAMED_CAPTURE_BODY_RE = /\(\?<([A-Za-z_][A-Za-z0-9_]*)>([^()|]*)\)/
940
842
  private_constant :NAMED_CAPTURE_BODY_RE
941
843
 
@@ -954,13 +856,12 @@ module Rigor
954
856
  # Recognised CallNode predicates:
955
857
  # - `recv.nil?` (Slice 6 phase 1, no args, no block)
956
858
  # - unary `!recv` (`name == :!`, no args, no block)
957
- # - `recv.is_a?(C)` / `recv.kind_of?(C)` / `recv.instance_of?(C)`
958
- # with a single static-constant argument and no block
959
- # (Slice 6 phase 2 sub-phase 1).
960
- # - `local == literal` / `literal == local` and the `!=` mirror
961
- # for trusted static literals (Slice 6 phase 2 sub-phase 2).
962
- # Anything else returns nil so the surrounding analyser falls
963
- # through to the no-narrowing fallback.
859
+ # - `recv.is_a?(C)` / `recv.kind_of?(C)` / `recv.instance_of?(C)` with a single
860
+ # static-constant argument and no block (Slice 6 phase 2 sub-phase 1).
861
+ # - `local == literal` / `literal == local` and the `!=` mirror for trusted static
862
+ # literals (Slice 6 phase 2 sub-phase 2).
863
+ # Anything else returns nil so the surrounding analyser falls through to the
864
+ # no-narrowing fallback.
964
865
  def analyse_call(node, scope)
965
866
  return nil if node.block
966
867
 
@@ -972,42 +873,32 @@ module Rigor
972
873
  string_predicate_result = analyse_string_predicate(node, scope)
973
874
  return apply_safe_nav_non_nil(node, scope, string_predicate_result) if string_predicate_result
974
875
 
975
- # A safe-navigation call (`v&.foo`) whose result is truthy
976
- # proves the receiver was non-nil — `&.` returns `nil` when
977
- # the receiver is nil, so a truthy outcome can only come from
978
- # a non-nil receiver. Narrow the receiver on the truthy edge
979
- # even when the call itself carries no other flow fact, so
980
- # `v&.start_with?('[') && v.end_with?(']')` sees `v` non-nil
981
- # in the `&&` right operand. The falsey edge is the
982
- # conservative no-op (falsey could mean nil receiver OR a
983
- # falsey method result).
876
+ # A safe-navigation call (`v&.foo`) whose result is truthy proves the receiver was
877
+ # non-nil — `&.` returns `nil` when the receiver is nil, so a truthy outcome can
878
+ # only come from a non-nil receiver. Narrow the receiver on the truthy edge even
879
+ # when the call itself carries no other flow fact, so `v&.start_with?('[') &&
880
+ # v.end_with?(']')` sees `v` non-nil in the `&&` right operand. The falsey edge is
881
+ # the conservative no-op (falsey could mean nil receiver OR a falsey method result).
984
882
  safe_nav_result = analyse_safe_nav_receiver(node, scope)
985
883
  return safe_nav_result if safe_nav_result
986
884
  end
987
885
 
988
- # Slice 7 phase 15 — RBS::Extended predicate
989
- # effects. When the method's RBS signature carries
990
- # `rigor:v1:predicate-if-true` / `predicate-if-false`
991
- # annotations, apply them to narrow the corresponding
992
- # local-variable arguments on each edge. v0.1.1 Track
993
- # 1 slice 3 — implicit-self calls (`recv == nil`,
994
- # e.g. `admin?` inside an instance method body) flow
995
- # through here too so `self`-targeted facts can edit
886
+ # Slice 7 phase 15 — RBS::Extended predicate effects. When the method's RBS signature
887
+ # carries `rigor:v1:predicate-if-true` / `predicate-if-false` annotations, apply them
888
+ # to narrow the corresponding local-variable arguments on each edge. v0.1.1 Track 1
889
+ # slice 3 implicit-self calls (`recv == nil`, e.g. `admin?` inside an instance
890
+ # method body) flow through here too so `self`-targeted facts can edit
996
891
  # `scope.self_type`.
997
892
  analyse_rbs_extended_contribution(node, scope)
998
893
  end
999
894
 
1000
- # v0.1.1 Track 1 slice 4 — `String#start_with?` /
1001
- # `#end_with?` / `#include?` against a `Constant<String>`
1002
- # needle attaches a relational `FactStore::Fact` to the
1003
- # local on each edge. The receiver's type does NOT
1004
- # change Rigor has no "starts-with-X" carrier today
1005
- # so the fact carries the predicate semantics for any
1006
- # downstream consumer that wants to read it (e.g. a
1007
- # plugin's `prepare(services)` hook in v0.1.x).
1008
- # Truthy edge: positive polarity. Falsey edge: negative
1009
- # polarity. Mirrors the equality-predicate fact pattern
1010
- # already used by `analyse_equality_predicate`.
895
+ # v0.1.1 Track 1 slice 4 — `String#start_with?` / `#end_with?` / `#include?` against a
896
+ # `Constant<String>` needle attaches a relational `FactStore::Fact` to the local on
897
+ # each edge. The receiver's type does NOT change — Rigor has no "starts-with-X" carrier
898
+ # today so the fact carries the predicate semantics for any downstream consumer that
899
+ # wants to read it (e.g. a plugin's `prepare(services)` hook in v0.1.x). Truthy edge:
900
+ # positive polarity. Falsey edge: negative polarity. Mirrors the equality-predicate fact
901
+ # pattern already used by `analyse_equality_predicate`.
1011
902
  STRING_PREDICATE_NAMES = %i[start_with? end_with? include?].freeze
1012
903
  private_constant :STRING_PREDICATE_NAMES
1013
904
 
@@ -1080,21 +971,17 @@ module Rigor
1080
971
  end
1081
972
  end
1082
973
 
1083
- # T3 (template-corpora survey) — `recv.respond_to?(sym)` truthy
1084
- # edge narrows `recv` non-nil. `nil.respond_to?(m)` is `false`
1085
- # for every method `m` that `NilClass` does not define, so a
1086
- # truthy `respond_to?` proves the receiver was not `nil` UNLESS
1087
- # the queried symbol is one of `NilClass`'s own methods (`:to_s`,
1088
- # `:inspect`, `:nil?`, …) — `nil` DOES respond to those, so the
1089
- # truthy edge admits a nil receiver and we narrow nothing.
974
+ # T3 (template-corpora survey) — `recv.respond_to?(sym)` truthy edge narrows `recv`
975
+ # non-nil. `nil.respond_to?(m)` is `false` for every method `m` that `NilClass` does not
976
+ # define, so a truthy `respond_to?` proves the receiver was not `nil` UNLESS the queried
977
+ # symbol is one of `NilClass`'s own methods (`:to_s`, `:inspect`, `:nil?`, …) — `nil`
978
+ # DOES respond to those, so the truthy edge admits a nil receiver and we narrow nothing.
1090
979
  #
1091
- # Conservative floor: narrow only on a literal `Symbol`/`String`
1092
- # argument resolved against the RBS environment; a non-literal
1093
- # symbol, a missing argument, or a symbol that IS in `NilClass`'s
1094
- # method set declines. The falsey edge is always the no-op
1095
- # ("does not respond" proves little about the receiver's type).
1096
- # Narrowing-only: it removes the `nil` constituent and never
1097
- # promotes a non-nil type.
980
+ # Conservative floor: narrow only on a literal `Symbol`/`String` argument resolved
981
+ # against the RBS environment; a non-literal symbol, a missing argument, or a symbol
982
+ # that IS in `NilClass`'s method set declines. The falsey edge is always the no-op
983
+ # ("does not respond" proves little about the receiver's type). Narrowing-only: it
984
+ # removes the `nil` constituent and never promotes a non-nil type.
1098
985
  def analyse_respond_to_predicate(node, scope)
1099
986
  return nil if node.block
1100
987
  return nil if node.arguments.nil? || node.arguments.arguments.size != 1
@@ -1115,12 +1002,11 @@ module Rigor
1115
1002
  [scope.public_send(writer, node.receiver.name, non_nil), scope]
1116
1003
  end
1117
1004
 
1118
- # True when `nil` responds to `sym` — i.e. `NilClass` (own,
1119
- # inherited Kernel/BasicObject) defines an instance method named
1120
- # `sym`. Resolved against the RBS environment; when the lookup
1121
- # is unavailable the answer is conservatively `true` (decline to
1122
- # narrow) so an unknown environment never manufactures a false
1123
- # non-nil narrowing.
1005
+ # True when `nil` responds to `sym` — i.e. `NilClass` (own, inherited
1006
+ # Kernel/BasicObject) defines an instance method named `sym`. Resolved against the RBS
1007
+ # environment; when the lookup is unavailable the answer is conservatively `true`
1008
+ # (decline to narrow) so an unknown environment never manufactures a false non-nil
1009
+ # narrowing.
1124
1010
  def nilclass_method?(sym, scope)
1125
1011
  name = sym.respond_to?(:to_sym) ? sym.to_sym : sym
1126
1012
  return true unless name.is_a?(Symbol)
@@ -1132,23 +1018,20 @@ module Rigor
1132
1018
  true
1133
1019
  end
1134
1020
 
1135
- # ADR-47 §4-4 (Elixir `tuple_size`/non-empty analogue) — a bare
1136
- # `arr.empty?` / `arr.any?` / `arr.none?` (no block, no args)
1137
- # narrows an Array-typed receiver to `non-empty-array[T]` on the
1138
- # edge that implies "at least one element":
1021
+ # ADR-47 §4-4 (Elixir `tuple_size`/non-empty analogue) — a bare `arr.empty?` /
1022
+ # `arr.any?` / `arr.none?` (no block, no args) narrows an Array-typed receiver to
1023
+ # `non-empty-array[T]` on the edge that implies "at least one element":
1139
1024
  #
1140
1025
  # - `empty?` → false edge (the array is NOT empty)
1141
1026
  # - `any?` → true edge (a truthy element exists ⇒ non-empty)
1142
1027
  # - `none?` → false edge (a truthy element exists ⇒ non-empty)
1143
1028
  #
1144
- # The opposite edge is the conservative no-op (`any?`/`none?`
1145
- # falseness does not imply emptiness — the array may hold only
1146
- # falsey elements; `empty?` truth could narrow to an empty array
1147
- # but that carrier move is deferred). Only `Nominal[Array, [T]]`
1148
- # receivers narrow `Tuple` is already known-length, `Dynamic`
1149
- # is left alone (gradual guarantee), and a non-Array receiver
1150
- # (`String#empty?`, `Range#any?`, …) bails so the existing string
1151
- # / predicate paths still run.
1029
+ # The opposite edge is the conservative no-op (`any?`/`none?` falseness does not imply
1030
+ # emptiness — the array may hold only falsey elements; `empty?` truth could narrow to an
1031
+ # empty array but that carrier move is deferred). Only `Nominal[Array, [T]]` receivers
1032
+ # narrow `Tuple` is already known-length, `Dynamic` is left alone (gradual
1033
+ # guarantee), and a non-Array receiver (`String#empty?`, `Range#any?`, …) bails so the
1034
+ # existing string / predicate paths still run.
1152
1035
  def analyse_array_emptiness_predicate(node, scope, name)
1153
1036
  return nil if node.block
1154
1037
  return nil unless node.arguments.nil? || node.arguments.arguments.empty?
@@ -1174,9 +1057,9 @@ module Rigor
1174
1057
  end
1175
1058
  end
1176
1059
 
1177
- # Refines `Array[T]` (and every Array member of a Union) to
1178
- # `non-empty-array[T]`; returns the input unchanged when nothing
1179
- # applies, so the caller can detect "no narrowing".
1060
+ # Refines `Array[T]` (and every Array member of a Union) to `non-empty-array[T]`;
1061
+ # returns the input unchanged when nothing applies, so the caller can detect "no
1062
+ # narrowing".
1180
1063
  def narrow_to_non_empty_array(type)
1181
1064
  case type
1182
1065
  when Type::Nominal
@@ -1190,18 +1073,16 @@ module Rigor
1190
1073
  end
1191
1074
  end
1192
1075
 
1193
- # ADR-47 §4-3 (Elixir `is_map_key/2` analogue) — `h.key?(:foo)`
1194
- # narrows the receiver on the truthy edge: a `HashShape` whose
1195
- # `:foo` is an OPTIONAL key has it promoted to REQUIRED, so a
1196
- # subsequent `h[:foo]` reads the declared value type instead of
1197
- # `value | nil` (the optionality nil is gone). Sound because key
1198
- # presence removes only the optionality-injected nil, never the
1199
- # value's own intrinsic nil (`h = {foo: nil}` keeps `h[:foo]`
1200
- # nil-typed). The falsey edge is left unchanged — "key absent"
1201
- # is the conservative no-op. Only literal `Symbol`/`String`
1202
- # arguments and `LocalVariableReadNode`/`InstanceVariableReadNode`
1203
- # receivers narrow; everything else (Dynamic, `Nominal[Hash]`,
1204
- # method-chain receivers, dynamic keys) bails to no narrowing.
1076
+ # ADR-47 §4-3 (Elixir `is_map_key/2` analogue) — `h.key?(:foo)` narrows the receiver on
1077
+ # the truthy edge: a `HashShape` whose `:foo` is an OPTIONAL key has it promoted to
1078
+ # REQUIRED, so a subsequent `h[:foo]` reads the declared value type instead of `value |
1079
+ # nil` (the optionality nil is gone). Sound because key presence removes only the
1080
+ # optionality-injected nil, never the value's own intrinsic nil (`h = {foo: nil}` keeps
1081
+ # `h[:foo]` nil-typed). The falsey edge is left unchanged — "key absent" is the
1082
+ # conservative no-op. Only literal `Symbol`/`String` arguments and
1083
+ # `LocalVariableReadNode`/`InstanceVariableReadNode` receivers narrow; everything else
1084
+ # (Dynamic, `Nominal[Hash]`, method-chain receivers, dynamic keys) bails to no
1085
+ # narrowing.
1205
1086
  def analyse_key_presence_predicate(node, scope)
1206
1087
  return nil if node.arguments.nil?
1207
1088
  return nil unless node.arguments.arguments.size == 1
@@ -1222,10 +1103,9 @@ module Rigor
1222
1103
  return nil if current.nil?
1223
1104
 
1224
1105
  truthy = narrow_hash_key_present(current, key)
1225
- # §4-3 false edge — `h.key?(:foo)` being false proves `:foo` is
1226
- # absent, so on that edge `h[:foo]` reads `nil`. Remove the
1227
- # (optional) key from the shape; a required key makes the false
1228
- # edge dead and an unknown key is already nil, both no-ops.
1106
+ # §4-3 false edge — `h.key?(:foo)` being false proves `:foo` is absent, so on that
1107
+ # edge `h[:foo]` reads `nil`. Remove the (optional) key from the shape; a required
1108
+ # key makes the false edge dead and an unknown key is already nil, both no-ops.
1229
1109
  falsey = narrow_hash_key_absent(current, key)
1230
1110
  return nil if truthy.equal?(current) && falsey.equal?(current) # predicate is opaque
1231
1111
 
@@ -1241,9 +1121,9 @@ module Rigor
1241
1121
  end
1242
1122
  end
1243
1123
 
1244
- # Promotes `key` from optional to required across a HashShape (or
1245
- # every HashShape member of a Union). Returns the input unchanged
1246
- # when nothing applies, so the caller can detect "no narrowing".
1124
+ # Promotes `key` from optional to required across a HashShape (or every HashShape
1125
+ # member of a Union). Returns the input unchanged when nothing applies, so the caller
1126
+ # can detect "no narrowing".
1247
1127
  def narrow_hash_key_present(type, key)
1248
1128
  case type
1249
1129
  when Type::HashShape
@@ -1268,12 +1148,11 @@ module Rigor
1268
1148
  )
1269
1149
  end
1270
1150
 
1271
- # §4-3 false edge — drops `key` from a HashShape (or every HashShape
1272
- # member of a Union) so the proven-absent key reads `nil`. Returns
1273
- # the input unchanged when nothing applies (caller detects "no
1274
- # narrowing"). Only an *optional* present key is removed: a required
1275
- # key makes `key?` always true (the false edge is dead, leave the
1276
- # shape opaque) and a key absent from `pairs` already reads `nil`.
1151
+ # §4-3 false edge — drops `key` from a HashShape (or every HashShape member of a Union)
1152
+ # so the proven-absent key reads `nil`. Returns the input unchanged when nothing
1153
+ # applies (caller detects "no narrowing"). Only an *optional* present key is removed: a
1154
+ # required key makes `key?` always true (the false edge is dead, leave the shape opaque)
1155
+ # and a key absent from `pairs` already reads `nil`.
1277
1156
  def narrow_hash_key_absent(type, key)
1278
1157
  case type
1279
1158
  when Type::HashShape
@@ -1298,27 +1177,21 @@ module Rigor
1298
1177
  )
1299
1178
  end
1300
1179
 
1301
- # Survey item (b): `/regex/ =~ str` and `str =~ /regex/`
1302
- # bind the regex match-data globals on each edge.
1180
+ # Survey item (b): `/regex/ =~ str` and `str =~ /regex/` bind the regex match-data
1181
+ # globals on each edge.
1303
1182
  #
1304
- # - Truthy edge (`=~` returned an Integer position — the
1305
- # match succeeded): `$~` to `Nominal[MatchData]`; `$&`
1306
- # and `$1..$N` (where N is the number of capture groups
1307
- # in the regex source) to `Nominal[String]`. This is the
1308
- # same optimistic-narrowing shape the existing
1309
- # `analyse_match_write` uses for named captures inside
1310
- # `if /(?<x>...)/ =~ str` optional groups in the
1311
- # regex source (`(\d+)?`) would bind `$N` to `nil` at
1312
- # runtime, but the floor here matches the common idiom
1313
- # (required captures) and lets `unless /(\d+)/ =~ s;
1314
- # raise; end; $1.to_i` resolve cleanly.
1315
- # - Falsey edge (`=~` returned nil — no match): `$~` and
1316
- # every numbered / back-reference global bound to
1317
- # `Constant<nil>`.
1183
+ # - Truthy edge (`=~` returned an Integer position — the match succeeded): `$~` to
1184
+ # `Nominal[MatchData]`; `$&` and `$1..$N` (where N is the number of capture groups in
1185
+ # the regex source) to `Nominal[String]`. This is the same optimistic-narrowing shape
1186
+ # the existing `analyse_match_write` uses for named captures inside `if /(?<x>...)/
1187
+ # =~ str` optional groups in the regex source (`(\d+)?`) would bind `$N` to `nil`
1188
+ # at runtime, but the floor here matches the common idiom (required captures) and
1189
+ # lets `unless /(\d+)/ =~ s; raise; end; $1.to_i` resolve cleanly.
1190
+ # - Falsey edge (`=~` returned nil — no match): `$~` and every numbered / back-reference
1191
+ # global bound to `Constant<nil>`.
1318
1192
  #
1319
- # Returns nil (no narrowing) when the receiver / argument
1320
- # pair does not include a `RegularExpressionNode` literal
1321
- # we can count.
1193
+ # Returns nil (no narrowing) when the receiver / argument pair does not include a
1194
+ # `RegularExpressionNode` literal we can count.
1322
1195
  def analyse_regex_match_predicate(node, scope)
1323
1196
  return nil if node.arguments.nil?
1324
1197
  return nil unless node.arguments.arguments.size == 1
@@ -1343,14 +1216,12 @@ module Rigor
1343
1216
  REGEX_MATCH_GLOBALS = %i[$~ $& $` $' $+].freeze
1344
1217
  private_constant :REGEX_MATCH_GLOBALS
1345
1218
 
1346
- # `unconditional` is the Set of 1-based numbered-capture
1347
- # indices whose group is guaranteed to participate in any
1348
- # successful match (no optional quantifier on the group or
1349
- # an ancestor, no alternation in the pattern). Those `$N`
1350
- # are bound to `String`; every other numbered group present
1351
- # in the pattern stays `String | nil` on both edges (a
1352
- # truthy match leaves an optional group nil at runtime), so
1353
- # we do not narrow it on the truthy edge.
1219
+ # `unconditional` is the Set of 1-based numbered-capture indices whose group is
1220
+ # guaranteed to participate in any successful match (no optional quantifier on the
1221
+ # group or an ancestor, no alternation in the pattern). Those `$N` are bound to
1222
+ # `String`; every other numbered group present in the pattern stays `String | nil` on
1223
+ # both edges (a truthy match leaves an optional group nil at runtime), so we do not
1224
+ # narrow it on the truthy edge.
1354
1225
  def regex_match_predicate_scopes(scope, unconditional)
1355
1226
  string_t = Type::Combinator.nominal_of("String")
1356
1227
  match_data_t = Type::Combinator.nominal_of("MatchData")
@@ -1374,31 +1245,24 @@ module Rigor
1374
1245
  [truthy, falsey]
1375
1246
  end
1376
1247
 
1377
- # Returns the Set of 1-based numbered-capture indices that
1378
- # are UNCONDITIONAL in `source`: present on every successful
1379
- # match because no optional quantifier (`?`, `*`, `{0,…}`)
1380
- # applies to the group or any ancestor group, and the
1381
- # pattern contains no alternation (`|`). Optional and
1382
- # alternation-reachable groups are excluded at runtime `$N`
1383
- # is `nil` for them even when the overall match succeeds, so
1384
- # narrowing them to non-nil `String` would be unsound. The
1385
- # walker is intentionally light (char scan, not a regex-AST
1386
- # parse): backslash escapes are skipped; `(?:…)`, lookahead
1387
- # `(?=…)`/`(?!…)`, and lookbehind `(?<=…)`/`(?<!…)` do not
1388
- # capture; named groups `(?<name>…)` do. Conservatism is
1389
- # one-directional — when in doubt a group is treated as
1390
- # conditional (dropped from the Set), never the reverse.
1248
+ # Returns the Set of 1-based numbered-capture indices that are UNCONDITIONAL in
1249
+ # `source`: present on every successful match because no optional quantifier (`?`,
1250
+ # `*`, `{0,…}`) applies to the group or any ancestor group, and the pattern contains no
1251
+ # alternation (`|`). Optional and alternation-reachable groups are excluded at
1252
+ # runtime `$N` is `nil` for them even when the overall match succeeds, so narrowing
1253
+ # them to non-nil `String` would be unsound. The walker is intentionally light (char
1254
+ # scan, not a regex-AST parse): backslash escapes are skipped; `(?:…)`, lookahead
1255
+ # `(?=…)`/`(?!…)`, and lookbehind `(?<=…)`/`(?<!…)` do not capture; named groups
1256
+ # `(?<name>…)` do. Conservatism is one-directional when in doubt a group is treated
1257
+ # as conditional (dropped from the Set), never the reverse.
1391
1258
  def unconditional_capture_groups(source)
1392
- # `unconditional` collects every capturing index; a group is
1393
- # later removed (with its whole subtree) when it is optionally
1394
- # quantified, nested under an optional ancestor, or sits in an
1395
- # alternation branch. `stack` holds one frame per open group
1396
- # (plus a virtual root frame for the top level) as
1397
- # `[group_index_or_nil, descendant_indices, alternated?]`.
1398
- # A `|` marks the CURRENT group's frame alternated its
1399
- # branches are mutually exclusive, so its descendant captures
1400
- # may be absent on a successful match; the group itself still
1401
- # participates. Closing a frame rolls its subtree up to the
1259
+ # `unconditional` collects every capturing index; a group is later removed (with its
1260
+ # whole subtree) when it is optionally quantified, nested under an optional ancestor,
1261
+ # or sits in an alternation branch. `stack` holds one frame per open group (plus a
1262
+ # virtual root frame for the top level) as `[group_index_or_nil, descendant_indices,
1263
+ # alternated?]`. A `|` marks the CURRENT group's frame alternated its branches are
1264
+ # mutually exclusive, so its descendant captures may be absent on a successful match;
1265
+ # the group itself still participates. Closing a frame rolls its subtree up to the
1402
1266
  # parent so an optional / alternated ancestor disqualifies it.
1403
1267
  state = { unconditional: Set.new, stack: [[nil, [], false]], group_index: 0 }
1404
1268
  pos = 0
@@ -1422,8 +1286,8 @@ module Rigor
1422
1286
  end
1423
1287
 
1424
1288
  # Updates the walk `state` at a group-relevant char during
1425
- # {#unconditional_capture_groups}: `(` pushes a frame, `)` pops
1426
- # and resolves it, `|` flags the current frame as alternated.
1289
+ # {#unconditional_capture_groups}: `(` pushes a frame, `)` pops and resolves it, `|`
1290
+ # flags the current frame as alternated.
1427
1291
  def scan_group_char(source, pos, chr, state)
1428
1292
  case chr
1429
1293
  when "("
@@ -1440,11 +1304,10 @@ module Rigor
1440
1304
  end
1441
1305
  end
1442
1306
 
1443
- # Resolves a closed (or virtual-root) group frame: its subtree is
1444
- # its own index plus every descendant index. The subtree is
1445
- # disqualified when the group is optionally quantified or its
1446
- # branches are alternated (only the descendants in that case —
1447
- # but a self subtree always keeps its own index unless optional).
1307
+ # Resolves a closed (or virtual-root) group frame: its subtree is its own index plus
1308
+ # every descendant index. The subtree is disqualified when the group is optionally
1309
+ # quantified or its branches are alternated (only the descendants in that case but a
1310
+ # self subtree always keeps its own index unless optional).
1448
1311
  def finalize_frame(state, frame, optional:)
1449
1312
  idx, descendants, alternated = frame
1450
1313
  subtree = descendants.dup
@@ -1456,11 +1319,10 @@ module Rigor
1456
1319
  state[:stack].last && state[:stack].last[1].concat(subtree)
1457
1320
  end
1458
1321
 
1459
- # True when the group whose closing paren is at `source[pos]`
1460
- # is followed by a quantifier that permits zero repetitions
1461
- # (`?`, `*`, `{0…}`). `+` and `{1,…}` do NOT make a group
1462
- # optional. A lazy/possessive suffix (`*?`, `*+`) is still
1463
- # zero-permitting on the base quantifier.
1322
+ # True when the group whose closing paren is at `source[pos]` is followed by a
1323
+ # quantifier that permits zero repetitions (`?`, `*`, `{0…}`). `+` and `{1,…}` do NOT
1324
+ # make a group optional. A lazy/possessive suffix (`*?`, `*+`) is still zero-permitting
1325
+ # on the base quantifier.
1464
1326
  def next_quantifier_optional?(source, pos)
1465
1327
  case source[pos]
1466
1328
  when "?", "*" then true
@@ -1499,9 +1361,8 @@ module Rigor
1499
1361
  end
1500
1362
  end
1501
1363
 
1502
- # `:positive?` / `:negative?` / `:zero?` / `:nonzero?` are
1503
- # zero-arg predicates on `Numeric`. We model them as
1504
- # comparisons against the literal 0 so the existing range
1364
+ # `:positive?` / `:negative?` / `:zero?` / `:nonzero?` are zero-arg predicates on
1365
+ # `Numeric`. We model them as comparisons against the literal 0 so the existing range
1505
1366
  # narrowing handles them uniformly.
1506
1367
  ZERO_CLASS_PREDICATE_RULES = Ractor.make_shareable({
1507
1368
  positive?: { truthy: [:>, 0], falsey: [:<=, 0] },
@@ -1534,11 +1395,9 @@ module Rigor
1534
1395
  end
1535
1396
  end
1536
1397
 
1537
- # `x.between?(a, b)` truthy edge narrows to
1538
- # `narrow_integer_comparison(>=, a)`
1539
- # `narrow_integer_comparison(<=, b)`. The falsey edge is left
1540
- # unchanged because the complement is a two-piece domain
1541
- # (`x < a || x > b`) that the lattice cannot express
1398
+ # `x.between?(a, b)` truthy edge narrows to `narrow_integer_comparison(>=, a)` ∩
1399
+ # `narrow_integer_comparison(<=, b)`. The falsey edge is left unchanged because the
1400
+ # complement is a two-piece domain (`x < a || x > b`) that the lattice cannot express
1542
1401
  # precisely. `a` and `b` MUST both be integer literals.
1543
1402
  def analyse_between_predicate(node, scope)
1544
1403
  return nil unless node.receiver.is_a?(Prism::LocalVariableReadNode)
@@ -1559,9 +1418,8 @@ module Rigor
1559
1418
  [scope.with_local(local_name, truthy), scope]
1560
1419
  end
1561
1420
 
1562
- # Helper for {.narrow_integer_not_equal}. Only adjusts when the
1563
- # value sits exactly on one endpoint, so the result stays
1564
- # contiguous; otherwise the input range is preserved.
1421
+ # Helper for {.narrow_integer_not_equal}. Only adjusts when the value sits exactly on
1422
+ # one endpoint, so the result stays contiguous; otherwise the input range is preserved.
1565
1423
  def narrow_integer_range_not_equal(range, value)
1566
1424
  return range if range.lower > value || range.upper < value
1567
1425
  return Type::Combinator.bot if single_point_range_equal?(range, value)
@@ -1607,9 +1465,8 @@ module Rigor
1607
1465
  # Comparison predicate analyser. Recognised shapes:
1608
1466
  # x < Int x <= Int x > Int x >= Int
1609
1467
  # Int < x Int <= x Int > x Int >= x
1610
- # The reversed (literal-on-left) form is normalised by
1611
- # transposing the operator so the receiver-local always
1612
- # appears on the left of the rule.
1468
+ # The reversed (literal-on-left) form is normalised by transposing the operator so the
1469
+ # receiver-local always appears on the left of the rule.
1613
1470
  INVERT_COMPARISON_OP = { :< => :>=, :<= => :>, :> => :<=, :>= => :< }.freeze
1614
1471
  REVERSE_COMPARISON_OP = { :< => :>, :<= => :>=, :> => :<, :>= => :<= }.freeze
1615
1472
  private_constant :INVERT_COMPARISON_OP, :REVERSE_COMPARISON_OP
@@ -1778,12 +1635,10 @@ module Rigor
1778
1635
  narrowed == original ? :relational : :local_binding
1779
1636
  end
1780
1637
 
1781
- # `recv.is_a?(C)` / `recv.kind_of?(C)` / `recv.instance_of?(C)`
1782
- # narrowing. The receiver MUST be a `LocalVariableReadNode`
1783
- # (so we have a name to rebind), and the argument MUST be a
1784
- # single static constant reference (`Foo` or `Foo::Bar`) we
1785
- # can resolve to a qualified class name. Anything else falls
1786
- # through to "no narrowing".
1638
+ # `recv.is_a?(C)` / `recv.kind_of?(C)` / `recv.instance_of?(C)` narrowing. The receiver
1639
+ # MUST be a `LocalVariableReadNode` (so we have a name to rebind), and the argument
1640
+ # MUST be a single static constant reference (`Foo` or `Foo::Bar`) we can resolve to a
1641
+ # qualified class name. Anything else falls through to "no narrowing".
1787
1642
  def analyse_class_predicate(node, scope, exact:)
1788
1643
  return nil if node.arguments.nil?
1789
1644
  return nil unless node.arguments.arguments.size == 1
@@ -1791,16 +1646,12 @@ module Rigor
1791
1646
  bare_name = static_class_name(node.arguments.arguments.first)
1792
1647
  return nil if bare_name.nil?
1793
1648
 
1794
- # Resolve `bare_name` through the lexical-scope chain
1795
- # so a name shadowed by the current class / enclosing
1796
- # module wins over the top-level constant. Mirrors
1797
- # Ruby's `Module.nesting`-driven constant lookup. The
1798
- # canonical motivating case: inside
1799
- # `Rigor::Type::Singleton#==`, `is_a?(Singleton)`
1800
- # should resolve to `Rigor::Type::Singleton`, not the
1801
- # top-level stdlib `Singleton` mixin (which would
1802
- # surface as a spurious `undefined-method` on
1803
- # subsequent `other.class_name` calls).
1649
+ # Resolve `bare_name` through the lexical-scope chain so a name shadowed by the
1650
+ # current class / enclosing module wins over the top-level constant. Mirrors Ruby's
1651
+ # `Module.nesting`-driven constant lookup. The canonical motivating case: inside
1652
+ # `Rigor::Type::Singleton#==`, `is_a?(Singleton)` should resolve to
1653
+ # `Rigor::Type::Singleton`, not the top-level stdlib `Singleton` mixin (which would
1654
+ # surface as a spurious `undefined-method` on subsequent `other.class_name` calls).
1804
1655
  class_name = resolve_class_name_lexically(bare_name, scope)
1805
1656
 
1806
1657
  case node.receiver
@@ -1818,27 +1669,20 @@ module Rigor
1818
1669
  class_predicate_scopes(scope, node.receiver.name, current, class_name, exact: exact)
1819
1670
  end
1820
1671
 
1821
- # Stable single-hop method-chain narrowing (ROADMAP §
1822
- # Future cycles "Method-call receiver narrowing across
1823
- # stable receivers"). When the predicate's receiver is
1824
- # `<local/ivar>.<method>` with no args and no block,
1825
- # record the truthy / falsey narrowing in
1826
- # `Scope#method_chain_narrowings` keyed on the chain
1827
- # address. The dominated body's identical chain reads
1828
- # then observe the narrowed type through
1672
+ # Stable single-hop method-chain narrowing (ROADMAP § Future cycles — "Method-call
1673
+ # receiver narrowing across stable receivers"). When the predicate's receiver is
1674
+ # `<local/ivar>.<method>` with no args and no block, record the truthy / falsey
1675
+ # narrowing in `Scope#method_chain_narrowings` keyed on the chain address. The
1676
+ # dominated body's identical chain reads then observe the narrowed type through
1829
1677
  # `ExpressionTyper#call_type_for`'s lookup.
1830
1678
  #
1831
- # Heuristic-by-design (ROADMAP § "Soundness gap"): a
1832
- # second call to `x.last` could in principle return a
1833
- # different value than the first. The chain is dropped
1834
- # on (1) receiver variable rebind (handled inside
1835
- # `Scope#with_local` / `#with_ivar`), and (2) any
1836
- # intervening call against the same root receiver
1837
- # (handled by `StatementEvaluator#eval_call`'s
1838
- # invalidation step). The Law of Demeter justifies the
1839
- # single-hop restriction: a single-hop chain is the
1840
- # idiomatic Ruby shape where re-evaluation soundness is
1841
- # the strongest.
1679
+ # Heuristic-by-design (ROADMAP § "Soundness gap"): a second call to `x.last` could in
1680
+ # principle return a different value than the first. The chain is dropped on (1)
1681
+ # receiver variable rebind (handled inside `Scope#with_local` / `#with_ivar`), and (2)
1682
+ # any intervening call against the same root receiver (handled by
1683
+ # `StatementEvaluator#eval_call`'s invalidation step). The Law of Demeter justifies the
1684
+ # single-hop restriction: a single-hop chain is the idiomatic Ruby shape where
1685
+ # re-evaluation soundness is the strongest.
1842
1686
  def analyse_class_predicate_on_chain(node, scope, class_name, exact)
1843
1687
  address = stable_chain_address(node.receiver)
1844
1688
  return nil if address.nil?
@@ -1855,12 +1699,10 @@ module Rigor
1855
1699
  ]
1856
1700
  end
1857
1701
 
1858
- # Returns `[receiver_kind, receiver_name, method_name]`
1859
- # iff `chain_call` is a stable single-hop chain whose
1860
- # root is a local / ivar read and whose own call shape
1861
- # has no positional arguments and no block. Other shapes
1862
- # (multi-hop, args, block, method-defined-on-arbitrary-
1863
- # receiver) lose stability for one of the reasons
1702
+ # Returns `[receiver_kind, receiver_name, method_name]` iff `chain_call` is a stable
1703
+ # single-hop chain whose root is a local / ivar read and whose own call shape has no
1704
+ # positional arguments and no block. Other shapes (multi-hop, args, block,
1705
+ # method-defined-on-arbitrary-receiver) lose stability for one of the reasons
1864
1706
  # enumerated in the slice's design notes.
1865
1707
  def stable_chain_address(chain_call)
1866
1708
  return nil unless chain_call.is_a?(Prism::CallNode)
@@ -1877,13 +1719,10 @@ module Rigor
1877
1719
  end
1878
1720
  end
1879
1721
 
1880
- # Walks the lexical-nesting chain derived from
1881
- # `scope.self_type` and returns the first
1882
- # `<prefix>::<bare_name>` (or bare `<bare_name>` at the
1883
- # top level) that the environment recognises. Falls back
1884
- # to `bare_name` itself when nothing in the chain
1885
- # resolves; the downstream `narrow_class` then yields
1886
- # the conservative answer for unknown receivers.
1722
+ # Walks the lexical-nesting chain derived from `scope.self_type` and returns the first
1723
+ # `<prefix>::<bare_name>` (or bare `<bare_name>` at the top level) that the environment
1724
+ # recognises. Falls back to `bare_name` itself when nothing in the chain resolves; the
1725
+ # downstream `narrow_class` then yields the conservative answer for unknown receivers.
1887
1726
  def resolve_class_name_lexically(bare_name, scope)
1888
1727
  return bare_name if bare_name.include?("::") # Already qualified.
1889
1728
 
@@ -1895,9 +1734,8 @@ module Rigor
1895
1734
  bare_name
1896
1735
  end
1897
1736
 
1898
- # Combines the environment's RBS-known set with the
1899
- # scope's in-source `discovered_classes` table so a
1900
- # lexical-nesting candidate matches a class the project
1737
+ # Combines the environment's RBS-known set with the scope's in-source
1738
+ # `discovered_classes` table so a lexical-nesting candidate matches a class the project
1901
1739
  # declares but has no RBS for.
1902
1740
  def class_known_to_scope?(scope, candidate)
1903
1741
  return true if scope.environment.class_known?(candidate)
@@ -1905,14 +1743,11 @@ module Rigor
1905
1743
  scope.discovered_classes.key?(candidate)
1906
1744
  end
1907
1745
 
1908
- # Approximates `Module.nesting` from the inferable
1909
- # `self_type`. Today's implementation handles the common
1910
- # case: when the surrounding method is a regular
1911
- # instance method (`self_type = Nominal[T]`) or a
1912
- # class-body / singleton (`self_type = Singleton[T]`),
1913
- # the chain is `T`'s namespace path — `Foo::Bar::Baz`
1914
- # → `["Foo::Bar::Baz", "Foo::Bar", "Foo"]`. Returns an
1915
- # empty array when `self_type` is unknown.
1746
+ # Approximates `Module.nesting` from the inferable `self_type`. Today's implementation
1747
+ # handles the common case: when the surrounding method is a regular instance method
1748
+ # (`self_type = Nominal[T]`) or a class-body / singleton (`self_type = Singleton[T]`),
1749
+ # the chain is `T`'s namespace path — `Foo::Bar::Baz` `["Foo::Bar::Baz", "Foo::Bar",
1750
+ # "Foo"]`. Returns an empty array when `self_type` is unknown.
1916
1751
  def lexical_nesting_for(scope)
1917
1752
  self_type = scope.self_type
1918
1753
  base = case self_type
@@ -1937,33 +1772,24 @@ module Rigor
1937
1772
  ]
1938
1773
  end
1939
1774
 
1940
- # Slice 7 phase 4 — `===`-narrowing. The case-equality
1941
- # predicate `<receiver> === local` is the operator that
1942
- # backs Ruby's `case`/`when` dispatch. Three receiver
1943
- # shapes produce sound narrowing rules:
1775
+ # Slice 7 phase 4 — `===`-narrowing. The case-equality predicate `<receiver> === local`
1776
+ # is the operator that backs Ruby's `case`/`when` dispatch. Three receiver shapes
1777
+ # produce sound narrowing rules:
1944
1778
  #
1945
- # - **Class / Module receiver**: `Foo === x` is
1946
- # isomorphic to `x.is_a?(Foo)` (the default behaviour
1947
- # of `Module#===`). Reuse `class_predicate_scopes` so
1948
- # the truthy edge narrows down to `Foo` and the falsey
1949
- # edge subtracts `Foo` from a union.
1950
- # - **Range literal receiver** (`(1..10) === x`): the
1951
- # default `Range#===` includes `x` iff the endpoints
1952
- # compare it. We conservatively narrow `x` to
1953
- # `Numeric` for integer-endpoint ranges and to
1954
- # `String` for string-endpoint ranges; other endpoint
1779
+ # - **Class / Module receiver**: `Foo === x` is isomorphic to `x.is_a?(Foo)` (the
1780
+ # default behaviour of `Module#===`). Reuse `class_predicate_scopes` so the truthy
1781
+ # edge narrows down to `Foo` and the falsey edge subtracts `Foo` from a union.
1782
+ # - **Range literal receiver** (`(1..10) === x`): the default `Range#===` includes `x`
1783
+ # iff the endpoints compare it. We conservatively narrow `x` to `Numeric` for
1784
+ # integer-endpoint ranges and to `String` for string-endpoint ranges; other endpoint
1955
1785
  # types fall through.
1956
- # - **Regexp literal receiver** (`/foo/ === x`): the
1957
- # match operator coerces `x` to a String, so the
1958
- # truthy edge narrows `x` to `String`. The falsey
1959
- # edge keeps the entry type unchanged because
1960
- # `Regexp#===` returns false for non-Strings AND for
1961
- # Strings that simply do not match.
1786
+ # - **Regexp literal receiver** (`/foo/ === x`): the match operator coerces `x` to a
1787
+ # String, so the truthy edge narrows `x` to `String`. The falsey edge keeps the entry
1788
+ # type unchanged because `Regexp#===` returns false for non-Strings AND for Strings
1789
+ # that simply do not match.
1962
1790
  #
1963
- # Anything else — non-local LHS argument, dynamic
1964
- # receiver, custom `===` method falls through to
1965
- # the no-narrowing branch (nil), preserving the
1966
- # entry scope on both edges.
1791
+ # Anything else — non-local LHS argument, dynamic receiver, custom `===` method — falls
1792
+ # through to the no-narrowing branch (nil), preserving the entry scope on both edges.
1967
1793
  def analyse_case_equality_predicate(node, scope)
1968
1794
  return nil if node.arguments.nil?
1969
1795
  return nil unless node.arguments.arguments.size == 1
@@ -1980,14 +1806,12 @@ module Rigor
1980
1806
  end
1981
1807
  end
1982
1808
 
1983
- # `Class === <local/ivar>.<method>` — the case-equality counterpart
1984
- # of {.analyse_class_predicate_on_chain}. The `open3` idiom
1985
- # `if Hash === cmd.last` narrows `cmd.last` to the class inside the
1986
- # branch, recorded as a single-hop method-chain narrowing keyed on
1987
- # the chain address (same stability rules as `is_a?` on a chain).
1988
- # Only static class/module receivers narrow here the Range /
1989
- # Regexp literal receivers (`(1..10) === x.foo`) are not a common
1990
- # method-chain shape and stay deferred.
1809
+ # `Class === <local/ivar>.<method>` — the case-equality counterpart of
1810
+ # {.analyse_class_predicate_on_chain}. The `open3` idiom `if Hash === cmd.last` narrows
1811
+ # `cmd.last` to the class inside the branch, recorded as a single-hop method-chain
1812
+ # narrowing keyed on the chain address (same stability rules as `is_a?` on a chain).
1813
+ # Only static class/module receivers narrow here the Range / Regexp literal receivers
1814
+ # (`(1..10) === x.foo`) are not a common method-chain shape and stay deferred.
1991
1815
  def analyse_case_equality_on_chain(receiver, chain_arg, scope)
1992
1816
  class_name = static_class_name(receiver)
1993
1817
  return nil if class_name.nil?
@@ -2021,14 +1845,11 @@ module Rigor
2021
1845
  ]
2022
1846
  end
2023
1847
 
2024
- # Maps a case-equality literal receiver to the class
2025
- # whose membership is implied by the truthy edge.
2026
- # `Prism::ParenthesesNode` wrappers are transparently
2027
- # unwrapped (`(1..10) === x` is parsed with the range
2028
- # inside parentheses). Range literals: integer
2029
- # endpoints → `Numeric`; string endpoints → `String`.
2030
- # Regexp literals → `String`. Other shapes return nil
2031
- # so the caller falls through.
1848
+ # Maps a case-equality literal receiver to the class whose membership is implied by the
1849
+ # truthy edge. `Prism::ParenthesesNode` wrappers are transparently unwrapped (`(1..10)
1850
+ # === x` is parsed with the range inside parentheses). Range literals: integer
1851
+ # endpoints `Numeric`; string endpoints → `String`. Regexp literals `String`. Other
1852
+ # shapes return nil so the caller falls through.
2032
1853
  def case_equality_target_class(receiver)
2033
1854
  receiver = unwrap_parens(receiver)
2034
1855
  case receiver
@@ -2045,34 +1866,24 @@ module Rigor
2045
1866
  node
2046
1867
  end
2047
1868
 
2048
- # Slice 4b-1 (ADR-7 § "Slice 4-A/4-B") — single-point
2049
- # RBS::Extended contribution analyser. Replaces the
2050
- # earlier sibling pair (`analyse_rbs_extended_predicate`
2051
- # + `analyse_rbs_extended_assert_if`) with one path that
2052
- # routes through `RbsExtended.read_flow_contribution` and
2053
- # `Rigor::FlowContribution::Merger.merge`. The bundle's
2054
- # `truthy_facts` slot already includes both the
2055
- # `predicate-if-true` and `assert-if-true` Facts (slice
2056
- # 4a routing closed the v0.0.9 imperfection); the
2057
- # `falsey_facts` slot mirrors that. The merger composes
2058
- # any future plugin contribution at the same site
2059
- # alongside the RBS::Extended bundle without changing
2060
- # this analyser.
1869
+ # Slice 4b-1 (ADR-7 § "Slice 4-A/4-B") — single-point RBS::Extended contribution
1870
+ # analyser. Replaces the earlier sibling pair (`analyse_rbs_extended_predicate` +
1871
+ # `analyse_rbs_extended_assert_if`) with one path that routes through
1872
+ # `RbsExtended.read_flow_contribution` and `Rigor::FlowContribution::Merger.merge`. The
1873
+ # bundle's `truthy_facts` slot already includes both the `predicate-if-true` and
1874
+ # `assert-if-true` Facts (slice 4a routing closed the v0.0.9 imperfection); the
1875
+ # `falsey_facts` slot mirrors that. The merger composes any future plugin contribution
1876
+ # at the same site alongside the RBS::Extended bundle without changing this analyser.
2061
1877
  #
2062
- # Conservative envelope (carried over from the previous
2063
- # implementation):
2064
- # - Receiver type must be `Type::Nominal`,
2065
- # `Type::Singleton`, or `Type::Constant`.
1878
+ # Conservative envelope (carried over from the previous implementation):
1879
+ # - Receiver type must be `Type::Nominal`, `Type::Singleton`, or `Type::Constant`.
2066
1880
  # - The method must be present in the loader.
2067
- # - For each fact, the corresponding positional
2068
- # argument (matched by parameter name in the selected
2069
- # overload) MUST be a `Prism::LocalVariableReadNode`
2070
- # for narrowing to apply.
2071
- # - When the target is `self`, narrowing applies to the
2072
- # receiver — but the engine does not yet narrow
2073
- # `self` itself, so `self`-targeted facts are
2074
- # accepted by the merger but currently produce no
2075
- # scope edits.
1881
+ # - For each fact, the corresponding positional argument (matched by parameter name in
1882
+ # the selected overload) MUST be a `Prism::LocalVariableReadNode` for narrowing to
1883
+ # apply.
1884
+ # - When the target is `self`, narrowing applies to the receiver — but the engine does
1885
+ # not yet narrow `self` itself, so `self`-targeted facts are accepted by the merger
1886
+ # but currently produce no scope edits.
2076
1887
  def analyse_rbs_extended_contribution(node, scope)
2077
1888
  method_def = resolve_rbs_extended_method(node, scope)
2078
1889
  return nil if method_def.nil?
@@ -2113,10 +1924,9 @@ module Rigor
2113
1924
  target_scope.with_local(local_name, narrowed)
2114
1925
  end
2115
1926
 
2116
- # v0.1.1 Track 1 slice 3 — `target_kind == :self` facts
2117
- # (`predicate-if-true self is T`, `predicate-if-false
2118
- # self is T`) narrow whichever scope binding is bound to
2119
- # the call's receiver. Four receiver shapes participate:
1927
+ # v0.1.1 Track 1 slice 3 — `target_kind == :self` facts (`predicate-if-true self is T`,
1928
+ # `predicate-if-false self is T`) narrow whichever scope binding is bound to the call's
1929
+ # receiver. Four receiver shapes participate:
2120
1930
  #
2121
1931
  # `recv.method?` where recv is...
2122
1932
  # - `LocalVariableReadNode` -> narrow that local
@@ -2125,8 +1935,8 @@ module Rigor
2125
1935
  # - nil (implicit self call inside a method body)
2126
1936
  # -> narrow `self_type`
2127
1937
  #
2128
- # Other receiver shapes (method chains, expressions) have
2129
- # no scope binding to narrow against and fall through.
1938
+ # Other receiver shapes (method chains, expressions) have no scope binding to narrow
1939
+ # against and fall through.
2130
1940
  def apply_self_fact(fact, receiver_node, entry_scope, target_scope)
2131
1941
  case receiver_node
2132
1942
  when nil, Prism::SelfNode
@@ -2152,11 +1962,9 @@ module Rigor
2152
1962
  end
2153
1963
  end
2154
1964
 
2155
- # Resolves a Fact's target node. Mirrors the earlier
2156
- # `effect_target_node` helper but reads from the
2157
- # canonical Fact carrier; `:self` routes to the call
2158
- # receiver, otherwise we look up the matching
2159
- # positional argument by parameter name.
1965
+ # Resolves a Fact's target node. Mirrors the earlier `effect_target_node` helper but
1966
+ # reads from the canonical Fact carrier; `:self` routes to the call receiver, otherwise
1967
+ # we look up the matching positional argument by parameter name.
2160
1968
  def fact_target_node(fact, call_node, method_def)
2161
1969
  if fact.target_kind == :self
2162
1970
  call_node.receiver
@@ -2182,9 +1990,8 @@ module Rigor
2182
1990
  nil
2183
1991
  end
2184
1992
 
2185
- # Implicit self call (`admin?` with no receiver inside an
2186
- # instance method body) read the method definition from
2187
- # `scope.self_type` per v0.1.1 Track 1 slice 3.
1993
+ # Implicit self call (`admin?` with no receiver inside an instance method body) — read
1994
+ # the method definition from `scope.self_type` per v0.1.1 Track 1 slice 3.
2188
1995
  def receiver_type_for_resolve(node, scope)
2189
1996
  node.receiver.nil? ? scope.self_type : scope.type_of(node.receiver)
2190
1997
  end
@@ -2209,11 +2016,9 @@ module Rigor
2209
2016
  nil
2210
2017
  end
2211
2018
 
2212
- # Maps the effect's target parameter name to the call
2213
- # site argument by inspecting the selected overload's
2214
- # required-positional parameter list. Returns the Prism
2215
- # arg node at that position, or nil when the overload
2216
- # shape does not allow a precise match.
2019
+ # Maps the effect's target parameter name to the call site argument by inspecting the
2020
+ # selected overload's required-positional parameter list. Returns the Prism arg node at
2021
+ # that position, or nil when the overload shape does not allow a precise match.
2217
2022
  def lookup_positional_arg(call_node, method_def, target_name)
2218
2023
  arguments = call_node.arguments&.arguments || []
2219
2024
  method_def.method_types.each do |mt|
@@ -2224,18 +2029,13 @@ module Rigor
2224
2029
  nil
2225
2030
  end
2226
2031
 
2227
- # Slice 7 phase 5 — case/when accumulator. Walks each
2228
- # `when` condition, computes the narrowed type for the
2229
- # subject as if `condition === subject`, and accumulates
2230
- # them. The body's narrowed type is the union across
2231
- # all conditions; the falsey type is the running result
2232
- # after subtracting every condition's class. Conditions
2233
- # whose shape we cannot statically classify are treated
2234
- # as "no narrowing": the body falls back to the union of
2235
- # what we did learn (or the entry type when nothing
2236
- # learned), and the falsey edge is the entry type
2237
- # (because we cannot prove the unknown condition didn't
2238
- # match).
2032
+ # Slice 7 phase 5 — case/when accumulator. Walks each `when` condition, computes the
2033
+ # narrowed type for the subject as if `condition === subject`, and accumulates them.
2034
+ # The body's narrowed type is the union across all conditions; the falsey type is the
2035
+ # running result after subtracting every condition's class. Conditions whose shape we
2036
+ # cannot statically classify are treated as "no narrowing": the body falls back to the
2037
+ # union of what we did learn (or the entry type when nothing learned), and the falsey
2038
+ # edge is the entry type (because we cannot prove the unknown condition didn't match).
2239
2039
  def accumulate_case_when_scopes(scope, local_name, current, conditions)
2240
2040
  truthy_members = []
2241
2041
  falsey_type = current
@@ -2259,9 +2059,9 @@ module Rigor
2259
2059
  ]
2260
2060
  end
2261
2061
 
2262
- # Per-condition rule. Returns `nil` when the condition shape
2263
- # is not recognised (caller marks `fully_narrowable = false`),
2264
- # or `{truthy:, falsey:, fully_narrowable:}` when it is.
2062
+ # Per-condition rule. Returns `nil` when the condition shape is not recognised (caller
2063
+ # marks `fully_narrowable = false`), or `{truthy:, falsey:, fully_narrowable:}` when it
2064
+ # is.
2265
2065
  def apply_case_when_condition(scope, current, condition, falsey_acc)
2266
2066
  int_range = case_equality_integer_range(condition)
2267
2067
  return integer_range_when_result(current, int_range, falsey_acc) if int_range && integer_rooted_type?(current)
@@ -2283,11 +2083,10 @@ module Rigor
2283
2083
  end
2284
2084
 
2285
2085
  def integer_literal_when_result(current, value, falsey_acc)
2286
- # `case n when k` is `k === n` which for Integer is value
2287
- # equality. The truthy edge collapses the local to
2288
- # `Constant[k]`; the falsey edge tightens via
2289
- # `narrow_integer_not_equal` (only effective when k sits
2290
- # at one endpoint of the current range).
2086
+ # `case n when k` is `k === n` which for Integer is value equality. The truthy edge
2087
+ # collapses the local to `Constant[k]`; the falsey edge tightens via
2088
+ # `narrow_integer_not_equal` (only effective when k sits at one endpoint of the
2089
+ # current range).
2291
2090
  {
2292
2091
  truthy: narrow_integer_equal(current, value),
2293
2092
  falsey: narrow_integer_not_equal(falsey_acc, value),
@@ -2301,10 +2100,9 @@ module Rigor
2301
2100
  narrow_integer_comparison(current, :>=, low),
2302
2101
  :<=, high
2303
2102
  )
2304
- # The falsey edge of `n in [a, b]` is two-piece; we cannot
2305
- # express the complement precisely with a single carrier,
2306
- # so keep the accumulator unchanged. `fully_narrowable: false`
2307
- # forces the else-branch to see `current` (the unmodified
2103
+ # The falsey edge of `n in [a, b]` is two-piece; we cannot express the complement
2104
+ # precisely with a single carrier, so keep the accumulator unchanged.
2105
+ # `fully_narrowable: false` forces the else-branch to see `current` (the unmodified
2308
2106
  # entry type), which mirrors `between?` falsey behaviour.
2309
2107
  { truthy: truthy, falsey: falsey_acc, fully_narrowable: false }
2310
2108
  end
@@ -2317,12 +2115,10 @@ module Rigor
2317
2115
  }
2318
2116
  end
2319
2117
 
2320
- # Returns `[low, high]` for a `Prism::RangeNode` whose
2321
- # endpoints are both `Prism::IntegerNode` literals, with
2322
- # `..`/`...` exclusivity respected. Open-ended ranges use
2323
- # the symbolic infinities so the existing comparison
2324
- # narrowing tier handles them. Returns `nil` for any other
2325
- # shape (Float endpoints, String endpoints, dynamic
2118
+ # Returns `[low, high]` for a `Prism::RangeNode` whose endpoints are both
2119
+ # `Prism::IntegerNode` literals, with `..`/`...` exclusivity respected. Open-ended
2120
+ # ranges use the symbolic infinities so the existing comparison narrowing tier handles
2121
+ # them. Returns `nil` for any other shape (Float endpoints, String endpoints, dynamic
2326
2122
  # expressions).
2327
2123
  def case_equality_integer_range(condition)
2328
2124
  condition = unwrap_parens(condition)
@@ -2369,9 +2165,8 @@ module Rigor
2369
2165
  node.is_a?(Prism::StringNode)
2370
2166
  end
2371
2167
 
2372
- # Walks a constant-reference subtree (`Prism::ConstantReadNode`,
2373
- # `Prism::ConstantPathNode`) and renders its qualified name.
2374
- # Returns nil for any non-constant argument shape so the
2168
+ # Walks a constant-reference subtree (`Prism::ConstantReadNode`, `Prism::ConstantPathNode`)
2169
+ # and renders its qualified name. Returns nil for any non-constant argument shape so the
2375
2170
  # caller can fall through.
2376
2171
  def static_class_name(node)
2377
2172
  case node
@@ -2390,11 +2185,10 @@ module Rigor
2390
2185
 
2391
2186
  # ----- narrow_class / narrow_not_class helpers -----
2392
2187
 
2393
- # Polarity-aware dispatch table for {.narrow_class} /
2394
- # {.narrow_not_class}. Avoids duplicating the per-carrier
2395
- # case statement and keeps each public surface a thin
2396
- # delegate; the per-carrier helpers know which polarity to
2397
- # apply by looking at `polarity:`.
2188
+ # Polarity-aware dispatch table for {.narrow_class} / {.narrow_not_class}. Avoids
2189
+ # duplicating the per-carrier case statement and keeps each public surface a thin
2190
+ # delegate; the per-carrier helpers know which polarity to apply by looking at
2191
+ # `polarity:`.
2398
2192
  def narrow_class_dispatch(type, class_name, context)
2399
2193
  case type
2400
2194
  when Type::Constant then narrow_constant_class(type, class_name, context)
@@ -2459,15 +2253,12 @@ module Rigor
2459
2253
  subclass_of?(rigor_class, class_name, context) ? Type::Combinator.bot : constant
2460
2254
  end
2461
2255
 
2462
- # Narrow a Nominal under `is_a?(class_name)`: when the
2463
- # nominal's class is already a subclass of `class_name`
2464
- # (or matches under `exact: true`) preserve it; when
2465
- # `class_name` is a subclass of the nominal's class
2466
- # (`Nominal[Numeric]` under `is_a?(Integer)`) narrow DOWN
2467
- # to `Nominal[class_name]`; otherwise (disjoint hierarchies
2468
- # under `is_a?`, mismatch under `instance_of?`) collapse to
2469
- # `Bot`. Conservative when the analyzer environment cannot
2470
- # resolve either class.
2256
+ # Narrow a Nominal under `is_a?(class_name)`: when the nominal's class is already a
2257
+ # subclass of `class_name` (or matches under `exact: true`) preserve it; when
2258
+ # `class_name` is a subclass of the nominal's class (`Nominal[Numeric]` under
2259
+ # `is_a?(Integer)`) narrow DOWN to `Nominal[class_name]`; otherwise (disjoint
2260
+ # hierarchies under `is_a?`, mismatch under `instance_of?`) collapse to `Bot`.
2261
+ # Conservative when the analyzer environment cannot resolve either class.
2471
2262
  def narrow_nominal_to_class(nominal, class_name, context)
2472
2263
  return nominal if nominal.class_name == class_name
2473
2264
  return Type::Combinator.bot if context.exact
@@ -2498,11 +2289,10 @@ module Rigor
2498
2289
  subclass_of?(projected_class, class_name, context) ? Type::Combinator.bot : shape
2499
2290
  end
2500
2291
 
2501
- # `Singleton[Foo]` is the *class object* `Foo`, an instance of
2502
- # `Class` (which is a subclass of `Module`). Asking
2503
- # `Foo.is_a?(Class)` returns true; `Foo.is_a?(Foo)` returns
2504
- # false unless `Foo` is `Class` itself. We approximate this
2505
- # by treating singletons uniformly as `Class` instances.
2292
+ # `Singleton[Foo]` is the *class object* `Foo`, an instance of `Class` (which is a
2293
+ # subclass of `Module`). Asking `Foo.is_a?(Class)` returns true; `Foo.is_a?(Foo)`
2294
+ # returns false unless `Foo` is `Class` itself. We approximate this by treating
2295
+ # singletons uniformly as `Class` instances.
2506
2296
  def narrow_singleton_to_class(singleton, class_name, context)
2507
2297
  subclass_of?("Class", class_name, context) ? singleton : Type::Combinator.bot
2508
2298
  end
@@ -2511,8 +2301,8 @@ module Rigor
2511
2301
  subclass_of?("Class", class_name, context) ? Type::Combinator.bot : singleton
2512
2302
  end
2513
2303
 
2514
- # Top/Dynamic narrow to `Nominal[class_name]` so dispatch
2515
- # can resolve through the asked class; Bot stays Bot.
2304
+ # Top/Dynamic narrow to `Nominal[class_name]` so dispatch can resolve through the asked
2305
+ # class; Bot stays Bot.
2516
2306
  def narrow_class_other(type, class_name)
2517
2307
  case type
2518
2308
  when Type::Dynamic, Type::Top then Type::Combinator.nominal_of(class_name)
@@ -2520,11 +2310,10 @@ module Rigor
2520
2310
  end
2521
2311
  end
2522
2312
 
2523
- # Returns `true` when an instance of `rigor_class_name`
2524
- # satisfies `is_a?(target_class_name)` (or
2525
- # `instance_of?(target_class_name)` when `exact: true`).
2526
- # Falls back to the safe `false` when either name does not
2527
- # resolve through the analyzer environment.
2313
+ # Returns `true` when an instance of `rigor_class_name` satisfies
2314
+ # `is_a?(target_class_name)` (or `instance_of?(target_class_name)` when `exact: true`).
2315
+ # Falls back to the safe `false` when either name does not resolve through the analyzer
2316
+ # environment.
2528
2317
  def subclass_of?(rigor_class_name, target_class_name, context)
2529
2318
  return rigor_class_name == target_class_name if context.exact
2530
2319
 
@@ -2533,11 +2322,9 @@ module Rigor
2533
2322
  )
2534
2323
  end
2535
2324
 
2536
- # Compares two class names through the analyzer environment.
2537
- # Returns `:equal` when they resolve to the same class,
2538
- # `:subclass` when `lhs <= rhs`, `:superclass` when
2539
- # `rhs <= lhs`, `:disjoint` when neither, and `:unknown` when
2540
- # either name does not resolve.
2325
+ # Compares two class names through the analyzer environment. Returns `:equal` when they
2326
+ # resolve to the same class, `:subclass` when `lhs <= rhs`, `:superclass` when `rhs <=
2327
+ # lhs`, `:disjoint` when neither, and `:unknown` when either name does not resolve.
2541
2328
  def class_ordering(lhs, rhs, context)
2542
2329
  return :equal if lhs == rhs
2543
2330
 
@@ -2565,11 +2352,10 @@ module Rigor
2565
2352
  end
2566
2353
  end
2567
2354
 
2568
- # Narrows a safe-navigation call's receiver (`v&.foo`) to its
2569
- # non-nil fragment on the truthy edge, returning `[truthy, falsey]`
2570
- # or nil when nothing applies (not safe-nav, opaque receiver, or
2571
- # already non-nil). Used standalone for a bare `v&.foo` truthy
2572
- # edge and as a post-pass over the existing predicate edges.
2355
+ # Narrows a safe-navigation call's receiver (`v&.foo`) to its non-nil fragment on the
2356
+ # truthy edge, returning `[truthy, falsey]` or nil when nothing applies (not safe-nav,
2357
+ # opaque receiver, or already non-nil). Used standalone for a bare `v&.foo` truthy edge
2358
+ # and as a post-pass over the existing predicate edges.
2573
2359
  def analyse_safe_nav_receiver(node, scope)
2574
2360
  return nil unless node.safe_navigation?
2575
2361
 
@@ -2590,12 +2376,11 @@ module Rigor
2590
2376
  [scope.public_send(writer, receiver.name, non_nil), scope]
2591
2377
  end
2592
2378
 
2593
- # Layers the safe-nav non-nil truthy narrowing over the edges an
2594
- # existing predicate path already produced, so a safe-nav string
2595
- # predicate (`v&.start_with?(x)`) keeps its relational fact AND
2596
- # proves `v` non-nil on the truthy edge. Re-runs the predicate's
2597
- # narrowing under the non-nil truthy scope so the fact is attached
2598
- # to the narrowed binding. No-op for non-safe-nav calls.
2379
+ # Layers the safe-nav non-nil truthy narrowing over the edges an existing predicate path
2380
+ # already produced, so a safe-nav string predicate (`v&.start_with?(x)`) keeps its
2381
+ # relational fact AND proves `v` non-nil on the truthy edge. Re-runs the predicate's
2382
+ # narrowing under the non-nil truthy scope so the fact is attached to the narrowed
2383
+ # binding. No-op for non-safe-nav calls.
2599
2384
  def apply_safe_nav_non_nil(node, scope, edges)
2600
2385
  return edges unless node.safe_navigation? && edges
2601
2386
 
@@ -2610,23 +2395,20 @@ module Rigor
2610
2395
  [truthy.public_send(writer, receiver.name, non_nil), falsey]
2611
2396
  end
2612
2397
 
2613
- # `a && b` short-circuits: the truthy edge is the truthy edge
2614
- # of `b` evaluated under `a`'s truthy scope; the falsey edge
2615
- # is the union of `a`'s falsey scope (b skipped) and `b`'s
2616
- # falsey scope (b ran but returned falsey). When a sub-edge
2617
- # cannot be narrowed we fall back to the entry scope so the
2618
- # caller still sees consistent keys across the two output
2619
- # scopes.
2398
+ # `a && b` short-circuits: the truthy edge is the truthy edge of `b` evaluated under
2399
+ # `a`'s truthy scope; the falsey edge is the union of `a`'s falsey scope (b skipped) and
2400
+ # `b`'s falsey scope (b ran but returned falsey). When a sub-edge cannot be narrowed we
2401
+ # fall back to the entry scope so the caller still sees consistent keys across the two
2402
+ # output scopes.
2620
2403
  def analyse_and(node, scope)
2621
2404
  truthy_a, falsey_a = analyse(node.left, scope) || [scope, scope]
2622
2405
  truthy_b, falsey_b = analyse(node.right, truthy_a) || [truthy_a, truthy_a]
2623
2406
  [truthy_b, falsey_a.join(falsey_b)]
2624
2407
  end
2625
2408
 
2626
- # `a || b` short-circuits: the truthy edge is the union of
2627
- # `a`'s truthy scope (b skipped) and `b`'s truthy scope (b
2628
- # ran and was truthy); the falsey edge is `b`'s falsey scope
2629
- # evaluated under `a`'s falsey scope.
2409
+ # `a || b` short-circuits: the truthy edge is the union of `a`'s truthy scope (b
2410
+ # skipped) and `b`'s truthy scope (b ran and was truthy); the falsey edge is `b`'s
2411
+ # falsey scope evaluated under `a`'s falsey scope.
2630
2412
  def analyse_or(node, scope)
2631
2413
  truthy_a, falsey_a = analyse(node.left, scope) || [scope, scope]
2632
2414
  truthy_b, falsey_b = analyse(node.right, falsey_a) || [falsey_a, falsey_a]