rigortype 0.4.6 → 0.4.7

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 (417) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/gem_overlay/activesupport/core_ext.rbs +16 -13
  4. data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
  5. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
  6. data/docs/handbook/01-getting-started.md +5 -2
  7. data/docs/handbook/11-sig-gen.md +50 -3
  8. data/docs/handbook/appendix-phpstan.md +4 -4
  9. data/docs/manual/02-cli-reference.md +152 -26
  10. data/docs/manual/03-configuration.md +36 -3
  11. data/docs/manual/06-baseline.md +6 -4
  12. data/docs/manual/09-editor-integration.md +13 -2
  13. data/docs/manual/10-mcp-server.md +38 -2
  14. data/docs/manual/12-caching.md +4 -3
  15. data/docs/manual/plugins/rigor-actionpack.md +19 -11
  16. data/docs/manual/plugins/rigor-activesupport-core-ext.md +20 -1
  17. data/lib/rigor/analysis/baseline.rb +24 -27
  18. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  19. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  20. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +4 -4
  21. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  22. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +85 -13
  23. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
  24. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +0 -1
  25. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  26. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +1 -1
  27. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +3 -3
  28. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  29. data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
  30. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  31. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  32. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  33. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +2 -2
  34. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  35. data/lib/rigor/analysis/check_rules.rb +124 -44
  36. data/lib/rigor/analysis/crash_signature.rb +2 -2
  37. data/lib/rigor/analysis/dependency_recorder.rb +4 -4
  38. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  39. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  40. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  41. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  42. data/lib/rigor/analysis/edit_replay.rb +93 -6
  43. data/lib/rigor/analysis/fact_store.rb +3 -3
  44. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -2
  45. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  46. data/lib/rigor/analysis/incremental_session.rb +27 -16
  47. data/lib/rigor/analysis/rule_catalog.rb +2 -2
  48. data/lib/rigor/analysis/run_stats.rb +2 -2
  49. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +2 -2
  50. data/lib/rigor/analysis/runner/declaration_position.rb +1 -1
  51. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +193 -93
  52. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +1 -1
  53. data/lib/rigor/analysis/runner/pool_coordinator.rb +147 -323
  54. data/lib/rigor/analysis/runner/project_pre_passes.rb +3 -3
  55. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  56. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  57. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  58. data/lib/rigor/analysis/runner.rb +77 -71
  59. data/lib/rigor/analysis/self_call_resolution_recorder.rb +1 -1
  60. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  61. data/lib/rigor/analysis/template_units.rb +9 -1
  62. data/lib/rigor/analysis/worker_session.rb +24 -31
  63. data/lib/rigor/bleeding_edge.rb +3 -6
  64. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  65. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  66. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  67. data/lib/rigor/cache/descriptor.rb +6 -5
  68. data/lib/rigor/cache/incremental_snapshot.rb +31 -12
  69. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  70. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  71. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  72. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  73. data/lib/rigor/cache/rbs_descriptor.rb +54 -3
  74. data/lib/rigor/cache/rbs_environment.rb +4 -1
  75. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  76. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  77. data/lib/rigor/cache/store.rb +11 -10
  78. data/lib/rigor/ci_detector.rb +1 -1
  79. data/lib/rigor/cli/annotate_command.rb +24 -19
  80. data/lib/rigor/cli/baseline_command.rb +145 -55
  81. data/lib/rigor/cli/catalogue.rb +256 -0
  82. data/lib/rigor/cli/check_command.rb +108 -102
  83. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  84. data/lib/rigor/cli/command.rb +15 -1
  85. data/lib/rigor/cli/coverage_command.rb +21 -8
  86. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  87. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  88. data/lib/rigor/cli/coverage_scan.rb +3 -5
  89. data/lib/rigor/cli/diff_command.rb +25 -20
  90. data/lib/rigor/cli/docs_command.rb +1 -2
  91. data/lib/rigor/cli/doctor_command.rb +12 -12
  92. data/lib/rigor/cli/effects_command.rb +50 -40
  93. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  94. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  95. data/lib/rigor/cli/effects_renderer.rb +2 -1
  96. data/lib/rigor/cli/effects_snapshot_command.rb +18 -17
  97. data/lib/rigor/cli/explain_command.rb +18 -8
  98. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  99. data/lib/rigor/cli/json_document.rb +41 -0
  100. data/lib/rigor/cli/lens_command.rb +12 -8
  101. data/lib/rigor/cli/lsp_command.rb +250 -23
  102. data/lib/rigor/cli/mcp_command.rb +12 -7
  103. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  104. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  105. data/lib/rigor/cli/options.rb +40 -1
  106. data/lib/rigor/cli/plugin_command.rb +93 -46
  107. data/lib/rigor/cli/plugins_command.rb +17 -18
  108. data/lib/rigor/cli/plugins_renderer.rb +10 -6
  109. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  110. data/lib/rigor/cli/protection_renderer.rb +2 -1
  111. data/lib/rigor/cli/show_bleedingedge_command.rb +21 -15
  112. data/lib/rigor/cli/sig_gen_command.rb +84 -27
  113. data/lib/rigor/cli/skill_command.rb +32 -3
  114. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  115. data/lib/rigor/cli/skill_describe.rb +1 -1
  116. data/lib/rigor/cli/trace_command.rb +14 -9
  117. data/lib/rigor/cli/triage_command.rb +10 -11
  118. data/lib/rigor/cli/triage_renderer.rb +2 -1
  119. data/lib/rigor/cli/type_of_command.rb +16 -11
  120. data/lib/rigor/cli/type_of_project_scope.rb +1 -4
  121. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  122. data/lib/rigor/cli/type_scan_command.rb +14 -9
  123. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  124. data/lib/rigor/cli/unused_command.rb +27 -17
  125. data/lib/rigor/cli/upgrade_command.rb +5 -0
  126. data/lib/rigor/cli.rb +209 -54
  127. data/lib/rigor/config_audit.rb +1 -1
  128. data/lib/rigor/configuration/severity_profile.rb +5 -5
  129. data/lib/rigor/configuration.rb +72 -30
  130. data/lib/rigor/effects/definition_context.rb +0 -2
  131. data/lib/rigor/effects/envelope.rb +1 -1
  132. data/lib/rigor/effects/framework_units.rb +1 -1
  133. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  134. data/lib/rigor/effects/propagator.rb +1 -1
  135. data/lib/rigor/effects/registry.rb +2 -2
  136. data/lib/rigor/effects/snapshot.rb +1 -1
  137. data/lib/rigor/effects/taint_cause.rb +0 -2
  138. data/lib/rigor/effects/unit_scan.rb +9 -13
  139. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -9
  140. data/lib/rigor/environment/class_registry.rb +3 -3
  141. data/lib/rigor/environment/core_root.rb +77 -0
  142. data/lib/rigor/environment/default_libraries.rb +1 -1
  143. data/lib/rigor/environment/deferred_signature_plan.rb +283 -0
  144. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  145. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  146. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  147. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  148. data/lib/rigor/environment/member_consistency.rb +56 -7
  149. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  150. data/lib/rigor/environment/project_path_gems.rb +124 -0
  151. data/lib/rigor/environment/rbs_hierarchy.rb +6 -15
  152. data/lib/rigor/environment/rbs_loader.rb +480 -67
  153. data/lib/rigor/environment/reflection.rb +7 -7
  154. data/lib/rigor/environment/required_features.rb +0 -3
  155. data/lib/rigor/environment/ruby_library_roots.rb +1 -1
  156. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  157. data/lib/rigor/environment.rb +41 -10
  158. data/lib/rigor/flow_contribution/merger.rb +4 -4
  159. data/lib/rigor/inference/acceptance.rb +93 -32
  160. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  161. data/lib/rigor/inference/block_run_count.rb +178 -0
  162. data/lib/rigor/inference/budget_trace.rb +8 -2
  163. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  164. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  165. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  166. data/lib/rigor/inference/callee_mutation_chain.rb +88 -77
  167. data/lib/rigor/inference/callee_write_census.rb +815 -0
  168. data/lib/rigor/inference/construction_escape.rb +217 -0
  169. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  170. data/lib/rigor/inference/deep_erasure.rb +39 -7
  171. data/lib/rigor/inference/def_handle.rb +1 -1
  172. data/lib/rigor/inference/def_node_resolver.rb +1 -1
  173. data/lib/rigor/inference/def_return_typer.rb +46 -4
  174. data/lib/rigor/inference/definer_resolution.rb +4 -2
  175. data/lib/rigor/inference/definers_index.rb +106 -0
  176. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  177. data/lib/rigor/inference/element_read_widening.rb +49 -3
  178. data/lib/rigor/inference/expression_typer.rb +116 -49
  179. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  180. data/lib/rigor/inference/global_write_census.rb +76 -2
  181. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  182. data/lib/rigor/inference/hkt_body.rb +1 -2
  183. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  184. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  185. data/lib/rigor/inference/hkt_registry.rb +3 -5
  186. data/lib/rigor/inference/indexed_narrowing.rb +4 -7
  187. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  188. data/lib/rigor/inference/ivar_writer_census.rb +51 -0
  189. data/lib/rigor/inference/key_presence_guard.rb +2 -1
  190. data/lib/rigor/inference/loop_value_record.rb +8 -4
  191. data/lib/rigor/inference/match_rebinding/frame.rb +46 -0
  192. data/lib/rigor/inference/match_rebinding/yielding_match.rb +45 -0
  193. data/lib/rigor/inference/match_rebinding.rb +11 -1
  194. data/lib/rigor/inference/memo_audit.rb +55 -0
  195. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  196. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  197. data/lib/rigor/inference/method_dispatcher/block_folding.rb +1 -1
  198. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +3 -3
  199. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  200. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  201. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +1 -1
  202. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  203. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +39 -27
  204. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  205. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +203 -17
  206. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  207. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  208. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +2 -2
  209. data/lib/rigor/inference/method_dispatcher/set_folding.rb +9 -7
  210. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +83 -20
  211. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  212. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  213. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  214. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  215. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  216. data/lib/rigor/inference/method_dispatcher.rb +64 -14
  217. data/lib/rigor/inference/module_function_state.rb +11 -1
  218. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  219. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  220. data/lib/rigor/inference/mutation_widening.rb +157 -27
  221. data/lib/rigor/inference/narrowing.rb +47 -33
  222. data/lib/rigor/inference/optimistic_origin.rb +1 -1
  223. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  224. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  225. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  226. data/lib/rigor/inference/project_method_ownership.rb +6 -3
  227. data/lib/rigor/inference/project_yielder.rb +11 -17
  228. data/lib/rigor/inference/receiver_alias.rb +20 -0
  229. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +3 -1
  230. data/lib/rigor/inference/refined_dispatch/own_method.rb +2 -1
  231. data/lib/rigor/inference/remover_holder_scan.rb +233 -0
  232. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  233. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  234. data/lib/rigor/inference/scope_indexer/certainty.rb +0 -1
  235. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  236. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +58 -15
  237. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  238. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  239. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +135 -0
  240. data/lib/rigor/inference/scope_indexer.rb +137 -46
  241. data/lib/rigor/inference/self_call_ivar_writes.rb +84 -12
  242. data/lib/rigor/inference/set_mutation.rb +189 -0
  243. data/lib/rigor/inference/singleton_hook_decline.rb +1 -2
  244. data/lib/rigor/inference/statement_evaluator.rb +2328 -138
  245. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  246. data/lib/rigor/inference/synthetic_method_scanner.rb +5 -6
  247. data/lib/rigor/inference/toplevel_def_census.rb +59 -0
  248. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  249. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  250. data/lib/rigor/inference/void_tail_summary.rb +2 -1
  251. data/lib/rigor/language_server/buffer_table.rb +3 -3
  252. data/lib/rigor/language_server/completion_provider.rb +26 -20
  253. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  254. data/lib/rigor/language_server/diagnostic_publisher.rb +8 -3
  255. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  256. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  257. data/lib/rigor/language_server/hover_provider.rb +1 -7
  258. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  259. data/lib/rigor/language_server/project_context.rb +39 -1
  260. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  261. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  262. data/lib/rigor/language_server/server.rb +7 -3
  263. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  264. data/lib/rigor/lens/plugins.rb +1 -1
  265. data/lib/rigor/mcp/server.rb +73 -2
  266. data/lib/rigor/mcp.rb +1 -1
  267. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  268. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  269. data/lib/rigor/plugin/io_boundary.rb +1 -1
  270. data/lib/rigor/plugin/loader.rb +25 -8
  271. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  272. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  273. data/lib/rigor/plugin/manifest.rb +9 -13
  274. data/lib/rigor/plugin/registry.rb +3 -3
  275. data/lib/rigor/plugin/services.rb +2 -3
  276. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  277. data/lib/rigor/plugin/trust_policy.rb +2 -2
  278. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  279. data/lib/rigor/plugin.rb +6 -8
  280. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  281. data/lib/rigor/project_environment.rb +16 -2
  282. data/lib/rigor/protection/analysis_guard.rb +1 -1
  283. data/lib/rigor/protection/discovery_seed.rb +44 -5
  284. data/lib/rigor/protection/mutator.rb +1 -1
  285. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  286. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  287. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  288. data/lib/rigor/rbs_extended.rb +2 -3
  289. data/lib/rigor/reflection/constant_ancestors.rb +5 -3
  290. data/lib/rigor/rooted_glob.rb +15 -1
  291. data/lib/rigor/run_scoped_slots.rb +38 -0
  292. data/lib/rigor/runtime/jit.rb +1 -1
  293. data/lib/rigor/scope/discovery_index.rb +19 -6
  294. data/lib/rigor/scope/resolution_chain.rb +3 -1
  295. data/lib/rigor/scope.rb +365 -31
  296. data/lib/rigor/sig_gen/classification.rb +4 -0
  297. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  298. data/lib/rigor/sig_gen/generator.rb +371 -58
  299. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  300. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  301. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  302. data/lib/rigor/sig_gen/method_candidate.rb +23 -4
  303. data/lib/rigor/sig_gen/observation_collector.rb +3 -5
  304. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  305. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  306. data/lib/rigor/sig_gen/renderer.rb +15 -9
  307. data/lib/rigor/sig_gen/skip_reason_catalog.rb +15 -2
  308. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  309. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  310. data/lib/rigor/sig_gen/write_result.rb +23 -12
  311. data/lib/rigor/sig_gen/writer.rb +217 -37
  312. data/lib/rigor/type/acceptance_router.rb +2 -2
  313. data/lib/rigor/type/accepts_result.rb +1 -1
  314. data/lib/rigor/type/app.rb +4 -6
  315. data/lib/rigor/type/combinator.rb +9 -13
  316. data/lib/rigor/type/constant.rb +4 -5
  317. data/lib/rigor/type/hash_shape.rb +21 -40
  318. data/lib/rigor/type/union.rb +1 -1
  319. data/lib/rigor/type_node/identifier.rb +1 -2
  320. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  321. data/lib/rigor/type_node/string_literal.rb +1 -2
  322. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  323. data/lib/rigor/type_node/union.rb +3 -6
  324. data/lib/rigor/value_semantics.rb +1 -10
  325. data/lib/rigor/version.rb +1 -1
  326. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  327. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  328. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  329. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +38 -36
  330. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  331. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -4
  332. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  333. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  334. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  335. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  336. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  337. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +66 -19
  338. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  339. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -3
  340. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  341. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  342. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  343. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  344. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +2 -3
  345. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +0 -1
  346. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +5 -4
  347. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  348. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  349. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +7 -12
  350. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  351. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  352. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  353. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +19 -6
  354. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +26 -13
  355. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  356. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  357. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  358. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  359. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
  360. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  361. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  362. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  363. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  364. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  365. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  366. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  367. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -5
  368. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  369. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  370. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  371. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  372. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -2
  373. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  374. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  375. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  376. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  377. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  378. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +1 -3
  379. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  380. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  381. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  382. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  383. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  384. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
  385. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  386. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  387. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  388. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +3 -2
  389. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  390. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  391. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +1 -1
  392. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  393. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +17 -21
  394. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  395. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +3 -2
  396. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  397. data/sig/rigor/analysis/baseline.rbs +0 -3
  398. data/sig/rigor/analysis/fact_store.rbs +1 -0
  399. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  400. data/sig/rigor/cli/diff_command.rbs +1 -1
  401. data/sig/rigor/cli/explain_command.rbs +1 -1
  402. data/sig/rigor/effects/registry.rbs +2 -2
  403. data/sig/rigor/inference.rbs +4 -1
  404. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  405. data/sig/rigor/plugin/base.rbs +1 -0
  406. data/sig/rigor/plugin/effect_attribution.rbs +3 -3
  407. data/sig/rigor/plugin/effect_edge.rbs +1 -1
  408. data/sig/rigor/plugin/effect_entry_points.rbs +3 -3
  409. data/sig/rigor/plugin/io_boundary.rbs +2 -1
  410. data/sig/rigor/plugin/manifest.rbs +9 -9
  411. data/sig/rigor/plugin/protocol_contract.rbs +3 -7
  412. data/sig/rigor/scope.rbs +23 -3
  413. data/skills/rigor-plugin-author/SKILL.md +4 -4
  414. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  415. data/skills/rigor-plugin-review/SKILL.md +3 -3
  416. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +12 -1
  417. metadata +36 -3
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "block_call_timing"
6
+ require_relative "closure_escape_analyzer"
7
+ require_relative "project_method_ownership"
8
+
9
+ module Rigor
10
+ module Inference
11
+ # Issue #2303 — whether a project method's body may run the block it is called with more than once ON ONE PATH,
12
+ # for the expression-position block-value pass (`ExpressionTyper#block_may_repeat?` through
13
+ # {ProjectYielder.block_value_may_repeat?}). That pass lays the #587 (b) captured binding under a repeating block,
14
+ # which adds values a single run never reads, so a callee read as repeating when it is not reports diagnostics on
15
+ # correct code (`s = "a"; r = either(f) { t = s; s = nil; t }; r.upcase`). The rule here therefore errs toward
16
+ # "runs once", unlike {ProjectYielder.may_run_block_repeatedly?}, whose only asker widens a return type.
17
+ #
18
+ # The walk counts block runs per path, saturating at {MANY}:
19
+ #
20
+ # - a `yield`, or `call` / `yield` / `===` / `[]` on the `&blk` parameter, is one run;
21
+ # - a sequence sums, and a branch (`if` / `unless` / ternary / `case` / `case ... in`) takes the largest arm,
22
+ # so `f ? yield(1) : yield(2)` is one run;
23
+ # - a `return`, `break`, `next` or receiver-less `raise` / `fail` ends its path, so the `yield` after
24
+ # `return yield(1) if k` is a different path from the one before it;
25
+ # - the body of `while` / `until` / `for`, of `Kernel#loop`, or the block of a catalogued core iterator
26
+ # ({ClosureEscapeAnalyzer.iterator_name?}) is {MANY} when a run lies on a path that reaches the next
27
+ # iteration (falling through the body, or a `next`); a run only on a path that leaves it (`return`, `break`,
28
+ # `raise`) adds what that path holds, so `xs.each { |x| return yield(x) if x > 1 }` is one run;
29
+ # - a hand-off of the block (`g(&blk)`, `g(&)`, `...`) is {MANY};
30
+ # - a block under any other call runs its body once (`then { yield }`, `synchronize { yield }`,
31
+ # `File.open(f) { yield f }`), a lambda body is not run at its definition, and a nested `def` is not this
32
+ # method's body.
33
+ #
34
+ # Reading `&blk` other than calling it (`return unless blk`) is no run.
35
+ #
36
+ # A result is `[fall, total, nxt, brk]`: the most runs on a path that falls through the node, on any path
37
+ # through it, on a path that ends at a `next`, and on one that ends at a `break` (nil where no path does).
38
+ module BlockRunCount
39
+ module_function
40
+
41
+ MANY = 2
42
+ ZERO = [0, 0, nil, nil].freeze
43
+ ALWAYS = [MANY, MANY, nil, nil].freeze
44
+ BLOCK_RUN_CALLS = Set[:call, :yield, :===, :[]].freeze
45
+ LOOP_NODES = [Prism::WhileNode, Prism::UntilNode, Prism::ForNode].freeze
46
+ RAISING_CALLS = Set[:raise, :fail].freeze
47
+ private_constant :ZERO, :ALWAYS, :BLOCK_RUN_CALLS, :LOOP_NODES, :RAISING_CALLS
48
+
49
+ # Memoised per `def` node for the run ({ProjectMethodOwnership.memo}): every call site, and every pass over
50
+ # one, asks the same callee.
51
+ def may_repeat?(def_node, scope)
52
+ memo = ProjectMethodOwnership.memo(scope)[:block_runs]
53
+ return memo[def_node] if memo.key?(def_node)
54
+
55
+ memo[def_node] = runs(def_node.body, def_node.parameters&.block&.name)[1] >= MANY
56
+ end
57
+
58
+ def runs(node, block_name)
59
+ case node
60
+ when nil, Prism::DefNode, Prism::LambdaNode then ZERO
61
+ when Prism::YieldNode then run_after(sequence(node.compact_child_nodes, block_name))
62
+ when Prism::ForwardingArgumentsNode then ALWAYS
63
+ when Prism::BlockArgumentNode then block_argument_runs(node, block_name)
64
+ when Prism::IfNode then branch(runs(node.predicate, block_name), [node.statements, node.subsequent], block_name)
65
+ when Prism::UnlessNode
66
+ branch(runs(node.predicate, block_name), [node.statements, node.else_clause], block_name)
67
+ when Prism::CaseNode, Prism::CaseMatchNode then case_runs(node, block_name)
68
+ when Prism::CallNode then call_runs(node, block_name)
69
+ else other_runs(node, block_name)
70
+ end
71
+ end
72
+
73
+ def other_runs(node, block_name)
74
+ result = sequence(node.compact_child_nodes, block_name)
75
+ case node
76
+ when *LOOP_NODES then iterated(result)
77
+ when Prism::ReturnNode then [nil, result[1], result[2], result[3]]
78
+ when Prism::NextNode then [nil, result[1], larger(result[2], result[0]), result[3]]
79
+ when Prism::BreakNode then [nil, result[1], result[2], larger(result[3], result[0])]
80
+ else result
81
+ end
82
+ end
83
+
84
+ # A body run any number of times: {MANY} when a run reaches the next iteration, else the runs of the paths
85
+ # that leave it. It may run zero times or be left by a `break`, and the walk continues after it either way.
86
+ def iterated((fall, total, nxt, brk))
87
+ again = [fall, nxt].compact.max || 0
88
+ return ALWAYS if again.positive?
89
+
90
+ [[0, brk].compact.max, total, nil, nil]
91
+ end
92
+
93
+ # A body run once: a `next` or `break` in it ends the run, and the walk continues after the call.
94
+ def run_once((fall, total, nxt, brk))
95
+ [[fall, nxt, brk].compact.max, total, nil, nil]
96
+ end
97
+
98
+ def block_argument_runs(node, block_name)
99
+ expression = node.expression
100
+ forwards = expression.nil? || (expression.is_a?(Prism::LocalVariableReadNode) && expression.name == block_name)
101
+ forwards ? ALWAYS : runs(expression, block_name)
102
+ end
103
+
104
+ def sequence(nodes, block_name)
105
+ nodes.reduce(ZERO) do |acc, child|
106
+ break acc if acc[0].nil?
107
+
108
+ chain(acc, runs(child, block_name))
109
+ end
110
+ end
111
+
112
+ # `first` then `second`; nothing follows a path that does not fall through `first`.
113
+ def chain(first, second)
114
+ fall, total, nxt, brk = first
115
+ return first if fall.nil?
116
+
117
+ [second[0] && sum(fall, second[0]), [total, sum(fall, second[1])].max,
118
+ larger(nxt, second[2] && sum(fall, second[2])), larger(brk, second[3] && sum(fall, second[3]))]
119
+ end
120
+
121
+ def run_after(result)
122
+ chain(result, [1, 1, nil, nil])
123
+ end
124
+
125
+ # `head` runs first, then exactly one of `arms` (a nil arm is the path that takes none).
126
+ def branch(head, arms, block_name)
127
+ return head if head[0].nil?
128
+
129
+ arms.map { |arm| chain(head, runs(arm, block_name)) }.reduce do |a, b|
130
+ [larger(a[0], b[0]), [a[1], b[1]].max, larger(a[2], b[2]), larger(a[3], b[3])]
131
+ end
132
+ end
133
+
134
+ def case_runs(node, block_name)
135
+ clauses = node.conditions
136
+ tests = clauses.flat_map { |clause| clause.is_a?(Prism::WhenNode) ? clause.conditions : [clause.pattern] }
137
+ arms = clauses.map(&:statements) + [node.else_clause]
138
+ branch(sequence([node.predicate, *tests], block_name), arms, block_name)
139
+ end
140
+
141
+ def call_runs(node, block_name)
142
+ return run_after(runs(node.arguments, block_name)) if block_run_call?(node, block_name)
143
+
144
+ block = node.block
145
+ literal = block.is_a?(Prism::BlockNode)
146
+ head = sequence([node.receiver, node.arguments, (block unless literal)], block_name)
147
+ return [nil, head[1], head[2], head[3]] if raising_call?(node)
148
+ return head unless literal
149
+
150
+ body = runs(block.body, block_name)
151
+ chain(head, repeating_call?(node) ? iterated(body) : run_once(body))
152
+ end
153
+
154
+ def sum(left, right)
155
+ (left + right).clamp(0, MANY)
156
+ end
157
+
158
+ def larger(left, right)
159
+ [left, right].compact.max
160
+ end
161
+
162
+ def repeating_call?(node)
163
+ ClosureEscapeAnalyzer.iterator_name?(node.name) || BlockCallTiming.kernel_loop?(node)
164
+ end
165
+
166
+ def raising_call?(node)
167
+ node.receiver.nil? && RAISING_CALLS.include?(node.name)
168
+ end
169
+
170
+ # `blk.call` and its spellings.
171
+ def block_run_call?(node, block_name)
172
+ receiver = node.receiver
173
+ !block_name.nil? && receiver.is_a?(Prism::LocalVariableReadNode) && receiver.name == block_name &&
174
+ BLOCK_RUN_CALLS.include?(node.name)
175
+ end
176
+ end
177
+ end
178
+ end
@@ -16,7 +16,7 @@ module Rigor
16
16
  # non-stores) vs memo-key normalization (collapse arg-granularity thrash — a signature whose distinct-key
