rigortype 0.4.7 → 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 (268) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -1
  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 +29 -0
  6. data/data/gem_overlay/addressable/addressable.rbs +40 -0
  7. data/docs/handbook/01-getting-started.md +1 -1
  8. data/docs/handbook/11-sig-gen.md +35 -0
  9. data/docs/install.md +20 -0
  10. data/docs/manual/01-installation.md +67 -26
  11. data/docs/manual/02-cli-reference.md +307 -91
  12. data/docs/manual/03-configuration.md +41 -3
  13. data/docs/manual/04-diagnostics.md +5 -4
  14. data/docs/manual/08-skills.md +11 -0
  15. data/docs/manual/09-editor-integration.md +8 -0
  16. data/docs/manual/14-rails-quickstart.md +1 -1
  17. data/docs/manual/19-effect-labels.md +3 -4
  18. data/docs/manual/plugins/rigor-actionpack.md +15 -6
  19. data/docs/manual/plugins/rigor-activesupport-core-ext.md +3 -0
  20. data/docs/manual/plugins/rigor-sorbet.md +17 -3
  21. data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
  22. data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
  23. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +8 -0
  24. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +13 -2
  25. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -0
  26. data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
  27. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +40 -11
  28. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
  29. data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
  30. data/lib/rigor/analysis/check_rules/rule_ids.rb +8 -1
  31. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  32. data/lib/rigor/analysis/check_rules.rb +598 -99
  33. data/lib/rigor/analysis/crash_signature.rb +43 -1
  34. data/lib/rigor/analysis/dependency_recorder.rb +233 -76
  35. data/lib/rigor/analysis/incremental.rb +11 -1
  36. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -1
  37. data/lib/rigor/analysis/incremental_session.rb +279 -37
  38. data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
  39. data/lib/rigor/analysis/rule_catalog.rb +37 -1
  40. data/lib/rigor/analysis/run_cache_key.rb +27 -3
  41. data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
  42. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +4 -0
  43. data/lib/rigor/analysis/runner/declaration_position.rb +7 -7
  44. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +72 -20
  45. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +3 -8
  46. data/lib/rigor/analysis/runner/pool_coordinator.rb +26 -5
  47. data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
  48. data/lib/rigor/analysis/runner.rb +222 -39
  49. data/lib/rigor/analysis/self_call_resolution_recorder.rb +61 -22
  50. data/lib/rigor/analysis/worker_session.rb +42 -41
  51. data/lib/rigor/bleeding_edge.rb +19 -4
  52. data/lib/rigor/cache/file_digest.rb +39 -31
  53. data/lib/rigor/cache/incremental_snapshot.rb +66 -7
  54. data/lib/rigor/cache/rbs_descriptor.rb +37 -2
  55. data/lib/rigor/cache/rbs_environment.rb +5 -1
  56. data/lib/rigor/cli/baseline_command.rb +15 -2
  57. data/lib/rigor/cli/check_command.rb +4 -2
  58. data/lib/rigor/cli/effects_snapshot_command.rb +8 -1
  59. data/lib/rigor/cli/lens_command.rb +17 -2
  60. data/lib/rigor/cli/sig_gen_command.rb +8 -0
  61. data/lib/rigor/cli/skill_describe.rb +1 -0
  62. data/lib/rigor/cli.rb +5 -2
  63. data/lib/rigor/config_audit.rb +32 -18
  64. data/lib/rigor/configuration/severity_profile.rb +9 -0
  65. data/lib/rigor/configuration.rb +32 -5
  66. data/lib/rigor/effects/collector.rb +53 -21
  67. data/lib/rigor/environment/bundle_sig_discovery.rb +63 -28
  68. data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
  69. data/lib/rigor/environment/default_libraries.rb +17 -0
  70. data/lib/rigor/environment/deferred_signature_plan.rb +104 -6
  71. data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
  72. data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
  73. data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
  74. data/lib/rigor/environment/rbs_hierarchy.rb +6 -0
  75. data/lib/rigor/environment/rbs_loader.rb +312 -63
  76. data/lib/rigor/environment.rb +131 -35
  77. data/lib/rigor/inference/acceptance.rb +50 -15
  78. data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
  79. data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
  80. data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
  81. data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
  82. data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
  83. data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
  84. data/lib/rigor/inference/block_auto_splat.rb +1 -1
  85. data/lib/rigor/inference/block_run_count.rb +9 -2
  86. data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
  87. data/lib/rigor/inference/callee_mutation_chain.rb +1 -1
  88. data/lib/rigor/inference/captured_locals.rb +2 -0
  89. data/lib/rigor/inference/census_foreign_block.rb +354 -0
  90. data/lib/rigor/inference/clause_widening.rb +1 -1
  91. data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
  92. data/lib/rigor/inference/construction_escape.rb +56 -15
  93. data/lib/rigor/inference/declaration_walk.rb +4 -0
  94. data/lib/rigor/inference/def_node_resolver.rb +32 -14
  95. data/lib/rigor/inference/def_return_typer.rb +1 -1
  96. data/lib/rigor/inference/definer_resolution.rb +15 -7
  97. data/lib/rigor/inference/definers_index.rb +72 -16
  98. data/lib/rigor/inference/error_info.rb +16 -0
  99. data/lib/rigor/inference/expression_typer.rb +1459 -287
  100. data/lib/rigor/inference/flow_tracer.rb +31 -14
  101. data/lib/rigor/inference/fork_map.rb +3 -0
  102. data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
  103. data/lib/rigor/inference/global_write_census.rb +8 -6
  104. data/lib/rigor/inference/in_effect_refinements.rb +7 -0
  105. data/lib/rigor/inference/indexed_narrowing.rb +57 -8
  106. data/lib/rigor/inference/ivar_writer_census.rb +106 -11
  107. data/lib/rigor/inference/key_presence_guard.rb +51 -25
  108. data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
  109. data/lib/rigor/inference/last_line.rb +5 -2
  110. data/lib/rigor/inference/loop_value_record.rb +63 -15
  111. data/lib/rigor/inference/match_rebinding/frame.rb +11 -0
  112. data/lib/rigor/inference/match_rebinding/yielding_match.rb +74 -18
  113. data/lib/rigor/inference/match_rebinding.rb +21 -8
  114. data/lib/rigor/inference/method_dispatcher/block_folding.rb +43 -29
  115. data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
  116. data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
  117. data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
  118. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
  119. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
  120. data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
  121. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +12 -0
  122. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +409 -67
  123. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +1 -1
  124. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +384 -64
  125. data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
  126. data/lib/rigor/inference/method_dispatcher.rb +66 -18
  127. data/lib/rigor/inference/method_parameter_binder.rb +136 -56
  128. data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
  129. data/lib/rigor/inference/module_function_state.rb +13 -2
  130. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  131. data/lib/rigor/inference/mutation_widening.rb +44 -4
  132. data/lib/rigor/inference/narrowing.rb +201 -27
  133. data/lib/rigor/inference/operand_effects.rb +43 -2
  134. data/lib/rigor/inference/optimistic_origin.rb +133 -8
  135. data/lib/rigor/inference/origin_lookup.rb +1 -1
  136. data/lib/rigor/inference/precision_scanner.rb +1 -1
  137. data/lib/rigor/inference/project_method_ownership.rb +28 -10
  138. data/lib/rigor/inference/project_patched_methods.rb +65 -13
  139. data/lib/rigor/inference/project_patched_scanner.rb +120 -25
  140. data/lib/rigor/inference/project_patched_visibility.rb +39 -3
  141. data/lib/rigor/inference/rbs_type_translator.rb +68 -15
  142. data/lib/rigor/inference/receiver_writer_call.rb +67 -0
  143. data/lib/rigor/inference/recursion_rebinding.rb +157 -0
  144. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +56 -18
  145. data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -17
  146. data/lib/rigor/inference/refined_dispatch.rb +13 -0
  147. data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
  148. data/lib/rigor/inference/remover_holder_scan.rb +76 -22
  149. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
  150. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +12 -11
  151. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +188 -28
  152. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
  153. data/lib/rigor/inference/scope_indexer.rb +2079 -183
  154. data/lib/rigor/inference/self_call_ivar_writes.rb +26 -7
  155. data/lib/rigor/inference/self_preserving_block.rb +87 -0
  156. data/lib/rigor/inference/singleton_hook_decline.rb +41 -6
  157. data/lib/rigor/inference/singleton_object_constant.rb +32 -2
  158. data/lib/rigor/inference/statement_evaluator.rb +970 -250
  159. data/lib/rigor/inference/string_mutation.rb +14 -0
  160. data/lib/rigor/inference/synthetic_method_scanner.rb +35 -11
  161. data/lib/rigor/inference/toplevel_def_census.rb +14 -6
  162. data/lib/rigor/inference/unknown_store_widening.rb +1 -1
  163. data/lib/rigor/inference/untyped_rejoin.rb +1 -1
  164. data/lib/rigor/inference/void_tail_summary.rb +29 -10
  165. data/lib/rigor/lens/claims/comparison.rb +78 -0
  166. data/lib/rigor/lens/claims/request.rb +131 -0
  167. data/lib/rigor/lens/claims/type_reader.rb +216 -0
  168. data/lib/rigor/lens/claims.rb +208 -0
  169. data/lib/rigor/lens/contract_reader.rb +5 -0
  170. data/lib/rigor/lens/document.rb +1 -1
  171. data/lib/rigor/lens/file_reader.rb +21 -8
  172. data/lib/rigor/lens/run.rb +8 -0
  173. data/lib/rigor/lens.rb +1 -1
  174. data/lib/rigor/library_probe.rb +68 -0
  175. data/lib/rigor/memo_audit.rb +282 -0
  176. data/lib/rigor/plugin/base.rb +15 -2
  177. data/lib/rigor/plugin/io_boundary.rb +25 -3
  178. data/lib/rigor/plugin/manifest.rb +7 -3
  179. data/lib/rigor/plugin/registry.rb +13 -2
  180. data/lib/rigor/plugin/trust_policy.rb +46 -0
  181. data/lib/rigor/protection/mutation_cache.rb +4 -1
  182. data/lib/rigor/reflection/constant_ancestors.rb +79 -22
  183. data/lib/rigor/reflection/declaration_collectors.rb +55 -0
  184. data/lib/rigor/reflection/declarations.rb +58 -0
  185. data/lib/rigor/reflection/project_method.rb +177 -0
  186. data/lib/rigor/reflection.rb +2 -0
  187. data/lib/rigor/rooted_glob.rb +18 -1
  188. data/lib/rigor/run_scoped_slots.rb +20 -20
  189. data/lib/rigor/scope/discovery_index.rb +149 -2
  190. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
  191. data/lib/rigor/scope/resolution_chain.rb +900 -55
  192. data/lib/rigor/scope.rb +307 -34
  193. data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
  194. data/lib/rigor/sig_gen/classification.rb +14 -1
  195. data/lib/rigor/sig_gen/generator.rb +222 -13
  196. data/lib/rigor/sig_gen/method_candidate.rb +14 -0
  197. data/lib/rigor/sig_gen/observation_collector.rb +11 -2
  198. data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
  199. data/lib/rigor/sig_gen/skip_reason_catalog.rb +47 -0
  200. data/lib/rigor/sig_gen/writer.rb +21 -0
  201. data/lib/rigor/sig_gen.rb +1 -0
  202. data/lib/rigor/source/forwardable_delegators.rb +122 -0
  203. data/lib/rigor/source/node_children.rb +13 -0
  204. data/lib/rigor/type/combinator.rb +164 -8
  205. data/lib/rigor/type/interface.rb +60 -0
  206. data/lib/rigor/type.rb +1 -0
  207. data/lib/rigor/type_of_memo/serving.rb +99 -0
  208. data/lib/rigor/type_of_memo/stats.rb +43 -0
  209. data/lib/rigor/type_of_memo.rb +337 -0
  210. data/lib/rigor/typing_state.rb +1094 -0
  211. data/lib/rigor/version.rb +1 -1
  212. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +2 -2
  213. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +57 -25
  214. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
  215. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
  216. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
  217. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
  218. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
  219. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +130 -30
  220. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -5
  221. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
  222. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +49 -13
  223. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
  224. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +4 -2
  225. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +13 -5
  226. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +29 -0
  227. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
  228. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
  229. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -2
  230. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
  231. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +6 -0
  232. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +199 -1
  233. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +3 -2
  234. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +4 -4
  235. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +209 -2
  236. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
  237. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
  238. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +80 -3
  239. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
  240. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +1 -6
  241. data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
  242. data/sig/rigor/analysis/fact_store.rbs +2 -3
  243. data/sig/rigor/cache/descriptor.rbs +14 -0
  244. data/sig/rigor/environment.rbs +4 -1
  245. data/sig/rigor/plugin/base.rbs +53 -19
  246. data/sig/rigor/plugin/interfaces.rbs +51 -0
  247. data/sig/rigor/plugin/io_boundary.rbs +2 -3
  248. data/sig/rigor/plugin/node_context.rbs +21 -0
  249. data/sig/rigor/reflection.rbs +37 -0
  250. data/sig/rigor/scope.rbs +22 -3
  251. data/sig/rigor/trinary.rbs +1 -0
  252. data/sig/rigor/type.rbs +9 -3
  253. data/sig/rigor.rbs +3 -2
  254. data/skills/rigor-ask/SKILL.md +1 -0
  255. data/skills/rigor-project-init/references/01-detect.md +1 -0
  256. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +5 -1
  257. data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
  258. data/skills/rigor-rbs-contract/SKILL.md +147 -0
  259. data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
  260. data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
  261. data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
  262. data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
  263. data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
  264. data/skills/rigor-rbs-setup/SKILL.md +3 -1
  265. data/skills/rigor-type-oracle/SKILL.md +26 -1
  266. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
  267. metadata +48 -2
  268. data/lib/rigor/inference/memo_audit.rb +0 -55
