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
@@ -0,0 +1,282 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "typing_state"
4
+
5
+ module Rigor
6
+ # ADR-127 WD3 — the memo audit. A memo hit is correct only when the key holds every input the value
7
+ # was computed from, and a key that misses one answers a caller with another caller's value (#2516, #2524). With the
8
+ # audit on, every hit in the callee floor's memos (`resolved_census`, the unrecorded hand-off memo, the successor
9
+ # store and `def_census`) recomputes the value and raises {Mismatch} when it differs from the stored one, so a key
10
+ # defect fails on the first hit instead of waiting for an order that exposes it.
11
+ #
12
+ # Like {BudgetTrace}, a switch for specs and CI (`RIGOR_MEMO_AUDIT=1` in the environment, read once at load, or
13
+ # {enable!}): off by default, a single boolean check when off. A recomputation
14
+ # runs with the audit suspended on its thread and the memos bypassed ({suspended?}), so it reads nothing stored.
15
+ #
16
+ # A mismatch raises, and is also kept on this thread's list until {drain} takes it (ADR-129 WD5, #2785): the
17
+ # typing path rescues `StandardError` in many places, and a raise one of them swallows would turn the defect into
18
+ # an ordinary fallback answer. The runner drains the list after each file and reports what it holds the way it
19
+ # reports a mismatch no rescue caught, and {raise_undrained!} fails the run on what a file never drained.
20
+ #
21
+ # **The key audit** (ADR-129 WD1) checks the other half at every miss: a Memo, or a Table a computation fills (the
22
+ # ADR-84 return memo), whose miss computation reads a Frame must key on the state id or on that Frame's value.
23
+ # {key_audit} runs a miss computation with its Frame reads noted ({TypingState::KeyAudit}) and compares them with
24
+ # the Frame terms the key read through {TypingState::State#key_frame} when {key} built it. A read Frame the key
25
+ # does not carry, and that neither the computation installed itself nor the site declared ({key_declaration}), is a
26
+ # {KeyMismatch}: kept on this thread's list like a raised mismatch, and not raised, since the computation's answer
27
+ # is correct for this caller.
28
+ module MemoAudit
29
+ # A memo hit whose stored value differs from a fresh computation.
30
+ class Mismatch < StandardError; end
31
+
32
+ # A memo miss whose computation read a Frame its key does not carry (ADR-129 WD1's key audit).
33
+ class KeyMismatch < Mismatch; end
34
+
35
+ # What a miss site declares beside its key terms (ADR-129 WD1), by Frame slot name:
36
+ # - `forced`: Frames the memo installs at a constant (or a value derived from its key) before the key is built,
37
+ # so the computation reads that form rather than the caller's;
38
+ # - `gaps`: `slot name => reason` naming the issue of a known key gap, read without a key term and reported as a
39
+ # named exception rather than a mismatch;
40
+ # - `carried`: `slot name => reason` for Frames another key term carries (a run-level flag, fixed while the run
41
+ # generation the memo's bucket keys on is), counted as key terms.
42
+ KeyDeclaration = Data.define(:forced, :gaps, :carried)
43
+
44
+ # ADR-129 WD1 — a Frame, installed around a recomputation. `@enabled` is a process switch, outside the typing
45
+ # state (WD5). G1′ witness: a recomputation types a callee body, and the memo hits inside it read the flag.
46
+ SUSPENDED = TypingState.frame(
47
+ :memo_audit_suspended, reason: "set around an audit recomputation, which bypasses every memo",
48
+ witness: "spec/fixtures/typing_state_witnesses/memo_audit_suspended.rb"
49
+ )
50
+ private_constant :SUSPENDED
51
+
52
+ # ADR-129 WD1 — a Resource: the mismatches raised on this thread and not drained yet. Typing never reads it; an
53
+ # append refuses every open capture, which only an audited run reaches.
54
+ MISMATCHES = TypingState.resource(
55
+ :memo_audit_mismatches,
56
+ run_scoped: true,
57
+ reason: "the audit's mismatches on this thread, appended where raised, drained per file, never read by typing"
58
+ )
59
+ private_constant :MISMATCHES
60
+
61
+ EMPTY = [].freeze
62
+ private_constant :EMPTY
63
+
64
+ # ADR-129 WD1 — the Frames every key audit names as exceptions rather than mismatches, with their reasons: the
65
+ # audit's own switch and the process switches whose installed recorders a computation may file into.
66
+ KEY_AUDIT_SWITCHES = {
67
+ memo_audit_suspended: "the audit's own switch: a nested hit's check reads it",
68
+ flow_tracer: "a process switch (WD5): the tracer is an output, and the type_of memo is shut under one (WD4)",
69
+ effects_collector: "a process switch (WD5): recording into its contents touches a Resource",
70
+ dependency_recorder: "a process switch (WD5): the recorder files ADR-46 dependency edges, which no type reads",
71
+ self_call_resolution_recorder: "a process switch (WD5): its records are a journaled Table, never read back"
72
+ }.freeze
73
+
74
+ # ADR-129 WD1 — a Resource: the named exceptions key audits on this thread met, `[label, slot name] => reason`.
75
+ # Typing never reads it.
76
+ KEY_EXCEPTIONS = TypingState.resource(
77
+ :memo_audit_key_exceptions,
78
+ run_scoped: true,
79
+ reason: "the key audit's named exceptions met on this thread, for specs and reports; never read by typing"
80
+ )
81
+ private_constant :KEY_EXCEPTIONS
82
+
83
+ NO_DECLARATION = KeyDeclaration.new(forced: EMPTY, gaps: {}.freeze, carried: {}.freeze)
84
+ private_constant :NO_DECLARATION
85
+
86
+ @enabled = %w[1 true].include?(ENV["RIGOR_MEMO_AUDIT"].to_s.downcase)
87
+
88
+ module_function
89
+
90
+ def enabled? = @enabled
91
+
92
+ # True while an audit recomputation runs on this thread. The callee floor's memos are then bypassed (read and
93
+ # write), so the recomputation reads no stored entry of another memo: a defect shared across a table (a stale
94
+ # `resolved_census(:edges)` entry) would otherwise make the recomputed value match the stored one.
95
+ def suspended? = TypingState.current[SUSPENDED]
96
+
97
+ def enable! = (@enabled = true)
98
+
99
+ def disable! = (@enabled = false)
100
+
101
+ # `stored`, after checking it against `yield` when the audit is on. `label` and the key parts (`key`, `key2`) name
102
+ # the memo entry in the failure; they are passed apart so an audit that is off allocates nothing.
103
+ def check(label, stored, key, key2 = nil, &)
104
+ return stored unless @enabled && !TypingState.current[SUSPENDED]
105
+
106
+ fresh = suspended(&)
107
+ return stored if fresh == stored
108
+
109
+ mismatch = Mismatch.new(
110
+ "#{label} memo hit differs from a fresh computation for #{[key, key2].inspect[0, 120]}: " \
111
+ "stored #{stored.inspect[0, 200]}, fresh #{fresh.inspect[0, 200]}"
112
+ )
113
+ state = TypingState.current
114
+ state.append(MISMATCHES, mismatch)
115
+ raise mismatch
116
+ end
117
+
118
+ # ADR-129 G1′ — a `type_of` begins on this thread, for the matching {leave_type_of}; the caller guards on
119
+ # {enabled?}, so an audit that is off costs one boolean check. Fails (as a {Mismatch} on the list, not raised) on an
120
+ # installed `typing_unreachable` Frame ({TypingState::FrameAudit}).
121
+ def enter_type_of
122
+ state = TypingState.current
123
+ state.extend(TypingState::FrameAudit) unless state.is_a?(TypingState::FrameAudit)
124
+ state.enter_type_of
125
+ end
126
+
127
+ # ADR-129 G1′ — the `type_of` memo's shadow reached its table on `state`; the caller guards on {enabled?}. Fails (as
128
+ # a {Mismatch} on the list, not raised) on an installed `memo_shut_while_installed` Frame.
129
+ def note_memo_lookup(state)
130
+ state.extend(TypingState::FrameAudit) unless state.is_a?(TypingState::FrameAudit)
131
+ state.note_memo_lookup
132
+ end
133
+
134
+ # ADR-129 G1′ — the `type_of` {enter_type_of} tracked ends.
135
+ def leave_type_of
136
+ TypingState.current.leave_type_of
137
+ end
138
+
139
+ # Keeps one {Mismatch} per message on the list until it is drained: a Frame declaration that does not hold is
140
+ # reported at every `type_of` it meets.
141
+ def report_frame_declaration(state, message)
142
+ pending = state.read(MISMATCHES)
143
+ return if pending&.any? { |mismatch| mismatch.message == message }
144
+
145
+ mismatch = Mismatch.new(message)
146
+ mismatch.set_backtrace(caller.drop_while { |frame| frame.include?("/typing_state.rb:") })
147
+ state.append(MISMATCHES, mismatch)
148
+ end
149
+
150
+ # The mismatches raised on this thread since the last drain, oldest first, and an empty list for the next file.
151
+ # Each has been raised, so it carries the backtrace a crash row is built from.
152
+ def drain
153
+ state = TypingState.current
154
+ mismatches = state.read(MISMATCHES)
155
+ return EMPTY if mismatches.nil? || mismatches.empty?
156
+
157
+ state.write(MISMATCHES, nil)
158
+ mismatches
159
+ end
160
+
161
+ # ADR-129 WD1 — checks a `carried:` claim at the install of a run-level flag (`flag`, ADR-130's erasure or
162
+ # dispatch-off frame). The return memo counts the flag as carried by its bucket's run-generation key, which holds
163
+ # only while the flag is installed around a whole run: with the audit on, an install that finds the return memo
164
+ # holding entries (a run in progress, or one that was not released) raises instead of silently mixing answers
165
+ # computed under both values.
166
+ def check_run_level_install(flag)
167
+ return unless @enabled
168
+
169
+ held = TypingState.current[TypingState.slot(:user_method_return_memo).index]
170
+ return if held.nil? || held[1].empty?
171
+
172
+ raise Mismatch, "#{flag} installed while the return memo holds entries: the memo's key carries it only as a " \
173
+ "flag installed around a whole run (ADR-129 WD1 key audit)"
174
+ end
175
+
176
+ # Raises the first mismatch no file drained (one raised outside every file's analysis), so a run that ends with
177
+ # one fails instead of releasing it with the run's other slots.
178
+ def raise_undrained!
179
+ return unless @enabled
180
+
181
+ mismatches = drain
182
+ raise mismatches.first unless mismatches.empty?
183
+ end
184
+
185
+ def suspended(&)
186
+ TypingState.current.with(SUSPENDED, true, &)
187
+ end
188
+
189
+ # A frozen {KeyDeclaration} for a miss site, built once into a constant so an audit that is off allocates nothing.
190
+ def key_declaration(forced: EMPTY, gaps: {}, carried: {})
191
+ KeyDeclaration.new(forced: forced.map(&:to_sym).freeze, gaps: gaps.transform_keys(&:to_sym).freeze,
192
+ carried: carried.transform_keys(&:to_sym).freeze)
193
+ end
194
+
195
+ # The key the block builds. When the audit is on, the Frames the block reads through
196
+ # {TypingState::State#key_frame} are remembered as the key's terms for the {key_audit} of the miss that follows.
197
+ def key(&)
198
+ return yield unless @enabled
199
+
200
+ auditing_state.collect_key_terms(&)
201
+ end
202
+
203
+ # The block's value: a memo's miss computation, run under ADR-129 WD1's Memo rule
204
+ # ({TypingState::State#memo_miss}: a miss that writes a Table while a capture is open touches), keyed by `key`
205
+ # ({key}, or nil for a key that reads no Frame) and declared by `declaration` ({key_declaration}). When the audit
206
+ # is on, a Frame the computation reads that the key does not carry is a {KeyMismatch} on this thread's list, named
207
+ # by `label` (the slot's `audit:` or `computed:` label), unless it is internal, forced, carried, a declared gap or
208
+ # a {KEY_AUDIT_SWITCHES} switch.
209
+ #
210
+ # A computation that raises or throws is not judged, and its audit is closed on the way out. A computation that
211
+ # returns is judged whether or not its memo then stores it (the return memo's WD3 bracket may refuse it), which
212
+ # only reports more. An audit recomputation ({suspended?}) is not judged either, but it still opens the site's
213
+ # declaration, so a gap it declares reaches the audits around it as it does on a miss: the verdict of an enclosing
214
+ # miss does not depend on whether this memo hit.
215
+ def key_audit(label, declaration = nil, key = nil, &)
216
+ return TypingState.current.memo_miss(&) unless @enabled
217
+
218
+ state = auditing_state
219
+ declaration ||= NO_DECLARATION
220
+ judged = !state[SUSPENDED]
221
+ terms = state.key_terms_for(key) if judged
222
+ state.open_key_audit(label, declaration.gaps)
223
+ begin
224
+ value = state.memo_miss(&)
225
+ ensure
226
+ reads = state.close_key_audit
227
+ end
228
+ judge_key_audit(state, label, declaration, terms, reads) if judged
229
+ value
230
+ end
231
+
232
+ # The named exceptions key audits on this thread have met since the run began, `[label, slot name] => reason`.
233
+ def key_exceptions
234
+ TypingState.current.read(KEY_EXCEPTIONS) || {}
235
+ end
236
+
237
+ def auditing_state
238
+ state = TypingState.current
239
+ state.extend(TypingState::KeyAudit) unless state.is_a?(TypingState::KeyAudit)
240
+ state
241
+ end
242
+
243
+ def judge_key_audit(state, label, declaration, terms, reads)
244
+ return if reads.empty? || terms.include?(TypingState::KeyAudit::STATE_ID)
245
+
246
+ reads.each do |slot, via|
247
+ name = TypingState.slots[slot].name
248
+ next note_key_exception(state, label, name, "through the #{via[0]} miss: #{via[1]}") unless via == true
249
+ next if terms.include?(slot) || declaration.forced.include?(name) || declaration.carried.key?(name)
250
+
251
+ reason = key_exception_reason(declaration, name)
252
+ reason ? note_key_exception(state, label, name, reason) : report_key_mismatch(state, label, name)
253
+ end
254
+ end
255
+
256
+ # Why a miss may read the Frame `name` without a key term, or nil: a gap its site declares, or a switch.
257
+ def key_exception_reason(declaration, name) = declaration.gaps[name] || KEY_AUDIT_SWITCHES[name]
258
+
259
+ def note_key_exception(state, label, name, reason)
260
+ seen = state.acquire(KEY_EXCEPTIONS) { {} }
261
+ seen[[label, name]] ||= reason
262
+ end
263
+
264
+ # Keeps one {KeyMismatch} per message on the list until it is drained: a key defect repeats at every miss.
265
+ def report_key_mismatch(state, label, name)
266
+ message = "key audit: the #{label} miss read the Frame #{name}, which its key does not carry"
267
+ pending = state.read(MISMATCHES)
268
+ return if pending&.any? { |mismatch| mismatch.message == message }
269
+
270
+ mismatch = KeyMismatch.new(message)
271
+ mismatch.set_backtrace(caller.drop_while { |frame| frame.include?("/memo_audit.rb:") })
272
+ state.append(MISMATCHES, mismatch)
273
+ end
274
+ end
275
+
276
+ # The engine reaches the audit as `Inference::MemoAudit`; it lives at `lib/rigor/memo_audit.rb` so the value layer
277
+ # (`Reflection`, `Scope`) can load it without loading an `inference/` file (ADR-104's warm effects probe budget), as
278
+ # `TypingState` does (#2796).
279
+ module Inference
280
+ MemoAudit = Rigor::MemoAudit
281
+ end
282
+ end
@@ -945,6 +945,14 @@ module Rigor
945
945
  @io_boundary ||= services.io_boundary_for(manifest.id)
