rigortype 0.3.8 → 0.3.9

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 (447) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/data/capability_roles/capability_roles.rbs +36 -0
  4. data/data/gem_overlay/activesupport/core_ext.rbs +109 -13
  5. data/docs/handbook/02-everyday-types.md +17 -14
  6. data/docs/handbook/03-narrowing.md +37 -4
  7. data/docs/handbook/05-methods-and-blocks.md +1 -1
  8. data/docs/handbook/06-classes.md +2 -2
  9. data/docs/handbook/07-rbs-and-extended.md +14 -1
  10. data/docs/handbook/11-sig-gen.md +72 -1
  11. data/docs/handbook/README.md +1 -1
  12. data/docs/handbook/appendix-elixir.md +2 -2
  13. data/docs/handbook/appendix-go.md +2 -2
  14. data/docs/handbook/appendix-java-csharp.md +2 -2
  15. data/docs/handbook/appendix-mypy.md +2 -2
  16. data/docs/handbook/appendix-phpstan.md +1 -1
  17. data/docs/handbook/appendix-rust.md +1 -1
  18. data/docs/handbook/appendix-type-theory.md +4 -4
  19. data/docs/handbook/appendix-typescript.md +1 -1
  20. data/docs/llms.txt +2 -0
  21. data/docs/manual/02-cli-reference.md +32 -4
  22. data/docs/manual/03-configuration.md +11 -0
  23. data/docs/manual/04-diagnostics.md +11 -0
  24. data/docs/manual/07-plugins.md +15 -5
  25. data/docs/manual/08-skills.md +23 -2
  26. data/docs/manual/11-ci.md +9 -0
  27. data/docs/manual/15-type-protection-coverage.md +8 -0
  28. data/docs/manual/16-rbs-extended-annotations.md +6 -1
  29. data/docs/manual/18-removing-dead-code.md +10 -8
  30. data/docs/manual/plugins/rigor-actionpack.md +18 -0
  31. data/docs/manual/plugins/rigor-activerecord.md +28 -0
  32. data/docs/manual/plugins/rigor-activesupport-core-ext.md +32 -2
  33. data/docs/manual/plugins/rigor-rbs-inline.md +42 -1
  34. data/lib/rigor/analysis/baseline.rb +2 -2
  35. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +1 -1
  36. data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +1 -5
  37. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +10 -2
  38. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +1 -1
  39. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +15 -6
  40. data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
  41. data/lib/rigor/analysis/check_rules/rule_walk.rb +39 -4
  42. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +1 -1
  43. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +1 -1
  44. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +2 -2
  45. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +0 -1
  46. data/lib/rigor/analysis/check_rules.rb +200 -32
  47. data/lib/rigor/analysis/crash_signature.rb +8 -24
  48. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +1 -1
  49. data/lib/rigor/analysis/dependency_source_inference/builder.rb +0 -2
  50. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +0 -2
  51. data/lib/rigor/analysis/dependency_source_inference/index.rb +5 -5
  52. data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +1 -2
  53. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  54. data/lib/rigor/analysis/effects_cache_probe.rb +3 -4
  55. data/lib/rigor/analysis/erb_template_detector.rb +1 -2
  56. data/lib/rigor/analysis/fact_store.rb +6 -1
  57. data/lib/rigor/analysis/incremental.rb +16 -0
  58. data/lib/rigor/analysis/incremental_session.rb +81 -22
  59. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +0 -1
  60. data/lib/rigor/analysis/reachability/graph.rb +3 -5
  61. data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -5
  62. data/lib/rigor/analysis/reachability/project_files.rb +2 -2
  63. data/lib/rigor/analysis/reachability/scan.rb +5 -5
  64. data/lib/rigor/analysis/reachability/scan_cache.rb +2 -2
  65. data/lib/rigor/analysis/reachability/signature_scan.rb +2 -2
  66. data/lib/rigor/analysis/result.rb +1 -3
  67. data/lib/rigor/analysis/rule_catalog.rb +4 -1
  68. data/lib/rigor/analysis/run_cache_key.rb +1 -1
  69. data/lib/rigor/analysis/run_cache_probe.rb +3 -5
  70. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -8
  71. data/lib/rigor/analysis/runner/declaration_position.rb +1 -2
  72. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +177 -26
  73. data/lib/rigor/analysis/runner/effect_annotation_residual_pass.rb +2 -3
  74. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -10
  75. data/lib/rigor/analysis/runner/pool_coordinator.rb +256 -88
  76. data/lib/rigor/analysis/runner/project_pre_passes.rb +12 -6
  77. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  78. data/lib/rigor/analysis/runner.rb +133 -35
  79. data/lib/rigor/analysis/worker_session.rb +9 -10
  80. data/lib/rigor/bleeding_edge.rb +12 -24
  81. data/lib/rigor/builtins/hkt_builtins.rb +2 -2
  82. data/lib/rigor/builtins/imported_refinements.rb +155 -21
  83. data/lib/rigor/builtins/predefined_constant_refinements.rb +5 -7
  84. data/lib/rigor/builtins/regex_refinement.rb +4 -4
  85. data/lib/rigor/builtins/static_return_refinements.rb +5 -6
  86. data/lib/rigor/cache/annotation_location.rb +2 -4
  87. data/lib/rigor/cache/descriptor.rb +58 -20
  88. data/lib/rigor/cache/engine_source.rb +4 -4
  89. data/lib/rigor/cache/file_digest.rb +9 -1
  90. data/lib/rigor/cache/incremental_snapshot.rb +40 -6
  91. data/lib/rigor/cache/rbs_class_ancestor_table.rb +0 -3
  92. data/lib/rigor/cache/rbs_class_type_param_names.rb +0 -3
  93. data/lib/rigor/cache/rbs_constant_table.rb +0 -3
  94. data/lib/rigor/cache/rbs_descriptor.rb +79 -11
  95. data/lib/rigor/cache/rbs_environment.rb +8 -4
  96. data/lib/rigor/cache/rbs_known_class_names.rb +0 -3
  97. data/lib/rigor/cache/store.rb +77 -32
  98. data/lib/rigor/cli/annotate_command.rb +5 -6
  99. data/lib/rigor/cli/check_command.rb +57 -10
  100. data/lib/rigor/cli/check_invocation.rb +6 -11
  101. data/lib/rigor/cli/check_runner_factory.rb +1 -5
  102. data/lib/rigor/cli/coverage_command.rb +1 -1
  103. data/lib/rigor/cli/coverage_mutation.rb +12 -3
  104. data/lib/rigor/cli/coverage_scan.rb +1 -5
  105. data/lib/rigor/cli/diff_command.rb +1 -1
  106. data/lib/rigor/cli/doc_links.rb +3 -3
  107. data/lib/rigor/cli/docs_command.rb +2 -2
  108. data/lib/rigor/cli/doctor_command.rb +85 -38
  109. data/lib/rigor/cli/effects_command.rb +2 -2
  110. data/lib/rigor/cli/effects_diff_renderer.rb +3 -3
  111. data/lib/rigor/cli/effects_snapshot_command.rb +1 -1
  112. data/lib/rigor/cli/explain_command.rb +34 -1
  113. data/lib/rigor/cli/fused_protection_report.rb +7 -1
  114. data/lib/rigor/cli/lsp_command.rb +1 -1
  115. data/lib/rigor/cli/mcp_command.rb +1 -1
  116. data/lib/rigor/cli/measurement_integrity_warning.rb +4 -5
  117. data/lib/rigor/cli/mutation_fork_scan.rb +6 -6
  118. data/lib/rigor/cli/mutation_protection_report.rb +7 -1
  119. data/lib/rigor/cli/plugin_command.rb +1 -1
  120. data/lib/rigor/cli/plugins_command.rb +2 -1
  121. data/lib/rigor/cli/prism_colorizer.rb +2 -2
  122. data/lib/rigor/cli/protection_fork_scan.rb +6 -6
  123. data/lib/rigor/cli/show_bleedingedge_command.rb +1 -1
  124. data/lib/rigor/cli/sig_gen_command.rb +2 -2
  125. data/lib/rigor/cli/skill_command.rb +1 -1
  126. data/lib/rigor/cli/skill_deep_probe.rb +4 -4
  127. data/lib/rigor/cli/skill_describe.rb +36 -35
  128. data/lib/rigor/cli/trace_command.rb +4 -4
  129. data/lib/rigor/cli/trace_renderer.rb +5 -6
  130. data/lib/rigor/cli/triage_command.rb +1 -1
  131. data/lib/rigor/cli/type_of_command.rb +10 -6
  132. data/lib/rigor/cli/type_scan_command.rb +4 -4
  133. data/lib/rigor/cli/unused_command.rb +11 -3
  134. data/lib/rigor/cli/upgrade_command.rb +1 -1
  135. data/lib/rigor/cli.rb +68 -6
  136. data/lib/rigor/config_audit.rb +30 -5
  137. data/lib/rigor/configuration/severity_profile.rb +6 -6
  138. data/lib/rigor/configuration.rb +43 -4
  139. data/lib/rigor/effects/attribution.rb +1 -3
  140. data/lib/rigor/effects/config_envelopes.rb +6 -8
  141. data/lib/rigor/effects/definition_lines.rb +25 -6
  142. data/lib/rigor/effects/effect_table.rb +0 -1
  143. data/lib/rigor/effects/entry_points.rb +0 -2
  144. data/lib/rigor/effects/envelope_check.rb +8 -8
  145. data/lib/rigor/effects/envelope_index.rb +5 -8
  146. data/lib/rigor/effects/framework_units.rb +4 -6
  147. data/lib/rigor/effects/identity.rb +4 -6
  148. data/lib/rigor/effects/inline_anchor.rb +7 -7
  149. data/lib/rigor/effects/label_intent.rb +3 -4
  150. data/lib/rigor/effects/liskov_check.rb +8 -8
  151. data/lib/rigor/effects/method_key.rb +1 -1
  152. data/lib/rigor/effects/plugin_facts.rb +4 -6
  153. data/lib/rigor/effects/propagator.rb +2 -3
  154. data/lib/rigor/effects/scanner.rb +1 -1
  155. data/lib/rigor/effects/signature_sources.rb +3 -5
  156. data/lib/rigor/effects/snapshot.rb +10 -10
  157. data/lib/rigor/effects/snapshot_diff.rb +1 -1
  158. data/lib/rigor/effects/unit_scan.rb +10 -10
  159. data/lib/rigor/effects/unknown_label_check.rb +3 -8
  160. data/lib/rigor/effects/unknown_label_report.rb +3 -4
  161. data/lib/rigor/environment/bundle_sig_discovery.rb +6 -6
  162. data/lib/rigor/environment/class_registry.rb +3 -1
  163. data/lib/rigor/environment/failure_slot.rb +2 -2
  164. data/lib/rigor/environment/installed_gem_set.rb +85 -0
  165. data/lib/rigor/environment/lockfile_resolver.rb +51 -4
  166. data/lib/rigor/environment/missing_gem_constant_index.rb +4 -4
  167. data/lib/rigor/environment/rbs_collection_discovery.rb +5 -5
  168. data/lib/rigor/environment/rbs_coverage_report.rb +5 -5
  169. data/lib/rigor/environment/rbs_hierarchy.rb +3 -1
  170. data/lib/rigor/environment/rbs_loader.rb +388 -59
  171. data/lib/rigor/environment.rb +105 -20
  172. data/lib/rigor/flow_contribution/merger.rb +0 -2
  173. data/lib/rigor/flow_contribution.rb +11 -13
  174. data/lib/rigor/inference/acceptance.rb +153 -8
  175. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  176. data/lib/rigor/inference/body_fixpoint.rb +5 -5
  177. data/lib/rigor/inference/budget_trace.rb +1 -5
  178. data/lib/rigor/inference/captured_locals.rb +2 -3
  179. data/lib/rigor/inference/closure_escape_analyzer.rb +2 -4
  180. data/lib/rigor/inference/coverage_scanner.rb +4 -6
  181. data/lib/rigor/inference/dynamic_origin.rb +1 -1
  182. data/lib/rigor/inference/element_read_widening.rb +168 -0
  183. data/lib/rigor/inference/expression_typer.rb +763 -143
  184. data/lib/rigor/inference/fork_map.rb +5 -7
  185. data/lib/rigor/inference/hkt_reducer.rb +2 -3
  186. data/lib/rigor/inference/hkt_registry.rb +4 -7
  187. data/lib/rigor/inference/index_write_widening.rb +1 -5
  188. data/lib/rigor/inference/macro_block_self_type.rb +1 -4
  189. data/lib/rigor/inference/method_dispatcher/block_folding.rb +2 -9
  190. data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +1 -1
  191. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +305 -19
  192. data/lib/rigor/inference/method_dispatcher/data_folding.rb +1 -1
  193. data/lib/rigor/inference/method_dispatcher/file_folding.rb +1 -1
  194. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +2 -2
  195. data/lib/rigor/inference/method_dispatcher/json_folding.rb +1 -1
  196. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -1
  197. data/lib/rigor/inference/method_dispatcher/math_folding.rb +41 -3
  198. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -5
  199. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +61 -47
  200. data/lib/rigor/inference/method_dispatcher/random_folding.rb +82 -0
  201. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +165 -31
  202. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +25 -4
  203. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +2 -8
  204. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +1 -1
  205. data/lib/rigor/inference/method_dispatcher/set_folding.rb +1 -1
  206. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +1 -1
  207. data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +1 -1
  208. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +1 -2
  209. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +4 -5
  210. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +85 -4
  211. data/lib/rigor/inference/method_dispatcher/time_folding.rb +1 -1
  212. data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +1 -2
  213. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  214. data/lib/rigor/inference/method_dispatcher.rb +12 -13
  215. data/lib/rigor/inference/method_parameter_binder.rb +4 -6
  216. data/lib/rigor/inference/multi_target_binder.rb +1 -3
  217. data/lib/rigor/inference/mutation_rejoin.rb +168 -0
  218. data/lib/rigor/inference/mutation_widening.rb +100 -51
  219. data/lib/rigor/inference/narrowing.rb +299 -46
  220. data/lib/rigor/inference/optimistic_origin.rb +1 -7
  221. data/lib/rigor/inference/origin_lookup.rb +2 -3
  222. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  223. data/lib/rigor/inference/pre_eval_constants.rb +6 -5
  224. data/lib/rigor/inference/precision_scanner.rb +3 -4
  225. data/lib/rigor/inference/project_patched_methods.rb +2 -2
  226. data/lib/rigor/inference/project_patched_scanner.rb +3 -3
  227. data/lib/rigor/inference/protection_scanner.rb +1 -2
  228. data/lib/rigor/inference/range_constant.rb +57 -0
  229. data/lib/rigor/inference/rbs_type_translator.rb +49 -11
  230. data/lib/rigor/inference/receiver_alias.rb +3 -3
  231. data/lib/rigor/inference/refinement_mutation.rb +72 -0
  232. data/lib/rigor/inference/scope_indexer.rb +688 -151
  233. data/lib/rigor/inference/statement_evaluator.rb +221 -50
  234. data/lib/rigor/inference/string_mutation.rb +60 -0
  235. data/lib/rigor/inference/struct_fold_safety.rb +6 -7
  236. data/lib/rigor/inference/synthetic_method_index.rb +1 -2
  237. data/lib/rigor/inference/synthetic_method_scanner.rb +4 -6
  238. data/lib/rigor/inference/version_guard.rb +28 -21
  239. data/lib/rigor/inference/void_origin.rb +3 -3
  240. data/lib/rigor/inference/void_tail_summary.rb +2 -4
  241. data/lib/rigor/language_server/buffer_table.rb +4 -4
  242. data/lib/rigor/language_server/completion_provider.rb +1 -1
  243. data/lib/rigor/language_server/debouncer.rb +1 -1
  244. data/lib/rigor/language_server/diagnostic_publisher.rb +4 -4
  245. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  246. data/lib/rigor/language_server/folding_range_provider.rb +1 -1
  247. data/lib/rigor/language_server/hover_provider.rb +1 -1
  248. data/lib/rigor/language_server/hover_renderer.rb +2 -2
  249. data/lib/rigor/language_server/incremental_sync.rb +6 -6
  250. data/lib/rigor/language_server/project_context.rb +2 -5
  251. data/lib/rigor/language_server/publish_batcher.rb +2 -2
  252. data/lib/rigor/language_server/selection_range_provider.rb +2 -2
  253. data/lib/rigor/language_server/server.rb +9 -9
  254. data/lib/rigor/language_server/signature_help_provider.rb +1 -1
  255. data/lib/rigor/language_server/uri.rb +1 -1
  256. data/lib/rigor/plugin/base.rb +90 -21
  257. data/lib/rigor/plugin/bundled_catalog.rb +167 -0
  258. data/lib/rigor/plugin/effect_attribution.rb +7 -7
  259. data/lib/rigor/plugin/effect_entry_points.rb +3 -3
  260. data/lib/rigor/plugin/fact_store.rb +6 -6
  261. data/lib/rigor/plugin/io_boundary.rb +93 -8
  262. data/lib/rigor/plugin/isolation.rb +29 -6
  263. data/lib/rigor/plugin/loader.rb +31 -12
  264. data/lib/rigor/plugin/macro/heredoc_template.rb +1 -1
  265. data/lib/rigor/plugin/macro/trait_registry.rb +1 -1
  266. data/lib/rigor/plugin/manifest.rb +23 -1
  267. data/lib/rigor/plugin/registry.rb +42 -12
  268. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +12 -1
  269. data/lib/rigor/plugin/trust_policy.rb +2 -4
  270. data/lib/rigor/plugin/type_node_resolver.rb +3 -3
  271. data/lib/rigor/plugin_gap_advisory.rb +96 -0
  272. data/lib/rigor/project_environment.rb +138 -0
  273. data/lib/rigor/protection/analysis_guard.rb +93 -14
  274. data/lib/rigor/protection/closure_kill_oracle.rb +67 -23
  275. data/lib/rigor/protection/dependency_closure.rb +5 -8
  276. data/lib/rigor/protection/diagnostic_oracle.rb +2 -2
  277. data/lib/rigor/protection/discovery_seed.rb +11 -11
  278. data/lib/rigor/protection/kill_signature.rb +2 -4
  279. data/lib/rigor/protection/measurement_integrity.rb +1 -3
  280. data/lib/rigor/protection/mutation_cache.rb +6 -9
  281. data/lib/rigor/protection/mutation_scanner.rb +12 -15
  282. data/lib/rigor/protection/mutator.rb +2 -1
  283. data/lib/rigor/protection/test_suite_oracle.rb +5 -5
  284. data/lib/rigor/rbs_extended/conformance_checker.rb +4 -3
  285. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  286. data/lib/rigor/rbs_extended/reporter.rb +36 -10
  287. data/lib/rigor/rbs_extended.rb +61 -21
  288. data/lib/rigor/reflection/constant_path.rb +126 -0
  289. data/lib/rigor/reflection.rb +26 -10
  290. data/lib/rigor/runtime/jit.rb +6 -8
  291. data/lib/rigor/scope/discovery_index.rb +30 -0
  292. data/lib/rigor/scope.rb +203 -4
  293. data/lib/rigor/sig_gen/generator.rb +115 -22
  294. data/lib/rigor/sig_gen/layout_index.rb +3 -4
  295. data/lib/rigor/sig_gen/meta_class_shape.rb +3 -5
  296. data/lib/rigor/sig_gen/observation_collector.rb +17 -15
  297. data/lib/rigor/sig_gen/path_mapper.rb +4 -6
  298. data/lib/rigor/sig_gen/rbs_validity.rb +4 -4
  299. data/lib/rigor/sig_gen/renderer.rb +4 -5
  300. data/lib/rigor/sig_gen/skip_reason_catalog.rb +111 -0
  301. data/lib/rigor/sig_gen/superclass_spelling.rb +27 -0
  302. data/lib/rigor/sig_gen/type_elaborator.rb +1 -3
  303. data/lib/rigor/sig_gen/writer.rb +23 -6
  304. data/lib/rigor/signature_path_audit.rb +153 -6
  305. data/lib/rigor/source/literals.rb +0 -23
  306. data/lib/rigor/source/node_children.rb +0 -2
  307. data/lib/rigor/source/node_locator.rb +5 -11
  308. data/lib/rigor/source/node_walker.rb +2 -5
  309. data/lib/rigor/triage/catalogue.rb +1 -3
  310. data/lib/rigor/triage.rb +3 -5
  311. data/lib/rigor/type/accepts_result.rb +24 -6
  312. data/lib/rigor/type/combinator.rb +68 -5
  313. data/lib/rigor/type/data_class.rb +2 -2
  314. data/lib/rigor/type/data_instance.rb +4 -4
  315. data/lib/rigor/type/float_range.rb +128 -0
  316. data/lib/rigor/type/hash_shape.rb +5 -5
  317. data/lib/rigor/type/integer_range.rb +13 -8
  318. data/lib/rigor/type/nominal.rb +8 -2
  319. data/lib/rigor/type/refined.rb +3 -3
  320. data/lib/rigor/type/struct_class.rb +3 -3
  321. data/lib/rigor/type/struct_instance.rb +4 -4
  322. data/lib/rigor/type.rb +1 -0
  323. data/lib/rigor/type_node/generic.rb +1 -1
  324. data/lib/rigor/type_node/range_literal.rb +25 -0
  325. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  326. data/lib/rigor/type_node.rb +1 -0
  327. data/lib/rigor/version.rb +1 -1
  328. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +0 -4
  329. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +0 -1
  330. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +0 -2
  331. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -0
  332. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +0 -4
  333. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +3 -5
  334. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +1 -4
  335. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +1 -0
  336. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +33 -30
  337. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +0 -1
  338. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -3
  339. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +28 -2
  340. data/plugins/rigor-actionpack/sig/action_controller.rbs +71 -0
  341. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +0 -4
  342. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  343. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +0 -1
  344. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +0 -2
  345. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +1 -1
  346. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +1 -0
  347. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +3 -4
  348. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +1 -2
  349. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +1 -2
  350. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +42 -7
  351. data/plugins/rigor-activerecord/sig/active_record/framework.rbs +123 -0
  352. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +1 -0
  353. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +9 -2
  354. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +119 -14
  355. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +1 -0
  356. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +1 -0
  357. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +9 -7
  358. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +7 -3
  359. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +1 -0
  360. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +8 -6
  361. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +11 -7
  362. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +13 -11
  363. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +9 -4
  364. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +1 -0
  365. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +0 -5
  366. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +0 -1
  367. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +0 -1
  368. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +8 -1
  369. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +36 -4
  370. data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +1 -0
  371. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +8 -6
  372. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +6 -3
  373. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +1 -0
  374. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -1
  375. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +0 -3
  376. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +1 -0
  377. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +0 -5
  378. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -2
  379. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +0 -1
  380. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +0 -1
  381. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +1 -0
  382. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +0 -12
  383. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +1 -3
  384. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +0 -1
  385. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +1 -0
  386. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/acronyms.rb +5 -5
  387. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +1 -10
  388. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +3 -3
  389. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +2 -2
  390. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +2 -2
  391. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +2 -2
  392. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +9 -10
  393. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +2 -3
  394. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -0
  395. data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +1 -0
  396. data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +13 -1
  397. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +128 -3
  398. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +0 -3
  399. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +0 -3
  400. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -5
  401. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +2 -3
  402. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +1 -0
  403. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +0 -3
  404. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +1 -0
  405. data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +1 -0
  406. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +1 -9
  407. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +1 -0
  408. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +0 -4
  409. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/effects.rb +1 -1
  410. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +1 -1
  411. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +0 -1
  412. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +0 -2
  413. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +1 -0
  414. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -0
  415. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +2 -5
  416. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +1 -4
  417. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +3 -9
  418. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +4 -4
  419. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +1 -2
  420. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +4 -5
  421. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +7 -5
  422. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +5 -5
  423. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +1 -0
  424. data/sig/rigor/analysis/reachability/scan_cache.rbs +12 -0
  425. data/sig/rigor/cache.rbs +2 -3
  426. data/sig/rigor/environment.rbs +5 -0
  427. data/sig/rigor/inference.rbs +3 -10
  428. data/sig/rigor/plugin/io_boundary.rbs +4 -0
  429. data/sig/rigor/plugin/manifest.rbs +2 -1
  430. data/sig/rigor/reflection.rbs +3 -1
  431. data/sig/rigor/scope.rbs +24 -3
  432. data/sig/rigor/sig_gen/skip_reason_catalog.rbs +14 -0
  433. data/sig/rigor/type.rbs +27 -1
  434. data/sig/rigor.rbs +1 -0
  435. data/skills/rigor-ask/SKILL.md +8 -6
  436. data/skills/rigor-plugin-author/references/02-walker-and-types.md +13 -2
  437. data/skills/rigor-project-init/SKILL.md +6 -3
  438. data/skills/rigor-project-init/references/02-configure.md +10 -6
  439. data/skills/rigor-project-init/references/06-agent-contract.md +67 -0
  440. data/skills/rigor-type-oracle/SKILL.md +228 -0
  441. data/skills/rigor-type-oracle/references/01-oracle-commands.md +261 -0
  442. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +52 -0
  443. data/skills/rigor-type-oracle/references/03-gap-protocol.md +128 -0
  444. data/skills/rigor-unused-adjudicate/SKILL.md +8 -5
  445. metadata +26 -3
  446. data/lib/rigor/cli/probe_environment.rb +0 -85
  447. data/sig/rigor/inference/builtins/numeric_catalog.rbs +0 -3