17
17
  # count far exceeds its body-eval savings).
18
18
  #
19
- # Three categories, one per guard site:
19
+ # The cutoff categories, one per guard site:
20
20
  #
21
21
  # - {RECURSION_GUARD} — `ExpressionTyper#infer_user_method_return` detected a `(receiver, method)` cycle
22
22
  # and returned `Dynamic[top]` (the de-facto recursion-depth budget, effective depth 1).
@@ -27,6 +27,8 @@ module Rigor
27
27
  # fuel and fell back to the plain `(receiver, method)` guard (in-cycle call → `Dynamic[top]`).
28
28
  # - {RECURSION_FIXPOINT_CAP} — the fixpoint return-summary iteration (ADR-55 slice 2) hit its 3-evaluation
29
29
  # cap without converging and collapsed the summary to `untyped` (today's behaviour).
30
+ # - {BLOCK_WRITEBACK_CAP} — the block / loop write-back fixpoint ({BodyFixpoint.converge}) collapsed a
31
+ # local that had not converged to `Dynamic[top]`.
30
32
  #
31
33
  # Enabled only when `RIGOR_BUDGET_TRACE` is set (to any non-empty value) in the environment, or via
32
34
  # {enable!} in tests. When disabled, {hit} is a single boolean check and returns immediately, so normal