946
946
  end
947
947
 
948
+ # The boundary {#io_boundary} has built, or nil when this plugin never asked for one, without building it: a
949
+ # plugin that never built one read nothing through it. Engine-internal ({Analysis::RunDisclosureCredit} sends it,
950
+ # being outside the class), so it is private and part of no plugin-facing contract.
951
+ #
952
+ # @api private
953
+ def built_io_boundary = @io_boundary
954
+ private :built_io_boundary
955
+
948
956
  # ADR-7 § "Slice 6-A" / ADR-60 WD3 — returns a callable that performs a `Cache::Store#fetch_or_validate`
949
957
  # round-trip for the named producer (the ADR-45 record-and-validate path). The entry is KEYED on the
950
958
  # stable identity inputs — the plugin's `PluginEntry` template (id, version, config_hash) composed with
@@ -1064,14 +1072,19 @@ module Rigor
1064
1072
  # The gate for one `dynamic_return` rule. Method-name gate first — a Symbol-array probe vs the receiver
1065
1073
  # ancestry resolution below (ADR-52 WD1); both are pure predicates, so order only affects cost. A
1066
1074
  # receiver-less rule (ADR-52 WD2) skips the ancestry check entirely and fires on the method name alone.
1075
+ # The engine also offers the nameless nodes of #2717 (the `&:sym` BlockArgumentNode, the compound
1076
+ # write nodes) to a plugin `build_name_gates` left ungated; only `CallNode` answers `name`, so a
1077
+ # name-gated rule declines them (`include?(nil)` is false) rather than raising past the later rules
1078
+ # (#2764).
1067
1079
  def dynamic_return_rule_applies?(rule, call_node, receiver_type, environment, scope)