@@ -109,23 +109,23 @@ module Rigor
109
109
  Prism::SelfNode => :type_of_self_node,
110
110
  Prism::InstanceVariableReadNode => :type_of_instance_variable_read,
111
111
  Prism::InstanceVariableWriteNode => :type_of_assignment_write,
112
- Prism::InstanceVariableOperatorWriteNode => :type_of_assignment_write,
113
- Prism::InstanceVariableOrWriteNode => :type_of_assignment_write,
114
- Prism::InstanceVariableAndWriteNode => :type_of_assignment_write,
112
+ Prism::InstanceVariableOperatorWriteNode => :type_of_compound_variable_write,
113
+ Prism::InstanceVariableOrWriteNode => :type_of_compound_variable_write,
114
+ Prism::InstanceVariableAndWriteNode => :type_of_compound_variable_write,
115
115
  Prism::ClassVariableReadNode => :type_of_class_variable_read,
116
116
  Prism::ClassVariableWriteNode => :type_of_assignment_write,
117
- Prism::ClassVariableOperatorWriteNode => :type_of_assignment_write,
118
- Prism::ClassVariableOrWriteNode => :type_of_assignment_write,
119
- Prism::ClassVariableAndWriteNode => :type_of_assignment_write,
117
+ Prism::ClassVariableOperatorWriteNode => :type_of_compound_variable_write,
118
+ Prism::ClassVariableOrWriteNode => :type_of_compound_variable_write,
119
+ Prism::ClassVariableAndWriteNode => :type_of_compound_variable_write,
120
120
  Prism::GlobalVariableReadNode => :type_of_global_variable_read,
121
121
  Prism::GlobalVariableWriteNode => :type_of_assignment_write,
122
- Prism::GlobalVariableOperatorWriteNode => :type_of_assignment_write,
123
- Prism::GlobalVariableOrWriteNode => :type_of_assignment_write,
124
- Prism::GlobalVariableAndWriteNode => :type_of_assignment_write,
122
+ Prism::GlobalVariableOperatorWriteNode => :type_of_compound_variable_write,
123
+ Prism::GlobalVariableOrWriteNode => :type_of_compound_variable_write,
124
+ Prism::GlobalVariableAndWriteNode => :type_of_compound_variable_write,
125
125
  # Compound writes that share the `.value` rvalue accessor
126
- Prism::LocalVariableOperatorWriteNode => :type_of_assignment_write,
127
- Prism::LocalVariableOrWriteNode => :type_of_assignment_write,
128
- Prism::LocalVariableAndWriteNode => :type_of_assignment_write,
126
+ Prism::LocalVariableOperatorWriteNode => :type_of_compound_variable_write,
127
+ Prism::LocalVariableOrWriteNode => :type_of_compound_variable_write,
128
+ Prism::LocalVariableAndWriteNode => :type_of_compound_variable_write,
129
129
  Prism::IndexOperatorWriteNode => :type_of_assignment_write,
