rigortype 0.4.6 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (550) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -2
  3. data/data/core_overlay/bigdecimal.rbs +33 -0
  4. data/data/core_overlay/resolv.rbs +30 -0
  5. data/data/gem_overlay/activesupport/core_ext.rbs +45 -13
  6. data/data/gem_overlay/addressable/addressable.rbs +40 -0
  7. data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
  8. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
  9. data/docs/handbook/01-getting-started.md +6 -3
  10. data/docs/handbook/11-sig-gen.md +85 -3
  11. data/docs/handbook/appendix-phpstan.md +4 -4
  12. data/docs/install.md +20 -0
  13. data/docs/manual/01-installation.md +67 -26
  14. data/docs/manual/02-cli-reference.md +455 -113
  15. data/docs/manual/03-configuration.md +76 -5
  16. data/docs/manual/04-diagnostics.md +5 -4
  17. data/docs/manual/06-baseline.md +6 -4
  18. data/docs/manual/08-skills.md +11 -0
  19. data/docs/manual/09-editor-integration.md +21 -2
  20. data/docs/manual/10-mcp-server.md +38 -2
  21. data/docs/manual/12-caching.md +4 -3
  22. data/docs/manual/14-rails-quickstart.md +1 -1
  23. data/docs/manual/19-effect-labels.md +3 -4
  24. data/docs/manual/plugins/rigor-actionpack.md +32 -15
  25. data/docs/manual/plugins/rigor-activesupport-core-ext.md +23 -1
  26. data/docs/manual/plugins/rigor-sorbet.md +17 -3
  27. data/lib/rigor/analysis/baseline.rb +24 -27
  28. data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
  29. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  30. data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
  31. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  32. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +12 -4
  33. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  34. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +85 -13
  35. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +16 -4
  36. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -1
  37. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  38. data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
  39. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +41 -12
  40. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
  41. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +3 -3
  42. data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
  43. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  44. data/lib/rigor/analysis/check_rules/rule_ids.rb +9 -2
  45. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  46. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  47. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  48. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +2 -2
  49. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +2 -2
  50. data/lib/rigor/analysis/check_rules.rb +720 -141
  51. data/lib/rigor/analysis/crash_signature.rb +45 -3
  52. data/lib/rigor/analysis/dependency_recorder.rb +237 -80
  53. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  54. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  55. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  56. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  57. data/lib/rigor/analysis/edit_replay.rb +93 -6
  58. data/lib/rigor/analysis/fact_store.rb +3 -3
  59. data/lib/rigor/analysis/incremental.rb +11 -1
  60. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +4 -3
  61. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  62. data/lib/rigor/analysis/incremental_session.rb +306 -53
  63. data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
  64. data/lib/rigor/analysis/rule_catalog.rb +39 -3
  65. data/lib/rigor/analysis/run_cache_key.rb +27 -3
  66. data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
  67. data/lib/rigor/analysis/run_stats.rb +2 -2
  68. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
  69. data/lib/rigor/analysis/runner/declaration_position.rb +8 -8
  70. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +264 -112
  71. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +4 -9
  72. data/lib/rigor/analysis/runner/pool_coordinator.rb +173 -328
  73. data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
  74. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  75. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  76. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  77. data/lib/rigor/analysis/runner.rb +298 -109
  78. data/lib/rigor/analysis/self_call_resolution_recorder.rb +62 -23
  79. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  80. data/lib/rigor/analysis/template_units.rb +9 -1
  81. data/lib/rigor/analysis/worker_session.rb +65 -71
  82. data/lib/rigor/bleeding_edge.rb +22 -10
  83. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  84. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  85. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  86. data/lib/rigor/cache/descriptor.rb +6 -5
  87. data/lib/rigor/cache/file_digest.rb +39 -31
  88. data/lib/rigor/cache/incremental_snapshot.rb +95 -17
  89. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  90. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  91. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  92. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  93. data/lib/rigor/cache/rbs_descriptor.rb +90 -4
  94. data/lib/rigor/cache/rbs_environment.rb +8 -1
  95. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  96. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  97. data/lib/rigor/cache/store.rb +11 -10
  98. data/lib/rigor/ci_detector.rb +1 -1
  99. data/lib/rigor/cli/annotate_command.rb +24 -19
  100. data/lib/rigor/cli/baseline_command.rb +160 -57
  101. data/lib/rigor/cli/catalogue.rb +256 -0
  102. data/lib/rigor/cli/check_command.rb +112 -104
  103. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  104. data/lib/rigor/cli/command.rb +15 -1
  105. data/lib/rigor/cli/coverage_command.rb +21 -8
  106. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  107. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  108. data/lib/rigor/cli/coverage_scan.rb +3 -5
  109. data/lib/rigor/cli/diff_command.rb +25 -20
  110. data/lib/rigor/cli/docs_command.rb +1 -2
  111. data/lib/rigor/cli/doctor_command.rb +12 -12
  112. data/lib/rigor/cli/effects_command.rb +50 -40
  113. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  114. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  115. data/lib/rigor/cli/effects_renderer.rb +2 -1
  116. data/lib/rigor/cli/effects_snapshot_command.rb +26 -18
  117. data/lib/rigor/cli/explain_command.rb +18 -8
  118. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  119. data/lib/rigor/cli/json_document.rb +41 -0
  120. data/lib/rigor/cli/lens_command.rb +29 -10
  121. data/lib/rigor/cli/lsp_command.rb +250 -23
  122. data/lib/rigor/cli/mcp_command.rb +12 -7
  123. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  124. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  125. data/lib/rigor/cli/options.rb +40 -1
  126. data/lib/rigor/cli/plugin_command.rb +93 -46
  127. data/lib/rigor/cli/plugins_command.rb +17 -18
  128. data/lib/rigor/cli/plugins_renderer.rb +10 -6
  129. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  130. data/lib/rigor/cli/protection_renderer.rb +2 -1
  131. data/lib/rigor/cli/show_bleedingedge_command.rb +21 -15
  132. data/lib/rigor/cli/sig_gen_command.rb +92 -27
  133. data/lib/rigor/cli/skill_command.rb +32 -3
  134. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  135. data/lib/rigor/cli/skill_describe.rb +2 -1
  136. data/lib/rigor/cli/trace_command.rb +14 -9
  137. data/lib/rigor/cli/triage_command.rb +10 -11
  138. data/lib/rigor/cli/triage_renderer.rb +2 -1
  139. data/lib/rigor/cli/type_of_command.rb +16 -11
  140. data/lib/rigor/cli/type_of_project_scope.rb +1 -4
  141. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  142. data/lib/rigor/cli/type_scan_command.rb +14 -9
  143. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  144. data/lib/rigor/cli/unused_command.rb +27 -17
  145. data/lib/rigor/cli/upgrade_command.rb +5 -0
  146. data/lib/rigor/cli.rb +214 -56
  147. data/lib/rigor/config_audit.rb +33 -19
  148. data/lib/rigor/configuration/severity_profile.rb +14 -5
  149. data/lib/rigor/configuration.rb +104 -35
  150. data/lib/rigor/effects/collector.rb +53 -21
  151. data/lib/rigor/effects/definition_context.rb +0 -2
  152. data/lib/rigor/effects/envelope.rb +1 -1
  153. data/lib/rigor/effects/framework_units.rb +1 -1
  154. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  155. data/lib/rigor/effects/propagator.rb +1 -1
  156. data/lib/rigor/effects/registry.rb +2 -2
  157. data/lib/rigor/effects/snapshot.rb +1 -1
  158. data/lib/rigor/effects/taint_cause.rb +0 -2
  159. data/lib/rigor/effects/unit_scan.rb +9 -13
  160. data/lib/rigor/environment/bundle_sig_discovery.rb +64 -33
  161. data/lib/rigor/environment/class_registry.rb +3 -3
  162. data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
  163. data/lib/rigor/environment/core_root.rb +77 -0
  164. data/lib/rigor/environment/default_libraries.rb +18 -1
  165. data/lib/rigor/environment/deferred_signature_plan.rb +381 -0
  166. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  167. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  168. data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
  169. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  170. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  171. data/lib/rigor/environment/member_consistency.rb +56 -7
  172. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  173. data/lib/rigor/environment/project_path_gems.rb +124 -0
  174. data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
  175. data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
  176. data/lib/rigor/environment/rbs_hierarchy.rb +12 -15
  177. data/lib/rigor/environment/rbs_loader.rb +772 -110
  178. data/lib/rigor/environment/reflection.rb +7 -7
  179. data/lib/rigor/environment/required_features.rb +0 -3
  180. data/lib/rigor/environment/ruby_library_roots.rb +1 -1
  181. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  182. data/lib/rigor/environment.rb +169 -42
  183. data/lib/rigor/flow_contribution/merger.rb +4 -4
  184. data/lib/rigor/inference/acceptance.rb +136 -40
  185. data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
  186. data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
  187. data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
  188. data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
  189. data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
  190. data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
  191. data/lib/rigor/inference/block_auto_splat.rb +1 -1
  192. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  193. data/lib/rigor/inference/block_run_count.rb +185 -0
  194. data/lib/rigor/inference/budget_trace.rb +8 -2
  195. data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
  196. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  197. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  198. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  199. data/lib/rigor/inference/callee_mutation_chain.rb +88 -77
  200. data/lib/rigor/inference/callee_write_census.rb +815 -0
  201. data/lib/rigor/inference/captured_locals.rb +2 -0
  202. data/lib/rigor/inference/census_foreign_block.rb +354 -0
  203. data/lib/rigor/inference/clause_widening.rb +1 -1
  204. data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
  205. data/lib/rigor/inference/construction_escape.rb +258 -0
  206. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  207. data/lib/rigor/inference/declaration_walk.rb +4 -0
  208. data/lib/rigor/inference/deep_erasure.rb +39 -7
  209. data/lib/rigor/inference/def_handle.rb +1 -1
  210. data/lib/rigor/inference/def_node_resolver.rb +33 -15
  211. data/lib/rigor/inference/def_return_typer.rb +47 -5
  212. data/lib/rigor/inference/definer_resolution.rb +19 -9
  213. data/lib/rigor/inference/definers_index.rb +162 -0
  214. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  215. data/lib/rigor/inference/element_read_widening.rb +49 -3
  216. data/lib/rigor/inference/error_info.rb +16 -0
  217. data/lib/rigor/inference/expression_typer.rb +1559 -320
  218. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  219. data/lib/rigor/inference/flow_tracer.rb +31 -14
  220. data/lib/rigor/inference/fork_map.rb +3 -0
  221. data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
  222. data/lib/rigor/inference/global_write_census.rb +81 -5
  223. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  224. data/lib/rigor/inference/hkt_body.rb +1 -2
  225. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  226. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  227. data/lib/rigor/inference/hkt_registry.rb +3 -5
  228. data/lib/rigor/inference/in_effect_refinements.rb +7 -0
  229. data/lib/rigor/inference/indexed_narrowing.rb +61 -15
  230. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  231. data/lib/rigor/inference/ivar_writer_census.rb +154 -8
  232. data/lib/rigor/inference/key_presence_guard.rb +51 -24
  233. data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
  234. data/lib/rigor/inference/last_line.rb +5 -2
  235. data/lib/rigor/inference/loop_value_record.rb +66 -14
  236. data/lib/rigor/inference/match_rebinding/frame.rb +57 -0
  237. data/lib/rigor/inference/match_rebinding/yielding_match.rb +101 -0
  238. data/lib/rigor/inference/match_rebinding.rb +28 -5
  239. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  240. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  241. data/lib/rigor/inference/method_dispatcher/block_folding.rb +44 -30
  242. data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
  243. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +3 -3
  244. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  245. data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
  246. data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
  247. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
  248. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
  249. data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
  250. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  251. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +1 -1
  252. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  253. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +51 -27
  254. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  255. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +606 -78
  256. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  257. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  258. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +3 -3
  259. data/lib/rigor/inference/method_dispatcher/set_folding.rb +9 -7
  260. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +467 -84
  261. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  262. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  263. data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
  264. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  265. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  266. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  267. data/lib/rigor/inference/method_dispatcher.rb +123 -25
  268. data/lib/rigor/inference/method_parameter_binder.rb +136 -56
  269. data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
  270. data/lib/rigor/inference/module_function_state.rb +24 -3
  271. data/lib/rigor/inference/multi_target_binder.rb +2 -2
  272. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  273. data/lib/rigor/inference/mutation_widening.rb +199 -29
  274. data/lib/rigor/inference/narrowing.rb +238 -50
  275. data/lib/rigor/inference/operand_effects.rb +43 -2
  276. data/lib/rigor/inference/optimistic_origin.rb +134 -9
  277. data/lib/rigor/inference/origin_lookup.rb +1 -1
  278. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  279. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  280. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  281. data/lib/rigor/inference/precision_scanner.rb +1 -1
  282. data/lib/rigor/inference/project_method_ownership.rb +30 -9
  283. data/lib/rigor/inference/project_patched_methods.rb +65 -13
  284. data/lib/rigor/inference/project_patched_scanner.rb +120 -25
  285. data/lib/rigor/inference/project_patched_visibility.rb +39 -3
  286. data/lib/rigor/inference/project_yielder.rb +11 -17
  287. data/lib/rigor/inference/rbs_type_translator.rb +68 -15
  288. data/lib/rigor/inference/receiver_alias.rb +20 -0
  289. data/lib/rigor/inference/receiver_writer_call.rb +67 -0
  290. data/lib/rigor/inference/recursion_rebinding.rb +157 -0
  291. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +57 -17
  292. data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -16
  293. data/lib/rigor/inference/refined_dispatch.rb +13 -0
  294. data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
  295. data/lib/rigor/inference/remover_holder_scan.rb +287 -0
  296. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  297. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  298. data/lib/rigor/inference/scope_indexer/certainty.rb +0 -1
  299. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
  300. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  301. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +70 -26
  302. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  303. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  304. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +295 -0
  305. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
  306. data/lib/rigor/inference/scope_indexer.rb +2213 -226
  307. data/lib/rigor/inference/self_call_ivar_writes.rb +103 -12
  308. data/lib/rigor/inference/self_preserving_block.rb +87 -0
  309. data/lib/rigor/inference/set_mutation.rb +189 -0
  310. data/lib/rigor/inference/singleton_hook_decline.rb +42 -8
  311. data/lib/rigor/inference/singleton_object_constant.rb +32 -2
  312. data/lib/rigor/inference/statement_evaluator.rb +3268 -358
  313. data/lib/rigor/inference/string_mutation.rb +14 -0
  314. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  315. data/lib/rigor/inference/synthetic_method_scanner.rb +40 -17
  316. data/lib/rigor/inference/toplevel_def_census.rb +67 -0
  317. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  318. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  319. data/lib/rigor/inference/untyped_rejoin.rb +1 -1
  320. data/lib/rigor/inference/void_tail_summary.rb +29 -9
  321. data/lib/rigor/language_server/buffer_table.rb +3 -3
  322. data/lib/rigor/language_server/completion_provider.rb +26 -20
  323. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  324. data/lib/rigor/language_server/diagnostic_publisher.rb +8 -3
  325. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  326. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  327. data/lib/rigor/language_server/hover_provider.rb +1 -7
  328. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  329. data/lib/rigor/language_server/project_context.rb +39 -1
  330. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  331. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  332. data/lib/rigor/language_server/server.rb +7 -3
  333. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  334. data/lib/rigor/lens/claims/comparison.rb +78 -0
  335. data/lib/rigor/lens/claims/request.rb +131 -0
  336. data/lib/rigor/lens/claims/type_reader.rb +216 -0
  337. data/lib/rigor/lens/claims.rb +208 -0
  338. data/lib/rigor/lens/contract_reader.rb +5 -0
  339. data/lib/rigor/lens/document.rb +1 -1
  340. data/lib/rigor/lens/file_reader.rb +21 -8
  341. data/lib/rigor/lens/plugins.rb +1 -1
  342. data/lib/rigor/lens/run.rb +8 -0
  343. data/lib/rigor/lens.rb +1 -1
  344. data/lib/rigor/library_probe.rb +68 -0
  345. data/lib/rigor/mcp/server.rb +73 -2
  346. data/lib/rigor/mcp.rb +1 -1
  347. data/lib/rigor/memo_audit.rb +282 -0
  348. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  349. data/lib/rigor/plugin/base.rb +15 -2
  350. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  351. data/lib/rigor/plugin/io_boundary.rb +26 -4
  352. data/lib/rigor/plugin/loader.rb +25 -8
  353. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  354. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  355. data/lib/rigor/plugin/manifest.rb +16 -16
  356. data/lib/rigor/plugin/registry.rb +16 -5
  357. data/lib/rigor/plugin/services.rb +2 -3
  358. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  359. data/lib/rigor/plugin/trust_policy.rb +48 -2
  360. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  361. data/lib/rigor/plugin.rb +6 -8
  362. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  363. data/lib/rigor/project_environment.rb +16 -2
  364. data/lib/rigor/protection/analysis_guard.rb +1 -1
  365. data/lib/rigor/protection/discovery_seed.rb +44 -5
  366. data/lib/rigor/protection/mutation_cache.rb +4 -1
  367. data/lib/rigor/protection/mutator.rb +1 -1
  368. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  369. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  370. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  371. data/lib/rigor/rbs_extended.rb +2 -3
  372. data/lib/rigor/reflection/constant_ancestors.rb +81 -22
  373. data/lib/rigor/reflection/declaration_collectors.rb +55 -0
  374. data/lib/rigor/reflection/declarations.rb +58 -0
  375. data/lib/rigor/reflection/project_method.rb +177 -0
  376. data/lib/rigor/reflection.rb +2 -0
  377. data/lib/rigor/rooted_glob.rb +32 -1
  378. data/lib/rigor/run_scoped_slots.rb +38 -0
  379. data/lib/rigor/runtime/jit.rb +1 -1
  380. data/lib/rigor/scope/discovery_index.rb +168 -8
  381. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
  382. data/lib/rigor/scope/resolution_chain.rb +901 -54
  383. data/lib/rigor/scope.rb +644 -37
  384. data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
  385. data/lib/rigor/sig_gen/classification.rb +18 -1
  386. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  387. data/lib/rigor/sig_gen/generator.rb +588 -66
  388. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  389. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  390. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  391. data/lib/rigor/sig_gen/method_candidate.rb +37 -4
  392. data/lib/rigor/sig_gen/observation_collector.rb +14 -7
  393. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  394. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  395. data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
  396. data/lib/rigor/sig_gen/renderer.rb +15 -9
  397. data/lib/rigor/sig_gen/skip_reason_catalog.rb +62 -2
  398. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  399. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  400. data/lib/rigor/sig_gen/write_result.rb +23 -12
  401. data/lib/rigor/sig_gen/writer.rb +238 -37
  402. data/lib/rigor/sig_gen.rb +1 -0
  403. data/lib/rigor/source/forwardable_delegators.rb +122 -0
  404. data/lib/rigor/source/node_children.rb +13 -0
  405. data/lib/rigor/type/acceptance_router.rb +2 -2
  406. data/lib/rigor/type/accepts_result.rb +1 -1
  407. data/lib/rigor/type/app.rb +4 -6
  408. data/lib/rigor/type/combinator.rb +173 -21
  409. data/lib/rigor/type/constant.rb +4 -5
  410. data/lib/rigor/type/hash_shape.rb +21 -40
  411. data/lib/rigor/type/interface.rb +60 -0
  412. data/lib/rigor/type/union.rb +1 -1
  413. data/lib/rigor/type.rb +1 -0
  414. data/lib/rigor/type_node/identifier.rb +1 -2
  415. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  416. data/lib/rigor/type_node/string_literal.rb +1 -2
  417. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  418. data/lib/rigor/type_node/union.rb +3 -6
  419. data/lib/rigor/type_of_memo/serving.rb +99 -0
  420. data/lib/rigor/type_of_memo/stats.rb +43 -0
  421. data/lib/rigor/type_of_memo.rb +337 -0
  422. data/lib/rigor/typing_state.rb +1094 -0
  423. data/lib/rigor/value_semantics.rb +1 -10
  424. data/lib/rigor/version.rb +1 -1
  425. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  426. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  427. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  428. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +38 -36
  429. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  430. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -4
  431. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  432. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  433. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  434. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  435. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +3 -3
  436. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +115 -36
  437. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
  438. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
  439. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  440. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
  441. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
  442. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -3
  443. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
  444. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  445. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  446. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  447. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  448. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +132 -33
  449. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -6
  450. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
  451. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +54 -17
  452. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
  453. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  454. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  455. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +11 -14
  456. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  457. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  458. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  459. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +30 -9
  460. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +55 -13
  461. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
  462. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
  463. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  464. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  465. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  466. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  467. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
  468. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  469. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  470. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  471. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  472. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  473. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  474. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  475. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +8 -7
  476. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  477. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  478. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
  479. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  480. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  481. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +8 -2
  482. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  483. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  484. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  485. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  486. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  487. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +200 -4
  488. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  489. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  490. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  491. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +4 -3
  492. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  493. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +5 -5
  494. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  495. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  496. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  497. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +3 -2
  498. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  499. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  500. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +210 -3
  501. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
  502. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  503. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
  504. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +97 -24
  505. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  506. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
  507. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +4 -8
  508. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  509. data/sig/rigor/analysis/baseline.rbs +0 -3
  510. data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
  511. data/sig/rigor/analysis/fact_store.rbs +2 -2
  512. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  513. data/sig/rigor/cache/descriptor.rbs +14 -0
  514. data/sig/rigor/cli/diff_command.rbs +1 -1
  515. data/sig/rigor/cli/explain_command.rbs +1 -1
  516. data/sig/rigor/effects/registry.rbs +2 -2
  517. data/sig/rigor/environment.rbs +4 -1
  518. data/sig/rigor/inference.rbs +4 -1
  519. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  520. data/sig/rigor/plugin/base.rbs +53 -18
  521. data/sig/rigor/plugin/effect_attribution.rbs +3 -3
  522. data/sig/rigor/plugin/effect_edge.rbs +1 -1
  523. data/sig/rigor/plugin/effect_entry_points.rbs +3 -3
  524. data/sig/rigor/plugin/interfaces.rbs +51 -0
  525. data/sig/rigor/plugin/io_boundary.rbs +3 -3
  526. data/sig/rigor/plugin/manifest.rbs +9 -9
  527. data/sig/rigor/plugin/node_context.rbs +21 -0
  528. data/sig/rigor/plugin/protocol_contract.rbs +3 -7
  529. data/sig/rigor/reflection.rbs +37 -0
  530. data/sig/rigor/scope.rbs +43 -4
  531. data/sig/rigor/trinary.rbs +1 -0
  532. data/sig/rigor/type.rbs +9 -3
  533. data/sig/rigor.rbs +3 -2
  534. data/skills/rigor-ask/SKILL.md +1 -0
  535. data/skills/rigor-plugin-author/SKILL.md +4 -4
  536. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  537. data/skills/rigor-plugin-review/SKILL.md +3 -3
  538. data/skills/rigor-project-init/references/01-detect.md +1 -0
  539. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +17 -2
  540. data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
  541. data/skills/rigor-rbs-contract/SKILL.md +147 -0
  542. data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
  543. data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
  544. data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
  545. data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
  546. data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
  547. data/skills/rigor-rbs-setup/SKILL.md +3 -1
  548. data/skills/rigor-type-oracle/SKILL.md +26 -1
  549. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
  550. metadata +82 -3