1068
- return false if rule[:methods] && !resolved_dynamic_return_methods(rule).include?(call_node.name)
1080
+ name = call_node.respond_to?(:name) ? call_node.name : nil
1081
+ return false if rule[:methods] && !resolved_dynamic_return_methods(rule).include?(name)
1069
1082
 
1070
1083
  if rule[:file_methods]
1071
1084
  # The path is read here, not in `dynamic_return_type`, so a spec-double scope without
1072
1085
  # `source_path` only affects `file_methods:` rules (other gate forms never touch it).
1073
1086
  path = scope.respond_to?(:source_path) ? scope.source_path : nil
1074
- return false unless resolved_dynamic_return_file_methods(rule, path).include?(call_node.name)
1087
+ return false unless resolved_dynamic_return_file_methods(rule, path).include?(name)
1075
1088
  end
1076
1089
 
1077
1090
  entries = resolved_dynamic_return_receiver_entries(rule)
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "digest"
4
+ require_relative "../typing_state"
4
5
 
5
6
  require_relative "access_denied_error"
6
7
  require_relative "../rooted_glob"
@@ -52,11 +53,20 @@ module Rigor
52
53
  # descriptor and any enclosing producer's descriptor then carry the served value's inputs.
53
54
  class IoBoundary
54
55
  URL_TIMEOUT_SECONDS = 10