130
130
  Prism::IndexOrWriteNode => :type_of_assignment_write,
131
131
  Prism::IndexAndWriteNode => :type_of_assignment_write,
@@ -201,7 +201,7 @@ module Rigor
201
201
  Prism::NextNode => :type_of_jump,
202
202
  Prism::RetryNode => :type_of_jump,
203
203
  Prism::RedoNode => :type_of_jump,
204
- Prism::YieldNode => :type_of_dynamic_top,
204
+ Prism::YieldNode => :type_of_yield,
205
205
  Prism::SuperNode => :type_of_dynamic_top,
206
206
  Prism::ForwardingArgumentsNode => :type_of_non_value,
207
207
  Prism::WhileNode => :type_of_loop,
@@ -287,6 +287,32 @@ module Rigor
287
287
  slot[1].transform_values(&:values)
288
288
  end
289
289
 
290
+ # Issue #720 — the value the block passed by the call site currently being re-typed evaluates to, or
291
+ # `nil` outside such a frame. Read by {#type_of_yield}.
292
+ #
293
+ # A thread-local rather than a `Scope` field for the same reason the `return` / `next` / `break` sinks
294
+ # are: `yield` names the frame's caller, not a binding, and the body is walked through scopes the
295
+ # inference rebuilds freely. It is installed unconditionally — with `nil` — at every user-method
296
+ # inference frame, so a callee reached WITHOUT a block from inside a yielding body cannot read its
297
+ # caller's block.
298
+ YIELD_VALUE_KEY = :__rigor_yield_value_type__
299
+ private_constant :YIELD_VALUE_KEY
300
+
301
+ def self.current_yield_value_type
302
+ Thread.current[YIELD_VALUE_KEY]
303
+ end
304
+
305
+ # Runs `block` with `type` installed as the frame's yield value, restoring the previous frame on exit.
306
+ def self.with_yield_value_type(type)
307
+ previous = Thread.current[YIELD_VALUE_KEY]
308
+ Thread.current[YIELD_VALUE_KEY] = type
309
+ begin
310
+ yield
311
+ ensure
312
+ Thread.current[YIELD_VALUE_KEY] = previous
313
+ end
314
+ end
315
+
290
316
  private
291
317
 
292
318
  attr_reader :scope, :tracer
@@ -318,6 +344,71 @@ module Rigor
318
344
  type_of(node.value)
319
345
  end
320
346
 
347
+ # `v += 1` / `@v ||= x` / `$v &&= x` as an EXPRESSION. The value of a compound write is the value it
348
+ # stores, which is a function of the variable's CURRENT binding — not of the rvalue alone. Typing it as
349
+ # `type_of(node.value)` answered `1` for `total += 1` whatever `total` held.
350
+ #
351
+ # `StatementEvaluator#compound_eval` already computes the right answer on the straight-line path, so the
352
+ # gap only showed where a body is typed WITHOUT that evaluator: the block-return pass types a
353
+ # single-statement body by expression alone, and issue #617 residue (3) is the live consequence —
354
+ # `total = 0; r = [1, 2].map { total += 1 }` folded to `[1, 1]`, and `r.last == 1` fired a false
355
+ # `flow.always-truthy-condition` on a program where it is `2`. Under the per-element fold the captured
356
+ # local is already bound to its converged `Integer` (issue #587 (b)); reading that binding here is what
357
+ # turns the stale pin into the honest `Integer`.
358
+ #
359
+ # The algebra is `StatementEvaluator#compound_result_type`'s, deliberately: the two must not disagree
360
+ # about what a compound write evaluates to. An unbound target is `Dynamic[top]` — the same fallback the
361
+ # evaluator takes — and an operator the receiver does not answer widens to `Dynamic[top]` rather than
362
+ # inventing the rvalue.
363
+ #
364
+ # Constant and index targets keep {#type_of_assignment_write}: a constant is not rebound in a loop body,
365
+ # and `IndexOperatorWriteNode` is typed through `Scope#type_of`'s own indexed path by
366
+ # `StatementEvaluator#eval_index_write`.
367
+ def type_of_compound_variable_write(node)
368
+ current = compound_write_current_binding(node)
369
+ rhs = type_of(node.value)
370
+
371
+ case node
372
+ when Prism::LocalVariableOrWriteNode, Prism::InstanceVariableOrWriteNode,
373
+ Prism::ClassVariableOrWriteNode, Prism::GlobalVariableOrWriteNode
374
+ # An UNBOUND target is the memoization idiom (`def self.default = @default ||= new`): nothing
375
+ # has written the variable on any path the analyzer saw, so the stored value is the rvalue.
376
+ # Reading it as `Dynamic[top] | rhs` would skip every memoized singleton in `sig-gen`
377
+ # (ADR-5 optimism; three `.default` readers went `sig.skipped.untyped-return` without this).
378
+ return rhs if current.nil?
379
+
380
+ Type::Combinator.union(Narrowing.narrow_truthy(current), rhs)
381
+ when Prism::LocalVariableAndWriteNode, Prism::InstanceVariableAndWriteNode,
382
+ Prism::ClassVariableAndWriteNode, Prism::GlobalVariableAndWriteNode
383
+ return rhs if current.nil?
384
+
385
+ Type::Combinator.union(Narrowing.narrow_falsey(current), rhs)
386
+ else
387
+ compound_operator_result(current || dynamic_top, rhs, node.binary_operator)
388
+ end
389
+ end
390
+
391
+ def compound_write_current_binding(node)
392
+ case node
393
+ when Prism::LocalVariableOperatorWriteNode, Prism::LocalVariableOrWriteNode,
394
+ Prism::LocalVariableAndWriteNode then scope.local(node.name)
395
+ when Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableOrWriteNode,
396
+ Prism::InstanceVariableAndWriteNode then scope.ivar(node.name)
397
+ when Prism::ClassVariableOperatorWriteNode, Prism::ClassVariableOrWriteNode,
398
+ Prism::ClassVariableAndWriteNode then scope.cvar(node.name)
399
+ else scope.global(node.name)
400
+ end
401
+ end
402
+
403
+ def compound_operator_result(current, rhs, operator)
404
+ MethodDispatcher.dispatch(
405
+ receiver_type: current,
406
+ method_name: operator.to_sym,
407
+ arg_types: [rhs],
408
+ environment: scope.environment
409
+ ) || dynamic_top
410
+ end
411
+
321
412
  # Slice 7 phase 1 — instance/class/global variable reads. Each lookup returns the type currently bound
322
413
  # in the surrounding scope's per-kind binding map (populated by `StatementEvaluator` write handlers
323
414
  # within the same method body), falling through to `Dynamic[Top]` when no binding is recorded.
@@ -372,6 +463,19 @@ module Rigor
372
463
  dynamic_top
373
464
  end
374
465
 
466
+ # Issue #720 — `yield` evaluates the block the CALLER passed, so its value is that block's value. The
467
+ # caller is only known when this body is being re-typed on behalf of one call site
468
+ # ({#infer_user_method_return} installs the frame); on the main walk over a `def` no caller exists and
469
+ # the answer stays `Dynamic[top]`.
470
+ #
471
+ # Nothing about the yield's arguments is checked here. The block's return type was computed at the call
472
+ # site under the parameter bindings the call implies, so it already answers "what does this block
473
+ # evaluate to for this caller"; a `yield` that passes different arguments than the block's parameters
474
+ # accept is a separate (arity) question this frame does not decide.
475
+ def type_of_yield(_node)
476
+ ExpressionTyper.current_yield_value_type || dynamic_top
477
+ end
478
+
375
479
  # `defined?(expr)` returns `String | nil` per Ruby semantics — a description of the expression's
376
480
  # category (`"local-variable"`, `"method"`, ...) when defined, or `nil` when not. The argument is not
377
481
  # evaluated (it is statically inspected by the runtime), so the typer does not recurse into it.
@@ -843,9 +947,21 @@ module Rigor
843
947
  #
844
948
  # Other pattern shapes (Range, Regexp, custom `===`) stay `:maybe` — the existing union fallback
845
949
  # handles them.
950
+ #
951
+ # The class reference is resolved through {Narrowing.lexical_class_name}, the walk the narrowing
952
+ # side of the same `case` already uses (#655). Matching on the as-written spelling instead made a
953
+ # SHADOWED name confidently wrong rather than merely unresolved: `when Random` inside
954
+ # `Bar::Nested` names `Bar::Nested::Random`, the environment answered the core `::Random` the
955
+ # spelling matches at top level, and a subject that is a core `Random` then made the arm certain
956
+ # — dropping the `else` Ruby actually takes and typing the expression as a branch that never
957
+ # runs. The documented argument that an unresolvable pattern can only lose certainty holds for an
958
+ # unresolvable name and not for a shadowed one.
846
959
  def case_when_pattern_certainty(subject_type, pattern_node)
847
- class_name = Source::ConstantPath.qualified_name_or_nil(pattern_node)
848
- return Narrowing.class_pattern_certainty(subject_type, class_name, environment: scope.environment) if class_name
960
+ class_name = Narrowing.lexical_class_name(pattern_node, scope)
961
+ if class_name
962
+ return Narrowing.class_pattern_certainty(subject_type, class_name, environment: scope.environment,
963
+ scope: scope)
964
+ end
849
965
 
850
966
  literal = literal_pattern_value(pattern_node)
851
967
  return Narrowing.value_pattern_certainty(subject_type, literal[:value]) if literal
@@ -963,6 +1079,8 @@ module Rigor
963
1079
  Type::Combinator.nominal_of(value.class.name)
964
1080
  when Type::IntegerRange
965
1081
  Type::Combinator.nominal_of("Integer")
1082
+ when Type::FloatRange
1083
+ Type::Combinator.nominal_of("Float")
966
1084
  else
967
1085
  type
968
1086
  end
@@ -1177,12 +1295,12 @@ module Rigor
1177
1295
  # and fired `undefined method 'upcase' for nil` on correct code. The candidate is still looked up
1178
1296
  # first — that lookup is a hash probe and owns the ADR-46 cross-file dependency edge — and
1179
1297
  # {#self_type_answers?} then vetoes the bind for a name the enclosing class answers itself.
1180
- def try_local_def_dispatch(node, receiver, arg_types)
1298
+ def try_local_def_dispatch(node, receiver, arg_types, block_type = nil)
1181
1299
  local_def = node.receiver.nil? ? scope.bindable_top_level_def_for(node.name) : nil
1182
1300
  return nil unless local_def
1183
1301
  return nil if self_type_answers?(node.name)
1184
1302
 
1185
- local_inference = infer_top_level_user_method(local_def, receiver, arg_types)
1303
+ local_inference = infer_top_level_user_method(local_def, receiver, arg_types, block_type)
1186
1304
  return local_inference if local_inference
1187
1305
 
1188
1306
  # The local def matches by name but the inference was disqualified — the parameter shape is too
@@ -1194,6 +1312,55 @@ module Rigor
1194
1312
  dynamic_top
1195
1313
  end
1196
1314
 
1315
+ # ADR-110 WD1 / #856 — the receiver's own `def` outranks a declaration that only an ANCESTOR carries.
1316
+ #
1317
+ # RBS resolves through the fully built method table, so `RbsDispatch` answers a subclass call with a
1318
+ # signature written about the base. redmine's `FieldFormat::Base#target_class` honestly declares
1319
+ # `-> nil`; `RecordList` overrides it with a real lookup, and every call on a `RecordList` receiver
1320
+ # typed as `nil` and fired `undefined method ... for nil` on the subclass's own working code (#744).
1321
+ # A signature is authoritative for a receiver only where someone wrote it about that receiver;
1322
+ # reaching it through an ancestor is a lookup convenience, not an act of authorship.
1323
+ #
1324
+ # This is {#try_local_def_dispatch}'s shape one level up — there the enclosing class shadows a
1325
+ # top-level `def`, here the receiver's class shadows an ancestor's declaration — and it answers the
1326
+ # same way: re-type the override's body, and when that is not possible answer `Dynamic[Top]` rather
1327
+ # than let the inherited declaration speak for a method it does not describe.
1328
+ #
1329
+ # Three conditions, and the third is the blast radius. Clause A of the ADR (an own source `def`, no
1330
+ # own declaration) does NOT by itself stay clear of [ADR-43]: `class Foo; def each; end` inheriting
1331
+ # `Enumerable#each` satisfies it, and disqualifying bundled declarations that way is the blanket fix
1332
+ # ADR-43 rejected — the same reason {#instance_self_answers?}'s RBS arm is own-class only. So the
1333
+ # ancestor whose declaration is being disqualified must itself be PROJECT-declared: a project sidecar
1334
+ # describes the source under analysis, while a bundled signature describes a class the project does
1335
+ # not own, where a project `def` is a monkey-patch and [ADR-17] owns the question.
1336
+ # `project_declared_class?` fail-softs to false, so an unattributable environment changes nothing.
1337
+ def try_overriding_def_dispatch(node, receiver, arg_types)
1338
+ return nil unless user_inference_receiver?(receiver)
1339
+
1340
+ class_name = receiver.class_name
1341
+ return nil if class_name.nil?
1342
+ # `Scope#user_def_for`, not `discovered_method?`: the cross-file table deliberately withholds a
1343
+ # plain instance `def` under the ADR-17 monkey-patch contract, and this gate must see one.
1344
+ return nil if scope.user_def_for(class_name, node.name).nil?
1345
+
1346
+ definition = safe_rbs_method_definition(class_name, node.name, :instance)
1347
+ return nil if definition.nil?
1348
+ return nil if rbs_declared_on_class?(definition, class_name)
1349
+ return nil unless project_declared_owner?(definition)
1350
+
1351
+ try_user_method_inference(receiver, node, arg_types) || dynamic_top
1352
+ end
1353
+
1354
+ # Whether the class an inherited declaration was written about is one the project declares itself.
1355
+ def project_declared_owner?(definition)
1356
+ defined_in = definition.defined_in
1357
+ return false if defined_in.nil?
1358
+
1359
+ Rigor::Reflection.project_declared_class?(defined_in.to_s, scope: scope)
1360
+ rescue StandardError
1361
+ false
1362
+ end
1363
+
1197
1364
  # Issue #618 — whether the call's enclosing `self` already answers `method_name`. Only a `self` whose