data/lib/rigor/scope.rb CHANGED
@@ -3,6 +3,8 @@
3
3
  require_relative "type"
4
4
  require_relative "environment"
5
5
  require_relative "scope/discovery_index"
6
+ require_relative "typing_state"
7
+ require_relative "memo_audit"
6
8
  require_relative "scope/resolution_chain"
7
9
  require_relative "inference/definer_resolution"
8
10
  require_relative "inference/in_effect_refinements"
@@ -13,6 +15,10 @@ require_relative "inference/flow_tracer"
13
15
  require_relative "inference/optimistic_origin"
14
16
  require_relative "inference/statement_evaluator"
15
17
  require_relative "inference/def_node_resolver"
18
+ # Issue #2608 — read by `MutationWidening` at a remover call, which only an analysis makes; loaded with the engine
19
+ # so the engine-free value layer that requires `MutationWidening` stays as small as it was.
20
+ require_relative "inference/remover_narrowing"
21
+ require_relative "inference/alias_mutation_floor"
16
22
 
17
23
  module Rigor
18
24
  # Immutable analyzer scope: holds local-variable bindings and a reference to the surrounding Environment. State
@@ -30,7 +36,7 @@ module Rigor
30
36
  :opaque_block_self, :block_self_unknown, :singleton_class_body, :lexical_nesting,