55
- URL_MAX_BYTES = 10 * 1024 * 1024
56
+ URL_MAX_BYTES = 10_485_760 # 10 MiB
56
57
 
57
58
  ATTRIBUTION_KEY = :rigor_io_boundary_attribution
58
59
  private_constant :ATTRIBUTION_KEY
59
60
 
61
+ # ADR-129 WD3 / #2814 — the attribution sink is a Resource: a `type_of` memo hit that served an answer without
62
+ # re-running the plugin read would not append what the miss appended, so every {.attribute} refuses each open
63
+ # capture. Its storage stays the thread variable above (ADR-129 WD5 shape 2); this slot declares its kind.
64
+ SINK = TypingState.resource(
65
+ :io_boundary_attribution,
66
+ reason: "every plugin boundary read appends to the attributing sink, which a memo hit would not replay"
67
+ )
68
+ private_constant :SINK
69
+
60
70
  # ADR-45 WD2 (#1507) — runs the block with `sink` (an Array) receiving every file and listing row ANY
61
71
  # boundary records on this thread meanwhile, in addition to the boundary's own table, and returns the
62
72
  # block's value. `Analysis::Runner` wraps each file's analysis in one when it records dependencies, so the
@@ -67,16 +77,28 @@ module Rigor
67
77
  # lands in the sink. The boundary's own table is unchanged by it, including its first-existence-row-stands