data/lib/rigor/scope.rb CHANGED
@@ -3,7 +3,8 @@
3
3
  require_relative "type"
4
4
  require_relative "environment"
5
5
  require_relative "scope/discovery_index"
6
- require_relative "run_scoped_slots"
6
+ require_relative "typing_state"
7
+ require_relative "memo_audit"
7
8
  require_relative "scope/resolution_chain"
8
9
  require_relative "inference/definer_resolution"
9
10
  require_relative "inference/in_effect_refinements"
@@ -17,6 +18,7 @@ require_relative "inference/def_node_resolver"
17
18
  # Issue #2608 — read by `MutationWidening` at a remover call, which only an analysis makes; loaded with the engine
18
19
  # so the engine-free value layer that requires `MutationWidening` stays as small as it was.
19
20
  require_relative "inference/remover_narrowing"
21
+ require_relative "inference/alias_mutation_floor"
20
22
 
21
23
  module Rigor
22
24
  # Immutable analyzer scope: holds local-variable bindings and a reference to the surrounding Environment. State
@@ -157,7 +159,13 @@ module Rigor
157
159
  # renames the foreign declaration. `published_constant_alias_names` holds the last segments of exactly
158
160
  # those aliases, and matching on the last segment over-answers in the WITHHOLDING direction, which is the