@@ -52,6 +54,10 @@ module Rigor
52
54
  # `Dynamic[top]` (the escaping-block floor). Shared by slice B's loop-body fixpoint and by slice C's
53
55
  # content-evidence fixpoint, where the collapsed name is one element / key / value slot of a collection join.
54
56
  BLOCK_WRITEBACK_CAP = :block_writeback_cap
57
+ # `CalleeMutationChain` discovered more than {CalleeMutationChain::MAX_NODES} nodes from one parameter and took
58
+ # the rest as mutating (the callee floor's FP-safe cut). A label: it may vary with the order, the floor does not
59
+ # (ADR-127).
60
+ CALLEE_CHAIN_CAP = :callee_chain_cap
55
61
 
56
62
  # ADR-57 return-memo profile counters (not cutoffs — see the module doc). All bumped by
57
63
  # `ExpressionTyper#infer_user_method_return` / `#compute_user_method_return`.
@@ -82,7 +88,7 @@ module Rigor
82
88
 
83
89
  CATEGORIES = [
84
90
  RECURSION_GUARD, ANCESTOR_WALK_LIMIT, HKT_FUEL_EXHAUSTED, RECURSION_UNROLL_FUEL,
85
- RECURSION_FIXPOINT_CAP, BLOCK_WRITEBACK_CAP,
91
+ RECURSION_FIXPOINT_CAP, BLOCK_WRITEBACK_CAP, CALLEE_CHAIN_CAP,
86
92
  MEMO_ENTRIES, MEMO_HITS, MEMO_MISSES, MEMO_BODY_EVALS,
87
93
  MEMO_REFUSE_ON_STACK, MEMO_REFUSE_UNROLL, MEMO_REFUSE_TRANSIENT
88
94
  ].freeze