68
78
  # rule: the sink records what each read observed. Only reads are credited: a value a plugin memoised reaches
69
79
  # later files without one, so the first file to trigger the read is the only one credited with it.
80
+ #
81
+ # The installing fiber's typing state rides with the sink, so a read from inside an `Enumerator` refuses the
82
+ # captures open where the analysis runs, not only those of the `Enumerator`'s own fiber ({.attribute}).
70
83
  def self.attributing(sink)
71
84
  previous = Thread.current.thread_variable_get(ATTRIBUTION_KEY)
72
- Thread.current.thread_variable_set(ATTRIBUTION_KEY, sink)
85
+ Thread.current.thread_variable_set(ATTRIBUTION_KEY, [sink, TypingState.current].freeze)
73
86
  yield
74
87
  ensure
75
88
  Thread.current.thread_variable_set(ATTRIBUTION_KEY, previous)
76
89
  end
77
90
 
91
+ # Records `entry` in the attributing sink, if one is installed, and touches {SINK} on the calling fiber's typing
92
+ # state and on the installing fiber's (ADR-129 WD3, #2814).
78
93
  def self.attribute(entry)
79
- Thread.current.thread_variable_get(ATTRIBUTION_KEY)&.push(entry)
94
+ state = TypingState.current
95
+ state.touch(SINK)
96
+ sink, owner = Thread.current.thread_variable_get(ATTRIBUTION_KEY)
97
+ return nil if sink.nil?
98
+
99
+ owner.touch(SINK)
100
+ sink.push(entry)
101
+ nil
80
102
  end
81
103
 
82
104
  attr_reader :policy, :plugin_id
@@ -304,11 +304,15 @@ module Rigor
304
304
  FirstParty.bundled?(id)
305
305
  end
306
306
 
307
- # Whether the plugin contributes anything to the effect surfaces at all. The compiled per-run tables
308
- # skip a plugin that answers false, so a project whose plugins predate this contract pays nothing.
307
+ # Whether the manifest declares anything for the effect surfaces — a convenience predicate over the
308
+ # manifest-declared fields only, true exactly when a `Registry::Contribution` built from these
309
+ # declarations would not be empty (`Contribution#empty?` reads the same five fields,
310
+ # `effect_ancestry:` included). A plugin that overrides the `Base#effect_*` readers instead of
311
+ # declaring rows here (rigor-sidekiq) can contribute while this answers false. The compiled
312
+ # per-run tables filter on `Contribution#empty?` themselves; nothing consults this predicate.
309
313
  def effects?
310
314
  !effect_labels.empty? || !effect_attributions.empty? || !effect_edges.empty? ||
311
- !effect_entry_points.empty?
315
+ !effect_entry_points.empty? || !effect_ancestry.empty?
312
316
  end
313
317
 
314
318
  def ==(other)
@@ -4,6 +4,7 @@ require_relative "blueprint"
4
4
  # Issue #806 — {Registry#guarded_manifest} constructs one. `Rigor::Plugin::LoadError` MUST be defined by
5
5
  # then: an unresolved bare `LoadError` in this lexical scope silently falls back to Ruby's own ::LoadError.
6
6
  require_relative "load_error"
7
+ require_relative "../typing_state"
7
8
 
8
9
  module Rigor
9
10
  module Plugin
@@ -20,6 +21,10 @@ module Rigor
20
21
  # exactly the order the ungated walk produced — diagnostics stay byte-identical. The receiver-class