159
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.
160
167
  def published_constant?(name)
168
+ Analysis::DependencyRecorder.read_last_segment(:constant, name) if Analysis::DependencyRecorder.active?
161
169
  names = @discovery.published_constant_names
162
170
  return true if published_constant_alias?(name)
163
171
  return false if names.empty?
@@ -287,6 +295,32 @@ module Rigor
287
295
  :EMPTY_PUBLISHED_CONSTANT_IVARS, :EMPTY_REPEATED_OR_WRITES,
288
296
  :EMPTY_CONSTANT_NARROWINGS, :EMPTY_GUARD_RECORDS, :EMPTY_BOT_GUARD_CLASSES
289
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
+
290
324
  class << self
291
325
  def empty(environment: Environment.default, source_path: nil)
292
326
  new(environment: environment, locals: {}.freeze,
@@ -295,16 +329,33 @@ module Rigor
295
329
  end
296
330
 
297
331
  def record_dynamic_origin(node, cause)
332
+ TypingState.current.log_bound_write(DYNAMIC_ORIGINS, @dynamic_origins, node, cause)
298
333
  @dynamic_origins[node] = cause
299
334
  self
300
335
  end
301
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
+
302
352
  # ADR-100 WD3 — records that the value introduced at `node` (a call node) is a `top` recovered from an
303
353
  # author-declared `-> void` return, keyed by the origin site (`origin`, an {Inference::VoidOrigin}). The
304
354
  # value-context check rule `static.value-use.void` consumes this table. Mirrors {#record_dynamic_origin}
305
355
  # exactly: identity-keyed advisory metadata, mutated in place on the shared table (threaded by reference
306
356
  # through `#join` / `#rebuild`), excluded from `==` / `hash`, so it never forks a flow-dedup or cache key.
307
357
  def record_void_origin(node, origin)
358
+ TypingState.current.log_bound_write(VOID_ORIGINS, @void_origins, node, origin)
308
359
  @void_origins[node] = origin
309
360
  self
310
361
  end
@@ -317,17 +368,22 @@ module Rigor
317
368
  # identity-keyed advisory metadata on a table threaded by reference through `#join` / `#rebuild`,
318
369
  # excluded from `==` / `hash`, so it never forks a flow-dedup or cache key.
319
370
  def record_plugin_typed_call(node)
371
+ TypingState.current.log_bound_write(PLUGIN_TYPED_CALLS, @plugin_typed_calls, node, true)
320
372
  @plugin_typed_calls[node] = true
321
373
  self
322
374
  end
323
375
 
324
376
  # Issue #653 — whether a plugin answered the return type for `node` on this run.
325
377
  def plugin_typed_call?(node)
326
- @plugin_typed_calls.key?(node)
378
+ !log_advisory_read(PLUGIN_TYPED_CALLS, @plugin_typed_calls, node).nil?
327
379
  end
328
380
 
329
- # Every keyword past the 31st takes CRuby's allocating unspecified-keywords path when omitted with a non-literal
330
- # default; see the note at {#rebuild} before adding a field.
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.
331
387
  def initialize( # rubocop:disable Metrics/AbcSize, Metrics/MethodLength -- one keyword and assignment per field
332
388
  environment:, locals:,
333
389
  fact_store: Analysis::FactStore.empty,
@@ -342,8 +398,6 @@ module Rigor
342
398
  published_constant_sourced: EMPTY_PUBLISHED_CONSTANT_SOURCED,
343
399
  source_path: nil,
344
400
  struct_fold_safe_locals: EMPTY_FOLD_SAFE,
345
- opaque_block_self: false,
346
- block_self_unknown: false,
347
401
  singleton_class_body: false,
348
402
  lexical_nesting: nil,
349
403
  dynamic_origins: {}.compare_by_identity,
@@ -360,7 +414,10 @@ module Rigor
360
414
  constant_narrowings: EMPTY_CONSTANT_NARROWINGS,
361
415
  guard_records: EMPTY_GUARD_RECORDS,
362
416
  bot_guard_classes: EMPTY_BOT_GUARD_CLASSES,
363
- 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
364
421
  )
365
422
  @environment = environment
366
423
  @locals = locals
@@ -403,10 +460,20 @@ module Rigor
403
460
  # nil-freeness is a property of its class. Mirrors {#dynamic_origins} / {#void_origins}: advisory
404
461
  # metadata, ignored by `==` / `hash`, and never varying a flow decision on its own.
405
462
  def record_optimistic_origin(node, cause)
463
+ TypingState.current.log_bound_write(OPTIMISTIC_ORIGINS, @optimistic_origins, node, cause)
406
464
  @optimistic_origins[node] = cause
407
465
  self
408
466
  end
409
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
+
410
477
  # Issue #1703 — this scope with private copies of the identity-keyed side tables (dynamic, void and optimistic
411
478
  # origins, plugin-typed calls), so a second walk from it — the `key?`-guarded re-walk — records into its own
412
479
  # tables and never into the ones the file's analysis reads.
@@ -639,9 +706,36 @@ module Rigor
639
706
  rebuild(opaque_block_self: true, block_self_unknown: true)
640
707
  end
641
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
+
642
729
  # True when this scope sits inside a block whose `self` is unmodelled ({#entering_opaque_block}).
643
730
  def opaque_block_self? = @opaque_block_self
644
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
+
645
739
  # Issue #1717 — true when this scope sits in a block whose `self` the engine does not know, so an explicit
646
740
  # `self.m` there is no evidence `m` is missing: the yielding method may `instance_exec` the block on another
647
741
  # object. Read ONLY by `call.undefined-method`'s explicit-`self` exemption, and kept apart from
@@ -897,6 +991,56 @@ module Rigor
897
991
  @declaration_sourced.include?([:inferred_param, name.to_sym])
898
992
  end
899
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
+
900
1044
  def with_cvar(name, type)
901
1045
  rebuild(cvars: @cvars.merge(name.to_sym => type).freeze)
902
1046
  end
@@ -1280,12 +1424,33 @@ module Rigor
1280
1424
  # a file that starts or stops defining it re-analyses the reader (issue #1459's `defines:` edge).
1281
1425
  def project_method_name?(method_name)
1282
1426
  Analysis::DependencyRecorder.read_name(:defines, method_name) if Analysis::DependencyRecorder.active?
1283
- table = (Thread.current[PROJECT_METHOD_NAMES_KEY] ||= ObjectSpace::WeakKeyMap.new)
1284
- return true if (table[@discovery] ||= collect_project_method_names).include?(method_name.to_sym)
1427
+ return true if project_method_names.include?(method_name.to_sym)
1285
1428
 
1286
1429
  false
1287
1430
  end
1288
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
+
1289
1454
  def collect_project_method_names
1290
1455
  names = Set.new
1291
1456
  [@discovery.discovered_methods, @discovery.discovered_def_nodes, @discovery.discovered_singleton_def_nodes,
@@ -1296,9 +1461,13 @@ module Rigor
1296
1461
  end
1297
1462
  private :collect_project_method_names
1298
1463
 
1299
- # A run-scoped slot ({RunScopedSlots}): emptied when a run ends, so an edited project is read afresh.
1300
- PROJECT_METHOD_NAMES_KEY = RunScopedSlots.register(:__rigor_project_method_names)
1301
- private_constant :PROJECT_METHOD_NAMES_KEY
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
1302
1471
 
1303
1472
  def record_discovered_method(class_name, method_name)
1304
1473
  record_class_dependency(class_name)
@@ -1352,6 +1521,9 @@ module Rigor
1352
1521
  def user_instance_def_owners(method_name)
1353
1522
  Analysis::DependencyRecorder.read_name(:defines, method_name) if Analysis::DependencyRecorder.active?
1354
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).
1355
1527
  owners = nil
1356
1528
  @discovery.discovered_def_nodes.each do |class_name, defs|
1357
1529
  next unless defs.key?(name) && class_name != Inference::ScopeIndexer::TOP_LEVEL_DEF_KEY
@@ -1540,18 +1712,31 @@ module Rigor
1540
1712
  # call's literal name and a def in a block. Held by the table's identity, as {#toplevel_definition_rows} is.
1541
1713
  def deferred_definition_names
1542
1714
  ranges = @discovery.discovered_deferred_ranges
1543
- slots = (Thread.current[DEFERRED_NAMES_KEY] ||= [])
1715
+ return collect_deferred_definition_names(ranges) if memo_audit_bypass?
1716
+
1717
+ state = TypingState.current
1718
+ slots = (state[DEFERRED_NAMES] ||= [])
1544
1719
  slot = slots.find { |entry| entry[0].equal?(ranges) }
1545
- return slot[1] if slot
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
1546
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)
1547
1733
  names = Set.new