@@ -9,8 +9,8 @@ module Rigor
9
9
  #
10
10
  # Exception is the base of every Ruby error class (RuntimeError, StandardError, KeyError, …). The
11
11
  # Init_Exception block in `references/ruby/error.c` registers the entire hierarchy in one pass, so
12
- # the YAML carries 27 classes — but only the base `Exception` row is wired into `CATALOG_BY_CLASS`
13
- # for v0.0.5. A `RuntimeError` receiver hits the Exception arm via `is_a?(Exception)` and the
12
+ # the YAML carries 27 classes — but only the base `Exception` row is wired into `CATALOG_BY_CLASS`.
13
+ # A `RuntimeError` receiver hits the Exception arm via `is_a?(Exception)` and the
14
14
  # catalog answers with the base-class entries; subclass-specific methods (`KeyError#receiver`,
15
15
  # `NameError#name`, …) intentionally miss the lookup until a later slice routes per-subclass
16
16
  # class_names.
@@ -16,11 +16,8 @@ module Rigor
16
16
  #
17
17
  # No mutation blocklist is needed. The numeric classes expose no foldable bang or indirect-mutator
18
18
  # method that the static C classifier mis-attributes (every `:leaf` numeric method is a pure value
19
- # computation), so the generic `MethodCatalog` loader — shared with the eighteen other per-class
20
- # catalogs — covers it directly. This previously hand-rolled its own `safe_for_folding?` /
21
- # `method_entry` / `load_catalog` copy of `MethodCatalog`; folding it onto the shared loader also
22
- # picks up alias resolution (e.g. `Integer#magnitude` → `abs`, `Integer#inspect` → `to_s`), which the
23
- # old bespoke loader silently dropped.
19
+ # computation), so the generic `MethodCatalog` loader covers it directly. Using the shared loader
20
+ # rather than a bespoke copy also gives it alias resolution (e.g. `Integer#magnitude` → `abs`).
24
21
  NUMERIC_CATALOG = MethodCatalog.for_topic("numeric")