1198
1365
  # class is KNOWN participates: at genuine top level, and inside a block whose `self` is unmodelled,
1199
1366
  # `scope.self_type` is nil, the predicate is false, and the historical top-level binding stands — that
@@ -1212,30 +1379,65 @@ module Rigor
1212
1379
  end
1213
1380
  end
1214
1381
 
1215
- # The instance side of {#self_type_answers?}: the class's own discovered methods (`def`, `attr_*`,
1216
- # `define_method`, `alias`), its `Struct.new` / `Data.define` member accessors, a `def` reached through
1217
- # its project ancestors (superclass chain and included modules), and an RBS method declared on the
1218
- # class ITSELF.
1382
+ # The instance side of {#self_type_answers?}: the discovered methods (`def`, `attr_*`,
1383
+ # `define_method`, `alias`) of the class or any project ancestor, its `Struct.new` / `Data.define`
1384
+ # member accessors, a `def` reached through its project ancestors (superclass chain and included
1385
+ # modules), and an RBS method declared on an owner that PRECEDES `::Object` in the MRO.
1219
1386
  #
1220
- # The RBS arm is own-class only, deliberately. An inherited-declaration test would match every
1221
- # `Object` / `Kernel` / `Enumerable` name and retract the binding v0.0.3 A exists for — a `def
1222
- # select(...)` collocated with its DSL-block call site would route straight back through
1223
- # `Enumerable#select`.
1387
+ # Issue #633 — the discovery arm walks ancestors because inheritance does not distinguish how a
1388
+ # method got there: a superclass `attr_accessor :stext` and an included module's `attr_reader` bind
1389
+ # ahead of a top-level `def stext` exactly as an inherited `def` does, and only the latter had a walk.
1390
+ #
1391
+ # The RBS arm's cut-off is `::Object`, not the own class. A top-level `def` IS a private `Object`
1392
+ # instance method, the last link of every MRO, so any declared owner that comes before `::Object`
1393
+ # wins at runtime — `Exception#message` for a `StandardError` subclass, `Array#first`,
1394
+ # `Comparable#clamp`. What the cut-off keeps out is the tier v0.0.3 A exists for and #316 / #319
1395
+ # refined: a name owned by `Object` or `Kernel` themselves (`inspect`, `format`) is at or after the
1396
+ # top-level `def`'s own rung, so the historical binding still stands there.
1224
1397
  def instance_self_answers?(class_name, method_name)
1225
1398
  return false if class_name.nil?
1226
- return true if scope.discovered_method?(class_name, method_name, :instance)
1399
+ return true if scope.discovered_method_through_ancestors?(class_name, method_name, :instance)
1227
1400
  return true if meta_member?(class_name, method_name)
1228
1401
  return true if resolve_user_def_through_ancestors(class_name, method_name)
1229
1402
 
1230
- rbs_declared_on_class?(safe_rbs_method_definition(class_name, method_name, :instance), class_name)
1403
+ rbs_ancestor_answers?(class_name, method_name)
1404
+ end
1405
+
1406
+ # The RBS arm of {#instance_self_answers?}. A project class is usually absent from the RBS
1407
+ # environment, so the declaration that decides the question is written about an ancestor the project
1408
+ # does not declare (`StandardError`, `Array`, `Comparable`) — each is asked on its own terms, and
1409
+ # each of them is itself before `::Object` in the reader's MRO by construction.
1410
+ def rbs_ancestor_answers?(class_name, method_name)
1411
+ definition = safe_rbs_method_definition(class_name, method_name, :instance)
1412
+ return true if rbs_declared_before_object?(definition, class_name)
1413
+
1414
+ scope.external_ancestor_name_candidates(class_name, name_memo: class_graph_buckets[:name])
1415
+ .any? { |candidates| external_ancestor_answers?(candidates, method_name) }
1416
+ end
1417
+
1418
+ # The first candidate spelling the RBS environment knows is the ancestor Ruby resolves; a name it
1419
+ # knows nothing about contributes no evidence either way.
1420
+ def external_ancestor_answers?(candidates, method_name)
1421
+ candidates.each do |candidate|
1422
+ next if instance_ancestor_names(candidate).empty?
1423
+
1424
+ return rbs_declared_before_object?(
1425
+ safe_rbs_method_definition(candidate, method_name, :instance), candidate
1426
+ )
1427
+ end
1428
+ false
1231
1429
  end
1232
1430
 
1233
1431
  # The singleton side: a class-body `self` is `Singleton[Foo]`, where an implicit-self call reaches
1234
1432
  # `Foo`'s own class methods before `Object`'s private top-level `def`.
1433
+ #
1434
+ # Issue #633 — both discovery arms follow the superclass chain, because a subclass inherits its
1435
+ # parent's class methods; `Scope` narrows the singleton walk to superclasses for the same reason
1436
+ # (an `include`d module's `def self.x` is not callable on the includer).
1235
1437
  def singleton_self_answers?(class_name, method_name)
1236
1438
  return false if class_name.nil?
1237
- return true if scope.discovered_method?(class_name, method_name, :singleton)
1238
- return true unless scope.singleton_def_for(class_name, method_name).nil?
1439
+ return true if scope.discovered_method_through_ancestors?(class_name, method_name, :singleton)
1440
+ return true unless scope.singleton_def_through_ancestors(class_name, method_name).first.nil?
1239
1441
 
1240
1442
  rbs_declared_on_class?(safe_rbs_method_definition(class_name, method_name, :singleton), class_name)
1241
1443
  end
@@ -1272,6 +1474,36 @@ module Rigor
1272
1474
  defined_in.to_s.delete_prefix("::") == class_name.to_s.delete_prefix("::")
1273
1475
  end
1274
1476
 
1477
+ # Issue #633 — true when the declaration's owner sits strictly before `::Object` in `class_name`'s
1478
+ # instance MRO, i.e. Ruby dispatches to it ahead of a top-level `def` (which is `Object`'s own
1479
+ # private instance method). The own class trivially qualifies. An owner absent from the ancestor
1480
+ # list, an unbuildable class, and an ancestry that does not reach `Object` (a `BasicObject`
1481
+ # descendant) all answer false, leaving the historical top-level binding untouched.
1482
+ def rbs_declared_before_object?(definition, class_name)
1483
+ return true if rbs_declared_on_class?(definition, class_name)
1484
+ return false if definition.nil? || !definition.respond_to?(:defined_in)
1485
+
1486
+ owner = definition.defined_in
1487
+ return false if owner.nil?
1488
+
1489
+ ancestors = instance_ancestor_names(class_name)
1490
+ object_index = ancestors.index("Object")
1491
+ owner_index = ancestors.index(owner.to_s.delete_prefix("::"))
1492
+ !object_index.nil? && !owner_index.nil? && owner_index < object_index
1493
+ end
1494
+
1495
+ # The class's instance-side ancestors in MRO order, `::`-stripped, or `[]` for a class the RBS
1496
+ # environment does not know or cannot build. Read through the loader's accessor rather than
1497
+ # `instance_definition(...).ancestors` because that is the one wired to the ancestor-name cache and
1498
+ # marked as RIGOR'S OWN demand — ordering two ancestors is not the analysis asking whether either
1499
+ # one's methods resolve, and the `rbs.coverage` bookkeeping must not record it as such.
1500
+ def instance_ancestor_names(class_name)
1501
+ loader = scope.environment&.rbs_loader
1502
+ loader ? loader.ancestor_names_for(class_name.to_s) : []
1503
+ rescue StandardError
1504
+ []
1505
+ end
1506
+
1275
1507
  # Issue #520 — Ruby defines the value of an attribute / index assignment (`x.attr = v`, `h[k] = v`)
1276
1508
  # as the RHS object itself, whatever the writer method returns. The dispatch pipeline still runs
1277
1509
  # first for everything it observes on the side (effect collection, provenance, recorders, and the
@@ -1376,7 +1608,22 @@ module Rigor
1376
1608
  # `receiver_override` substitutes the receiver type for the whole pipeline (folds, dispatch, the
1377
1609
  # inference tiers) without re-reading the receiver node — the safe-navigation path (#518) dispatches
1378
1610
  # on the nil-stripped fragment, and the optional-receiver retry (#519) re-runs the pipeline on it.
1611
+ # Issue #853: `break value` terminates the yielding CALL and is that call's value, so the answer every
1612
+ # tier below produces — an RBS return, a precision fold, an inter-procedural inference — describes only
1613
+ # the path where the block never breaks. The call's value is that result unioned with every `break` arm
1614
+ # the block can reach. Modelling it here rather than inside each fold is what keeps the folds honest:
1615
+ # `ops.all? { |o| break false unless o; true }` still folds the no-break path to `Constant[true]`, and
1616
+ # the union with the `false` arm makes the call `bool` — no `flow.always-truthy-condition` on a program
1617
+ # that really can answer false.
1379
1618
  def call_dispatch_type_for(node, receiver_override: nil)
1619
+ result = call_result_type_for(node, receiver_override: receiver_override)
1620
+ arms = call_break_arm_types(node, receiver_override: receiver_override)
1621
+ return result if arms.empty?
1622
+
1623
+ Type::Combinator.union(result, *arms)
1624
+ end
1625
+
1626
+ def call_result_type_for(node, receiver_override: nil)
1380
1627
  narrowed = indexed_narrowing_for(node)
1381
1628
  return narrowed if narrowed
1382
1629
 
@@ -1392,7 +1639,7 @@ module Rigor
1392
1639
  literal_send = try_literal_send(node, receiver)
1393
1640
  return literal_send if literal_send
1394
1641
 
1395
- local_def_result = try_local_def_dispatch(node, receiver, arg_types)
1642
+ local_def_result = try_local_def_dispatch(node, receiver, arg_types, block_type)
1396
1643
  return local_def_result if local_def_result
1397
1644
 
1398
1645
  # v0.0.6 phase 2 — per-element block fold for Tuple receivers. When `[a, b, c].map { |x| f(x) }` and
@@ -1403,6 +1650,9 @@ module Rigor
1403
1650
  block_fold = try_receiver_block_folds(node, receiver, arg_types)
1404
1651
  return block_fold if block_fold
1405
1652
 
1653
+ overriding_def = try_overriding_def_dispatch(node, receiver, arg_types)
1654
+ return overriding_def if overriding_def
1655
+
1406
1656
  result = MethodDispatcher.dispatch(
1407
1657
  receiver_type: receiver,
1408
1658
  method_name: node.name,
@@ -1414,16 +1664,16 @@ module Rigor
1414
1664
  )
1415
1665
  return result if result
1416
1666
 
1417
- dispatch_miss_result(node, receiver, arg_types)
1667
+ dispatch_miss_result(node, receiver, arg_types, block_type)
1418
1668
  end
1419
1669
 
1420
1670
  # The post-dispatch tiers for a call `MethodDispatcher` could not answer, in their historical
1421
1671
  # order; extracted from {#call_dispatch_type_for} whole.
1422
- def dispatch_miss_result(node, receiver, arg_types)
1672
+ def dispatch_miss_result(node, receiver, arg_types, block_type = nil)
1423
1673
  # v0.0.2 #5 — inter-procedural inference for user-defined methods. When dispatch misses but the
1424
1674
  # receiver is a user class with a `def` body, re-type the body with the call's argument types bound
1425
1675
  # and return the body's last-expression type.
1426
- user_inference = try_user_method_inference(receiver, node, arg_types)
1676
+ user_inference = try_user_method_inference(receiver, node, arg_types, block_type: block_type)
1427
1677
  return user_inference if user_inference
1428
1678
 
1429
1679
  # Module-singleton call resolution (ADR-57 follow-up) — when the receiver is `Singleton[Foo]` (a
@@ -1431,7 +1681,7 @@ module Rigor
1431
1681
  # `module_function` body, re-type that body with the call args bound. Sits after the RBS dispatch
1432
1682
  # tier, so foreign / RBS-known singletons (`Math.sqrt`) keep their catalog answer; only
1433
1683
  # project-defined singleton methods reach here.
1434
- singleton_inference = try_project_singleton_inference(receiver, node, arg_types)
1684
+ singleton_inference = try_project_singleton_inference(receiver, node, arg_types, block_type: block_type)
1435
1685
  return singleton_inference if singleton_inference
1436
1686
 
1437
1687
  # Dynamic-origin propagation: when the receiver is Dynamic[T] and no positive rule resolves the call,
@@ -1634,8 +1884,8 @@ module Rigor
1634
1884
  # `scope.self_type` (or implicit `Object`) as the receiver carrier so the body's own self is
1635
1885
  # consistent with the call site's. Returns nil when the parameter shape disqualifies the def, when the
1636
1886
  # body is empty, or when a recursion cycle is detected.
1637
- def infer_top_level_user_method(def_node, receiver, arg_types)
1638
- infer_user_method_return(def_node, receiver, arg_types)
1887
+ def infer_top_level_user_method(def_node, receiver, arg_types, block_type = nil)
1888
+ infer_user_method_return(def_node, receiver, arg_types, yield_type: block_type)
1639
1889
  rescue StandardError
1640
1890
  nil
1641
1891
  end
@@ -1745,14 +1995,15 @@ module Rigor
1745
1995
  try_project_singleton_inference(receiver, node, inner_args, method_name: inner_name)
1746
1996
  end
1747
1997
 
1748
- def try_user_method_inference(receiver, call_node, arg_types, method_name: call_node.name)
1998
+ def try_user_method_inference(receiver, call_node, arg_types, method_name: call_node.name, block_type: nil)
1749
1999
  return nil unless user_inference_receiver?(receiver)
1750
2000
 
1751
2001
  def_node, owner = resolve_user_def_with_owner(receiver.class_name, method_name)
1752
2002
  return nil if def_node.nil?
1753
2003
 
1754
2004
  result = infer_user_method_return(def_node, receiver, arg_types,
1755
- self_fold_safe: fold_safe_call_receiver?(call_node, receiver))
2005
+ self_fold_safe: fold_safe_call_receiver?(call_node, receiver),
2006
+ yield_type: block_type)
1756
2007
  return result if result.nil?