1548
1734
  ranges.each_value do |entries|
1549
1735
  entries.each { |(_start, _finish, name, kind, owner)| names << name if name && kind.nil? && owner.nil? }
1550
1736
  end
1551
- slots.unshift([ranges, names.freeze]).slice!(2..)
1552
- names
1737
+ names.freeze
1553
1738
  end
1554
- private :deferred_definition_names
1739
+ private :collect_deferred_definition_names
1555
1740
 
1556
1741
  # Issue #2336 — {#module_function_def?}'s `:both` reading for a def in any file: whether `def_node`, an instance
1557
1742
  # def of `owner`, is a module function by a deferred-range row over it (`module_function def x`, or a bare
@@ -1571,10 +1756,24 @@ module Rigor
1571
1756
  # a singleton row. Held by the table's identity (a file's merged copy and the seed), released with the run.
1572
1757
  def toplevel_definition_rows
1573
1758
  ranges = @discovery.discovered_deferred_ranges
1574
- slots = (Thread.current[TOPLEVEL_ROWS_KEY] ||= [])
1759
+ return collect_toplevel_definition_rows(ranges) if memo_audit_bypass?
1760
+
1761
+ state = TypingState.current
1762
+ slots = (state[TOPLEVEL_ROWS] ||= [])
1575
1763
  slot = slots.find { |entry| entry[0].equal?(ranges) }
