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,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module TypeOfMemo
5
+ # The counters and mismatch samples of this process, behind a mutex: process counters like {MemoAudit}'s, never
6
+ # read back into a type (ADR-129 WD5).
7
+ class Stats
8
+ def initialize
9
+ @mutex = Mutex.new
10
+ @counts = Hash.new { |hash, phase| hash[phase] = Hash.new(0) }
11
+ @stale = Hash.new { |hash, phase| hash[phase] = Hash.new(0) }
12
+ @samples = []
13
+ end
14
+
15
+ def count(phase, counter) = @mutex.synchronize { @counts[phase][counter] += 1 }
16
+
17
+ def stale_miss(phase, names) = @mutex.synchronize { @stale[phase][names] += 1 }
18
+
19
+ def sample(limit)
20
+ @mutex.synchronize { @samples << yield if @samples.size < limit }
21
+ end
22
+
23
+ def counts = @mutex.synchronize { copy(@counts) }
24
+
25
+ def stale = @mutex.synchronize { copy(@stale) }
26
+
27
+ def samples = @mutex.synchronize { @samples.dup }
28
+
29
+ def reset
30
+ @mutex.synchronize do
31
+ @counts.clear
32
+ @stale.clear
33
+ @samples.clear
34
+ end
35
+ nil
36
+ end
37
+
38
+ private
39
+
40
+ def copy(by_phase) = by_phase.to_h { |phase, counters| [phase, counters.dup] }
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,337 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "prism"
5
+ require_relative "typing_state"
6
+ require_relative "memo_audit"
7
+ require_relative "type_of_memo/stats"
8
+ require_relative "type_of_memo/serving"
9
+
10
+ module Rigor
11
+ # ADR-129 stage 3 — the `ExpressionTyper#type_of` result memo in **shadow** form (WD5's G1, stage-3 form). It keys a
12
+ # computation on `(scope identity, node identity, state id)` (WD2), opens a journal capture around it, and stores
13
+ # what an unrefused computation read and wrote (WD3). It never serves anything: on a would-be hit, an entry whose read
14
+ # slice still holds, production computes as usual inside a priors window, and G1 checks afterwards that
15
+ # - the type equals the entry's;
16
+ # - the live tables equal their pre-computation state with the entry's write slice replayed;
17
+ # - the live computation touched no Resource.
18
+ # A read slice that no longer holds is a miss, not a mismatch, counted by the slots whose reads went stale, which
19
+ # measures what the journal costs in hits (#2853 for the set-only Tables). Output is production's by construction.
20
+ #
21
+ # Like {MemoAudit}, a process switch (`RIGOR_TYPE_OF_SHADOW=1`, read once at load, or {enable!}), a single boolean
22
+ # check when off. With `RIGOR_TYPE_OF_SHADOW_REPORT=path`, each process appends one JSON line of its counters and
23
+ # mismatch samples to `path` when it exits. The memo is shut while a tracer is attached or a typer carries
24
+ # instance state (`@tracer`, `@operand_types`: WD1's bypass), and leaf nodes are never stored (WD3).
25
+ module TypeOfMemo
26
+ # ADR-129 WD4 — the per-file table, a Memo by kind: entries keyed on the state id, which carries every Frame
27
+ # (WD2), and checked by G1 rather than by `MemoAudit.check` (its hits recompute in production already).
28
+ TABLE = TypingState.memo(
29
+ :type_of_memo,
30
+ run_scoped: true,
31
+ reason: "the type_of memo's per-file entries, keyed on scope, node and state id; G1 checks every would-be hit"
32
+ )
33
+ private_constant :TABLE
34
+
35
+ # WD3 — never stored: their typing is a read the memo could only repeat.
36
+ LEAVES = %w[
37
+ LocalVariableReadNode InstanceVariableReadNode ClassVariableReadNode GlobalVariableReadNode SelfNode
38
+ IntegerNode FloatNode RationalNode ImaginaryNode StringNode SymbolNode NilNode TrueNode FalseNode
39
+ RegularExpressionNode SourceFileNode SourceLineNode SourceEncodingNode ConstantReadNode ConstantPathNode
40
+ NumberedReferenceReadNode BackReferenceReadNode
41
+ ].to_h { |name| [Prism.const_get(name), true] }.freeze
42
+ private_constant :LEAVES
43
+
44
+ # The Tables whose reads only record a filing a later reader never consumes (#2853).
45
+ SET_ONLY = %i[dependency_reads self_call_misses].freeze
46
+
47
+ SAMPLE_LIMIT = 50
48
+ private_constant :SAMPLE_LIMIT
49
+
50
+ # One file's entries, `scope => node => state id => entry`, and the phase its analysis is in.
51
+ class Table
52
+ attr_accessor :phase
53
+ attr_reader :entries
54
+
55
+ # Whether the table keys on the ELIDE id ({TypingState::ElideId}) rather than the state id.
56
+ attr_reader :elide
57
+
58
+ # Recomputations of G1's stage-4 form open on this table ({TypeOfMemo.enable!} with `:serve`), during which the
59
+ # memo is suspended.
60
+ attr_accessor :suspended
61
+
62
+ def initialize(elide: false, phase: :other)
63
+ @entries = {}.compare_by_identity
64
+ @phase = phase
65
+ @elide = elide
66
+ @suspended = 0
67
+ end
68
+ end
69
+
70
+ @enabled = %w[1 true].include?(ENV["RIGOR_TYPE_OF_SHADOW"].to_s.downcase)
71
+ @stats = Stats.new
72
+
73
+ class << self
74
+ include Serving
75
+
76
+ def enabled? = @enabled
77
+
78
+ # Turns the shadow on. `serve: true` is G1's stage-4 form (ADR-129 WD5), for specs and audit tools only: a
79
+ # would-be hit is recomputed with the memo suspended, the recomputation's Table writes are undone, and the entry
80
+ # is served (validated, its reads merged upward, its writes replayed), so output depends on the memo.
81
+ def enable!(serve: false) = (@enabled = serve ? :serve : true)
82
+
83
+ def disable! = (@enabled = false)
84
+
85
+ # `{phase => {counter => n}}`, the counters since the last {reset}.
86
+ def counts = @stats.counts
87
+
88
+ # `{phase => {slot names => n}}`: the would-be hits whose read slice did not hold, by the slots that went stale.
89
+ def stale = @stats.stale
90
+
91
+ # The first mismatches seen since the last {reset}, each a Hash.
92
+ def samples = @stats.samples
93
+
94
+ # The number of mismatches since the last {reset}, all phases.
95
+ def mismatches = counts.sum { |_, counters| counters[:mismatch] }
96
+
97
+ def reset = @stats.reset
98
+
99
+ # A file's analysis starts: a fresh table, in the main pass. A no-op when off. With
100
+ # `RIGOR_TYPE_OF_SHADOW_ELIDE=1` (ADR-129 WD2's ELIDE, measured in stage 3), the table keys on the thread's ELIDE
101
+ # id, restarted here. The state is extended at the first file's start: every frame open then closes after that
102
+ # file ends (the per-file analysis is nested in it), so none exits through the plain `State#with`, which would
103
+ # not restore the ELIDE id.
104
+ def begin_file
105
+ return unless @enabled && Ractor.main?
106
+
107
+ elide = %w[1 true].include?(ENV["RIGOR_TYPE_OF_SHADOW_ELIDE"].to_s.downcase)
108
+ table = Table.new(elide: elide, phase: :main)
109
+ state = TypingState.current
110
+ if elide
111
+ state.extend(TypingState::ElideId) unless state.is_a?(TypingState::ElideId)
112
+ state.restart_elide_id
113
+ end
114
+ state[TABLE] = table
115
+ nil
116
+ end
117
+
118
+ # The file's analysis moved on to `phase` (`:rule` once the main pass has indexed it).
119
+ def phase!(phase)
120
+ return unless @enabled
121
+
122
+ table = TypingState.current[TABLE]
123
+ table.phase = phase if table
124
+ nil
125
+ end
126
+
127
+ # A file's analysis ended: its entries go (WD4: the table is per file).
128
+ def end_file
129
+ return unless @enabled
130
+
131
+ state = TypingState.current
132
+ state[TABLE] = nil
133
+ nil
134
+ end
135
+
136
+ # The block's value, the type `node` has in `scope`, computed by production under G1's shadow. Typing outside a
137
+ # file's analysis ({begin_file} .. {end_file}: a pre-pass, a language-server query, a non-main Ractor) has no
138
+ # table and is not shadowed.
139
+ # `callee` is whether the typing runs in a callee body (the ADR-84 guard stack is not empty).
140
+ def shadow(state, scope, node, callee: false, &)
141
+ return yield if LEAVES[node.class]
142
+
143
+ table = state[TABLE]
144
+ return yield if table.nil? || table.suspended.positive?
145
+
146
+ # G1′: the memo must not run while a `memo_shut_while_installed` Frame is installed.
147
+ MemoAudit.note_memo_lookup(state) if MemoAudit.enabled?
148
+
149
+ phase = callee ? :callee : table.phase
150
+ by_id = (table.entries[scope] ||= {}.compare_by_identity)[node] ||= {}
151
+ id = table.elide ? state.elide_id : state.id
152
+ entry = by_id[id]
153
+ count(phase, :lookups)
154
+ if entry && stale_names(state, phase, entry).nil?
155
+ return serve_hit(state, table, phase, scope, node, entry, &) if @enabled == :serve
156
+
157
+ return check_hit(state, phase, scope, node, entry, &)
158
+ end
159
+
160
+ compute_and_store(state, phase, by_id, id, &)
161
+ end
162
+
163
+ private
164
+
165
+ # A miss: compute under a capture and store an unrefused, unraised result (WD3).
166
+ def compute_and_store(state, phase, by_id, id)
167
+ state.open_capture
168
+ returned = false
169
+ begin
170
+ type = yield
171
+ returned = true
172
+ ensure
173
+ refused, records = state.close_capture
174
+ count(phase, :raised) unless returned
175
+ end
176
+ if refused
177
+ count(phase, :refused)
178
+ else
179
+ reads, writes = state.slices(records)
180
+ by_id[id] = reads.empty? && writes.empty? ? type : [type, reads, writes].freeze
181
+ count(phase, :stored)
182
+ end
183
+ type
184
+ end
185
+
186
+ # nil when the entry's read slice holds; else counts the stale slots and answers their names.
187
+ def stale_names(state, phase, entry)
188
+ return nil unless entry.is_a?(Array)
189
+
190
+ reads = entry[1]
191
+ return nil if state.stale_read(reads).nil?
192
+
193
+ names = state.stale_slots(reads).map { |slot| TypingState.slots[slot].name }.sort.freeze
194
+ count(phase, :stale)
195
+ count(phase, :stale_set_only) if (names - SET_ONLY).empty?
196
+ @stats.stale_miss(phase, names.join(","))
197
+ names
198
+ end
199
+
200
+ # A would-be hit: production computes, and G1 compares it with what serving the entry would have done.
201
+ def check_hit(state, phase, scope, node, entry)
202
+ stored, writes = entry.is_a?(Array) ? [entry[0], entry[2]] : [entry, EMPTY]
203
+ count(phase, :checked)
204
+ state.open_capture
205
+ state.open_priors
206
+ begin
207
+ fresh = yield
208
+ ensure
209
+ priors = state.close_priors
210
+ refused, = state.close_capture
211
+ end
212
+ judge(state, phase, scope, node, stored, fresh, writes, priors, refused)
213
+ fresh
214
+ end
215
+
216
+ EMPTY = [].freeze
217
+ private_constant :EMPTY
218
+
219
+ # G1's verdict on one would-be hit. An error in the comparison itself is counted as a mismatch rather than raised:
220
+ # the typing path's rescues would turn a raise into a different answer.
221
+ def judge(state, phase, scope, node, stored, fresh, writes, priors, refused) # rubocop:disable Metrics/ParameterLists
222
+ compare(state, phase, scope, node, stored, fresh, writes, priors, refused)
223
+ rescue StandardError => e
224
+ count(phase, :mismatch)
225
+ count(phase, :mismatch_judge_error)
226
+ sample(phase, scope, node, [:judge_error, "#{e.class}: #{e.message[0, 120]}"], stored, fresh, [])
227
+ end
228
+
229
+ def compare(state, phase, scope, node, stored, fresh, writes, priors, refused) # rubocop:disable Metrics/ParameterLists
230
+ kinds = []
231
+ kinds << :type unless same?(fresh, stored)
232
+ kinds << :resource if refused
233
+ diffs = table_diffs(state, writes, priors)
234
+ kinds << :table unless diffs.empty?
235
+ return if kinds.empty?
236
+
237
+ report(phase, scope, node, kinds, stored, fresh, diffs)
238
+ end
239
+
240
+ def report(phase, scope, node, kinds, stored, fresh, diffs)
241
+ count(phase, :mismatch)
242
+ kinds.each { |kind| count(phase, :"mismatch_#{kind}") if kind.is_a?(Symbol) }
243
+ sample(phase, scope, node, kinds, stored, fresh, diffs)
244
+ nil
245
+ end
246
+
247
+ # The entries where the live tables differ from their pre-computation state with `writes` replayed: each key
248
+ # the entry wrote must hold its last written value, and each key only the live computation wrote must hold
249
+ # the value it replaced.
250
+ def table_diffs(state, writes, priors)
251
+ expected = TypingState::KeyIndex.new
252
+ diffs = []
253
+ # The last write of a key is what a replay leaves there: walk backwards and take each key once.
254
+ writes.each_slice(3).reverse_each do |slot, key, value|
255
+ next if expected.key?(slot, key)
256
+
257
+ expected.store(slot, key, value)
258
+ now = state.entry(slot, key)
259
+ diffs << [slot, key, value, now] unless same?(value, now)
260
+ end
261
+ priors.each_slice(3) do |slot, key, prior|
262
+ next if expected.key?(slot, key)
263
+
264
+ now = state.entry(slot, key)
265
+ diffs << [slot, key, prior, now] unless same?(prior, now)
266
+ end
267
+ diffs.uniq { |slot, key, _, _| [slot, key.is_a?(Array) ? key.map(&:__id__) : key.__id__] }
268
+ end
269
+
270
+ # Entry values compare by identity, or by `==` element-wise, a scope (which the one-slot caches record) by
271
+ # identity alone: `Scope#==` ignores fields typing reads.
272
+ #
273
+ # `==` ignores a Hash's order, which output shows: a type also compares its rendering
274
+ # (`HashShape#==` compares its pairs as a Hash), and a Hash its entries in order.
275
+ def same?(left, right)
276
+ return true if left.equal?(right)
277
+ return false if left.is_a?(Scope) || right.is_a?(Scope)
278
+ return ordered_same?(left.to_a, right.to_a) if left.is_a?(Hash) && right.is_a?(Hash)
279
+ return ordered_same?(left, right) if left.is_a?(Array) && right.is_a?(Array)
280
+ return false unless left == right
281
+
282
+ !left.respond_to?(:describe) || !right.respond_to?(:describe) || left.describe(:short) == right.describe(:short)
283
+ end
284
+
285
+ def ordered_same?(left, right)
286
+ left.size == right.size && left.each_index.all? { |index| same?(left[index], right[index]) }
287
+ end
288
+
289
+ def count(phase, counter) = @stats.count(phase, counter)
290
+
291
+ def sample(phase, scope, node, kinds, stored, fresh, diffs)
292
+ @stats.sample(SAMPLE_LIMIT) do
293
+ {
294
+ phase: phase, kinds: kinds, path: scope.source_path.to_s, line: node.location.start_line,
295
+ node: node.class.name.delete_prefix("Prism::"),
296
+ stored: describe(stored), fresh: describe(fresh),
297
+ diffs: diffs.first(4).map do |slot, key, want, now|
298
+ [TypingState.slots[slot].name, describe(key), describe(want), describe(now)]
299
+ end
300
+ }
301
+ end
302
+ end
303
+
304
+ def describe(value)
305
+ return "ABSENT" if TypingState::ABSENT.equal?(value)
306
+ return value.describe(:short).to_s[0, 160] if value.respond_to?(:describe)
307
+
308
+ value.inspect[0, 160]
309
+ rescue StandardError => e
310
+ "<describe failed: #{e.class}>"
311
+ end
312
+
313
+ public
314
+
315
+ # A forked child starts: the counters it inherited are the parent's, which the parent reports.
316
+ def forked
317
+ reset if @enabled
318
+ nil
319
+ end
320
+
321
+ # Appends this process's report to `RIGOR_TYPE_OF_SHADOW_REPORT`, when set (one JSON line). Runs at exit, and a
322
+ # forked worker calls it before its `exit!`, which skips `at_exit`.
323
+ def write_report
324
+ path = ENV.fetch("RIGOR_TYPE_OF_SHADOW_REPORT", nil)
325
+ return unless @enabled && path
326
+
327
+ line = JSON.generate(pid: Process.pid, counts: counts, stale: stale, samples: samples)
328
+ File.open(path, "a") { |file| file.puts(line) }
329
+ rescue StandardError => e
330
+ # A report that cannot be written must not keep a forked worker from its `exit!`, nor change the exit status.
331
+ warn("rigor: RIGOR_TYPE_OF_SHADOW_REPORT not written: #{e.class}: #{e.message}")
332
+ end
333
+ end
334
+
335
+ at_exit { send(:write_report) }
336
+ end
337
+ end