1757
2008
 
1758
2009
  degrade_if_overridable(result, owner, method_name, :instance)
@@ -1823,13 +2074,14 @@ module Rigor
1823
2074
  # The OWNER — not the receiver class — is what the overridable gate keys on, exactly as the instance
1824
2075
  # side does: adopting `Base`'s literal return is unsound when a subclass redefines the method, and
1825
2076
  # after this change the resolved body is routinely not the receiver's own.
1826
- def try_singleton_method_inference(receiver, call_node, arg_types, method_name: call_node.name)
2077
+ def try_singleton_method_inference(receiver, call_node, arg_types, method_name: call_node.name,
2078
+ block_type: nil)
1827
2079
  return nil unless receiver.is_a?(Type::Singleton)
1828
2080
 
1829
2081
  def_node, owner = resolve_singleton_def_with_owner(receiver.class_name, method_name)
1830
2082
  return nil if def_node.nil?
1831
2083
 
1832
- result = infer_user_method_return(def_node, receiver, arg_types)
2084
+ result = infer_user_method_return(def_node, receiver, arg_types, yield_type: block_type)
1833
2085
  return result if result.nil?
1834
2086
 
1835
2087
  degrade_if_overridable(result, owner, method_name, :singleton)
@@ -1857,9 +2109,11 @@ module Rigor
1857
2109
  # body the project itself wrote. Which of the two tiers applies is decided by the receiver carrier —
1858
2110
  # `Singleton[Foo]` when the constant names a class or module, `Nominal[…]` when it holds an ordinary
1859
2111
  # object (#320) — so the two are mutually exclusive and consulting both is one resolution attempt.
1860
- def try_project_singleton_inference(receiver, call_node, arg_types, method_name: call_node.name)
1861
- try_singleton_method_inference(receiver, call_node, arg_types, method_name: method_name) ||
1862
- try_singleton_object_constant_inference(receiver, call_node, arg_types, method_name: method_name)
2112
+ def try_project_singleton_inference(receiver, call_node, arg_types, method_name: call_node.name,
2113
+ block_type: nil)
2114
+ kwargs = { method_name: method_name, block_type: block_type }
2115
+ try_singleton_method_inference(receiver, call_node, arg_types, **kwargs) ||
2116
+ try_singleton_object_constant_inference(receiver, call_node, arg_types, **kwargs)
1863
2117
  end
1864
2118
 
1865
2119
  # #320 — resolves a call whose receiver is a constant holding an ordinary object with a `class << Const`
@@ -1867,13 +2121,14 @@ module Rigor
1867
2121
  # that object, so the receiver carrier is passed through unchanged. Own-constant only, and only for a
1868
2122
  # name the project actually recorded — a miss degrades to today's `Dynamic[top]`, never a false
1869
2123
  # resolution. `Singleton` receivers never reach here: {#try_singleton_method_inference} owns them.
1870
- def try_singleton_object_constant_inference(receiver, call_node, arg_types, method_name: call_node.name)
2124
+ def try_singleton_object_constant_inference(receiver, call_node, arg_types, method_name: call_node.name,
2125
+ block_type: nil)
1871
2126
  return nil unless receiver.is_a?(Type::Nominal)
1872
2127
 
1873
2128
  def_node = SingletonObjectConstant.def_node_for(call_node, receiver, method_name, scope)
1874
2129
  return nil if def_node.nil?
1875
2130
 
1876
- infer_user_method_return(def_node, receiver, arg_types)
2131
+ infer_user_method_return(def_node, receiver, arg_types, yield_type: block_type)
1877
2132
  rescue StandardError
1878
2133
  nil
1879
2134
  end
@@ -1907,7 +2162,8 @@ module Rigor
1907
2162
  # `self_pure` is issue #525's grant scan (identity-keyed by def node); it belongs here because it
1908
2163
  # is a pure function of the same frozen index trio — the sibling resolver it walks reads nothing
1909
2164
  # else.
1910
- by_super[scope.discovered_includes] ||= { name: {}, user_def: {}, self_pure: {}.compare_by_identity }
2165
+ by_super[scope.discovered_includes] ||=
2166
+ { name: {}, user_def: {}, self_pure: {}.compare_by_identity, yields: {}.compare_by_identity }
1911
2167
  end
1912
2168
 
1913
2169
  def resolve_user_def_through_ancestors(class_name, method_name)
@@ -2078,14 +2334,6 @@ module Rigor
2078
2334
  INFERENCE_SUMMARY_KEY = :__rigor_user_method_return_summary__
2079
2335
  private_constant :INFERENCE_SUMMARY_KEY
2080
2336
 
2081
- # Monotonic per-thread counter, bumped once each time `consult_summary` actually reads an in-flight
2082
- # fixpoint assumption (ADR-55 slice 2). A method return computed across an interval in which this
2083
- # counter does NOT move depended on no transient Kleene iterate, so it is FINAL and safe to memoise —
2084
- # even when the `summaries` table is non-empty because some unrelated outermost frame merely *seeded*
2085
- # (but never consulted) its own entry. See `infer_user_method_return`'s post-hoc memo gate.
2086
- SUMMARY_CONSULT_COUNTER_KEY = :__rigor_user_method_summary_consults__
2087
- private_constant :SUMMARY_CONSULT_COUNTER_KEY
2088
-
2089
2337
  # Per-thread append-only log of the seed depths of every in-flight summary `consult_summary` read
2090
2338
  # (ADR-55 slice 2 mutual-recursion soundness fix, 2026-06-12). Each fixpoint owner records the guard
2091
2339
  # stack size at seed time on its entry (`depth:`); a consult appends the consulted entry's depth here.
@@ -2248,9 +2496,15 @@ module Rigor
2248
2496
  # the body both qualify. It does NOT need its own memo-key slot here — the bit is observable on the
2249
2497
  # built `body_scope`, which is where every downstream consumer (the memo key, the recursion context)
2250
2498
  # reads it from, so the two can never disagree.
2251
- def infer_user_method_return(def_node, receiver, arg_types, self_fold_safe: false)
2499
+ #
2500
+ # `yield_type` (issue #720) is the OTHER call-site-varying input, and unlike `self_fold_safe` it is
2501
+ # not observable on the body scope, so it does need its own memo-key slot. It is dropped for a def
2502
+ # that cannot reach a `yield` — nearly all of them — which keeps the key shape constant for the
2503
+ # methods the memo actually carries and confines the extra dimension to yielding callees.
2504
+ def infer_user_method_return(def_node, receiver, arg_types, self_fold_safe: false, yield_type: nil)
2252
2505
  return nil if def_node.body.nil?
2253
2506
 
2507
+ yield_type = nil unless yield_type && body_yields?(def_node)
2254
2508
  body_scope = build_user_method_body_scope(def_node, receiver, arg_types,
2255
2509
  self_fold_safe: self_fold_safe)
2256
2510
  return nil if body_scope.nil?
@@ -2283,42 +2537,87 @@ module Rigor
2283
2537
  # outermost method and thereafter the old `summaries.empty?` gate disabled the memo for every nested
2284
2538
  # call, re-walking the shared sub-readers combinatorially (~932k body evaluations for ~20 tiny
2285
2539
  # methods). The computation itself lives in `compute_user_method_return`.
2286
- unless memo_candidate?(stack, plain_signature)
2287
- trace_memo_refusal(stack, plain_signature)
2288
- return compute_user_method_return(def_node, body_scope, stack, summaries,
2540
+ # Issue #720 — the frame the body's `yield`s read, installed for the whole inference (the memo key
2541
+ # below reads it back from here rather than taking a second parameter, so key and frame cannot
2542
+ # disagree). It is installed even when nil, which is what stops a blockless callee reached from
2543
+ # inside a yielding body from inheriting the outer caller's block.
2544
+ ExpressionTyper.with_yield_value_type(yield_type) do
2545
+ unless memo_candidate?(stack, plain_signature)
2546
+ trace_memo_refusal(stack, plain_signature)
2547
+ next compute_user_method_return(def_node, body_scope, stack, summaries,
2289
2548
  receiver, arg_types, plain_signature)
2549
+ end
2550
+
2551
+ # INVARIANT (ADR-46 recording soundness, ADR-84 WD2) — the deep cross-file dependency edges (the
2552
+ # reads a callee body's dispatches perform through the instrumented `Scope` accessors) are recorded,
2553
+ # per consumer, only as a side effect of evaluating that body, and a memo hit serves the return
2554
+ # WITHOUT re-evaluating it. Since the bucket is run-scoped, hits CROSS consumer-file boundaries, so
2555
+ # every cross-file hit is PAIRED WITH CACHE-AND-REPLAY of the callee's read-set: under recording,
2556
+ # the first evaluation of a key captures the recorder events of its body walk
2557
+ # (`DependencyRecorder.capture` — observe-and-forward, so the first consumer's own record is
2558
+ # untouched) onto the entry, and a hit replays that set into the current consumer's accumulator
2559
+ # (`DependencyRecorder.replay`, which re-applies the per-consumer self-read filter). A memo hit is
2560
+ # thereby edge-equivalent to a fresh body evaluation for EVERY consumer. The naive alternative —
2561
+ # bypassing the memo while the recorder is active — measured >200x wall on analyzer-shaped files
2562
+ # (ActiveStorage video_analyzer.rb's subtree, 0.43s -> >90s; PR #79) and stays rejected. Pinned by
2563
+ # spec/rigor/inference/return_memo_recording_spec.rb (cross-file replay completeness) and
2564
+ # dependency_recorder_spec.rb's transitive deep-edge example.
2565
+ consult_and_store_return_memo(def_node, body_scope, stack, summaries,
2566
+ receiver, arg_types, plain_signature)
2290
2567
  end
2568
+ end
2569
+
2570
+ # Issue #720 — the constructs that own a `yield` written inside them, so a scan for "can this def's
2571
+ # body reach a `yield` that names THIS def's block?" must stop at them. A nested `def` / `class` /
2572
+ # `module` / `class << self` starts a new method-block binding; a block or a lambda does NOT — `yield`
2573
+ # inside `[1, 2].each { yield }` still calls the enclosing method's block, which is precisely the
2574
+ # `each`-wrapper idiom this fix has to keep seeing.
2575
+ YIELD_BOUNDARY_NODES = Set[
2576
+ Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode
2577
+ ].freeze
2578
+ private_constant :YIELD_BOUNDARY_NODES
2291
2579
 
2292
- # INVARIANT (ADR-46 recording soundness, ADR-84 WD2) the deep cross-file dependency edges (the
2293
- # reads a callee body's dispatches perform through the instrumented `Scope` accessors) are recorded,
2294
- # per consumer, only as a side effect of evaluating that body, and a memo hit serves the return
2295
- # WITHOUT re-evaluating it. Since the bucket is run-scoped, hits CROSS consumer-file boundaries, so
2296
- # every cross-file hit is PAIRED WITH CACHE-AND-REPLAY of the callee's read-set: under recording,
2297
- # the first evaluation of a key captures the recorder events of its body walk
2298
- # (`DependencyRecorder.capture` — observe-and-forward, so the first consumer's own record is
2299
- # untouched) onto the entry, and a hit replays that set into the current consumer's accumulator
2300
- # (`DependencyRecorder.replay`, which re-applies the per-consumer self-read filter). A memo hit is
2301
- # thereby edge-equivalent to a fresh body evaluation for EVERY consumer. The naive alternative —
2302
- # bypassing the memo while the recorder is active — measured >200x wall on analyzer-shaped files
2303
- # (ActiveStorage video_analyzer.rb's subtree, 0.43s -> >90s; PR #79) and stays rejected. Pinned by
2304
- # spec/rigor/inference/return_memo_recording_spec.rb (cross-file replay completeness) and
2305
- # dependency_recorder_spec.rb's transitive deep-edge example.
2306
- consult_and_store_return_memo(def_node, body_scope, stack, summaries,
2307
- receiver, arg_types, plain_signature)
2580
+ # Whether `def_node`'s body can reach a `yield`, memoised by def-node identity: it gates the
2581
+ # per-call-site memo dimension above and would otherwise be re-walked once per call site of every
2582
+ # user method. It rides in {#class_graph_buckets} for LIFETIME, not for correctness a pure
2583
+ # function of the AST depends on none of the tables that bucket keys on, but a store that is
2584
+ # replaced with the analysis generation cannot outlive the nodes it holds.
2585
+ def body_yields?(def_node)
2586
+ cache = class_graph_buckets[:yields]
2587
+ return cache[def_node] if cache.key?(def_node)
2588
+
2589
+ cache[def_node] = yield_reachable?(def_node.body)
2590
+ end
2591
+
2592
+ def yield_reachable?(node)
2593
+ return false if node.nil?
2594
+ return true if node.is_a?(Prism::YieldNode)
2595
+
2596
+ node.rigor_each_child do |child|
2597
+ next if YIELD_BOUNDARY_NODES.include?(child.class)
2598
+
2599
+ return true if yield_reachable?(child)
2600
+ end
2601
+ false
2308
2602
  end
2309
2603
 
2310
2604
  # The candidate-frame memo path: consult the current run generation's bucket, and on a miss compute
2311
2605
  # and store a FINAL result. Reached only when `memo_candidate?` held (see `infer_user_method_return`).
2312
- # The store gate (ADR-84 WD3): a result is stored when the ADR-55 fixpoint consult counter did not
2313
- # move across the compute (the WD0 `MEMO_REFUSE_CONSULT_TAINTED` non-store) AND no transient-machinery
2314
- # event during the bracket referenced a stack frame BELOW the bracket's entry depth (see
2315
- # TRANSIENT_EVENT_DEPTHS_KEY). Below-entry events an ancestor's in-flight Kleene iterate read by a
2316
- # guard hit, a shared-fuel exhaustion, a possibly-ancestor-caused WD1 clamp mean the ancestor
2317
- # context influenced `result`, which a standalone recompute would not reproduce; at-or-above-entry
2318
- # events are the compute's own deterministic machinery (its own converged fixpoint, sub-cycles that
2319
- # opened and closed inside the bracket) and do not block the store. A top-of-stack compute (entry
2320
- # depth 0) is standalone by construction. A hit under ADR-46 recording replays the entry's captured
2321
- # read-set into the current consumer (see the INVARIANT comment at the call site).
2606
+ # The store gate (ADR-84 WD3): a result is stored when no transient-machinery event during the
2607
+ # bracket referenced a stack frame BELOW the bracket's entry depth (see TRANSIENT_EVENT_DEPTHS_KEY).
2608
+ # A separate ADR-55 fixpoint-consult bracket counter used to sit in front of this test; it is gone
2609
+ # (issue #875) because the event log already subsumes it the sole `consult_summary` call site is
2610
+ # the in-cycle guard hit, which logs its event at the consulted owner's position first, so a consult
2611
+ # that could taint a bracket is a below-entry event by construction. Below-entry events — an
2612
+ # ancestor's in-flight Kleene iterate read by a guard hit, a shared-fuel exhaustion, a
2613
+ # possibly-ancestor-caused WD1 clamp mean the ancestor context influenced `result`, which a
2614
+ # standalone recompute would not reproduce; at-or-above-entry events are the compute's own
2615
+ # deterministic machinery (its own converged fixpoint, sub-cycles that opened and closed inside the
2616
+ # bracket) and do not block the store. A top-of-stack compute (entry depth 0) is standalone by
2617
+ # construction. ADR-84 WD6 adds one exemption on top: a tainted result that
2618
+ # is already `Dynamic[top]` is stored anyway — see `top_result?`. A hit under ADR-46 recording
2619
+ # replays the entry's captured read-set into the current consumer (see the INVARIANT comment at the
2620
+ # call site).
2322
2621
  def consult_and_store_return_memo(def_node, body_scope, stack, summaries,
2323
2622
  receiver, arg_types, plain_signature)
2324
2623
  per_def = (return_memo_bucket[def_node] ||= {})
@@ -2327,9 +2626,14 @@ module Rigor
2327
2626
  # was foldable and `Dynamic[top]` when it was not. Without it in the key the first call site to
2328
2627
  # reach a def would poison every later one with the other polarity. It is read off the body scope
2329
2628
  # rather than passed in, so the key cannot drift from the scope that produced the result.
2629
+ # Issue #720 — the FOURTH such dimension, and it is read off the installed frame rather than
2630
+ # passed in for the same reason: a yielding callee's return depends on the block THIS call site
2631
+ # hands it, so two call sites passing differently-typed blocks must not share an entry. It is nil
2632
+ # for every def that cannot reach a `yield`, which is where the memo's mass is.
2330
2633
  memo_key = [receiver.describe(:short),
2331
2634
  arg_types.map { |type| type.describe(:short) },
2332
- body_scope.struct_fold_safe?(:self)]
2635
+ body_scope.struct_fold_safe?(:self),
2636
+ ExpressionTyper.current_yield_value_type&.describe(:short)]
2333
2637
  if (entry = per_def[memo_key])