1576
- return slot[1] if slot
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
1577
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)
1578
1777
  rows = Hash.new(0)
1579
1778
  ranges.each_value do |entries|
1580
1779
  entries.each do |(_start, _finish, name, kind, owner)|
@@ -1583,30 +1782,55 @@ module Rigor
1583
1782
  rows[name] = kind == :singleton || rows[name] == MAIN_SINGLETON_ROWS ? MAIN_SINGLETON_ROWS : rows[name] + 1
1584
1783
  end
1585
1784
  end
1586
- slots.unshift([ranges, rows.freeze]).slice!(2..)
1587
- rows
1785
+ rows.freeze
1588
1786
  end
1589
- private :toplevel_definition_rows
1787
+ private :collect_toplevel_definition_rows
1590
1788
 
1591
1789
  # The names an `alias` statement or `alias_method` call introduces, from the global write census; held by the
1592
1790
  # census's identity as {#toplevel_definition_rows} is.
1593
1791
  def method_alias_names
1594
1792
  census = @discovery.discovered_global_write_census
1595
- slots = (Thread.current[METHOD_ALIASES_KEY] ||= [])
1793
+ return collect_method_alias_names(census) if memo_audit_bypass?
1794
+
1795
+ state = TypingState.current
1796
+ slots = (state[METHOD_ALIASES] ||= [])
1596
1797
  slot = slots.find { |entry| entry[0].equal?(census) }