31
37
  :dynamic_origins, :local_origins, :ivar_origins,
32
38
  :void_origins, :plugin_typed_calls,
33
- :optimistic_origins, :optimistic_locals, :optimistic_ivars,
39
+ :optimistic_origins, :optimistic_locals, :optimistic_ivars, :construction_facts,
34
40
  :repeated_or_writes, :match_frame,
35
41
  :constant_narrowings, :guard_records, :bot_guard_classes, :declared_refinements
36
42
 
@@ -89,7 +95,7 @@ module Rigor
89
95
  def discovered_prepends = @discovery.discovered_prepends.transform_values { |names| names.uniq.freeze }.freeze
90
96
  def discovered_extends = @discovery.discovered_extends
91
97
  def discovered_class_sources = @discovery.discovered_class_sources
92
- # Issue #644 — `{qualified constant name => Set[declaring file]}`; seeded only on an ADR-46 recording run.
98
+ # Issue #644 — `{qualified constant name => declaring files}`; seeded only on an ADR-46 recording run.
93
99
  def constant_sources = @discovery.constant_sources
94
100
  def published_constant_names = @discovery.published_constant_names
95
101
  def local_constant_names = @discovery.local_constant_names
@@ -153,7 +159,13 @@ module Rigor
153
159
  # renames the foreign declaration. `published_constant_alias_names` holds the last segments of exactly
154
160
  # those aliases, and matching on the last segment over-answers in the WITHHOLDING direction, which is the
155
161
  # safe one.
162
+ #
163
+ # Issue #3199 — the answer turns on another file's publication of the last segment, so the question records
164
+ # the ADR-46 `constant:<segment>` name edge itself, whatever it answers. The guard asks it about spellings no
165
+ # typing step reads (a `defined?(B::X)` probe, a callee body the one hop scans), and an edge left to the read
166
+ # would let an `--incremental` run keep a verdict the moved publication no longer earns.
156
167
  def published_constant?(name)
168
+ Analysis::DependencyRecorder.read_last_segment(:constant, name) if Analysis::DependencyRecorder.active?
157
169
  names = @discovery.published_constant_names
158
170
  return true if published_constant_alias?(name)
159
171
  return false if names.empty?
@@ -202,9 +214,9 @@ module Rigor
202
214
  # `:const` allowed; its entry marks presence only.
203
215
  IndexedKey = Data.define(:receiver_kind, :receiver_name, :key)
204
216
 
205
- # Narrowing key for a no-arg / no-block method-call chain `receiver.method_name` (a "single-hop" chain per A1
206
- # from the ROADMAP § Future cycles slice). The value of the map at this key is the narrowed type the next read
207
- # of the same chain MUST observe — typically the post-`is_a?(C)` narrowing established on a predicate edge.
217
+ # Narrowing key for a no-arg / no-block method-call chain `receiver.method_name` (a "single-hop" chain).
218
+ # The value of the map at this key is the narrowed type the next read of the same chain MUST observe —
219
+ # typically the post-`is_a?(C)` narrowing established on a predicate edge.
208
220
  #
209
221
  # - `receiver_kind` ∈ `{:local, :ivar}` — the analyzer only tracks chains rooted at a local or instance
210
222
  # variable today (Law-of-Demeter-style single-hop).
@@ -232,7 +244,7 @@ module Rigor
232
244
  # through flow transitions and ignored by `==` / `hash`.
233
245
  EMPTY_FOLD_SAFE = Set.new.freeze
234
246
  # ADR-82 WD1 — provenance propagation across the receiver-node lookup. `local_origins` / `ivar_origins`
235
- # map a `name` (Symbol) to the {Inference::DynamicOrigin} cause of the `Dynamic` value currently bound to
247
+ # map a `name` to the {Inference::DynamicOrigin} cause of the `Dynamic` value currently bound to
236
248
  # it, so a downstream dispatch whose receiver is a bare `x` / `@x` read resolves to why that value is
237
249
  # dynamic (the cause was recorded on the *assignment*'s rhs node, which the receiver-read node is not).
238
250
  # Advisory metadata like {#dynamic_origins}: ignored by `==` / `hash` (never varies a flow decision) and
@@ -283,6 +295,32 @@ module Rigor
283
295
  :EMPTY_PUBLISHED_CONSTANT_IVARS, :EMPTY_REPEATED_OR_WRITES,
284
296
  :EMPTY_CONSTANT_NARROWINGS, :EMPTY_GUARD_RECORDS, :EMPTY_BOT_GUARD_CLASSES
285
297
 
298
+ # ADR-129 WD1 — the four advisory tables are bound Tables: each is an identity Hash one file's scopes share by
299
+ # reference (a fresh body scope may start its own; {#with_isolated_side_tables} copies them), so a journal record
300
+ # names the store beside the node. Every entry read and write goes through the methods below, which log it while a
301
+ # capture is open; handing a store to a new Scope is not an entry read.
302
+ DYNAMIC_ORIGINS = TypingState.table(
303
+ :dynamic_origins,
304
+ bound: true,
305
+ reason: "node => DynamicOrigin cause; typing records it and reads it back for receiver provenance"
306
+ )
307
+ VOID_ORIGINS = TypingState.table(
308
+ :void_origins, bound: true, reason: "node => VoidOrigin site; recorded by dispatch, read by the value-use rule"
309
+ )
310
+ PLUGIN_TYPED_CALLS = TypingState.table(
311
+ :plugin_typed_calls, bound: true, reason: "node => true where a plugin's dynamic_return answered the call"
312
+ )
313
+ OPTIMISTIC_ORIGINS = TypingState.table(
314
+ :optimistic_origins,
315
+ bound: true,
316
+ reason: "node => OptimisticOrigin cause; typing records it and reads it back to keep a value nil-free"
317
+ )
318
+ private_constant :DYNAMIC_ORIGINS, :VOID_ORIGINS, :PLUGIN_TYPED_CALLS, :OPTIMISTIC_ORIGINS
319
+ TypingState.store_field(self, :@dynamic_origins, DYNAMIC_ORIGINS)
320
+ TypingState.store_field(self, :@void_origins, VOID_ORIGINS)
321
+ TypingState.store_field(self, :@plugin_typed_calls, PLUGIN_TYPED_CALLS)
322
+ TypingState.store_field(self, :@optimistic_origins, OPTIMISTIC_ORIGINS)
323
+
286
324
  class << self
287
325
  def empty(environment: Environment.default, source_path: nil)