2334
2638
  BudgetTrace.hit(BudgetTrace::MEMO_HITS)
2335
2639
  Analysis::DependencyRecorder.replay(entry.read_set) if Analysis::DependencyRecorder.active?
@@ -2340,13 +2644,10 @@ module Rigor
2340
2644
 
2341
2645
  entry_depth = stack.size
2342
2646
  event_mark = transient_event_mark
2343
- consults_before = summary_consult_count
2344
2647
  result, read_set = compute_with_read_capture(def_node, body_scope, stack, summaries,
2345
2648
  receiver, arg_types, plain_signature)
2346
2649
 
2347
- if summary_consult_count != consults_before
2348
- BudgetTrace.hit(BudgetTrace::MEMO_REFUSE_CONSULT_TAINTED)
2349
- elsif context_tainted?(event_mark, entry_depth)
2650
+ if context_tainted?(event_mark, entry_depth) && !top_result?(result)
2350
2651
  BudgetTrace.hit(BudgetTrace::MEMO_REFUSE_TRANSIENT)
2351
2652
  else
2352
2653
  per_def[memo_key] = MemoEntry.new(result: result, read_set: read_set,
@@ -2359,19 +2660,48 @@ module Rigor
2359
2660
  Thread.current[TRANSIENT_EVENT_DEPTHS_KEY]&.size || 0
2360
2661
  end
2361
2662
 
2362
- def summary_consult_count
2363
- Thread.current[SUMMARY_CONSULT_COUNTER_KEY] || 0
2364
- end
2365
-
2366
2663
  # ADR-84 WD3 — true when a transient-machinery event logged during the bracket (entries past
2367
2664
  # `event_mark`) referenced a frame below `entry_depth`. The log is cleared when the guard stack drains
2368
2665
  # (only possible mid-bracket for a top-of-stack compute, whose events are deterministic anyway), so a
2369
2666
  # missing / shorter log reads as untainted.
2667
+ #
2668
+ # Scanned by index rather than over `log[event_mark..]`: the log is append-only for the whole
2669
+ # outermost entry, so the suffix slice allocated an array per candidate compute — a term quadratic in
2670
+ # the event count, and the `ary_ensure_room_for_push` / GC-sweep profile issue #870 sampled.
2370
2671
  def context_tainted?(event_mark, entry_depth)
2371
2672
  log = Thread.current[TRANSIENT_EVENT_DEPTHS_KEY]
2372
2673
  return false if log.nil? || log.size <= event_mark
2373
2674
 
2374
- log[event_mark..].any? { |depth| depth < entry_depth }
2675
+ index = event_mark
2676
+ size = log.size
2677
+ while index < size
2678
+ return true if log[index] < entry_depth
2679
+
2680
+ index += 1
2681
+ end
2682
+ false
2683
+ end
2684
+
2685
+ # ADR-84 WD6 (issue #872) — the top-result exemption to the WD3 store gate.
2686
+ #
2687
+ # WD3 refuses a store whenever the bracket saw a below-entry transient event, because such a result
2688
+ # embeds an ancestor's in-flight state and a standalone recompute need not reproduce it. Inside a
2689
+ # strongly connected component of mutually recursive methods that holds for nearly every frame — each
2690
+ # nested compute re-enters a signature already on the guard stack and the guard site logs at the
2691
+ # ancestor's shallower position — so nothing in the component is ever memoised and every call edge
2692
+ # re-walks its whole callee subtree. Issue #870's fixture measured 18,412 of 18,436 memo misses
2693
+ # computing a result and discarding it, and rufo 0.18.2's `formatter.rb` did not finish in 25 minutes.
2694
+ #
2695
+ # The exemption: when the computed result is `Dynamic[top]` the entry carries no context-dependent
2696
+ # information to serve. `untyped` is the lattice top and the ADR-5 degradation floor — the only way a
2697
+ # standalone recompute can differ is by being MORE precise, so serving the stored top can never
2698
+ # surface a type the ancestor context invented, and can never raise a diagnostic that a fresh
2699
+ # evaluation would not (nothing is reported off `untyped`). The cost is a possible missed diagnostic
2700
+ # where the untainted recompute would have been precise; the corpus evidence that this does not
2701
+ # happen in practice is the byte-identical `--format json` runs over `lib`, `plugins/*/lib`,
2702
+ # `examples/*/lib` and the survey projects recorded on issue #872.
2703
+ def top_result?(result)
2704
+ result.equal?(Type::Combinator.untyped)
2375
2705
  end
2376
2706
 
2377
2707
  # ADR-84 WD2 — the memo-miss compute, wrapped in a `DependencyRecorder.capture` window when ADR-46
@@ -2444,10 +2774,10 @@ module Rigor
2444
2774
  # stable across the body walk, that is necessary (but not sufficient) for a FINAL result — this plain
2445
2775
  # signature must not itself be on the recursion guard stack (else we are inside its own cycle,
2446
2776
  # returning a Kleene iterate by construction). Sufficiency is decided post-hoc in
2447
- # `consult_and_store_return_memo` by the two bracket counters (fixpoint consults + ADR-84 WD3
2448
- # transient-machinery events) — so unlike the prior form this deliberately does NOT refuse while a
2449
- # constant-arg unroll is in flight: a nested frame whose compute finishes without a single transient
2450
- # event ran exactly as it would standalone (fuel consumption without exhaustion is invisible), and
2777
+ # `consult_and_store_return_memo` by the ADR-84 WD3 transient-machinery event bracket so unlike
2778
+ # the prior form this deliberately does NOT refuse while a constant-arg unroll is in flight: a
2779
+ # nested frame whose compute finishes without a single transient event ran exactly as it would
2780
+ # standalone (fuel consumption without exhaustion is invisible), and
2451
2781
  # the blanket exclusion refused 82% of mail's body evaluations for such final results (ADR-84).
2452
2782
  def memo_candidate?(stack, plain_signature)
2453
2783
  stack.none? { |frame| plain_part(frame) == plain_signature }
@@ -2578,7 +2908,6 @@ module Rigor
2578
2908
  summaries = context.summaries
2579
2909
  depth = seed_fixpoint_summary(summaries, plain_signature)
2580
2910
  consult_depths = (Thread.current[SUMMARY_CONSULT_DEPTHS_KEY] ||= [])
2581
- computed = nil
2582
2911
 
2583
2912
  RECURSION_FIXPOINT_CAP.times do |iteration|
2584
2913
  summaries[plain_signature][:consulted] = false
@@ -2612,9 +2941,15 @@ module Rigor
2612
2941
  return resolved unless resolved.nil?
2613
2942
  end
2614
2943
 
2615
- step = fixpoint_step(summaries, plain_signature, computed, iteration)
2616
- return step unless step == :continue
2944
+ converged = fixpoint_step(summaries, plain_signature, computed, iteration)
2945
+ return converged if converged
2617
2946
  end
2947
+
2948
+ # Out of iterations and still unstable. The collapse is the loop's own tail rather than a branch
2949
+ # inside `fixpoint_step`: `Integer#times` yields its receiver when no iteration returns, so a
2950
+ # tail-less loop reads as a path that returns an Integer summary (the `def.return-type-mismatch`
2951
+ # on `return_type_for` this shape retired), and the loop, not the step, owns its cap.
2952
+ collapse_fixpoint_cap(summaries, plain_signature)
2618
2953
  end
2619
2954
 
2620
2955
  # Seeds the thread-local summary entry for a fixpoint owner: the `bot` Kleene seed plus the
@@ -2650,8 +2985,10 @@ module Rigor
2650
2985
 
2651
2986
  # One Kleene-iteration step of the fixpoint loop. Joins `computed` into the running assumption
2652
2987
  # (widening value-pinned constituents on the final permitted iteration to force convergence) and
2653
- # either returns a final type — convergence, or the capped `untyped` collapseor `:continue` to
2654
- # request another body evaluation, having advanced the stored assumption. ADR-55 WD2.
2988
+ # returns the converged type, or `nil` no final type yet having advanced the stored assumption
2989
+ # so the caller's next body evaluation reads the wider iterate. The step never decides that the
2990
+ # loop is over: whether another evaluation follows is the caller's cap, and a `nil` on the final
2991
+ # iteration lands in `collapse_fixpoint_cap`. ADR-55 WD2.
2655
2992
  def fixpoint_step(summaries, plain_signature, computed, iteration)
2656
2993
  assumption = summaries[plain_signature][:assumption]
2657
2994
  last_iteration = iteration == RECURSION_FIXPOINT_CAP - 1
@@ -2662,17 +2999,19 @@ module Rigor
2662
2999
  # (joining it back changes nothing).
2663
3000
  return candidate if joined == assumption
2664
3001
 
2665
- if last_iteration
2666
- # Out of iterations and still unstable — collapse to today's widening behaviour. ADR-84 WD3: the
2667
- # cap is a per-owner constant, so the event references the owner's own frame.
2668
- note_transient_fallback(BudgetTrace::RECURSION_FIXPOINT_CAP, own_guard_frame_position)
2669
- scope.record_dynamic_origin(@typing_node, DynamicOrigin::ANALYZER_BUDGET_CUTOFF) if @typing_node
2670
- summaries[plain_signature][:assumption] = Type::Combinator.untyped
2671
- return Type::Combinator.untyped
2672
- end
2673
-
2674
3002
  summaries[plain_signature][:assumption] = joined
2675
- :continue
3003
+ nil
3004
+ end
3005
+
3006
+ # The capped collapse: `RECURSION_FIXPOINT_CAP` evaluations advanced the assumption without
3007
+ # converging, so the summary degrades to today's widening behaviour, `untyped`, parked in the
3008
+ # assumption for any consumer that still reads it. ADR-84 WD3: the cap is a per-owner constant, so
3009
+ # the event references the owner's own frame.
3010
+ def collapse_fixpoint_cap(summaries, plain_signature)
3011
+ note_transient_fallback(BudgetTrace::RECURSION_FIXPOINT_CAP, own_guard_frame_position)
3012
+ scope.record_dynamic_origin(@typing_node, DynamicOrigin::ANALYZER_BUDGET_CUTOFF) if @typing_node
3013
+ summaries[plain_signature][:assumption] = Type::Combinator.untyped
3014
+ Type::Combinator.untyped
2676
3015
  end
2677
3016
 
2678
3017
  # Rebuilds the user-method body scope with every bound positional parameter widened to its nominal
@@ -3109,22 +3448,87 @@ module Rigor
3109
3448
  nil
3110
3449
  end
3111
3450
 
3451
+ EMPTY_BREAK_ARMS = [].freeze
3452
+ private_constant :EMPTY_BREAK_ARMS
3453
+
3454
+ # The types the `break`s that terminate THIS call carry out of it, for {#call_dispatch_type_for} to union
3455
+ # with the result the dispatch tiers computed (issue #853).
3456
+ #
3457
+ # The syntactic scan runs first and answers "none" for every call whose block cannot reach a block-level
3458
+ # `break` — nearly all of them — so only that small minority pays the body evaluation. A `break` under a
3459
+ # nested block, lambda, `def`, or loop targets THAT construct instead; {JUMP_BOUNDARY_NODES} stops the
3460
+ # scan there, and the identity filter drops the ones the sink still collects because the nested
3461
+ # construct is walked under the same installation. `break` with no argument carries nil, so the call
3462
+ # becomes optional — which is what Ruby does.
3463
+ #
3464
+ # A failure yields no arms rather than propagating, matching {#block_return_type_for}: a call typed
3465
+ # without its break arms is the pre-#853 answer, while a raise here would take out the whole call.
3466
+ def call_break_arm_types(node, receiver_override: nil)
3467
+ block_node = node.block
3468
+ return EMPTY_BREAK_ARMS unless block_node.is_a?(Prism::BlockNode)
3469
+
3470
+ body = block_node.body
3471
+ return EMPTY_BREAK_ARMS if body.nil? || !block_level_jump?(body, Prism::BreakNode)
3472
+
3473
+ collect_break_arm_types(node, block_node, body, receiver_override)
3474
+ rescue StandardError
3475
+ EMPTY_BREAK_ARMS
3476
+ end
3477
+
3478
+ # Evaluates the block body once under a `break`-value sink, in the same entry scope the block-return pass
3479
+ # uses, so each arm is typed in the scope that actually reaches it — a `break v` after `v = "s"`
3480
+ # contributes `"s"`, not the entry binding — and an arm on a branch the analysis proved dead is never
3481
+ # reached at all.
3482
+ def collect_break_arm_types(call_node, block_node, body, receiver_override)
3483
+ targets = block_level_jump_nodes(body, Prism::BreakNode)
3484
+ receiver = receiver_override || call_receiver_type_for(call_node)
3485
+ return EMPTY_BREAK_ARMS if receiver.nil?
3486
+
3487
+ narrowed_self = MacroBlockSelfType.narrow_self_type_for(
3488
+ scope: scope, call_node: call_node, receiver_type: receiver
3489
+ )
3490
+ block_scope = block_entry_scope(
3491
+ block_node, break_arm_param_types(call_node, receiver), narrowed_self_type: narrowed_self
3492
+ )
3493
+ _result, collected = StatementEvaluator.with_break_value_sink do
3494
+ without_block_body_threading { block_scope.evaluate(body) }
3495
+ end
3496
+ collected.filter_map { |jump, type| type if targets.key?(jump) }
3497
+ end
3498
+
3499
+ def break_arm_param_types(call_node, receiver)
3500
+ MethodDispatcher.expected_block_param_types(
3501
+ receiver_type: receiver,
3502
+ method_name: call_node.name,
3503
+ arg_types: call_arg_types(call_node),
3504
+ environment: scope.environment
3505
+ )
3506
+ end
3507
+
3112
3508
  def block_return_for(block_arg, expected, narrowed_self_type: nil)
3113
3509
  case block_arg
3114
3510
  when Prism::BlockNode
3115
- bindings = BlockParameterBinder.new(expected_param_types: expected).bind(block_arg)
3116
- # Issue #316 — mirrors `StatementEvaluator#build_block_entry_scope`: the block body's `self` is the
3117
- # yielding method's business, so the return-typing pass must see the same unmodelled-self mark.
3118
- block_scope = bindings.reduce(scope.entering_opaque_block) do |acc, (name, type)|
3119
- acc.with_local(name, type)
3120
- end
3121
- block_scope = block_scope.with_self_type(narrowed_self_type) if narrowed_self_type
3122
- type_block_body(block_arg, block_scope)
3511
+ type_block_body(block_arg, block_entry_scope(block_arg, expected, narrowed_self_type: narrowed_self_type))
3123
3512
  when Prism::BlockArgumentNode
3124
3513
  symbol_block_return_type(block_arg, expected)
3125
3514
  end
3126
3515
  end
3127
3516
 
3517
+ # The scope a block body is typed under: the surrounding scope plus the parameter bindings the receiving
3518
+ # method's signature implies.
3519
+ #
3520
+ # Issue #316 — mirrors `StatementEvaluator#build_block_entry_scope`: the block body's `self` is the
3521
+ # yielding method's business, so the return-typing pass must see the same unmodelled-self mark.
3522
+ def block_entry_scope(block_node, expected, narrowed_self_type: nil)
3523
+ bindings = BlockParameterBinder.new(expected_param_types: expected).bind(block_node)
3524
+ block_scope = bindings.reduce(scope.entering_opaque_block) do |acc, (name, type)|
3525
+ acc.with_local(name, type)
3526
+ end
3527
+ return block_scope unless narrowed_self_type
3528
+
3529
+ block_scope.with_self_type(narrowed_self_type)
3530
+ end
3531
+
3128
3532
  # `&:symbol` desugars to a one-arg Proc that dispatches `symbol` against its argument. When the param
3129
3533
  # type is known and the resulting inner dispatch is precise, this returns the precise carrier;
3130
3534
  # otherwise it returns `Dynamic[Top]` (still non-nil) so the outer dispatcher selects the
@@ -3157,13 +3561,95 @@ module Rigor
3157
3561
  # — which threads scope statement by statement through `StatementEvaluator` — held `42` for the very
3158
3562
  # same node. {#threaded_block_body_type} closes that gap by reusing the main pass's evaluator; a decline
3159
3563
  # keeps the tail-only answer verbatim.
3564
+ #
3565
+ # Issue #841: the fall-through tail is only ONE of the block's exits. Every `next value` that leaves this
3566
+ # block makes `value` the block's result for that invocation, so the arms join into the value type the
3567
+ # same way {#evaluate_body_with_returns} joins a method's early `return`s with its tail. Without the
3568
+ # join, `ops.all? { |o| next false unless o; true }` read as `Constant[true]` and
3569
+ # {MethodDispatcher::BlockFolding} folded the call to always-truthy on a program that really can answer
3570
+ # false — a warning on correct code.
3160
3571
  def type_block_body(block_node, block_scope)
3161
3572
  body = block_node.body
3162
3573
  return Type::Combinator.constant_of(nil) if body.nil?
3163
3574
 
3575
+ arms = block_level_next_arms(body)
3576
+ return block_body_type_joining_nexts(body, block_scope, arms) if arms
3577
+
3164
3578
  threaded_block_body_type(body, block_scope) || block_scope.type_of(body)
3165
3579
  end
3166
3580
 
3581
+ # Evaluates the body once under a `next` sink and joins the arms that leave THIS block with the
3582
+ # fall-through tail the same evaluation produced. Both halves come from one `StatementEvaluator` run, so
3583
+ # each arm is typed in the scope that actually reaches it — a `next y` after `y = "s"` contributes
3584
+ # `"s"`, not the entry scope's stale binding.
3585
+ #
3586
+ # This run ignores the {#block_body_threading_suppressed?} flag that gates the precision fold: that flag
3587
+ # bounds the cost of an OPTIONAL widening, while skipping the join re-opens a false-positive class. It
3588
+ # still SETS the flag for the duration, so nothing folds below it and the cost stays one evaluation per
3589
+ # block-level `next` — a shape that is a small minority of blocks.
3590
+ #
3591
+ # A failure falls back to the tail-only answer rather than propagating, for the same reason
3592
+ # {#threaded_block_body_type} does: the enclosing `block_return_type_for` rescue would otherwise report
3593
+ # "no block" to the dispatcher, a far larger regression than a wide block return.
3594
+ def block_body_type_joining_nexts(body, block_scope, arms)
3595
+ (fall_through, _exit_scope), collected = StatementEvaluator.with_next_sink do
3596
+ without_block_body_threading { block_scope.evaluate(body) }
3597
+ end
3598
+ joined = collected.filter_map { |node, type| type if arms.key?(node) }
3599
+ joined.empty? ? fall_through : Type::Combinator.union(fall_through, *joined)
3600
+ rescue StandardError
3601
+ block_scope.type_of(body)
3602
+ end
3603
+
3604
+ # The identity-keyed set of `next` nodes whose value is THIS block's value — reachable from the body
3605
+ # without crossing a {JUMP_BOUNDARY_NODES} construct that retargets them — or nil for "do not join".
3606
+ #
3607
+ # nil means "do not join": a body with no block-level `next` keeps today's path exactly, and pays only
3608
+ # the allocation-free scan that answers so. A co-resident block-level `break` used to decline here as
3609
+ # well, because its value was dropped and joining the `next` arms would have dressed a still-incomplete
3610
+ # answer as a complete one; since issue #853 the `break` arms are unioned into the yielding call
3611
+ # ({#call_break_arm_types}), so the two exits are modelled at their own levels and neither blocks the
3612
+ # other.
3613
+ def block_level_next_arms(body)
3614
+ return nil unless block_level_jump?(body, Prism::NextNode)
3615
+
3616
+ block_level_jump_nodes(body, Prism::NextNode)
3617
+ end
3618
+
3619
+ # True when a `klass` jump is reachable from `node` without crossing a construct that retargets it.
3620
+ # Allocation-free and early-exiting: this is the scan every block body pays, and the overwhelming
3621
+ # majority of them answer false on it.
3622
+ def block_level_jump?(node, klass)
3623
+ return false if node.nil?
3624
+ return true if node.is_a?(klass)
3625
+
3626
+ node.rigor_each_child do |child|
3627
+ next if JUMP_BOUNDARY_NODES.include?(child.class)
3628
+
3629
+ return true if block_level_jump?(child, klass)
3630
+ end
3631
+ false
3632
+ end
3633
+
3634
+ # The identity-keyed set of `klass` jumps that target THIS block — every one {#block_level_jump?} would
3635
+ # answer true for, rather than the first. The sinks in `StatementEvaluator` also collect jumps from
3636
+ # nested blocks / loops / defs evaluated under the same installation, so the consumer filters against
3637
+ # this set by node identity.
3638
+ def block_level_jump_nodes(body, klass)
3639
+ found = {}.compare_by_identity
3640
+ collect_block_level_jumps(body, klass, found)
3641
+ found
3642
+ end
3643
+
3644
+ def collect_block_level_jumps(node, klass, found)
3645
+ found[node] = true if node.is_a?(klass)
3646
+ node.rigor_each_child do |child|
3647
+ next if JUMP_BOUNDARY_NODES.include?(child.class)
3648
+
3649
+ collect_block_level_jumps(child, klass, found)
3650
+ end
3651
+ end
3652
+
3167
3653
  # Re-typing the whole body would be wrong to do unconditionally: this path runs for EVERY block-bearing
3168
3654
  # call, and the statements ahead of the tail are pure cost whenever the tail does not depend on them.
3169
3655
  # Three declines keep that cost where the defect actually is, each falling back to the tail-only path:
@@ -3252,18 +3738,27 @@ module Rigor
3252
3738
  private_constant :VARIABLE_READ_NODES
3253
3739
 
3254
3740
  # A `next` / `break` that leaves THIS block carries a value the fold cannot see: `evaluate(body).first`
3255
- # is the fall-through value only, and no next-value join into the block return exists (`type_of_jump`
3256
- # types both as `Bot`). So `m.synchronize do next 5 if flag; v = 42; v end` really can answer 5 at
3257
- # runtime, and threading would type it `42`. Both forms escape with a value `next v` is the block's
3258
- # value for that yield, `break v` is the yielding CALL's value so both must decline.
3741
+ # is the fall-through value only.
3742
+ #
3743
+ # The `next` entry no longer fires: {#type_block_body} routes such a body past this scan entirely, since
3744
+ # issue #841 joins those arms into the block's value. It is kept because the question this predicate
3745
+ # answers ("can this prefix escape with a value the fall-through misses?") is the caller's premise
3746
+ # rather than the caller's filter restated.
3747
+ #
3748
+ # The `break` entry still fires, and since issue #853 it is conservative rather than load-bearing: the
3749
+ # arms are unioned into the CALL ({#call_break_arm_types}), so the missing value is no longer missing.
3750
+ # What the decline still costs is the threaded tail — `m.synchronize do break 5 if flag; v = 42; v end`
3751
+ # answers `5 | Dynamic[top]` where threading would reach `5 | 42`. Lifting it widens no answer and
3752
+ # invents no fact, but it moves the type of every block that carries a `break`, so it is left to a
3753
+ # change that can measure that.
3259
3754
  JUMP_NODES = Set[Prism::NextNode, Prism::BreakNode].freeze
3260
3755
  private_constant :JUMP_NODES
3261
3756
 
3262
3757
  # Constructs that RETARGET a `next` / `break` nested inside them, so a jump below one of these says
3263
- # nothing about our block's value and must not trigger the decline. A nested `BlockNode` / `LambdaNode`
3264
- # is the jump's own block (`do xs.each { next 1 }; v = 42; v end` threads soundly — the inner `next`
3265
- # ends the inner iteration); a loop consumes both forms (`while … next 5 … end` continues the loop);
3266
- # a `DefNode` body is a different method entirely.
3758
+ # nothing about our block's value it neither triggers the decline nor joins as an arm. A nested
3759
+ # `BlockNode` / `LambdaNode` is the jump's own block (`do xs.each { next 1 }; v = 42; v end` threads
3760
+ # soundly — the inner `next` ends the inner iteration); a loop consumes both forms (`while … next 5 …
3761
+ # end` continues the loop); a `DefNode` body is a different method entirely.
3267
3762
  JUMP_BOUNDARY_NODES = Set[
3268
3763
  Prism::BlockNode, Prism::LambdaNode, Prism::DefNode,
3269
3764
  Prism::WhileNode, Prism::UntilNode, Prism::ForNode
@@ -3293,11 +3788,8 @@ module Rigor
3293
3788
  # so `[1].each { v = 5 }` really does bind the outer `v`, and `[1].each { outer << 1 }` really does
3294
3789
  # mutate the outer `outer`), while a jump counts only above the nearest {JUMP_BOUNDARY_NODES} boundary.
3295
3790
  def tail_depends_on_body_binding?(statements)
3296
- written = Set.new
3297
- statements[0...-1].each do |statement|
3298
- return false unless prefix_statement_jump_free?(statement, written, false)
3299
- end
3300
- return false if written.empty?
3791
+ written = prefix_written_names(statements)
3792
+ return false if written.nil?
3301
3793
 
3302
3794
  Source::NodeWalker.each(statements.last) do |node|
3303
3795
  return true if VARIABLE_READ_NODES.include?(node.class) && written.include?(node.name)
@@ -3305,6 +3797,35 @@ module Rigor
3305
3797
  false
3306
3798
  end
3307
3799
 
3800
+ # WHICH names the predicate above answers YES on — every name the tail observes that the prefix binds
3801
+ # or mutates in place. Only the arity-cap floor ({#unanswered_tail_dependency?}) needs them, which is
3802
+ # why the predicate is not written over this method: the predicate runs for every multi-statement block
3803
+ # body and short-circuits on the first hit, the floor runs for a handful of calls. Sharing
3804
+ # {#prefix_written_names} is what keeps the two from drifting about what the prefix binds.
3805
+ def tail_dependent_body_names(statements)
3806
+ written = prefix_written_names(statements)
3807
+ return EMPTY_NAME_SET if written.nil?
3808
+
3809
+ Source::NodeWalker.each(statements.last).filter_map do |node|
3810
+ node.name if VARIABLE_READ_NODES.include?(node.class) && written.include?(node.name)
3811
+ end.to_set
3812
+ end
3813
+
3814
+ # The names the prefix binds or mutates in place, or `nil` when the scan declines — a prefix that can
3815
+ # jump out of the block with a value the fall-through misses, or one that binds nothing at all.
3816
+ def prefix_written_names(statements)
3817
+ written = Set.new
3818
+ statements[0...-1].each do |statement|
3819
+ return nil unless prefix_statement_jump_free?(statement, written, false)
3820
+ end
3821
+ return nil if written.empty?
3822
+
3823
+ written
3824
+ end
3825
+
3826
+ EMPTY_NAME_SET = Set.new.freeze
3827
+ private_constant :EMPTY_NAME_SET
3828
+
3308
3829
  # True when `node` cannot jump out of the block with a value, collecting into `written` the names it
3309
3830
  # binds (a variable-write node) or mutates in place (a {MutationWidening::SHAPE_MUTATORS} call, through
3310
3831
  # every variable its receiver can evaluate to) on the way down. `retargeted` is true once the descent
@@ -3370,7 +3891,38 @@ module Rigor
3370
3891
  per_position = per_element_block_results(call_node.block, element_types)
3371
3892
  return nil if per_position.nil? || per_position.any?(&:nil?)
3372
3893
 
3373
- assemble_per_element_result(call_node.name, per_position, element_types)
3894
+ assemble_per_element_result(call_node.name, per_position, element_types) ||
3895
+ find_family_floor(call_node.name, element_types)
3896
+ end
3897
+
3898
+ # The honest answer for `find` / `detect` / `find_index` / `index` when this fold walked every position
3899
+ # and the assembler still could not decide — which happens for exactly one reason: some position's
3900
+ # predicate is not a `Constant`, so "the first matching one" is not a static fact.
3901
+ #
3902
+ # Falling through to the dispatcher was WRONG for this family, and issue #617 residue (1) is the bill:
3903
+ # `seen = 0; [1, 2].find do |e| seen += 1; seen == 2 end` answered `nil` where the runtime answers `2`.
3904
+ # The dispatcher's `BlockFolding` reads ONE block-return type, typed from the call's ENTRY scope, so a
3905
+ # predicate over a rebound capture pins the first iteration (`Constant[false]`) and
3906
+ # `FALSEY_BLOCK_NIL_METHODS` short-circuits the whole call to `nil`. This walk already knows better: it
3907
+ # typed the predicate per position and saw that it does not fold. Answering here is what keeps that
3908
+ # knowledge from being thrown away in favour of a worse-informed tier.
3909
+ #
3910
+ # The floor is what `find` can return and no tighter: one of the receiver's own elements, or `nil` when
3911
+ # no element matches. Value pinning survives because it is still true of every candidate — `find` hands
3912
+ # back an element, it does not compute one. `find_index` / `index` answer a position in the receiver, so
3913
+ # `Integer?` is their floor.
3914
+ #
3915
+ # Nothing else in {PER_ELEMENT_TUPLE_METHODS} takes a floor: `map`'s assembler cannot decline,
3916
+ # and `select` / `reject` / `filter_map` / `flat_map` fall through to an RBS projection that is merely
3917
+ # wider, never wrong — `BlockFolding`'s filter folds decline on a non-Constant block instead of
3918
+ # answering.
3919
+ def find_family_floor(method_name, element_types)
3920
+ case method_name
3921
+ when :find, :detect
3922
+ Type::Combinator.union(*element_types, Type::Combinator.constant_of(nil))
3923
+ when :find_index, :index
3924
+ Type::Combinator.union(Type::Combinator.nominal_of("Integer"), Type::Combinator.constant_of(nil))
3925
+ end
3374
3926
  end
3375
3927
 
3376
3928
  # Evaluates the call's block once per receiver element. Two block shapes are supported:
@@ -3408,10 +3960,48 @@ module Rigor
3408
3960
  element_types.map { |element_type| type_block_body_with_param(block, [element_type], captured: captured) }
3409
3961
  end
3410
3962
  return results.call if element_types.size <= PER_ELEMENT_THREADING_LIMIT
3963
+ return uncapped_body_floor(element_types) if unanswered_tail_dependency?(block, captured)
3411
3964
 
3412
3965
  without_block_body_threading(&results)
3413
3966
  end
3414
3967
 
3968
+ # Above {PER_ELEMENT_THREADING_LIMIT} the threading is suppressed, and a position is typed tail-only.
3969
+ # For a tail that reads what the body's own prefix wrote or mutated, tail-only is not a wider answer —
3970
+ # it is the ENTRY binding, which the prefix has already falsified.
3971
+ #
3972
+ # #584's cliff comment promised `Dynamic[top]` above the cap, and that held for a body-LOCAL: `[1, …,
3973
+ # 9].map do v = e; v end` has no entry binding for `v`, so tail-only lands on `Dynamic[top]` by itself.
3974
+ # A mutated PARAMETER has one, and issue #617 residue (2) is what it buys: `([[]] * 9).map do |a| a <<
3975
+ # 1; a end` answered nine stale `[]`, a provably-empty array at every position of a result whose slots
3976
+ # each hold `[1]`. Flooring the whole walk restores the promise for both shapes — the cost the cap
3977
+ # refuses to pay is the per-position body evaluation, and declining to pay it means declining to know,
3978
+ # not answering the pre-state.
3979
+ #
3980
+ # {#tail_depends_on_body_binding?} is the same predicate the threading gate uses, so "would threading
3981
+ # have changed this tail" and "is tail-only untrustworthy here" stay one question. A body it answers
3982
+ # false for keeps its exact tail-only fold above the cap, which is every single-statement block and
3983
+ # every multi-statement block whose tail ignores its prefix.
3984
+ def uncapped_body_floor(element_types)
3985
+ Array.new(element_types.size) { Type::Combinator.untyped }
3986
+ end
3987
+
3988
+ # True when the tail reads something the prefix changed that NOTHING has re-answered for this walk.
3989
+ #
3990
+ # `captured` is the converged binding of every outer local the block rebinds ({#per_element_captured_bindings}),
3991
+ # and its cost is independent of the arity, so it keeps working above the cap: `total = 0; [1, …,
3992
+ # 9].map do total += e; total end` reads `total` as the fixpoint's `Integer` at every position and needs
3993
+ # no floor. What the cap actually withholds is the per-position body evaluation, so the names it leaves
3994
+ # unanswered are the ones the fixpoint does not cover — a mutated block PARAMETER (issue #617 residue
3995
+ # (2)'s `|a| a << 1; a`) or a mutated outer local the block never rebinds.
3996
+ def unanswered_tail_dependency?(block, captured)
3997
+ body = block.body
3998
+ return false unless body.is_a?(Prism::StatementsNode)
3999
+ return false if body.body.size < 2
4000
+
4001
+ answered = captured&.keys || []
4002
+ tail_dependent_body_names(body.body).any? { |name| !answered.include?(name) }
4003
+ end
4004
+
3415
4005
  # Issue #587 (b) — first-iteration pinning. Every position of this fold is typed from the SAME entry
3416
4006
  # scope, so a body that rebinds a captured outer local answers the FIRST iteration's value at every
3417
4007
  # position: `total = 0; [1, 2].map do total += 1; total end` folded to `[1, 1]` (runtime `[1, 2]`), and
@@ -3456,12 +4046,44 @@ module Rigor
3456
4046
 
3457
4047
  def converged_captured_bindings(block, names, element_types)
3458
4048
  param_types = [Type::Combinator.union(*element_types)]
3459
- BodyFixpoint.converge(
4049
+ seeds = names.to_h { |name| [name, scope.local(name)] }
4050
+ converged = BodyFixpoint.converge(
3460
4051
  names: names,
3461
- seed_bindings: names.to_h { |name| [name, scope.local(name)] },
4052
+ seed_bindings: seeds,
3462
4053
  widen: Type::Combinator.method(:widen_value_pinned),
3463
4054
  evaluate_body: ->(bindings) { captured_exit_bindings(block, param_types, bindings, names) }
3464
4055
  )
4056
+ unmoved_pins_floored(converged, seeds)
4057
+ end
4058
+
4059
+ # A name the write scan says this block REBINDS, whose fixpoint came back on exactly its value-pinned
4060
+ # seed, is floored rather than believed.
4061
+ #
4062
+ # The fixpoint reads each pass's exit binding out of `StatementEvaluator`, and that evaluator only
4063
+ # threads a write it sees as a STATEMENT: a rebind nested inside an expression — `(seen += 1) == 2` as
4064
+ # the block's whole body — leaves the exit scope holding the entry binding, so the fixpoint converges on
4065
+ # the seed and every position of the fold answers the first iteration again. That is issue #617 residue
4066
+ # (1)'s one-liner, `seen = 0; [1, 2].find { |e| (seen += 1) == 2 }` answering `nil` where Ruby answers
4067
+ # `2`: the pinned `Constant[0]` made both predicates `Constant[false]`, and `find` short-circuits on a
4068
+ # provably-falsey block.
4069
+ #
4070
+ # An unmoved pin cannot be distinguished from a write that genuinely restores its own entry value
4071
+ # (`x = 5; xs.each { x = 5 }`), so the floor gives that shape up too. It is the far cheaper side: a
4072
+ # value-pinned seed the block rebinds is the exact pre-state this fold exists to stop trusting, and
4073
+ # `Dynamic[top]` is the same escaping-block floor {#captured_floor} already uses. Seeds that carry no
4074
+ # value pinning are left alone — there is no first-iteration constant in them to remove, and widening a
4075
+ # `Nominal` here would only lose a class for nothing.
4076
+ def unmoved_pins_floored(converged, seeds)
4077
+ converged.to_h do |name, type|
4078
+ seed = seeds[name]
4079
+ next [name, type] unless type == seed && value_pinned?(seed)
4080
+
4081
+ [name, Type::Combinator.untyped]
4082
+ end
4083
+ end
4084
+
4085
+ def value_pinned?(type)
4086
+ !type.nil? && Type::Combinator.widen_value_pinned(type) != type
3465
4087
  end
3466
4088
 
3467
4089
  # One fixpoint pass: the body evaluated from `bindings` with the block parameters bound over them (the
@@ -3501,7 +4123,7 @@ module Rigor
3501
4123
  # `Constant<a..b>` → [Constant[a], …, Constant[b]]
3502
4124
  # everything else → nil
3503
4125
  #
3504
- # Note: `Type::IntegerRange` is the bounded-Integer carrier (`int<a, b>` represents "an Integer between
4126
+ # Note: `Type::IntegerRange` is the bounded-Integer carrier (`Integer[a..b]` represents "an Integer between
3505
4127
  # a and b"), not a Range value. Calls like `.map` / `.find` on an `IntegerRange` receiver would resolve
3506
4128
  # to `Integer#map` / `Integer#find` — neither exists — so IntegerRange does NOT participate in this
3507
4129
  # fold.
@@ -3541,7 +4163,7 @@ module Rigor
3541
4163
  #
3542
4164
  # Part 1 (soundness): the accumulator of a block-form fold must reach a fixpoint over an unknown
3543
4165
  # number of iterations — the RBS tier's generic `(S) { (S, E) -> S } -> S` binds `S` from a SINGLE
3544
- # block pass (acc=seed, elem=element-join), so `(1..5).inject(1) { |a, i| a * i }` types `int<1, 5>`
4166
+ # block pass (acc=seed, elem=element-join), so `(1..5).inject(1) { |a, i| a * i }` types `Integer[1..5]`
3545
4167
  # while the runtime is 120 (out of range — unsound). We iterate the accumulator type to a capped
3546
4168
  # fixpoint (ADR-55/56 `BodyFixpoint`) so the multiply converges to `Integer`, never a value-bounded
3547
4169
  # interval the runtime escapes.
@@ -3557,8 +4179,6 @@ module Rigor
3557
4179
  # tier. Captured-local write-back (ADR-56) runs at the statement level independent of this return-type
3558
4180
  # computation, so a block that both accumulates and mutates captured state keeps its write-back
3559
4181
  # regardless of which arm answers here.
3560
- #
3561
- # @return [Rigor::Type, nil]
3562
4182
  def try_block_inject_fold(call_node, receiver, arg_types)
3563
4183
  return nil unless INJECT_METHODS.include?(call_node.name)
3564
4184
 
@@ -3577,7 +4197,7 @@ module Rigor
3577
4197
  # Splits the positional args into the optional seed. A Symbol final arg (`inject(seed, :*)`) is the
3578
4198
  # no-block Symbol form and never reaches here (the block guard already failed for it).
3579
4199
  #
3580
- # @return [Array(Rigor::Type, nil), Boolean] `[seed, has_seed]`
4200
+ # @return `[seed, has_seed]`
3581
4201
  def inject_seed(arg_types)
3582
4202
  case arg_types.size
3583
4203
  when 0 then [nil, false]
@@ -3637,7 +4257,7 @@ module Rigor
3637
4257
  # member is folded; without a seed the first member seeds the memo and the rest are folded. The
3638
4258
  # accumulator is carried as a `Constant` type (so the block body sees a value-pinned param).
3639
4259
  #
3640
- # @return [Array(Rigor::Type::Constant, nil), Array] `[acc, rest]`
4260
+ # @return `[acc, rest]`
3641
4261
  def inject_constant_start(members, seed, has_seed)
3642
4262
  if has_seed
3643
4263
  return [nil, []] unless seed.is_a?(Type::Constant) && inject_foldable?(seed.value)