1597
- return slot[1] if slot
1798
+ if slot
1799
+ return Inference::MemoAudit.check("method alias names", slot[1], :census) { collect_method_alias_names(census) }
1800
+ end
1598
1801
 
1599
- names = census.each_with_object(Set.new) { |entry, acc| acc << entry[1] if entry[0] == :method_alias }.freeze
1802
+ names = Inference::MemoAudit.key_audit("method alias names") { collect_method_alias_names(census) }
1600
1803
  slots.unshift([census, names]).slice!(2..)
1601
1804
  names
1602
1805
  end
1603
1806
  private :method_alias_names
1604
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
+
1605
1813
  MAIN_SINGLETON_ROWS = -1
1606
- TOPLEVEL_ROWS_KEY = RunScopedSlots.register(:__rigor_toplevel_definition_rows)
1607
- METHOD_ALIASES_KEY = RunScopedSlots.register(:__rigor_method_alias_names)
1608
- DEFERRED_NAMES_KEY = RunScopedSlots.register(:__rigor_deferred_definition_names)
1609
- private_constant :MAIN_SINGLETON_ROWS, :TOPLEVEL_ROWS_KEY, :METHOD_ALIASES_KEY, :DEFERRED_NAMES_KEY
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
1610
1834
 
1611
1835
  # Issue #316 — the CONFIDENCE-GATED companion of {#top_level_def_for}, and the only accessor the type