25
22
  end
26
23
  end
@@ -20,8 +20,8 @@ module Rigor
20
20
  mutating_selectors: {
21
21
  "Random" => Set[
22
22
  # `rand_random` -> `random_real` / `random_ulong_limited` advance the MT state on the receiver
23
- # (instance #rand) and on `Random::DEFAULT` (singleton .rand). The classifier misses the
24
- # indirect mutator.
23
+ # (instance #rand) and on the process-wide default generator (singleton .rand). The classifier
24
+ # misses the indirect mutator.
25
25
  :rand,
26
26
  # `random_bytes` / `random_s_bytes` consume MT output the same way #rand does — every call
27
27
  # mutates the underlying generator.
@@ -8,107 +8,118 @@ module Rigor
8
8
  #
9
9
  # This module is the closure over a parameter-flow graph, independent of how the graph is read. `own` answers a
10
10
  # def's own census, `{ name => slot }` (a slot is a positional index or a keyword symbol); `edges` answers the
11
- # {Edge}s along which a def hands one of its parameters to a resolved callee. A node is one parameter of one def,
12
- # and the walk follows only the edges that hand on the parameter the node is, so a def's hand-offs of its OTHER
13
- # parameters cost nothing. A node is mutated when its def mutates it or it hands itself to a mutated node: the
14
- # least fixpoint, so a recursive or mutually recursive chain terminates and a cycle with no mutation on it mutates
15
- # nothing.
11
+ # {Edge}s along which a def hands one of its parameters to a resolved callee. A node is one parameter of one def
12
+ # under one owner context, and the walk follows only the edges that hand on the parameter the node is, so a def's
13
+ # hand-offs of its OTHER parameters cost nothing.
16
14
  #
17
- # The walk reads at most {MAX_NODES} nodes per closure. A hand-off to a node past the cap is read as no hand-off,
18
- # as the one-level floor reads a callee it cannot resolve: flooring there would drop a literal shape on evidence
19
- # the walk never saw, and a floored `Array[Dynamic[top]]` then fails a declared tuple on correct code.
15
+ # ADR-127 — the answer is a property of that graph, not of a walk. A parameter `r` is mutated iff an own-mutated
16
+ # node is reachable from `r` through nodes that are not themselves own-mutated, or more than {MAX_NODES} distinct
17
+ # nodes are reachable from `r` (a cut: the walk did not see them, and a literal shape kept on evidence it never saw
18
+ # reports a false positive on correct code). Nothing a walk concludes is kept, so what one walk did cannot change
19
+ # another's answer; only graph facts are persisted (the successor `store`). Which label an answer carries (an
20
+ # exact one, or a cut and its {BudgetTrace::CALLEE_CHAIN_CAP} hit) may vary with the order, the floor does not.
20
21
  module CalleeMutationChain
21
22
  # `name` and `slot` are the forwarding def's parameter, `callee` the def it is handed to, and `callee_names`
22
- # the callee's parameters it may bind there.
23
- Edge = Data.define(:name, :slot, :callee, :callee_names)
23
+ # the callee's parameters it may bind there. Issue #2515 — `owner` is the evaluator the callee's own census and
24
+ # hand-offs resolve in when the hand-off crosses classes (`Help.c(p, o)`, `Helper.new.c(p, o)`), nil for a
25
+ # self-call, which stays in the evaluator the hop started in. `approx` is true when the hand-off resolved to more
26
+ # than one candidate, or by name; the floor it implies is an over-approximation, not proof the callee writes.
27
+ Edge = Data.define(:name, :slot, :callee, :callee_names, :owner, :approx) do
28
+ def initialize(name:, slot:, callee:, callee_names:, owner: nil, approx: false) = super
29
+ end
30
+
31
+ # ADR-127 — the most distinct nodes one parameter may reach before the walk is cut and reads as mutated.
32
+ MAX_NODES = 64
24
33
 
25
- MAX_NODES = 32
34
+ NO_TRIPLES = [].freeze
35
+ private_constant :NO_TRIPLES
26
36
 
27
37
  module_function
28
38
 
29
- def close(root, own:, edges:)
30
- census = own.call(root)
31
- handed = edges.call(root).reject { |edge| census.key?(edge.name) }
39
+ # `own` and `edges` take the def and the owner evaluator its hop resolves in (nil: the closure's own). `store`
40
+ # (nil: none) is the run's successor store, `{ def => { owner_key => { name => triples } } }`, and `ctx` the key
41
+ # the nil owner is stored under (the side and class the closure runs in).
42
+ def close(root, own:, edges:, store: nil, ctx: nil)
43
+ census = own.call(root, nil)
44
+ handed = edges.call(root, nil)
32
45
  return census if handed.empty?
33
46
 
34
- flow = Flow.new(own, edges)
35
- handed.each { |edge| flow.visit(root, edge.name) }
36
- flow.settle
37
- handed.each_with_object(census.dup) do |edge, result|
38
- result[edge.name] = edge.slot if flow.mutated?(root, edge.name)
39
- end
40
- end
41
-
42
- # The parameter-flow graph one closure reads: per def, per parameter name, whether the node is mutated and the
43
- # nodes it hands itself on to.
44
- class Flow
45
- Node = Struct.new(:mutated, :successors)
47
+ result = nil
48
+ handed.each do |edge|
49
+ name = edge.name
50
+ next if census.key?(name) || result&.key?(name)
51
+ next unless reaches_mutation?(root, name, own, edges, store, ctx)
46
52
 
47
- def initialize(own, edges)
48
- @own = own
49
- @edges = edges
50
- @nodes = {}.compare_by_identity
51
- @count = 0
52
- end
53
-
54
- def mutated?(def_node, name)
55
- node = @nodes.dig(def_node, name)
56
- !node.nil? && node.mutated
53
+ (result ||= census.dup)[name] = edge.slot
57
54
  end
55
+ result || census
56
+ end
58
57
 
59
- # Reads the nodes reachable from `def_node`'s `name`, breadth first, while fewer than {MAX_NODES} are read.
60
- def visit(def_node, name)
61
- queue = [[def_node, name]]
62
- until queue.empty?
63
- current, parameter = queue.shift
64
- next if known?(current, parameter)
65
- break if @count >= MAX_NODES
58
+ # Breadth first from `root`'s `name`, over a walk-local `seen`. A node counts when it is first discovered and its
59
+ # own mutation is checked then, so more than {MAX_NODES} discovered nodes is decided without computing the
60
+ # frontier's edges. The first own-mutated node proves the parameter mutated; it is reachable through nodes that
61
+ # are not own-mutated by construction, since those are the only ones expanded.
62
+ def reaches_mutation?(root, name, own, edges, store, ctx)
63
+ seen = {}.compare_by_identity
64
+ mark(seen, root, name, nil)
65
+ queue = [root, name, nil]
66
+ discovered = 1
67
+ i = 0
68
+ while i < queue.size
69
+ triples = successors(queue[i], queue[i + 1], queue[i + 2], edges, store, ctx)
70
+ i += 3
71
+ j = 0
72
+ while j < triples.size
73
+ callee = triples[j]
74
+ callee_name = triples[j + 1]
75
+ owner = triples[j + 2]
76
+ j += 3
77
+ next unless mark(seen, callee, callee_name, owner)
78
+
79
+ discovered += 1
80
+ if discovered > MAX_NODES
81
+ BudgetTrace.hit(BudgetTrace::CALLEE_CHAIN_CAP)
82
+ return true
83
+ end
84
+ return true if own.call(callee, owner).key?(callee_name)
66
85
 
67
- queue.concat(add(current, parameter))
86
+ queue.push(callee, callee_name, owner)
68
87
  end
69
88
  end
89
+ false
90
+ end
70
91
 
71
- def settle
72
- changed = true
73
- while changed
74
- changed = false
75
- each_node do |node|
76
- next if node.mutated
77
- next unless node.successors.any? { |callee, name| mutated?(callee, name) }
78
-
79
- node.mutated = true
80
- changed = true
81
- end
82
- end
83
- end
92
+ # True when the node was not yet in the walk-local `seen`, which now holds it.
93
+ def mark(seen, def_node, name, owner)
94
+ by_name = ((seen[def_node] ||= {})[owner] ||= {})
95
+ return false if by_name.key?(name)
84
96
 
85
- private
97
+ by_name[name] = true
98
+ end
86
99
 
87
- def known?(def_node, name)
88
- by_name = @nodes[def_node]
89
- !by_name.nil? && by_name.key?(name)
90
- end
100
+ # The flat `[callee, name, owner, ...]` triples the node `(def_node, name, owner)` hands itself to: a graph fact,
101
+ # persisted in `store` when there is one. A node its def mutates is never asked.
102
+ def successors(def_node, name, owner, edges, store, ctx)
103
+ return compute_successors(def_node, name, owner, edges) if store.nil?
91
104
 
92
- # Records the node and answers its successors. A node its def mutates needs none.
93
- def add(def_node, name)
94
- @count += 1
95
- mutated = @own.call(def_node).key?(name)
96
- successors = mutated ? [] : successors_of(def_node, name)
97
- (@nodes[def_node] ||= {})[name] = Node.new(mutated, successors)
98
- successors
105
+ by_name = ((store[def_node] ||= {})[owner.nil? ? ctx : owner] ||= {})
106
+ if by_name.key?(name)
107
+ return MemoAudit.check("successor", by_name[name], name, owner) do
108
+ compute_successors(def_node, name, owner, edges)
109
+ end
99
110
  end
100
111
 
101
- def successors_of(def_node, name)
102
- @edges.call(def_node).each_with_object([]) do |edge, successors|
103
- next unless edge.name == name
112
+ by_name[name] = compute_successors(def_node, name, owner, edges)
113
+ end
104
114
 
105
- edge.callee_names.each { |callee_name| successors << [edge.callee, callee_name] }
106
- end
107
- end
115
+ def compute_successors(def_node, name, owner, edges)
116
+ triples = nil
117
+ edges.call(def_node, owner).each do |edge|
118
+ next unless edge.name == name
108
119
 
109
- def each_node(&)
110
- @nodes.each_value { |by_name| by_name.each_value(&) }
120
+ edge.callee_names.each { |callee_name| (triples ||= []).push(edge.callee, callee_name, edge.owner || owner) }
111
121
  end
122
+ triples.nil? ? NO_TRIPLES : triples.freeze
112
123
  end
113
124
  end
114
125
  end