21
22
  # ancestry match for `dynamic_return` still happens per-dispatch inside `Plugin::Base#dynamic_return_type`.
22
23
  class ContributionIndex
24
+ TypingState.memo_field(self, :@owns_receiver_memo,
25
+ reason: "keyed on the environment's identity and the class name; the plugins' " \
26
+ "owns_receivers set is fixed at construction")
27
+
23
28
  # Ordered (registry-order) subsets relevant to each collector, and the membership sets used to gate the
24
29
  # paths within a plugin. `for_file_diagnostics` is the subset the runner's per-file diagnostic loop
25
30
  # visits: plugins overriding `#diagnostics_for_file` or declaring at least one `node_rule`.
@@ -103,8 +108,8 @@ module Rigor
103
108
  # The categorisation sets + the ordered per-collector subsets.
104
109
  def compile_memberships(plugins)
105
110
  plugins.each { |p| reject_legacy_flow_hook!(p) }
106
- @dynamic = plugins.reject { |p| p.class.dynamic_returns.empty? }.to_set
107
- @narrowing_facts = plugins.reject { |p| p.class.narrowing_facts_rules.empty? }.to_set
111
+ @dynamic = plugins.reject { |p| p.class.dynamic_returns.empty? }.to_set.freeze
112
+ @narrowing_facts = plugins.reject { |p| p.class.narrowing_facts_rules.empty? }.to_set.freeze
108
113
  compile_collector_subsets(plugins)
109
114
  end
110
115
 
@@ -218,6 +223,12 @@ module Rigor
218
223
  # Ractor. The eventual Phase 4 pool ships `blueprints` across the boundary and calls {.materialize}
219
224
  # per-Ractor; the live `plugins` carriage on the coordinator registry stays unchanged.
220
225
  class Registry
226
+ TypingState.memo_field(self, :@contracts_by_path, :@effect_memo,
227
+ reason: "keyed on the path, and on the one effect-contribution compile; the contract " \
228
+ "set, glob semantics and plugin set are fixed for the run (ADR-52 WD1), and the " \
229
+ "compile, which may read project facts across the I/O boundary, rests on the " \
230
+ "run's stable-filesystem premise, as FileDigest's run tables do")
231
+
221
232
  attr_reader :plugins, :load_errors, :blueprints, :contribution_index, :resolved_gem_paths
222
233
 
223
234
  # @param plugins — instantiated plugin instances in deterministic order.
@@ -45,6 +45,52 @@ module Rigor
45
45
  freeze
46
46
  end
47
47
 
48
+ # Builds the run's policy from the project configuration — the one derivation `rigor check`
49
+ # ({Analysis::Runner}), pooled workers ({Analysis::WorkerSession}) and `rigor unused`'s plugin roots
50
+ # ({Analysis::Reachability::PluginRoots}) share, so every `Plugin::Services` container enforces the
51
+ # same scope (#2586).
52
+ #
53
+ # `trusted_gems` is the gem-name half of every `Configuration#plugins` entry. `allowed_read_roots`
54
+ # is the project root (CWD), every `signature_paths` entry, each trusted gem's
55
+ # `Gem.loaded_specs` `full_gem_path` (silently skipped when the gem has no loaded spec), and every
56
+ # `plugins_io.allowed_paths` entry. `network_policy` / `allowed_url_hosts` come from
57
+ # `plugins_io.network` / `plugins_io.allowed_url_hosts`.
58
+ def self.for_configuration(configuration)
59
+ trusted_gems = configuration.plugins.map { |entry| trusted_gem_name(entry) }.uniq
60
+ roots = [Dir.pwd]
61
+ Array(configuration.signature_paths).each { |sp| roots << File.absolute_path(sp) }
62
+ trusted_gems.each do |gem_name|
63
+ path = trusted_gem_root(gem_name)
64
+ roots << path if path
65
+ end
66
+ configuration.plugins_io_allowed_paths.each { |p| roots << File.absolute_path(p) }
67
+
68
+ new(
69
+ trusted_gems: trusted_gems,
70
+ allowed_read_roots: roots,
71
+ network_policy: configuration.plugins_io_network,
72
+ allowed_url_hosts: configuration.plugins_io_allowed_url_hosts
73
+ )
74
+ end
75
+
76
+ def self.trusted_gem_name(entry)
77
+ case entry
78
+ when String then entry
79
+ when Hash then entry["gem"] || entry["id"]
80
+ end
81
+ end
82
+ private_class_method :trusted_gem_name
83
+
84
+ def self.trusted_gem_root(gem_name)
85
+ return nil if gem_name.nil? || gem_name.empty?
86
+
87
+ spec = Gem.loaded_specs[gem_name]
88
+ spec&.full_gem_path
89
+ rescue StandardError
90
+ nil
91
+ end
92
+ private_class_method :trusted_gem_root
93
+
48
94
  # @return true when the absolute path falls inside any allowed read root. Symlinks are