1612
1836
  # inference may bind through. {#top_level_def_for} stays unrestricted because it also serves the
@@ -1847,6 +2071,14 @@ module Rigor
1847
2071
  @discovery.discovered_includes[class_name.to_s] || []
1848
2072
  end
1849
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
+
1850
2082
  # Issue #898 — the module names `extend`ed onto `class_name`'s SINGLETON, as written, gathered up the
1851
2083
  # as-written superclass chain because a singleton class inherits its superclass's singleton class
1852
2084
  # (`class Base; extend Comparable; end; class Widget < Base; end` leaves `Widget.is_a?(Comparable)`
@@ -1913,6 +2145,7 @@ module Rigor
1913
2145
  end
1914
2146
  return found if found
1915
2147
 
2148
+ chain.record_external_absence
1916
2149
  chain.truncated? ? ancestor_walk_gave_up : [nil, nil]
1917
2150
  end
1918
2151
 
@@ -1952,6 +2185,7 @@ module Rigor
1952
2185
  end
1953
2186
  return found if found
1954
2187
 
2188
+ chain.record_external_absence
1955
2189
  chain.truncated? ? ancestor_walk_gave_up : [nil, nil]
1956
2190
  end
1957
2191
 
@@ -2035,6 +2269,8 @@ module Rigor
2035
2269
  discovered_method?(entry.name, method_name, entry.side == :singleton ? :singleton : kind_for(kind))