288
326
  new(environment: environment, locals: {}.freeze,
@@ -291,16 +329,33 @@ module Rigor
291
329
  end
292
330
 
293
331
  def record_dynamic_origin(node, cause)
332
+ TypingState.current.log_bound_write(DYNAMIC_ORIGINS, @dynamic_origins, node, cause)
294
333
  @dynamic_origins[node] = cause
295
334
  self
296
335
  end
297
336
 
337
+ # The {Inference::DynamicOrigin} cause recorded at `node`, or nil (ADR-129: a logged Table read).
338
+ def dynamic_origin(node)
339
+ log_advisory_read(DYNAMIC_ORIGINS, @dynamic_origins, node)
340
+ end
341
+
342
+ # The {Inference::VoidOrigin} recorded at `node`, or nil (ADR-129: a logged Table read).
343
+ def void_origin(node)
344
+ log_advisory_read(VOID_ORIGINS, @void_origins, node)
345
+ end
346
+
347
+ # The {Inference::OptimisticOrigin} cause recorded at `node`, or nil (ADR-129: a logged Table read).
348
+ def optimistic_origin(node)
349
+ log_advisory_read(OPTIMISTIC_ORIGINS, @optimistic_origins, node)
350
+ end
351
+
298
352
  # ADR-100 WD3 — records that the value introduced at `node` (a call node) is a `top` recovered from an
299
353
  # author-declared `-> void` return, keyed by the origin site (`origin`, an {Inference::VoidOrigin}). The
300
354
  # value-context check rule `static.value-use.void` consumes this table. Mirrors {#record_dynamic_origin}
301
355
  # exactly: identity-keyed advisory metadata, mutated in place on the shared table (threaded by reference
302
356
  # through `#join` / `#rebuild`), excluded from `==` / `hash`, so it never forks a flow-dedup or cache key.
303
357
  def record_void_origin(node, origin)
358
+ TypingState.current.log_bound_write(VOID_ORIGINS, @void_origins, node, origin)
304
359
  @void_origins[node] = origin
305
360
  self
306
361
  end
@@ -313,15 +368,22 @@ module Rigor
313
368
  # identity-keyed advisory metadata on a table threaded by reference through `#join` / `#rebuild`,
314
369
  # excluded from `==` / `hash`, so it never forks a flow-dedup or cache key.
315
370
  def record_plugin_typed_call(node)
371
+ TypingState.current.log_bound_write(PLUGIN_TYPED_CALLS, @plugin_typed_calls, node, true)
316
372
  @plugin_typed_calls[node] = true
317
373
  self
318
374
  end
319
375
 
320
376
  # Issue #653 — whether a plugin answered the return type for `node` on this run.
321
377
  def plugin_typed_call?(node)
322
- @plugin_typed_calls.key?(node)
378
+ !log_advisory_read(PLUGIN_TYPED_CALLS, @plugin_typed_calls, node).nil?
323
379
  end
324
380
 
381
+ # Every keyword past the 31st (`opaque_block_self`, `block_self_unknown`) must keep a literal default. A keyword at
382
+ # that index with a non-literal default makes a call that omits it allocate CRuby's unspecified-keywords record
383
+ # during argument setup, and on CRuby 4.0 a GC started by that allocation frees the values already moved into the
384
+ # keyword locals (ruby/ruby Bug #22500, issue #2850): `Scope.new` stored a freed `match_frame`.
385
+ # `spec/rigor/keyword_gc_safety_spec.rb` holds every method under `lib/` and `plugins/*/lib` to this. See the note
386
+ # at {#rebuild} before adding a field.
325
387
  def initialize( # rubocop:disable Metrics/AbcSize, Metrics/MethodLength -- one keyword and assignment per field
326
388
  environment:, locals:,
327
389
  fact_store: Analysis::FactStore.empty,
@@ -336,8 +398,6 @@ module Rigor
336
398
  published_constant_sourced: EMPTY_PUBLISHED_CONSTANT_SOURCED,
337
399
  source_path: nil,
338
400
  struct_fold_safe_locals: EMPTY_FOLD_SAFE,
339
- opaque_block_self: false,
340
- block_self_unknown: false,
341
401
  singleton_class_body: false,
342
402
  lexical_nesting: nil,
343
403
  dynamic_origins: {}.compare_by_identity,
@@ -348,12 +408,16 @@ module Rigor
348
408
  optimistic_origins: {}.compare_by_identity,
349
409
  optimistic_locals: EMPTY_ORIGINS,
350
410
  optimistic_ivars: EMPTY_ORIGINS,
411
+ construction_facts: EMPTY_ORIGINS,
351
412
  repeated_or_writes: EMPTY_REPEATED_OR_WRITES,
352
413
  match_frame: nil,
353
414
  constant_narrowings: EMPTY_CONSTANT_NARROWINGS,
354
415
  guard_records: EMPTY_GUARD_RECORDS,
355
416
  bot_guard_classes: EMPTY_BOT_GUARD_CLASSES,
356
- declared_refinements: Inference::InEffectRefinements::EMPTY
417
+ declared_refinements: Inference::InEffectRefinements::EMPTY,
418
+ # Literal defaults only from here on; see the note above.
419
+ opaque_block_self: false,
420
+ block_self_unknown: false
357
421
  )
358
422
  @environment = environment
359
423
  @locals = locals
@@ -381,6 +445,7 @@ module Rigor
381
445
  @optimistic_origins = optimistic_origins
382
446
  @optimistic_locals = optimistic_locals
383
447
  @optimistic_ivars = optimistic_ivars
448
+ @construction_facts = construction_facts
384
449
  @repeated_or_writes = repeated_or_writes
385
450
  @match_frame = match_frame
386
451
  @constant_narrowings = constant_narrowings
@@ -395,10 +460,20 @@ module Rigor
395
460
  # nil-freeness is a property of its class. Mirrors {#dynamic_origins} / {#void_origins}: advisory
396
461
  # metadata, ignored by `==` / `hash`, and never varying a flow decision on its own.
397
462
  def record_optimistic_origin(node, cause)
463
+ TypingState.current.log_bound_write(OPTIMISTIC_ORIGINS, @optimistic_origins, node, cause)
398
464
  @optimistic_origins[node] = cause
399
465
  self
400
466
  end
401
467
 
468
+ # The entry of the advisory table `store` (the bound Table `slot`'s) at `node`, or nil, logged as a Table read: a
469
+ # key the store does not hold logs {TypingState::ABSENT}, so a recorded nil and a miss differ.
470
+ def log_advisory_read(slot, store, node)
471
+ value = store.fetch(node, TypingState::ABSENT)
472
+ TypingState.current.log_bound_read(slot, store, node, value)
473
+ TypingState::ABSENT.equal?(value) ? nil : value
474
+ end
475
+ private :log_advisory_read
476
+
402
477
  # Issue #1703 — this scope with private copies of the identity-keyed side tables (dynamic, void and optimistic
403
478
  # origins, plugin-typed calls), so a second walk from it — the `key?`-guarded re-walk — records into its own
404
479
  # tables and never into the ones the file's analysis reads.
@@ -433,6 +508,41 @@ module Rigor
433
508
  rebuild(optimistic_ivars: @optimistic_ivars.merge(name.to_sym => mark).freeze)
434
509
  end
435
510
 
511
+ # Issue #2555 L3 — the construction facts a LOCAL's binding carries: what the engine knows about how the object
512
+ # bound to it was made, as opposed to what its type says about its contents. `:synthesised` means the engine
513
+ # built the object itself (a collection literal, or an `Array.new` / `Hash.new` / `Set.new` / `X[...]`
514
+ # constructor on the core class), so its element types are this engine's own inference and no declaration's
515
+ # claim: the mutation seams may re-grow such a carrier precisely, where a declared one (a parameter, an RBS
516
+ # return) keeps the issue #561 boundary. The table is `{ name => frozen Set[Symbol] }`, so a second fact joins
517
+ # an existing entry without reshaping it.
518
+ #
519
+ # Locals only. An instance variable's binding is seeded across methods from the class-ivar index, whose
520
+ # construction no single body witnesses, so an ivar never carries a fact. Like {#optimistic_locals} the facts
521
+ # are kept by {#with_mutated_local} (the binding still names the same object), dropped by {#with_local} (a
522
+ # source-level rebind names a new one), intersected at a merge, and reset with the locals at a method body's
523
+ # entry. No fact is copied by an alias write (`b = a`): the type is shared, the provenance is not tracked
524
+ # through a second name (issue #2335), and a local that may escape anywhere in its body is never stamped
525
+ # ({Inference::ConstructionEscape}). Within an entry `:synthesised` intersects at a merge and `:grown` (a store
526
+ # has grown the carrier since construction) unions.
527
+ CONSTRUCTION_FACT_SETS = Ractor.make_shareable({ synthesised: Set[:synthesised] })
528
+ NO_CONSTRUCTION_FACTS = Set.new.freeze
529
+ private_constant :CONSTRUCTION_FACT_SETS, :NO_CONSTRUCTION_FACTS
530
+
531
+ def construction_fact?(name, fact = :synthesised)
532
+ @construction_facts.fetch(name.to_sym, NO_CONSTRUCTION_FACTS).include?(fact)
533
+ end
534
+
535
+ # Records `fact` on the local `name`. Applied AFTER the {#with_local} that binds the value, which drops any
536
+ # fact the old binding held.
537
+ def with_construction_fact(name, fact)
538
+ key = name.to_sym
539
+ existing = @construction_facts[key]
540
+ return self if existing&.include?(fact)
541
+
542
+ facts = existing ? (existing | [fact]).freeze : (CONSTRUCTION_FACT_SETS[fact] || Set[fact].freeze)
543
+ rebuild(@construction_facts.merge(key => facts).freeze)
544
+ end
545
+
436
546
  # ADR-82 WD1 — the propagated origin of the `Dynamic` value currently bound to a local / instance
437
547
  # variable, or `nil` when none is tracked. Consulted by `Inference::ProtectionScanner` when a dispatch's
438
548
  # receiver is a bare `x` / `@x` read whose own node carries no origin.
@@ -487,7 +597,8 @@ module Rigor
487
597
  # longer holds (the new value may carry a method-local nil). `with_declaration_sourced_local` re-establishes
488
598
  # the mark afterward when the RHS is a pure copy of a declaration-sourced ivar read; the default is to drop
489
599
  # it.
490
- rebuild(locals: new_locals, fact_store: new_fact_store,
600
+ rebuild(keep_marks ? @construction_facts : drop_origin(@construction_facts, name),
601
+ locals: new_locals, fact_store: new_fact_store,
491
602
  indexed_narrowings: new_indexed_narrowings,
492
603
  method_chain_narrowings: new_chain_narrowings,
493
604
  declaration_sourced: keep_marks ? @declaration_sourced : drop_local_declaration_marks(name),
@@ -595,9 +706,36 @@ module Rigor
595
706
  rebuild(opaque_block_self: true, block_self_unknown: true)
596
707
  end
597
708
 
709
+ # Issue #2861 — the {#block_self_unknown?} value a block whose yielding call provably runs it under the
710
+ # caller's own `self` ({Inference::SelfPreservingBlock}) is entered with, when the caller's `self` is not
711
+ # already unmodelled. It reads as unknown everywhere `true` does, so issue #1717's explicit-`self` exemption is
712
+ # unchanged; what it withholds is the #316 opaque mark, and it tells a block nested beneath it that it does
713
+ # not sit under an index walk ({#walked_block_self_unknown?}).
714
+ PRESERVED_BLOCK_SELF = :preserved
715
+
716
+ # Issue #2861 — the entry for a block {Inference::SelfPreservingBlock} proves runs under the caller's `self`
717
+ # (`rows.map { … }`, `5.times { … }`). Ruby gives the block the `self` the call site has, so the block is as
718
+ # opaque as the scope around it and no more: it keeps whatever #316 mark the caller's scope carries and adds
719
+ # none, so a cross-file top-level `def` binds there exactly where it binds beside the call
720
+ # ({#bindable_top_level_def_for}). A caller's unknown-`self` mark of `true` or {PRESERVED_BLOCK_SELF} is kept as
721
+ # it is (no rebuild); a {REFINEMENT_BLOCK_SELF} one is not, since the block's `self` is not the refinement module
722
+ # the mark describes.
723
+ def entering_self_preserving_block
724
+ return self if @block_self_unknown == true || @block_self_unknown == PRESERVED_BLOCK_SELF
725
+
726
+ rebuild(block_self_unknown: PRESERVED_BLOCK_SELF)
727
+ end
728
+
598
729
  # True when this scope sits inside a block whose `self` is unmodelled ({#entering_opaque_block}).
599
730
  def opaque_block_self? = @opaque_block_self
600
731
 
732
+ # Issue #2861 — true where the unknown-`self` mark came from the index walk or a value-position block
733
+ # ({#with_block_self_unknown}) rather than a block entry: the mark is set, the #316 one is not, and it is not
734
+ # a {PRESERVED_BLOCK_SELF} entry's. A block nested in such a scope keeps the #316 mark off (issue #1717).
735
+ def walked_block_self_unknown?
736
+ !!@block_self_unknown && !@opaque_block_self && @block_self_unknown != PRESERVED_BLOCK_SELF
737
+ end
738
+
601
739
  # Issue #1717 — true when this scope sits in a block whose `self` the engine does not know, so an explicit
602
740
  # `self.m` there is no evidence `m` is missing: the yielding method may `instance_exec` the block on another
603
741
  # object. Read ONLY by `call.undefined-method`'s explicit-`self` exemption, and kept apart from
@@ -681,9 +819,8 @@ module Rigor
681
819
 
682
820
  # Slice 7 phase 1 — instance/class/global variable bindings. `ivar(name)` / `cvar(name)` / `global(name)`
683
821
  # return the type currently bound for the named variable, or `nil` when the variable has not been written in
684
- # the analyzed slice of the program. The first cut tracks bindings only within a single method body (each
685
- # `def` enters with a fresh binding map), so reads in other methods of the same class fall through to
686
- # `Dynamic[Top]`. Cross-method ivar/cvar inference is a follow-up slice.
822
+ # the analyzed slice of the program. Each `def` enters with a fresh binding map seeded from the class-level
823
+ # accumulators ({#class_ivars_for}, {#class_cvars_for}).
687
824
  def ivar(name)
688
825
  @ivars[name.to_sym]
689
826
  end
@@ -854,6 +991,56 @@ module Rigor
854
991
  @declaration_sourced.include?([:inferred_param, name.to_sym])
855
992
  end
856
993
 
994
+ # Issue #2656 — stamp the gradual-narrowing mark on the local `name`: a class guard narrowed it from a `Dynamic`
995
+ # value, so its `Nominal` reading is the guard's, not a contract the value was checked against. Rides the ADR-58
996
+ # side-mark machinery under a distinct `:gradual_narrowing` kind, as `:inferred_param` does: the displayed type
997
+ # stays the narrowed `Nominal` (dispatch, `call.undefined-method` and `call.wrong-arity` read it), and only
998
+ # `call.argument-type-mismatch` consults the mark ({Analysis::CheckRules::DeclarationSourcedGuard.gradual_narrowed?}).
999
+ # Like the `:local` mark it is dropped by `with_local` and intersected by `join`. Applied after the binding.
1000
+ def with_gradual_narrowing_mark(name)
1001
+ marks = add_declaration_sourced(:gradual_narrowing, name)
1002
+ return self if marks.equal?(@declaration_sourced)
1003
+
1004
+ marks = (marks.dup << GRADUAL_HINT).freeze unless marks.include?(GRADUAL_HINT)
1005
+ rebuild(declaration_sourced: marks)
1006
+ end
1007
+
1008
+ # Issue #2656 — a ref stamped beside every gradual-narrowing mark, so {#gradual_narrowing_names} can answer "none"
1009
+ # with one hash lookup instead of a walk of the set, which the ADR-67 marks keep non-empty in most bodies. It is a
1010
+ # hint: a drop leaves it, and the walk then finds no mark; a join keeps it only where both arms carry it.
1011
+ GRADUAL_HINT = [:gradual_narrowing_hint].freeze
1012
+ private_constant :GRADUAL_HINT
1013
+
1014
+ # Issue #2656 — this scope without the gradual-narrowing mark of every local `block_node` writes: a block may run
1015
+ # again after its own write (`loop do mode(x); x = "zz".dup; end`), and its entry does not see that back edge, so
1016
+ # a mark from before the block must not reach a read the write may precede. Zero-alloc when no local is marked.
1017
+ def entering_block_without_gradual_marks(block_node)
1018
+ names = gradual_narrowing_names
1019
+ return self if names.empty?
1020
+
1021
+ written = Inference::OperandEffects.written_variables(block_node).map(&:to_sym)
1022
+ dropped = names.reduce(@declaration_sourced) do |marks, name|
1023
+ written.include?(name) ? drop_gradual_narrowing_mark(marks, name) : marks
1024
+ end
1025
+ dropped.equal?(@declaration_sourced) ? self : rebuild(declaration_sourced: dropped)
1026
+ end
1027
+
1028
+ # Issue #2656 — the locals carrying the gradual-narrowing mark, empty when none does.
1029
+ def gradual_narrowing_names
1030
+ return EMPTY_GRADUAL_NAMES unless @declaration_sourced.include?(GRADUAL_HINT)
1031
+
1032
+ @declaration_sourced.filter_map { |ref| ref[1] if ref[0] == :gradual_narrowing }
1033
+ end
1034
+ EMPTY_GRADUAL_NAMES = [].freeze
1035
+ private_constant :EMPTY_GRADUAL_NAMES
1036
+
1037
+ # Issue #2656 — true when the local `name`'s binding is a class guard's narrowing of a `Dynamic` value.
1038
+ def gradual_narrowing?(name)
1039
+ return false if @declaration_sourced.empty?
1040
+
1041
+ @declaration_sourced.include?([:gradual_narrowing, name.to_sym])
1042
+ end
1043
+
857
1044
  def with_cvar(name, type)
858
1045
  rebuild(cvars: @cvars.merge(name.to_sym => type).freeze)
859
1046
  end
@@ -1186,8 +1373,8 @@ module Rigor
1186
1373
  end
1187
1374
 
1188
1375
  # Slice 7 phase 2 — class-level ivar accumulator. Keyed by the qualified class name (e.g. `"Rigor::Scope"`);
1189
- # the value is a `Hash[Symbol, Type::t]` of every ivar that appears as a write target inside any def body of
1190
- # that class. `StatementEvaluator#build_method_entry_scope` seeds the method body's `ivars` map from this
1376
+ # the value maps every ivar that appears as a write target inside any def body of that class to its type.
1377
+ # `StatementEvaluator#build_method_entry_scope` seeds the method body's `ivars` map from this
1191
1378
  # table so a `def get; @x; end` reads the type written in a sibling `def init; @x = 1; end`.
1192
1379
  #
1193
1380
  # `ScopeIndexer` populates the table once at index time through a separate pre-pass over the program. The map
@@ -1207,7 +1394,7 @@ module Rigor
1207
1394
  @discovery.class_cvars[class_name.to_s] || EMPTY_VAR_BINDINGS
1208
1395
  end
1209
1396
 
1210
- # Slice 7 phase 12 — in-source method discovery. Maps a qualified class name to a `Hash[Symbol, Symbol]` of
1397
+ # Slice 7 phase 12 — in-source method discovery. Maps a qualified class name to a table of
1211
1398
  # `method_name => :instance | :singleton`. Populated by `ScopeIndexer` from every `Prism::DefNode` and
1212
1399
  # recognised `define_method` invocation inside class/module bodies. The `rigor check` undefined-method and
1213
1400
  # wrong-arity rules consult this map to suppress diagnostics for methods the user has defined dynamically,
@@ -1230,6 +1417,58 @@ module Rigor
1230
1417
  true
1231
1418
  end
1232
1419
 
1420
+ # Issue #2555 — whether the project defines a method named `method_name` anywhere: on any class or module,
1421
+ # instance or singleton side (the def-node tables, which hold a plain cross-file `def` on an RBS-declared class
1422
+ # that `discovered_methods` withholds, and `discovered_methods`), or in a `refine` block. The name set is built
1423
+ # once per discovery index. The answer depends on every file, so an incremental run records the name either way:
1424
+ # a file that starts or stops defining it re-analyses the reader (issue #1459's `defines:` edge).
1425
+ def project_method_name?(method_name)
1426
+ Analysis::DependencyRecorder.read_name(:defines, method_name) if Analysis::DependencyRecorder.active?
1427
+ return true if project_method_names.include?(method_name.to_sym)
1428
+
1429
+ false
1430
+ end
1431
+
1432
+ # The name set {#project_method_name?} reads, held per discovery index. ADR-129 G2: a hit is checked against a
1433
+ # fresh collection under `RIGOR_MEMO_AUDIT=1`, and an audit recomputation bypasses the table.
1434
+ def project_method_names
1435
+ return collect_project_method_names if memo_audit_bypass?
1436
+
1437
+ table = TypingState.current.weak_key_map(PROJECT_METHOD_NAMES)
1438
+ names = table[@discovery]
1439
+ if names.nil?
1440
+ return table[@discovery] = Inference::MemoAudit.key_audit("project method names") do
1441
+ collect_project_method_names
1442
+ end
1443
+ end
1444
+
1445
+ Inference::MemoAudit.check("project method names", names, :discovery) { collect_project_method_names }
1446
+ end
1447
+ private :project_method_names
1448
+
1449
+ # ADR-129 G2 — true while a MemoAudit recomputation runs: the memos wired here are then bypassed, neither read
1450
+ # nor written. One boolean check when the audit is off.
1451
+ def memo_audit_bypass? = Inference::MemoAudit.enabled? && Inference::MemoAudit.suspended?
1452
+ private :memo_audit_bypass?
1453
+
1454
+ def collect_project_method_names
1455
+ names = Set.new
1456
+ [@discovery.discovered_methods, @discovery.discovered_def_nodes, @discovery.discovered_singleton_def_nodes,
1457
+ @discovery.discovered_refinements].each do |by_owner|
1458
+ by_owner.each_value { |methods| methods.each_key { |method_name| names << method_name.to_sym } }
1459
+ end
1460
+ names.freeze
1461
+ end
1462
+ private :collect_project_method_names
1463
+
1464
+ # ADR-129 WD1 — a run-scoped Memo ({TypingState}): emptied when a run ends, so an edited project is read afresh.
1465
+ PROJECT_METHOD_NAMES = TypingState.memo(
1466
+ :project_method_names,
1467
+ run_scoped: true, audit: "project method names",
1468
+ reason: "a WeakKeyMap keyed on the discovery index; the name set reads only its tables"
1469
+ )
1470
+ private_constant :PROJECT_METHOD_NAMES
1471
+
1233
1472
  def record_discovered_method(class_name, method_name)
1234
1473
  record_class_dependency(class_name)
1235
1474
  Analysis::DependencyRecorder.read_name(:defines, method_name)
@@ -1258,7 +1497,7 @@ module Rigor
1258
1497
  per_class && per_class[method_name]
1259
1498
  end
1260
1499
 
1261
- # v0.0.2 #5 — per-class table mapping `method_name (Symbol) → Prism::DefNode`. Populated by `ScopeIndexer`
1500
+ # v0.0.2 #5 — per-class table mapping `method_name → Prism::DefNode`. Populated by `ScopeIndexer`
1262
1501
  # alongside `discovered_methods` for instance-side defs only (singleton-side and `define_method`-introduced
1263
1502
  # methods do not contribute a static body the engine can re-type). Consumed by `ExpressionTyper` to do
1264
1503
  # inter-procedural return-type inference when the receiver class is user-defined and has no RBS sig.
@@ -1272,6 +1511,89 @@ module Rigor
1272
1511
  Inference::DefNodeResolver.resolve(entry)
1273
1512
  end
1274
1513
 
1514
+ # Issue #2515 — `[class_name, open]` for each class or module whose instance def table holds `method_name`, in
1515
+ # table order (frozen; empty when no user def has the name). The callee floor's hand-off on a receiver it cannot
1516
+ # type reads every def of the name, so the answer depends on the whole project's defs of that name: a recorder
1517
+ # files the `defines:<name>` name edge, which fires when a file starts or stops defining it. `open` is true when
1518
+ # the real receiver may not be the class the def sits on: the owner is a module (an includer's methods are
1519
+ # called from it), or a project class names it as its superclass (a subclass may override what the def calls).
1520
+ # A recorder files `subclass_of:<owner>` for the second.
1521
+ def user_instance_def_owners(method_name)
1522
+ Analysis::DependencyRecorder.read_name(:defines, method_name) if Analysis::DependencyRecorder.active?
1523
+ name = method_name.to_sym
1524
+ # `each` with a lazily built array, not `filter_map`: this runs once per call on every discovered class, and
1525
+ # `Hash#filter_map` allocates a `[key, value]` pair per entry (#2895 cost 3.35% of the release gate's
1526
+ # allocations that way).
1527
+ owners = nil
1528
+ @discovery.discovered_def_nodes.each do |class_name, defs|
1529
+ next unless defs.key?(name) && class_name != Inference::ScopeIndexer::TOP_LEVEL_DEF_KEY
1530
+
1531
+ (owners ||= []) << [class_name, open_to_override?(class_name)].freeze
1532
+ end
1533
+ owners.nil? ? EMPTY_OWNERS : owners.freeze
1534
+ end
1535
+
1536
+ EMPTY_OWNERS = [].freeze
1537
+ private_constant :EMPTY_OWNERS
1538
+
1539
+ # Issue #2515 — `[opaque, keys]`: whether the project holds a subclass relation the superclass table does not
1540
+ # spell as `class X < Name`, so that no owner can be called free of an override. It holds when a class takes
1541
+ # `Class.new(Parent)` ({DiscoveryIndex::ENVELOPE_META_SUPERCLASS_MARK}), when a class's superclass expression is
1542
+ # unreadable (`nil` in the table), or when a written superclass is not a declared class and some file assigns a
1543
+ # constant of that name (`H2 = Helper; class Sub < H2`; the writer census {#bound_constant_names} or this file's
1544
+ # constant table).
1545
+ # `keys` are the frozen ADR-46 name keys a recorder files for the answer; the caller files them, since the answer
1546
+ # is memoised per discovery.
1547
+ def subclassing_opacity
1548
+ keys = OPACITY_KEYS.dup
1549
+ opaque = @discovery.discovered_parameter_envelopes.each_value.any? do |bucket|
1550
+ bucket.key?(DiscoveryIndex::ENVELOPE_META_SUPERCLASS_MARK)
1551
+ end
1552
+ segments = {}
1553
+ @discovery.discovered_superclasses.each_value do |parent|
1554
+ opaque = true if parent.nil? || alias_superclass?(parent, segments, keys)
1555
+ end
1556
+ [opaque ? true : false, keys.each(&:freeze).freeze].freeze
1557
+ end
1558
+
1559
+ # True when `parent`, a written superclass that is no declared class, names a constant some file assigns. Files
1560
+ # the `constant:<segment>` key for it once per segment.
1561
+ def alias_superclass?(parent, segments, keys)
1562
+ return false if @discovery.discovered_classes.key?(parent)
1563
+
1564
+ cut = parent.rindex("::")
1565
+ segment = cut ? parent.byteslice(cut + 2, parent.bytesize - cut - 2) : parent
1566
+ return false if segments.key?(segment)
1567
+
1568
+ segments[segment] = true
1569
+ keys << "constant:#{segment}"
1570
+ @discovery.constant_writers.key?(segment) || @discovery.in_source_constants.key?(parent)
1571
+ end
1572
+ private :alias_superclass?
1573
+
1574
+ OPACITY_KEYS = %w[subclass_of:* mark:*].freeze
1575
+ private_constant :OPACITY_KEYS
1576
+
1577
+ def open_to_override?(class_name)
1578
+ Analysis::DependencyRecorder.read_last_segment(:subclass_of, class_name) if Analysis::DependencyRecorder.active?
1579
+ return true if @discovery.discovered_parameter_envelopes[class_name]&.key?(DiscoveryIndex::ENVELOPE_MODULE_MARK)
1580
+
1581
+ @discovery.discovered_superclasses.any? do |child, parent|
1582
+ child != class_name && parent && names_class?(parent, class_name)
1583
+ end
1584
+ end
1585
+ private :open_to_override?
1586
+
1587
+ # True when a superclass written as `written` (`Helper`, `A::Helper`, `::Helper`) may name `qualified`.
1588
+ def names_class?(written, qualified)
1589
+ written = written[2..] if written.start_with?("::")
1590
+ return true if written == qualified
1591
+ return false unless qualified.end_with?(written)
1592
+
1593
+ qualified.getbyte(qualified.bytesize - written.bytesize - 1) == 58
1594
+ end
1595
+ private :names_class?
1596
+
1275
1597
  # Module-singleton call resolution (ADR-57 follow-up) — companion of {#user_def_for} for SINGLETON-side defs
1276
1598
  # (`def self.x`, `def Foo.x`, `class << self` bodies, and `module_function` defs). Returns the
1277
1599
  # `Prism::DefNode` for `class_name.method_name` invoked on the module/class constant itself, or nil. The
@@ -1317,6 +1639,199 @@ module Rigor
1317
1639
  Inference::DefNodeResolver.resolve(entry)
1318
1640
  end
1319
1641
 
1642
+ # Issue #2515 — why the `<toplevel>` table's def for `method_name` may not be the method a receiverless call of that
1643
+ # name runs, or nil when the project's own tables show nothing else that could be. The table keeps one def per name
1644
+ # (the last writer's) and also files the defs of an anonymous `Class.new` body, so:
1645
+ #
1646
+ # - `:contested` — a def in a block or under a condition wrote the slot last;
1647
+ # - `:several` — the project holds more than one ownerless definition row of the name: two top-level defs (across
1648
+ # files, or redefined), a def in a block or in another def, `define_method` or `alias_method` of the name;
1649
+ # - `:main_singleton` — a `def self.name` (or a `class << self` def) at the top level defines it on `main`;
1650
+ # - `:aliased` — an `alias` statement or `alias_method` call names it as the new name;
1651
+ # - `:anonymous` — an anonymous class body defines it;
1652
+ # - `:object` — `Object` itself defines it (`class Object; def name`, `define_method`) or has a module prepended,
1653
+ # which Ruby searches ahead of the top-level def. A module included at the top level is searched after `Object`'s
1654
+ # own methods, so it is no reason.
1655
+ #
1656
+ # A name with no row at all (a scope built without the project pre-pass) is not `:several`: the rows say nothing.
1657
+ # The answer depends on every file, so an incremental run records the `defines:<name>` edge and the census edge of
1658
+ # an alias either way. The two project-wide scans are built once per table, not per call.
1659
+ def toplevel_def_ambiguity(method_name)
1660
+ name = method_name.to_sym
1661
+ record_toplevel_ambiguity_reads(name) if Analysis::DependencyRecorder.active?
1662
+ key = Inference::ScopeIndexer::TOP_LEVEL_DEF_KEY
1663
+ return :contested if @discovery.contested_discovered_def_nodes.include?([key, name])
1664
+
1665
+ rows = toplevel_definition_rows[name]
1666
+ return :main_singleton if rows == MAIN_SINGLETON_ROWS
1667
+ return :several if rows > 1
1668
+ return :aliased if method_alias_names.include?(name)
1669
+
1670
+ return :anonymous if @discovery.discovered_def_nodes.any? do |class_name, defs|
1671
+ defs.key?(name) && class_name.start_with?(Type::AnonymousClassName::PREFIX)
1672
+ end
1673
+
1674
+ :object if object_answers_first?(name)
1675
+ end
1676
+
1677
+ # True when `Object` defines `name` itself or has a module prepended (see {#toplevel_def_ambiguity}).
1678
+ def object_answers_first?(name)
1679
+ !user_def_for("Object", name).nil? || discovered_method?("Object", name, :instance) ||
1680
+ !(@discovery.discovered_prepends["Object"] || EMPTY_PREPENDS).empty?
1681
+ end
1682
+ private :object_answers_first?
1683
+
1684
+ EMPTY_PREPENDS = [].freeze
1685
+ private_constant :EMPTY_PREPENDS
1686
+
1687
+ # True for a class or module the project declares, even one with no method of its own.
1688
+ def declared_class?(class_name) = @discovery.discovered_classes.key?(class_name.to_s)
1689
+
1690
+ def record_toplevel_ambiguity_reads(name)
1691
+ Analysis::DependencyRecorder.read_name(:defines, name)
1692
+ Analysis::DependencyRecorder.read_name(:global_census, "method_alias:#{name}")
1693
+ Analysis::DependencyRecorder.read_last_segment(:class, "Object")
1694
+ record_class_dependency("Object")
1695
+ end
1696
+ private :record_toplevel_ambiguity_reads
1697
+
1698
+ # Issue #2336 — whether the project gives `method_name` a body through an ownerless definition row
1699
+ # ({#toplevel_definition_rows}): a `define_method` / `define_singleton_method` / `alias_method` call with that
1700
+ # literal name on any receiver, a def in a block, or (with `top_level:`) a def outside every class body. The
1701
+ # callee mutation floor reads a call no def table answers as one such body may run. The answer depends on every
1702
+ # file, so an incremental run records the `defines:<name>` edge either way.
1703
+ def ownerless_definition?(method_name, top_level: true)
1704
+ Analysis::DependencyRecorder.read_name(:defines, method_name) if Analysis::DependencyRecorder.active?
1705
+ name = method_name.to_sym
1706
+ return true if top_level ? toplevel_definition_rows.key?(name) : deferred_definition_names.include?(name)
1707
+
1708
+ false
1709
+ end
1710
+
1711
+ # The names of the ownerless rows that claim no side ({#ownerless_definition?} without `top_level:`): a definer
1712
+ # call's literal name and a def in a block. Held by the table's identity, as {#toplevel_definition_rows} is.
1713
+ def deferred_definition_names
1714
+ ranges = @discovery.discovered_deferred_ranges
1715
+ return collect_deferred_definition_names(ranges) if memo_audit_bypass?
1716
+
1717
+ state = TypingState.current
1718
+ slots = (state[DEFERRED_NAMES] ||= [])
1719
+ slot = slots.find { |entry| entry[0].equal?(ranges) }
1720
+ if slot
1721
+ return Inference::MemoAudit.check("deferred definition names", slot[1], :ranges) do
1722
+ collect_deferred_definition_names(ranges)
1723
+ end
1724
+ end
1725
+
1726
+ names = Inference::MemoAudit.key_audit("deferred definition names") { collect_deferred_definition_names(ranges) }
1727
+ slots.unshift([ranges, names]).slice!(2..)
1728
+ names
1729
+ end
1730
+ private :deferred_definition_names
1731
+
1732
+ def collect_deferred_definition_names(ranges)
1733
+ names = Set.new
1734
+ ranges.each_value do |entries|
1735
+ entries.each { |(_start, _finish, name, kind, owner)| names << name if name && kind.nil? && owner.nil? }
1736
+ end
1737
+ names.freeze
1738
+ end
1739
+ private :collect_deferred_definition_names
1740
+
1741
+ # Issue #2336 — {#module_function_def?}'s `:both` reading for a def in any file: whether `def_node`, an instance
1742
+ # def of `owner`, is a module function by a deferred-range row over it (`module_function def x`, or a bare
1743
+ # `module_function` before it), which the singleton def table does not mirror for the argument form.
1744
+ def module_function_row_def?(def_node, owner)
1745
+ start = def_node.location.start_offset
1746
+ name = def_node.name
1747
+ owner = owner.to_s
1748
+ discovered_deferred_ranges.each_value.any? do |entries|
1749
+ entries.any? do |(row_start, _finish, row_name, kind, row_owner)|
1750
+ row_start == start && kind == :both && row_name == name && row_owner == owner
1751
+ end
1752
+ end
1753
+ end
1754
+
1755
+ # `{ name => ownerless definition rows }` over `discovered_deferred_ranges`, {MAIN_SINGLETON_ROWS} for a name with
1756
+ # a singleton row. Held by the table's identity (a file's merged copy and the seed), released with the run.
1757
+ def toplevel_definition_rows
1758
+ ranges = @discovery.discovered_deferred_ranges
1759
+ return collect_toplevel_definition_rows(ranges) if memo_audit_bypass?
1760
+
1761
+ state = TypingState.current
1762
+ slots = (state[TOPLEVEL_ROWS] ||= [])
1763
+ slot = slots.find { |entry| entry[0].equal?(ranges) }
1764
+ if slot
1765
+ return Inference::MemoAudit.check("toplevel definition rows", slot[1], :ranges) do
1766
+ collect_toplevel_definition_rows(ranges)
1767
+ end
1768
+ end
1769
+
1770
+ rows = Inference::MemoAudit.key_audit("toplevel definition rows") { collect_toplevel_definition_rows(ranges) }
1771
+ slots.unshift([ranges, rows]).slice!(2..)
1772
+ rows
1773
+ end
1774
+ private :toplevel_definition_rows
1775
+
1776
+ def collect_toplevel_definition_rows(ranges)
1777
+ rows = Hash.new(0)
1778
+ ranges.each_value do |entries|
1779
+ entries.each do |(_start, _finish, name, kind, owner)|
1780
+ next if name.nil? || !owner.nil?
1781
+
1782
+ rows[name] = kind == :singleton || rows[name] == MAIN_SINGLETON_ROWS ? MAIN_SINGLETON_ROWS : rows[name] + 1
1783
+ end
1784
+ end
1785
+ rows.freeze
1786
+ end
1787
+ private :collect_toplevel_definition_rows
1788
+
1789
+ # The names an `alias` statement or `alias_method` call introduces, from the global write census; held by the
1790
+ # census's identity as {#toplevel_definition_rows} is.
1791
+ def method_alias_names
1792
+ census = @discovery.discovered_global_write_census
1793
+ return collect_method_alias_names(census) if memo_audit_bypass?
1794
+
1795
+ state = TypingState.current
1796
+ slots = (state[METHOD_ALIASES] ||= [])
1797
+ slot = slots.find { |entry| entry[0].equal?(census) }
1798
+ if slot
1799
+ return Inference::MemoAudit.check("method alias names", slot[1], :census) { collect_method_alias_names(census) }
1800
+ end
1801
+
1802
+ names = Inference::MemoAudit.key_audit("method alias names") { collect_method_alias_names(census) }
1803
+ slots.unshift([census, names]).slice!(2..)
1804
+ names
1805
+ end
1806
+ private :method_alias_names
1807
+
1808
+ def collect_method_alias_names(census)
1809
+ census.each_with_object(Set.new) { |entry, acc| acc << entry[1] if entry[0] == :method_alias }.freeze
1810
+ end
1811
+ private :collect_method_alias_names
1812
+
1813
+ MAIN_SINGLETON_ROWS = -1
1814
+ # ADR-129 WD1 — three run-scoped Memos, each a two-entry list of `[table, derived]` held by the identity of the
1815
+ # table the derived value reads (the deferred ranges, or the global write census).
1816
+ TOPLEVEL_ROWS = TypingState.memo(
1817
+ :toplevel_definition_rows,
1818
+ run_scoped: true, audit: "toplevel definition rows",
1819
+ reason: "[deferred ranges, rows] entries keyed on the identity of the ranges table, the only input the rows read"
1820
+ )
1821
+ METHOD_ALIASES = TypingState.memo(
1822
+ :method_alias_names,
1823
+ run_scoped: true, audit: "method alias names",
1824
+ reason: "[census, names] entries keyed on the identity of the global write census, the only input the names " \
1825
+ "read"
1826
+ )
1827
+ DEFERRED_NAMES = TypingState.memo(
1828
+ :deferred_definition_names,
1829
+ run_scoped: true, audit: "deferred definition names",
1830
+ reason: "[deferred ranges, names] entries keyed on the identity of the ranges table, the only input the names " \
1831
+ "read"
1832
+ )
1833
+ private_constant :MAIN_SINGLETON_ROWS, :TOPLEVEL_ROWS, :METHOD_ALIASES, :DEFERRED_NAMES
1834
+
1320
1835
  # Issue #316 — the CONFIDENCE-GATED companion of {#top_level_def_for}, and the only accessor the type
1321
1836
  # inference may bind through. {#top_level_def_for} stays unrestricted because it also serves the
1322
1837
  # *suppression* side (`call.unresolved-toplevel`, `call.undefined-method`): a name the project defines at
@@ -1556,6 +2071,14 @@ module Rigor
1556
2071
  @discovery.discovered_includes[class_name.to_s] || []
1557
2072
  end
1558
2073
 
2074
+ # Issue #2986 — the modules `class_name` prepends, as written, in search order: the prepended part of
2075
+ # {#includes_of}, which lists them ahead of the included ones. An ActiveSupport::Concern's `prepended do … end`
2076
+ # block runs for a prepend edge only, and its `included do … end` block for an include edge only.
2077
+ def prepends_of(class_name)
2078
+ record_class_dependency(class_name) if Analysis::DependencyRecorder.active?
2079
+ @discovery.discovered_prepends[class_name.to_s] || EMPTY_PREPENDS
2080
+ end
2081
+
1559
2082
  # Issue #898 — the module names `extend`ed onto `class_name`'s SINGLETON, as written, gathered up the
1560
2083
  # as-written superclass chain because a singleton class inherits its superclass's singleton class
1561
2084
  # (`class Base; extend Comparable; end; class Widget < Base; end` leaves `Widget.is_a?(Comparable)`
@@ -1622,6 +2145,7 @@ module Rigor
1622
2145
  end
1623
2146
  return found if found
1624
2147
 
2148
+ chain.record_external_absence
1625
2149
  chain.truncated? ? ancestor_walk_gave_up : [nil, nil]
1626
2150
  end
1627
2151
 
@@ -1661,6 +2185,7 @@ module Rigor
1661
2185
  end
1662
2186
  return found if found
1663
2187
 
2188
+ chain.record_external_absence
1664
2189
  chain.truncated? ? ancestor_walk_gave_up : [nil, nil]
1665
2190
  end
1666
2191
 
@@ -1744,6 +2269,8 @@ module Rigor
1744
2269
  discovered_method?(entry.name, method_name, entry.side == :singleton ? :singleton : kind_for(kind))
1745
2270
  end
1746
2271
  return true if found
2272
+
2273
+ record_discovered_miss(chain, method_name) if Analysis::DependencyRecorder.active?
1747
2274
  # Budget exhaustion is uncertainty, not absence: answering "not declared" here would hand a
1748
2275
  # `call.undefined-method` a fired verdict it has no evidence for. Suppress and record the hit.
1749
2276
  return false unless chain.truncated?
@@ -1752,6 +2279,28 @@ module Rigor
1752
2279
  true
1753
2280
  end
1754
2281
 
2282
+ # Issue #2689 — the ADR-46 negative edges of a discovery-walk miss: the answer is "no project entry on the chain
2283
+ # defines it", so a later edit that defines it on any of them, or a new file that reopens one of them (and may
2284
+ # change the chain, `include` a module that defines it), turns it. Each project entry files the `method:` key
2285
+ # `Incremental.appeared_symbols` emits for that definition (`Name.method` for a class object's own side,
2286
+ # `Name#method` for an instance-chain entry and for a module on the singleton chain, which answers with its
2287
+ # instance methods) and the `class:` key a declaring file appearing fires — the edges the def walk files for the
2288
+ # same miss (`#user_def_for` per entry, `ResolutionChain#settle` for the classes). A hit files none: existence is
2289
+ # a union, so no other entry gaining the method can turn a found answer.
2290
+ # An external entry (an ancestor declared with no methods, or not yet) files its `class:` key
2291
+ # ({ResolutionChain#record_external_absence}), as the def walks do on a miss.
2292
+ def record_discovered_miss(chain, method_name)
2293
+ chain.record_external_absence
2294
+ chain.entries.each do |entry|
2295
+ next if entry.external?
2296
+
2297
+ separator = entry.side == :singleton ? "." : "#"
2298
+ Analysis::DependencyRecorder.read_missing(:method, "#{entry.name}#{separator}#{method_name}")
2299
+ Analysis::DependencyRecorder.read_missing(:class, entry.last_segment)
2300
+ end
2301
+ end
2302
+ private :record_discovered_miss
2303
+
1755
2304
  # A module entry on the singleton chain answers with its INSTANCE methods; on the instance chain, the
1756
2305
  # caller's kind stands.
1757
2306
  def kind_for(kind) = kind == :singleton ? :instance : kind
@@ -1970,7 +2519,7 @@ module Rigor
1970
2519
  EMPTY_PARAMETER_ENVELOPES = {}.freeze
1971
2520
  private_constant :EMPTY_PARAMETER_ENVELOPES
1972
2521
 
1973
- # v0.1.2 — per-class table mapping `method_name (Symbol) → :public | :private | :protected`. Populated by
2522
+ # v0.1.2 — per-class table mapping `method_name → :public | :private | :protected`. Populated by
1974
2523
  # `ScopeIndexer` for every `def` and `attr_*` member it sees inside a class body, with the visibility taken from
1975
2524
  # a wrapping `private def` / `private attr_reader` call, else the surrounding `private` / `protected` / `public`
1976
2525
  # modifier state, plus any post-hoc `private :name, ...` named-argument calls. Consumed by the
@@ -1989,13 +2538,14 @@ module Rigor
1989
2538
  table[singleton ? [:singleton, method_name.to_sym] : method_name.to_sym]
1990
2539
  end
1991
2540
 
1992
- # Closes the "`params[:f] ||= []; params[:f] << x`" precision gap (ROADMAP § Type-language / engine —
1993
- # indexed-collection narrowing through `Hash[k] ||= default`). After `receiver[key] ||= default`, the next
2541
+ # Closes the "`params[:f] ||= []; params[:f] << x`" precision gap. After `receiver[key] ||= default`, the next
1994
2542
  # read at `receiver[key]` is known non-nil; recording the post-`||=` type keyed on
1995
2543
  # `(receiver_kind, receiver_name, literal_key)` lets the ExpressionTyper's `[]` dispatch hand back the
1996
2544
  # narrowed type. Receiver-rebind and `[]=`/mutator invalidation rules are documented at the call sites in
1997
2545
  # `Inference::StatementEvaluator`.
1998
2546
  def indexed_narrowing(receiver_kind, receiver_name, key)
2547
+ return nil if @indexed_narrowings.empty?
2548
+
1999
2549
  @indexed_narrowings[indexed_key(receiver_kind, receiver_name, key)]
2000
2550
  end
2001
2551
 
@@ -2007,6 +2557,8 @@ module Rigor
2007
2557
  end
2008
2558
 
2009
2559
  def without_indexed_narrowing(receiver_kind, receiver_name, key)
2560
+ return self if @indexed_narrowings.empty?
2561
+
2010
2562
  lookup = indexed_key(receiver_kind, receiver_name, key)
2011
2563
  return self unless @indexed_narrowings.key?(lookup)
2012
2564
 
@@ -2021,14 +2573,15 @@ module Rigor
2021
2573
  rebuild(indexed_narrowings: new_table)
2022
2574
  end
2023
2575
 
2024
- # Closes the "stable receiver method-chain narrowing" gap (ROADMAP § Future cycles / Type-language / engine —
2025
- # "Method-call receiver narrowing across stable receivers"; 2026-05-28 Redmine survey). After
2576
+ # Closes the "stable receiver method-chain narrowing" gap (2026-05-28 Redmine survey). After
2026
2577
  # `if x.last.is_a?(Array)` the dominated body's `x.last` reads MUST observe the truthy-narrowed type; the same
2027
2578
  # chain reaching the falsey edge observes the negative narrowing.
2028
2579
  #
2029
2580
  # Address shape mirrors {.indexed_narrowing}: stable root variable + no-arg single-hop method name. See
2030
2581
  # {ChainKey} for the precise contract.
2031
2582
  def method_chain_narrowing(receiver_kind, receiver_name, method_name)
2583
+ return nil if @method_chain_narrowings.empty?
2584
+
2032
2585
  @method_chain_narrowings[chain_key(receiver_kind, receiver_name, method_name)]
2033
2586
  end
2034
2587
 
@@ -2040,6 +2593,8 @@ module Rigor
2040
2593
  end
2041
2594
 
2042
2595
  def without_method_chain_narrowing(receiver_kind, receiver_name, method_name)
2596
+ return self if @method_chain_narrowings.empty?
2597
+
2043
2598
  lookup = chain_key(receiver_kind, receiver_name, method_name)
2044
2599
  return self unless @method_chain_narrowings.key?(lookup)
2045
2600
 
@@ -2070,8 +2625,9 @@ module Rigor
2070
2625
  # against THIS scope's discovery index (so cross-file dispatches in the body resolve). The incremental
2071
2626
  # session re-drives a declaration-stable changed callee at each previously-observed call key to prove its
2072
2627
  # return is unchanged before skipping its symbol dependents.
2073
- def user_method_return(def_node, receiver, arg_types)
2074
- Inference::ExpressionTyper.new(scope: self).return_type_for(def_node, receiver, arg_types)
2628
+ def user_method_return(def_node, receiver, arg_types, positionals_unknown: false)
2629
+ typer = Inference::ExpressionTyper.new(scope: self)
2630
+ typer.return_type_for(def_node, receiver, arg_types, positionals_unknown: positionals_unknown)
2075
2631
  end
2076
2632
 
2077
2633
  # Statement-level evaluation: returns the pair `[type, scope']` where `type` is what the node produces and
@@ -2149,11 +2705,13 @@ module Rigor
2149
2705
 
2150
2706
  private
2151
2707
 
2152
- # The marks {#==} compares: ADR-58's, issue #667's and the repeated `||=` sites, and #1429's guard state.
2708
+ # The marks {#==} compares: ADR-58's, issue #667's, the repeated `||=` sites, #2555's construction facts (they
2709
+ # change what a mutation seam answers), and #1429's guard state.
2153
2710
  def same_marks?(other)
2154
2711
  @declaration_sourced == other.declaration_sourced &&
2155
2712
  @published_constant_sourced == other.published_constant_sourced &&
2156
2713
  @repeated_or_writes == other.repeated_or_writes &&
2714
+ @construction_facts == other.construction_facts &&
2157
2715
  same_guard_state?(other)
2158
2716
  end
2159
2717
 
@@ -2162,7 +2720,16 @@ module Rigor
2162
2720
  @bot_guard_classes == other.bot_guard_classes
2163
2721
  end
2164
2722
 
2723
+ # `construction_facts` is the one positional parameter. CRuby records which keywords a call left out in a bit
2724
+ # field indexed by each keyword's position among ALL the method's keywords, required and optional
2725
+ # (`VM_KW_SPECIFIED_BITS_MAX`, 31); a keyword past that index whose default is not a literal makes every call that
2726
+ # omits it allocate the record instead. A 32nd keyword here did that on every scope transition, about 1% of an
2727
+ # analysis's allocations (issue #2555). On CRuby 4.0 that allocation is also unsafe: a GC it starts frees the
2728
+ # values already moved into the keyword locals (ruby/ruby Bug #22500, issue #2850). `#initialize` is past the index
2729
+ # and keeps literal defaults there for that reason. The next Scope field crosses it in this method again, so it
2730
+ # needs a positional slot or a restructuring, not a 32nd keyword.
2165
2731
  def rebuild(
2732
+ construction_facts = @construction_facts,
2166
2733
  locals: @locals, fact_store: @fact_store, self_type: @self_type,
2167
2734
  ivars: @ivars, cvars: @cvars, globals: @globals,
2168
2735
  discovery: @discovery,
@@ -2213,7 +2780,7 @@ module Rigor
2213
2780
  plugin_typed_calls: plugin_typed_calls,
2214
2781
  optimistic_origins: optimistic_origins,
2215
2782
  optimistic_locals: optimistic_locals,
2216
- optimistic_ivars: optimistic_ivars,
2783
+ optimistic_ivars: optimistic_ivars, construction_facts: construction_facts,
2217
2784
  repeated_or_writes: repeated_or_writes,
2218
2785
  match_frame: match_frame,
2219
2786
  constant_narrowings: constant_narrowings,
@@ -2223,11 +2790,18 @@ module Rigor
2223
2790
  )
2224
2791
  end
2225
2792
 
2793
+ # A join with an empty side has no key in common (usually both are empty, the common case at a branch merge), so
2794
+ # its answer is this one frozen hash rather than a fresh one.
2795
+ EMPTY_BINDINGS = {}.freeze
2796
+ private_constant :EMPTY_BINDINGS
2797
+
2226
2798
  def join_bindings(left, right)
2227
2799
  # Keys present in both, unioned. Iterating `left` and probing `right.key?` yields the same keys in the same
2228
2800
  # order as the prior `(left.keys & right.keys)` while avoiding the two key arrays and the intersection
2229
2801
  # array — this is the control-flow join, run at every branch merge, and was a top allocation site (~75% of
2230
2802
  # `Hash#keys`).
2803
+ return EMPTY_BINDINGS if left.empty? || right.empty?
2804
+
2231
2805
  result = {}
2232
2806
  left.each do |name, ltype|
2233
2807
  next unless right.key?(name)
@@ -2306,6 +2880,8 @@ module Rigor
2306
2880
  optimistic_origins: @optimistic_origins,
2307
2881
  optimistic_locals: join_optimistic_marks(@optimistic_locals, other.optimistic_locals),
2308
2882
  optimistic_ivars: join_optimistic_marks(@optimistic_ivars, other.optimistic_ivars),
2883
+ # Issue #2555 L3 — INTERSECTION: a construction fact holds after a merge only when both arms established it.
2884
+ construction_facts: join_construction_facts(@construction_facts, other.construction_facts),
2309
2885
  # UNION, the published-constant mark's direction: the mark only withholds the memoizing `||=`
2310
2886
  # reading, so keeping a site either arm holds is the wider answer.
2311
2887
  repeated_or_writes: join_repeated_or_writes(other),
@@ -2403,6 +2979,31 @@ module Rigor
2403
2979
  .freeze
2404
2980
  end
2405
2981
 
2982
+ # Issue #2555 L3 — a fact survives a merge only for a name both arms hold it for, and only the facts both agree
2983
+ # on. Zero-alloc when either side is empty or both are the same table.
2984
+ def join_construction_facts(mine, theirs)
2985
+ return mine if mine.equal?(theirs)
2986
+ return EMPTY_ORIGINS if mine.empty? || theirs.empty?
2987
+
2988
+ joined = {}
2989
+ mine.each do |name, facts|
2990
+ their_facts = theirs[name]
2991
+ next if their_facts.nil?
2992
+
2993
+ common = facts == their_facts ? facts : join_fact_sets(facts, their_facts)
2994
+ joined[name] = common unless common.empty?
2995
+ end
2996
+ joined.empty? ? EMPTY_ORIGINS : joined.freeze
2997
+ end
2998
+
2999
+ # `:grown` joins by UNION among the names both arms keep: a store on either arm may reach the carrier, and the
3000
+ # fact only ever floors it ({Inference::MutationRejoin.floors_grown_carrier?}). Every other fact intersects.
3001
+ def join_fact_sets(facts, their_facts)
3002
+ common = facts & their_facts
3003
+ common << :grown if facts.include?(:grown) || their_facts.include?(:grown)
3004
+ common.freeze
3005
+ end
3006
+
2406
3007
  # ADR-82 WD1 — rebinding drops any propagated origin for the name (the new value's provenance is set
2407
3008
  # afterward by `with_local_origin` when it is a `Dynamic` with a recorded cause). Zero-alloc when the name
2408
3009
  # carries no origin — the overwhelming common case, so this stays off the with_local hot-path budget.
@@ -2464,19 +3065,12 @@ module Rigor
2464
3065
  end
2465
3066
 
2466
3067
  def indexed_key(receiver_kind, receiver_name, key)
2467
- IndexedKey.new(
2468
- receiver_kind: receiver_kind.to_sym,
2469
- receiver_name: receiver_name.to_sym,
2470
- key: key
2471
- )
3068
+ # Positional `new` (field-definition order) skips the keyword hash a keyword `new` allocates.
3069
+ IndexedKey.new(receiver_kind.to_sym, receiver_name.to_sym, key)
2472
3070
  end
2473
3071
 
2474
3072
  def chain_key(receiver_kind, receiver_name, method_name)
2475
- ChainKey.new(
2476
- receiver_kind: receiver_kind.to_sym,
2477
- receiver_name: receiver_name.to_sym,
2478
- method_name: method_name.to_sym
2479
- )
3073
+ ChainKey.new(receiver_kind.to_sym, receiver_name.to_sym, method_name.to_sym)
2480
3074
  end
2481
3075
 
2482
3076
  def drop_indexed_narrowings_for(receiver_kind, receiver_name)
@@ -2558,6 +3152,7 @@ module Rigor
2558
3152
  # beside it ({#with_global_copy_marks}).
2559
3153
  def drop_local_declaration_marks(name)
2560
3154
  dropped = drop_declaration_sourced_for(:local, name)
3155
+ dropped = drop_gradual_narrowing_mark(dropped, name)
2561
3156
  return dropped if dropped.equal?(@declaration_sourced)
2562
3157
 
2563
3158
  name = name.to_sym
@@ -2566,6 +3161,18 @@ module Rigor
2566
3161
  dropped.dup.delete_if { |ref| ref[0] == :global_copy && ref[1] == name }.freeze
2567
3162
  end
2568
3163
 
3164
+ # `marks` without the local `name`'s `:gradual_narrowing` mark (issue #2656); `marks` itself when it has none.
3165
+ def drop_gradual_narrowing_mark(marks, name)
3166
+ return marks if marks.empty?
3167
+
3168
+ ref = [:gradual_narrowing, name.to_sym]
3169
+ return marks unless marks.include?(ref)
3170
+
3171
+ dropped = marks.dup
3172
+ dropped.delete(ref)
3173
+ dropped.freeze
3174
+ end
3175
+
2569
3176
  def add_guard_record(key, pre_guard)
2570
3177
  return @guard_records if @guard_records.key?(key)
2571
3178