49
95
  # resolved through `File.absolute_path` only (no `realpath`); plugins with adversarial intent are out of
50
96
  # scope per ADR-2.
@@ -215,7 +215,10 @@ module Rigor
215
215
  end,
216
216
  "patched_overrides" => project_scan.project_patched_methods.overrides.map do |key, visibility|
217
217
  [*key, visibility].map(&:to_s)
218
- end
218
+ end,
219
+ "patched_singleton_copies" => project_scan.project_patched_methods.singleton_copies.map do |owner, name|
220
+ [owner.to_s, name.to_s]
221
+ end.sort
219
222
  }
220
223
  Digest::SHA256.hexdigest(JSON.generate(payload))
221
224
  rescue StandardError
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../run_scoped_slots"
3
+ require_relative "../typing_state"
4
+ require_relative "../memo_audit"
4
5
 
5
6
  module Rigor
6
7
  # {Reflection}'s constant-ancestor walk ([#354](https://github.com/rigortype/rigor/issues/354)), split out for
@@ -9,13 +10,23 @@ module Rigor
9
10
  # name's owner inherits constants from, and this file answers that once for both. Reopening the module
10
11
  # keeps the walk behind the facade its callers already read.
11
12
  module Reflection
12
- # #354 — thread-local slot for the per-run ancestor-scope memo. See {.ancestor_constant_scopes}.
13
- ANCESTOR_SCOPES_KEY = RunScopedSlots.register(:__rigor_ancestor_constant_scopes__)
14
- private_constant :ANCESTOR_SCOPES_KEY
15
-
16
- # The recording run's slot. See {.recorded_ancestor_constant_scopes}.
17
- RECORDED_ANCESTOR_SCOPES_KEY = RunScopedSlots.register(:__rigor_recorded_ancestor_constant_scopes__)
18
- private_constant :RECORDED_ANCESTOR_SCOPES_KEY
13
+ # #354 — the per-run ancestor-scope memo. See {.ancestor_constant_scopes}. ADR-129 WD1 — a run-scoped Memo.
14
+ ANCESTOR_SCOPES = TypingState.memo(
15
+ :ancestor_constant_scopes,
16
+ run_scoped: true, audit: "ancestor constant scopes",
17
+ reason: "one [discovery, scopes by class name] slot keyed on the discovery index's identity, as the " \
18
+ "resolution chain the scopes are read from is (#2848)"
19
+ )
20
+ private_constant :ANCESTOR_SCOPES
21
+
22
+ # The recording run's slot. See {.recorded_ancestor_constant_scopes}. ADR-129 WD1 — a run-scoped Memo.
23
+ RECORDED_ANCESTOR_SCOPES = TypingState.memo(
24
+ :recorded_ancestor_constant_scopes,
25
+ run_scoped: true, audit: "recorded ancestor constant scopes",
26
+ reason: "one [discovery, scopes by class name] slot keyed on the discovery index's identity; a hit replays " \
27
+ "the chain's edges, so the key is exact"
28
+ )
29
+ private_constant :RECORDED_ANCESTOR_SCOPES
19
30
 
20
31
  module_function
21
32
 
@@ -37,11 +48,13 @@ module Rigor
37
48
  # constant it owns resolves only if the bare name reaches it at step 3: widening to the superclass's
38
49
  # RBS graph is a separate question with its own FP surface. See {.external_constant_owner}.
39
50
  #
40
- # Memoised per run because step 2 runs on every constant reference whose lexical candidates all
41
- # miss — which is the common case for a core-class reference (`String` inside `class Foo`). The
42
- # bucket keys on the identity of the runner-seeded run-generation token (ADR-84 WD2), falling
43
- # back to the per-file discovery table for runner-less scopes, so a re-run in one process (LSP,
44
- # ADR-62 warm loop) cannot hit stale entries.
51
+ # Memoised because step 2 runs on every constant reference whose lexical candidates all miss — which
52
+ # is the common case for a core-class reference (`String` inside `class Foo`). The bucket keys on the
53
+ # identity of the discovery index, as the `Scope::ResolutionChain` memo it reads does: the chain's
54
+ # order depends on the analysing file's own view of the include order (a class declared in two files
55
+ # orders its `include`s from the reader's file first), so a run-generation key served one file's
56
+ # order to another (#2848). A re-run in one process (LSP, ADR-62 warm loop) builds a new index, so it
57
+ # cannot hit stale entries either.
45
58
  def ancestor_constant_scopes(class_name, scope)
46
59
  ancestor_constant_worlds(class_name, scope).first
47
60
  end
@@ -87,32 +100,76 @@ module Rigor
87
100
  # every call.