2036
2270
  end
2037
2271
  return true if found
2272
+
2273
+ record_discovered_miss(chain, method_name) if Analysis::DependencyRecorder.active?
2038
2274
  # Budget exhaustion is uncertainty, not absence: answering "not declared" here would hand a
2039
2275
  # `call.undefined-method` a fired verdict it has no evidence for. Suppress and record the hit.
2040
2276
  return false unless chain.truncated?
@@ -2043,6 +2279,28 @@ module Rigor
2043
2279
  true
2044
2280
  end
2045
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
+
2046
2304
  # A module entry on the singleton chain answers with its INSTANCE methods; on the instance chain, the
2047
2305
  # caller's kind stands.
2048
2306
  def kind_for(kind) = kind == :singleton ? :instance : kind
@@ -2367,8 +2625,9 @@ module Rigor
2367
2625
  # against THIS scope's discovery index (so cross-file dispatches in the body resolve). The incremental
2368
2626
  # session re-drives a declaration-stable changed callee at each previously-observed call key to prove its
2369
2627
  # return is unchanged before skipping its symbol dependents.
2370
- def user_method_return(def_node, receiver, arg_types)
2371
- 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)
2372
2631
  end
2373
2632
 
2374
2633
  # Statement-level evaluation: returns the pair `[type, scope']` where `type` is what the node produces and
@@ -2465,9 +2724,10 @@ module Rigor
2465
2724
  # field indexed by each keyword's position among ALL the method's keywords, required and optional
2466
2725
  # (`VM_KW_SPECIFIED_BITS_MAX`, 31); a keyword past that index whose default is not a literal makes every call that
2467
2726
  # omits it allocate the record instead. A 32nd keyword here did that on every scope transition, about 1% of an
2468
- # analysis's allocations (issue #2555). `#initialize` is already past the index: `bot_guard_classes` and
2469
- # `declared_refinements` take the allocating path whenever a caller omits them. The next Scope field crosses it in
2470
- # this method again, so it needs a positional slot or a restructuring, not a 32nd keyword.
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.
2471
2731
  def rebuild(
2472
2732
  construction_facts = @construction_facts,
2473
2733
  locals: @locals, fact_store: @fact_store, self_type: @self_type,
@@ -2892,6 +3152,7 @@ module Rigor
2892
3152
  # beside it ({#with_global_copy_marks}).
2893
3153
  def drop_local_declaration_marks(name)
2894
3154
  dropped = drop_declaration_sourced_for(:local, name)
3155
+ dropped = drop_gradual_narrowing_mark(dropped, name)
2895
3156
  return dropped if dropped.equal?(@declaration_sourced)
2896
3157
 
2897
3158
  name = name.to_sym
@@ -2900,6 +3161,18 @@ module Rigor
2900
3161
  dropped.dup.delete_if { |ref| ref[0] == :global_copy && ref[1] == name }.freeze
2901
3162
  end
2902
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
+
2903
3176
  def add_guard_record(key, pre_guard)
2904
3177
  return @guard_records if @guard_records.key?(key)
2905
3178