88
101
  return recorded_ancestor_constant_scopes(class_name, scope) if Analysis::DependencyRecorder.active?
89
102
 
90
- generation = scope.run_generation || scope.discovered_superclasses
91
- slot = Thread.current[ANCESTOR_SCOPES_KEY]
92
- unless slot && slot[0].equal?(generation)
93
- slot = [generation, {}]
94
- Thread.current[ANCESTOR_SCOPES_KEY] = slot
103
+ # ADR-129 G2 — an audit recomputation bypasses the memo (neither read nor written).
104
+ if Inference::MemoAudit.enabled? && Inference::MemoAudit.suspended?
105
+ return compute_ancestor_constant_scopes(class_name, scope)
106
+ end
107
+
108
+ discovery = scope.discovery
109
+ state = TypingState.current
110
+ slot = state[ANCESTOR_SCOPES]
111
+ unless slot && slot[0].equal?(discovery)
112
+ slot = [discovery, {}]
113
+ state[ANCESTOR_SCOPES] = slot
95
114
  end
96
115
  bucket = slot[1]
97
- bucket.fetch(class_name) { bucket[class_name] = compute_ancestor_constant_scopes(class_name, scope) }
116
+ worlds = bucket[class_name]
117
+ if worlds.nil?
118
+ return bucket[class_name] = Inference::MemoAudit.key_audit("ancestor constant scopes") do
119
+ compute_ancestor_constant_scopes(class_name, scope)
120
+ end
121
+ end
122
+ return worlds unless Inference::MemoAudit.enabled?
123
+
124
+ Inference::MemoAudit.check("ancestor constant scopes", audit_view(worlds), class_name) do
125
+ audit_view(compute_ancestor_constant_scopes(class_name, scope))
126
+ end
127
+ worlds
98
128
  end
99
129
  private_class_method :ancestor_constant_worlds
100
130
 
131
+ # `[scopes, chain]` compared by content: a recomputation builds a new chain object, equal to the stored one
132
+ # entry for entry when the key holds every input.
133
+ def audit_view(worlds) = [worlds[0], worlds[1].audit_signature]
134
+ private_class_method :audit_view
135
+
136
+ # A recomputation for the audit, its dependency reads withheld: the hit it checks files its own edges, so the
137
+ # recorded set stays what an unaudited run records.
138
+ def fresh_ancestor_constant_scopes(class_name, scope)
139
+ Analysis::DependencyRecorder.withhold { compute_ancestor_constant_scopes(class_name, scope) }.first
140
+ end
141
+ private_class_method :fresh_ancestor_constant_scopes
142
+
101
143
  # The recording run's memo. The answer is every project entry of the chain, so its edges are the
102
144
  # root's and every entry's — what `Scope::ResolutionChain#record` files, which a computation does and a
103
145
  # hit replays, in the same order. One slot keyed on the discovery index: the answer and the edges are
104
146
  # both read from it, so the key is exact, and the slot never pins more than the index it serves.
105
147
  def recorded_ancestor_constant_scopes(class_name, scope)
148
+ # ADR-129 G2 — an audit recomputation bypasses the memo (neither read nor written).
149
+ if Inference::MemoAudit.enabled? && Inference::MemoAudit.suspended?
150
+ return compute_ancestor_constant_scopes(class_name, scope)
151
+ end
152
+
106
153
  discovery = scope.discovery
107
- slot = Thread.current[RECORDED_ANCESTOR_SCOPES_KEY]
154
+ state = TypingState.current
155
+ slot = state[RECORDED_ANCESTOR_SCOPES]
108
156
  unless slot && slot[0].equal?(discovery)
109
157
  slot = [discovery, {}]
110
- Thread.current[RECORDED_ANCESTOR_SCOPES_KEY] = slot
158
+ state[RECORDED_ANCESTOR_SCOPES] = slot
111
159
  end
112
160
  bucket = slot[1]
113
161
  worlds = bucket[class_name]
114
- return bucket[class_name] = compute_ancestor_constant_scopes(class_name, scope) if worlds.nil?
162
+ if worlds.nil?
163
+ return bucket[class_name] = Inference::MemoAudit.key_audit("recorded ancestor constant scopes") do
164
+ compute_ancestor_constant_scopes(class_name, scope)
165
+ end
166
+ end
115
167
 
168
+ if Inference::MemoAudit.enabled?
169
+ Inference::MemoAudit.check("recorded ancestor constant scopes", audit_view(worlds), class_name) do
170
+ audit_view(fresh_ancestor_constant_scopes(class_name, scope))
171
+ end
172
+ end
116
173
  chain = Scope::ResolutionChain.for(scope, class_name.to_s, :instance, :constants)
117
174
  chain.record(scope)
118
175
  record_external_owner_edges(chain)