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
@@ -3,6 +3,8 @@
3
3
  require "rbs"
4
4
 
5
5
  require_relative "../type"
6
+ require_relative "../typing_state"
7
+ require_relative "../memo_audit"
6
8
 
7
9
  module Rigor
8
10
  module Inference
@@ -36,8 +38,9 @@ module Rigor
36
38
  # `untyped`. `Type::Intersection` stays out of this path: it is the same-base refinement composite
37
39
  # carrier, and the dispatch tiers do not accept it as a receiver.
38
40
  #
39
- # Interface types still degrade to `Dynamic[Top]`; they are bound to acceptance and dispatch rules
40
- # not yet implemented.
41
+ # Interface types degrade to `Dynamic[Top]`, except an interface in the `Rigor::Plugin` namespace, which
42
+ # translates to `Dynamic[Type::Interface]` (ADR-130 WD1/WD4): `untyped` to every rule, and resolvable only by a
43
+ # call of a member it declares (`RbsDispatch`).
41
44
  #
42
45
  # The optional `self_type:` and `instance_type:` arguments are the Rigor counterparts of RBS's
43
46
  # `self` and `instance` tokens:
@@ -78,7 +81,7 @@ module Rigor
78
81
  RBS::Types::Alias => :translate_alias,
79
82
  RBS::Types::Intersection => :translate_intersection,
80
83
  RBS::Types::Variable => :translate_variable,
81
- RBS::Types::Interface => :translate_untyped
84
+ RBS::Types::Interface => :translate_interface
82
85
  }.freeze
83
86
  private_constant :TRANSLATORS
84
87
 
@@ -106,14 +109,12 @@ module Rigor
106
109
  # repeat with one frozen String.
107
110
  @relative_names = {}
108
111
 
109
- # #775 — the translated form of a CLOSED alias expansion, keyed by the expansion. Closed means no
110
- # `self`, `instance` or type variable anywhere inside, so the caller's context cannot change the
111
- # answer; at expansion depth 0 the budget cannot either. rbs compares types by value, so the memo
112
- # is per distinct expansion, not per loader (a re-parsed identical alias hits), and the weak keys
113
- # let an entry go with the loader whose memo held the expansion. `Rigor::Type::t` on the self-check
114
- # is a 21-member union that was re-translated ~13k times, each time re-normalising the union from
115
- # scratch; every repeat now shares one Union.
116
- @closed_alias_translations = ObjectSpace::WeakKeyMap.new
112
+ # ADR-130 WD2 — the erasure baseline's flag, installed by {.with_interface_erasure}.
113
+ INTERFACE_ERASURE = TypingState.frame(
114
+ :interface_erasure, reason: "a flag installed around an erasure-baseline run (the parity harness)",
115
+ witness: "spec/fixtures/typing_state_witnesses/interface_erasure.rb"
116
+ )
117
+ private_constant :INTERFACE_ERASURE
117
118
 
118
119
  class << self
119
120
  # @param self_type — substitute for `Bases::Self`.
@@ -130,6 +131,20 @@ module Rigor
130
131
  translate_in(rbs_type, Context.new(self_type, instance_type, type_vars, alias_expander, 0))
131
132
  end
132
133
 
134
+ # ADR-130 WD2 — the erasure baseline. Inside the block every RBS interface translates to `untyped`, as it did
135
+ # before ADR-130, whatever carrier the default engine has; the behavioural parity gate
136
+ # (`spec/integration/interface_parity_spec.rb`) runs its baseline here, so the baseline never comes from the
137
+ # default engine. {translate_interface} reads {interface_erasure?}, and {translate_alias} bypasses the loader's
138
+ # closed-alias memo under it, so a translation made in one mode is never served in the other (ADR-130 WD4). The
139
+ # switch lives in the calling thread's (and fiber's) `TypingState`, so a run under it is sequential: a pooled
140
+ # run raises (`PoolCoordinator`).
141
+ def with_interface_erasure(&)
142
+ MemoAudit.check_run_level_install(:interface_erasure)
143
+ TypingState.current.with(INTERFACE_ERASURE, true, &)
144
+ end
145
+
146
+ def interface_erasure? = TypingState.current[INTERFACE_ERASURE] == true
147
+
133
148
  private
134
149
 
135
150
  def translate_in(rbs_type, context)
@@ -151,6 +166,28 @@ module Rigor
151
166
  Type::Combinator.untyped
152
167
  end
153
168
 
169
+ # ADR-130 WD4 — the namespace whose interfaces carry the facet: `::Rigor::Plugin::_Scope` and its siblings.
170
+ PLUGIN_INTERFACE_PATH = %i[Rigor Plugin].freeze
171
+ private_constant :PLUGIN_INTERFACE_PATH
172
+
173
+ # ADR-130 WD1/WD4 — a `Rigor::Plugin` interface is `Dynamic[Interface]`; every other interface, rbs core's
174
+ # conversion aliases (`int`, `string`, ...) included, is `untyped` as before. Which one is a pure function of
175
+ # the `TypeName` (its namespace path, read without allocating), so a closed alias translates alike whichever
176
+ # file or worker reaches it first. Under {with_interface_erasure} every interface is `untyped`.
177
+ def translate_interface(rbs_type, context)
178
+ name = rbs_type.name
179
+ return Type::Combinator.untyped unless name.namespace.path == PLUGIN_INTERFACE_PATH
180
+ return Type::Combinator.untyped if interface_erasure?
181
+
182
+ carrier =
183
+ if rbs_type.args.empty?
184
+ Type::Interface.new(relative_name(name))
185
+ else
186
+ Type::Interface.new(relative_name(name), rbs_type.args.map { |arg| translate_in(arg, context) })
187
+ end
188
+ Type::Combinator.dynamic(carrier)
189
+ end
190
+
154
191
  def translate_nil(_rbs_type, _context)
155
192
  Type::Combinator.constant_of(nil)
156
193
  end
@@ -251,18 +288,34 @@ module Rigor
251
288
 
252
289
  expanded = expander.expand_type_alias(rbs_type)
253
290
  return Type::Combinator.untyped if expanded.nil?
291
+
254
292
  # A nested expansion is translated under its parent's budget, and `rigor trace` records every
255
- # union merge as it happens, so neither consults the memo.
256
- return translate_in(expanded, context.deeper) unless context.alias_depth.zero? && !FlowTracer.active?
293
+ # union merge as it happens, so neither consults the memo. Nor does the erasure baseline (ADR-130 WD4): the
294
+ # memo holds the default engine's translations, where a `Rigor::Plugin` interface keeps its facet.
295
+ memo = closed_translations_of(expander)
296
+ unless memo && context.alias_depth.zero? && !FlowTracer.active? && !interface_erasure?
297
+ return translate_in(expanded, context.deeper)
298
+ end
257
299
 
258
- cached = @closed_alias_translations[expanded]
300
+ cached = memo[expanded]
259
301
  return cached if cached
260
302
 
261
303
  translated = translate_in(expanded, context.deeper)
262
- @closed_alias_translations[expanded] = translated if closed_type?(expanded)
304
+ memo[expanded] = translated if closed_type?(expanded)
263
305
  translated
264
306
  end
265
307
 
308
+ # #775 — the translated form of a CLOSED alias expansion, keyed by the expansion, lives with the expander.
309
+ # Closed means no `self`, `instance` or type variable anywhere inside, so the caller's context cannot change
310
+ # the answer; at expansion depth 0 the budget cannot either. But a nested alias inside the expansion is
311
+ # expanded through the expander (`type a = b | Integer` holds `b`), so the translation reads the expander's
312
+ # environment: a memo shared by every expander answered a second loader, whose `b` differs, with the first
313
+ # loader's translation (equal expansions are equal keys). The expander owns its memo, so it dies with it. An
314
+ # expander that offers none (anything answering only `expand_type_alias`) is translated afresh each time.
315
+ def closed_translations_of(expander)
316
+ expander.closed_alias_translations if expander.respond_to?(:closed_alias_translations)
317
+ end
318
+
266
319
  # True when nothing in `rbs_type`'s tree reads the translation context: no `self`, no `instance`,
267
320
  # no type variable. Every rbs type answers `each_type` (the leaves through `EmptyEachType`).
268
321
  def closed_type?(rbs_type)
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ module Rigor
6
+ module Inference
7
+ # Issue #2488 — a writer a class gains from a call on an explicit receiver outside its body: `E.attr_writer :w`,
8
+ # `E.send(:attr_accessor, :y)`, `E.define_method(:set) { @v = … }` (and `define_method` through `send`). Ruby made
9
+ # `attr_*` and `define_method` public in 2.5, so each is the receiver class's own declaration, exactly as the same
10
+ # call in a `E.class_eval { … }` block is. Both ivar censuses read the calls through here: the per-file class-ivar
11
+ # walk ({ScopeIndexer}) and the cross-file {IvarWriterCensus}. The receiver's name is the caller's to resolve, the
12
+ # same way each resolves a `class_eval` receiver.
13
+ module ReceiverWriterCall
14
+ SEND_CALLS = %i[send __send__ public_send].to_set.freeze
15
+ WRITER_MACROS = %i[attr_writer attr_accessor].to_set.freeze
16
+ # The names a direct call spells; a `send` reaches any of them through its literal first argument.
17
+ CALLS = (WRITER_MACROS | SEND_CALLS | %i[define_method]).freeze
18
+ NO_ARGUMENTS = [].freeze
19
+
20
+ module_function
21
+
22
+ # The receiver of `node` when it is one of these calls on a receiver other than `self`, else nil.
23
+ def receiver(node)
24
+ return nil unless node.is_a?(Prism::CallNode) && CALLS.include?(node.name)
25
+
26
+ recv = node.receiver
27
+ recv unless recv.nil? || recv.is_a?(Prism::SelfNode)
28
+ end
29
+
30
+ # The ivar names an explicit-receiver `attr_writer` / `attr_accessor` declares (`[:@w]`), or an empty Array.
31
+ def writer_ivars(node)
32
+ name, arguments = called(node)
33
+ return NO_ARGUMENTS unless WRITER_MACROS.include?(name)
34
+
35
+ arguments.filter_map do |argument|
36
+ text = literal_name(argument)
37
+ :"@#{text}" if text
38
+ end
39
+ end
40
+
41
+ # The literal block an explicit-receiver `define_method` installs as an instance method, or nil.
42
+ def define_method_block(node)
43
+ name, = called(node)
44
+ block = node.block
45
+ block if name == :define_method && block.is_a?(Prism::BlockNode)
46
+ end
47
+
48
+ # The method `node` calls and its arguments, seen through a `send` whose first argument spells the name.
49
+ def called(node)
50
+ return [nil, NO_ARGUMENTS] if receiver(node).nil?
51
+
52
+ arguments = node.arguments&.arguments || NO_ARGUMENTS
53
+ return [node.name, arguments] unless SEND_CALLS.include?(node.name)
54
+
55
+ sent = literal_name(arguments.first)
56
+ sent ? [sent, arguments.drop(1)] : [nil, NO_ARGUMENTS]
57
+ end
58
+
59
+ def literal_name(argument)
60
+ return unless argument.is_a?(Prism::SymbolNode) || argument.is_a?(Prism::StringNode)
61
+
62
+ text = argument.unescaped
63
+ text.to_sym if text.valid_encoding?
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+ require_relative "../source/node_walker"
5
+
6
+ module Rigor
7
+ module Inference
8
+ # Which of a def's parameters its own recursion rebinds (issue #3115). The bot-collapse retry of
9
+ # `ExpressionTyper#fixpoint_user_method_return` types the body once for every call the recursion makes, and a
10
+ # recursive call binds a parameter to whatever it passes: `tog(n: n - 1, f: !f)` gives `f` a value the
11
+ # outer call never held. A parameter is KEPT only when every call to the def's own name inside the body
12
+ # passes it the same parameter read unchanged (`cfg(n - 1, mode)` keeps `mode`), and the body never
13
+ # assigns it; every other parameter is rebound, and the retry binds it `Dynamic[top]`.
14
+ #
15
+ # The test is syntactic and reads only the def. A call counts by its method name alone, whatever its
16
+ # receiver (`child.depth(n + 1)` re-enters the same recursion guard), so a same-named method of another
17
+ # class is read as a recursive call too, which only rebinds more. The caller supplies the def's other
18
+ # names (`alias`), since the def does not carry them. A call to ANOTHER method that recurses back (mutual
19
+ # recursion) is not followed: its parameters are judged by the def's own calls alone.
20
+ #
21
+ # Two shapes the test cannot read rebind more instead. A body that calls a method by a computed name
22
+ # ({REFLECTIVE_CALLS}) may recurse with any arguments, so every parameter is rebound ({ALL}). A name a
23
+ # block or lambda in the body declares (a parameter, a block-local, `it`) is rebound, because a read of
24
+ # it inside that block is the block's variable, not the parameter.
25
+ module RecursionRebinding
26
+ module_function
27
+
28
+ LOCAL_WRITE_NODES = Set[
29
+ Prism::LocalVariableWriteNode, Prism::LocalVariableOperatorWriteNode, Prism::LocalVariableOrWriteNode,
30
+ Prism::LocalVariableAndWriteNode, Prism::LocalVariableTargetNode
31
+ ].freeze
32
+ private_constant :LOCAL_WRITE_NODES
33
+
34
+ # The calls that reach a method by a name the scan cannot read.
35
+ REFLECTIVE_CALLS = Set[:send, :__send__, :public_send, :method, :instance_method, :define_method].freeze
36
+ private_constant :REFLECTIVE_CALLS
37
+
38
+ # {.rebound_parameters}'s answer when every parameter is rebound.
39
+ ALL = :all
40
+
41
+ # The names the body's recursive calls, its blocks or its own assignments rebind, as a Set, or {ALL}. The
42
+ # Set may hold a name that is no parameter (any assigned local); the caller looks up parameters only.
43
+ # `names` is every name the def answers to: its own and its aliases.
44
+ def rebound_parameters(def_node, names = Set[def_node.name])
45
+ params = def_node.parameters
46
+ return Set.new unless params.is_a?(Prism::ParametersNode)
47
+
48
+ rebound = Set.new
49
+ catch(ALL) { collect(def_node.body, names, params, rebound) } == ALL ? ALL : rebound
50
+ end
51
+
52
+ # Every method name the body calls, for the caller to ask which of them are this def's aliases.
53
+ def called_names(node, names = Set.new)
54
+ return names if node.nil?
55
+
56
+ names << node.name if node.is_a?(Prism::CallNode)
57
+ node.rigor_each_child { |child| called_names(child, names) }
58
+ names
59
+ end
60
+
61
+ def collect(node, names, params, rebound)
62
+ return if node.nil?
63
+
64
+ case node
65
+ when *LOCAL_WRITE_NODES then rebound << node.name
66
+ when Prism::BlockNode, Prism::LambdaNode then note_block(node, rebound)
67
+ when Prism::CallNode
68
+ throw ALL, ALL if REFLECTIVE_CALLS.include?(node.name)
69
+
70
+ note_call(node, params, rebound) if names.include?(node.name)
71
+ end
72
+ node.rigor_each_child { |child| collect(child, names, params, rebound) }
73
+ end
74
+
75
+ # A block's `locals` are its parameters, its block-locals and the locals it first assigns; `it` is not
76
+ # among them.
77
+ def note_block(block, rebound)
78
+ rebound.merge(block.locals)
79
+ rebound << :it if block.parameters.is_a?(Prism::ItParametersNode)
80
+ end
81
+
82
+ def note_call(call, params, rebound)
83
+ args = call.arguments&.arguments || []
84
+ keywords = args.last if args.last.is_a?(Prism::KeywordHashNode)
85
+ positional = keywords ? args[0...-1] : args
86
+ forwarding = args.any?(Prism::ForwardingArgumentsNode)
87
+ note_positionals(params, positional, rebound)
88
+ note_keywords(params, keywords, forwarding, rebound)
89
+ end
90
+
91
+ # Positionals correspond by index up to the first `*splat` (or `...`), which may reach every slot from
92
+ # its own on. The rest parameter is kept only by `*rest` in its own place as the last positional.
93
+ def note_positionals(params, positional, rebound)
94
+ slots = params.requireds + params.optionals
95
+ cut = positional.index { |arg| arg.is_a?(Prism::SplatNode) || arg.is_a?(Prism::ForwardingArgumentsNode) }
96
+ cut ||= positional.size
97
+ slots.each_with_index do |param, index|
98
+ next if param.is_a?(Prism::MultiTargetNode)
99
+
100
+ rebound << param.name unless index < cut && reads?(positional[index], param.name)
101
+ end
102
+ note_rest(params.rest, positional[slots.size..] || [], cut >= slots.size, rebound)
103
+ end
104
+
105
+ def note_rest(rest, tail, aligned, rebound)
106
+ return unless rest.is_a?(Prism::RestParameterNode) && rest.name
107
+
108
+ splat = tail.first
109
+ kept = aligned && tail.size == 1 && splat.is_a?(Prism::SplatNode) && reads?(splat.expression, rest.name)
110
+ rebound << rest.name unless kept
111
+ end
112
+
113
+ # Keywords correspond by name. A keyword the call omits falls back to its default, so it is rebound. A
114
+ # `**splat` other than the def's own `**rest`, a non-Symbol key or `...` may carry any keyword, so each of
115
+ # them rebinds every keyword. `**rest` is kept by `**rest` alone, with no key beside it that it would collect.
116
+ def note_keywords(params, keywords, forwarding, rebound)
117
+ names = params.keywords.map { |param| param.name.to_s.delete_suffix(":").to_sym }
118
+ rest_name = keyword_rest_name(params)
119
+ passed, splats, opaque = keyword_entries(keywords)
120
+ opaque ||= forwarding || splats.any? { |value| !reads?(value, rest_name) }
121
+ return rebound.merge(names, [rest_name].compact) if opaque
122
+
123
+ names.each { |name| rebound << name unless reads?(passed[name], name) }
124
+ rebound << rest_name if rest_name && !(splats.size == 1 && (passed.keys - names).empty?)
125
+ end
126
+
127
+ def keyword_rest_name(params)
128
+ rest = params.keyword_rest
129
+ rest.name if rest.is_a?(Prism::KeywordRestParameterNode)
130
+ end
131
+
132
+ # The call's keyword hash as `[{ name => value node }, [**splat value nodes], opaque]`, `opaque` when
133
+ # a key is not a plain Symbol.
134
+ def keyword_entries(keywords)
135
+ passed = {}
136
+ splats = []
137
+ opaque = false
138
+ (keywords&.elements || []).each do |element|
139
+ if element.is_a?(Prism::AssocSplatNode)
140
+ splats << element.value
141
+ elsif element.key.is_a?(Prism::SymbolNode) && element.key.unescaped
142
+ passed[element.key.unescaped.to_sym] = element.value
143
+ else
144
+ opaque = true
145
+ end
146
+ end
147
+ [passed, splats, opaque]
148
+ end
149
+
150
+ # Whether `node` is a bare read of the local `name` (`n`, or the `n:` shorthand's implicit read).
151
+ def reads?(node, name)
152
+ node = node.value if node.is_a?(Prism::ImplicitNode)
153
+ !name.nil? && node.is_a?(Prism::LocalVariableReadNode) && node.name == name
154
+ end
155
+ end
156
+ end
157
+ end
@@ -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
  module Inference
@@ -18,9 +19,16 @@ module Rigor
18
19
  # the name itself, and no constant write of it. Any other shape answers false, as does a class a
19
20
  # `K = Class.new(...)` write names.
20
21
  module ConstructedClass
21
- MEMO_KEY = RunScopedSlots.register(:__rigor_constructed_class__)
22
+ # ADR-129 WD1 — a run-scoped Memo keyed on the run-generation token and the environment, whose tables every
23
+ # answer reads and which are fixed for a run.
24
+ MEMO = TypingState.memo(
25
+ :constructed_class,
26
+ run_scoped: true,
27
+ audit: ["constructed class verdict", "constructed class segments", "constructed class names"],
28
+ reason: "verdicts keyed on the run-generation token and environment identities"
29
+ )
22
30
  EMPTY = [].freeze
23
- private_constant :MEMO_KEY, :EMPTY
31
+ private_constant :MEMO, :EMPTY
24
32
 
25
33
  module_function
26
34
 
@@ -36,9 +44,19 @@ module Rigor
36
44
  return false if named.empty?
37
45
 
38
46
  by_class = (table[:verdicts][class_name] ||= {})
39
- return by_class[named] if by_class.key?(named)
47
+ if by_class.key?(named)
48
+ return MemoAudit.check("constructed class verdict", by_class[named], class_name, named) do
49
+ apart_verdict(scope, named, class_name, memo(scope))
50
+ end
51
+ end
52
+
53
+ by_class[named] = MemoAudit.key_audit("constructed class verdict") do
54
+ apart_verdict(scope, named, class_name, table)
55
+ end
56
+ end
40
57
 
41
- by_class[named] = named.all? { |constant| core_class_apart?(scope, constant, class_name.to_s, table) }
58
+ def apart_verdict(scope, named, class_name, table)
59
+ named.all? { |constant| core_class_apart?(scope, constant, class_name.to_s, table) }
42
60
  end
43
61
 
44
62
  def core_class_apart?(scope, constant, class_name, table)
@@ -68,7 +86,18 @@ module Rigor
68
86
 
69
87
  # The last segment of every class or module the project declares, reopenings included.
70
88
  def declared_segments(scope, table)
71
- table[:segments] ||= declared_names(scope).each_key.to_set { |key| key.to_s.split("::").last }.freeze
89
+ segments = table[:segments]
90
+ if segments.nil?
91
+ return table[:segments] = MemoAudit.key_audit("constructed class segments") do
92
+ compute_declared_segments(scope)
93
+ end
94
+ end
95
+
96
+ MemoAudit.check("constructed class segments", segments, :segments) { compute_declared_segments(scope) }
97
+ end
98
+
99
+ def compute_declared_segments(scope)
100
+ declared_names(scope).each_key.to_set { |key| key.to_s.split("::").last }.freeze
72
101
  end
73
102
 
74
103
  # The constants `name`'s superclass expression names, or an empty Array; memoised per envelope bucket,
@@ -78,15 +107,18 @@ module Rigor
78
107
  return EMPTY if bucket.nil?
79
108
 
80
109
  names = table[:names]
81
- return names[bucket] if names.key?(bucket)
82
-
83
- names[bucket] =
84
- if bucket.key?(Scope::DiscoveryIndex::ENVELOPE_META_SUPERCLASS_MARK)
85
- EMPTY
86
- else
87
- kind = Scope::DiscoveryIndex::ENVELOPE_CONSTRUCTED_KIND
88
- bucket.each_key.filter_map { |key| key[1] if key.is_a?(Array) && key[0] == kind }.freeze
89
- end
110
+ if names.key?(bucket)
111
+ return MemoAudit.check("constructed class names", names[bucket], name) { bucket_constructed_names(bucket) }
112
+ end
113
+
114
+ names[bucket] = MemoAudit.key_audit("constructed class names") { bucket_constructed_names(bucket) }
115
+ end
116
+
117
+ def bucket_constructed_names(bucket)
118
+ return EMPTY if bucket.key?(Scope::DiscoveryIndex::ENVELOPE_META_SUPERCLASS_MARK)
119
+
120
+ kind = Scope::DiscoveryIndex::ENVELOPE_CONSTRUCTED_KIND
121
+ bucket.each_key.filter_map { |key| key[1] if key.is_a?(Array) && key[0] == kind }.freeze
90
122
  end
91
123
 
92
124
  # The run-wide table of declared classes: the seeded `discovered_class_sources`, or a single-file run's
@@ -100,15 +132,21 @@ module Rigor
100
132
  # `discovered_def_nodes`, as `ExpressionTyper` falls back) and the environment: every table read here is
101
133
  # fixed for a run, so each answer is computed once a run, and a new run replaces the slot.
102
134
  def memo(scope)
103
- slot = Thread.current[MEMO_KEY]
135
+ return new_table if MemoAudit.enabled? && MemoAudit.suspended?
136
+
137
+ state = TypingState.current
138
+ slot = state[MEMO]
104
139
  generation = scope.run_generation || scope.discovered_def_nodes
105
140
  environment = scope.environment
106
141
  unless slot && slot[0].equal?(generation) && slot[1].equal?(environment)
107
- slot = [generation, environment, { names: {}.compare_by_identity, verdicts: {} }]
108
- Thread.current[MEMO_KEY] = slot
142
+ slot = [generation, environment, new_table]
143
+ state[MEMO] = slot
109
144
  end
110
145
  slot[2]
111
146
  end
147
+
148
+ # Empty tables: a new slot's, and a throwaway set while an audit recomputation bypasses the memo.
149
+ def new_table = { names: {}.compare_by_identity, verdicts: {} }
112
150
  end
113
151
  end
114
152
  end
@@ -2,7 +2,8 @@
2
2
 
3
3
  require_relative "../builtins/cruby_definers"
4
4
  require_relative "constructed_class"
5
- require_relative "../../run_scoped_slots"
5
+ require_relative "../../typing_state"
6
+ require_relative "../../memo_audit"
6
7
 
7
8
  module Rigor
8
9
  module Inference
@@ -307,30 +308,54 @@ module Rigor
307
308
 
308
309
  def unknown_superclass_classes(scope, class_name)
309
310
  Analysis::DependencyRecorder.read_name(:subclass_of, "*") if Analysis::DependencyRecorder.active?
310
- table = subclass_memo(scope)
311
- all = table[:unknown] ||= begin
312
- mark = Scope::DiscoveryIndex::ENVELOPE_META_SUPERCLASS_MARK
313
- computed = scope.discovered_superclasses.filter_map { |child, parent| child if parent.nil? }
314
- meta = scope.discovered_parameter_envelopes.filter_map { |name, bucket| name if bucket.key?(mark) }
315
- (computed + meta).uniq.freeze
316
- end
311
+ all = all_unknown_superclass_classes(scope)
317
312
  name = class_name.to_s
318
313
  all.include?(name) ? all - [name] : all
319
314
  end
320
315
 
321
- SUBCLASS_MEMO_KEY = RunScopedSlots.register(:__rigor_own_method_subclasses__)
322
- private_constant :SUBCLASS_MEMO_KEY
316
+ def all_unknown_superclass_classes(scope)
317
+ table = subclass_memo(scope)
318
+ stored = table[:unknown]
319
+ if stored.nil?
320
+ return table[:unknown] = MemoAudit.key_audit("own method unknown superclasses") do
321
+ compute_unknown_superclass_classes(scope)
322
+ end
323
+ end
324
+
325
+ MemoAudit.check("own method unknown superclasses", stored, :unknown) do
326
+ compute_unknown_superclass_classes(scope)
327
+ end
328
+ end
329
+
330
+ def compute_unknown_superclass_classes(scope)
331
+ mark = Scope::DiscoveryIndex::ENVELOPE_META_SUPERCLASS_MARK
332
+ computed = scope.discovered_superclasses.filter_map { |child, parent| child if parent.nil? }
333
+ meta = scope.discovered_parameter_envelopes.filter_map { |name, bucket| name if bucket.key?(mark) }
334
+ (computed + meta).uniq.freeze
335
+ end
336
+
337
+ # ADR-129 WD1 — a run-scoped Memo keyed on the identities of the discovery index and environment, the only
338
+ # inputs of the buckets it holds.
339
+ SUBCLASS_MEMO = TypingState.memo(
340
+ :own_method_subclasses,
341
+ run_scoped: true, audit: ["own method unknown superclasses", "own method children"],
342
+ reason: "subclass buckets keyed on the discovery index and environment identities"
343
+ )
344
+ private_constant :SUBCLASS_MEMO
323
345
 
324
346
  # One thread-local slot keyed on the scope's discovery index (a frozen `Data`) and environment, the only
325
347
  # inputs `children_by_parent` and the unknown-superclass list read; neither files a dependency edge
326
348
  # (`each_subclass` files its own `subclass_of:` edges on every call), so a hit drops none.
327
349
  def subclass_memo(scope)
328
- slot = Thread.current[SUBCLASS_MEMO_KEY]
350
+ return {} if MemoAudit.enabled? && MemoAudit.suspended?
351
+
352
+ state = TypingState.current
353
+ slot = state[SUBCLASS_MEMO]
329
354
  discovery = scope.discovery
330
355
  environment = scope.environment
331
356
  unless slot && slot[0].equal?(discovery) && slot[1].equal?(environment)
332
357
  slot = [discovery, environment, {}]
333
- Thread.current[SUBCLASS_MEMO_KEY] = slot
358
+ state[SUBCLASS_MEMO] = slot
334
359
  end
335
360
  slot[2]
336
361
  end
@@ -342,11 +367,20 @@ module Rigor
342
367
  end
343
368
 
344
369
  def children_by_parent(scope)
345
- subclass_memo(scope)[:children] ||=
346
- scope.discovered_superclasses.each_with_object({}) do |(child, raw_parent), children|
347
- parent = scope.ancestor_name_candidates(child, raw_parent).find { |name| known_class?(scope, name) }
348
- (children[parent] ||= []) << child if parent
349
- end.each_value(&:freeze).freeze
370
+ table = subclass_memo(scope)
371
+ stored = table[:children]
372
+ if stored.nil?
373
+ return table[:children] = MemoAudit.key_audit("own method children") { compute_children_by_parent(scope) }
374
+ end
375
+
376
+ MemoAudit.check("own method children", stored, :children) { compute_children_by_parent(scope) }
377
+ end
378
+
379
+ def compute_children_by_parent(scope)
380
+ scope.discovered_superclasses.each_with_object({}) do |(child, raw_parent), children|
381
+ parent = scope.ancestor_name_candidates(child, raw_parent).find { |name| known_class?(scope, name) }
382
+ (children[parent] ||= []) << child if parent
383
+ end.each_value(&:freeze).freeze
350
384
  end
351
385
 
352
386
  def known_class?(scope, name)
@@ -126,6 +126,19 @@ module Rigor
126
126
  out
127
127
  end
128
128
 
129
+ # ADR-121 WD7 — a names-wildcard row counts as a refinement of every name. True when a row `list` puts in
130
+ # effect names `method_name` (or the wildcard) for ANY refined class — which class the row refines is not
131
+ # decided here — or `list` carries {InEffectRefinements::UNKNOWN} and any row names it. {ExpressionTyper}
132
+ # asks it for a `&:name` block, whose receiver is not known; {SelfPreservingBlock} for the yielding method
133
+ # (issue #2909).
134
+ def refined_anywhere?(scope, method_name, list)
135
+ refinements = scope.discovered_refinements
136
+ return true if list.include?(InEffectRefinements::UNKNOWN) &&
137
+ refinements.any? { |_refined, methods| methods.key?(method_name) || methods.key?(wildcard) }
138
+
139
+ !targets(refinements, method_name, list).nil? || !targets(refinements, wildcard, list).nil?
140
+ end
141
+
129
142
  # The refinement table records every name a `refine` argument's spelling can denote (`refine String` inside
130
143
  # `module M` records `M::String` and `String`), which only withholds a check. To type, a key must be the class
131
144
  # Ruby's lexical lookup resolves: one the project or RBS knows, with no longer spelling of the same last segment
@@ -3,6 +3,7 @@
3
3
  require "prism"
4
4
 
5
5
  require_relative "../type"
6
+ require_relative "../typing_state"
6
7
 
7
8
  module Rigor
8
9
  module Inference
@@ -26,8 +27,13 @@ module Rigor
26
27
  IVAR_NAME = /\A@[A-Za-z_]\w*\z/
27
28
  private_constant :IVAR_NAME
28
29
 
29
- REPEATING_KEY = :__rigor_reflective_ivar_write_repeating__
30
- private_constant :REPEATING_KEY
30
+ # ADR-129 WD1 — a Frame: the depth is installed for a region and restored on the way out.
31
+ REPEATING = TypingState.frame(
32
+ :reflective_ivar_write_repeating,
33
+ reason: "the repeat depth of `for` bodies and loop predicates, installed per region",
34
+ witness: "spec/fixtures/typing_state_witnesses/reflective_ivar_write_repeating.rb"
35
+ )
36
+ private_constant :REPEATING
31
37
 
32
38
  module_function
33
39
 
@@ -69,18 +75,20 @@ module Rigor
69
75
  # rebind fixpoint, and a predicate runs before every iteration but is walked once, so a literal-name write there
70
76
  # must not replace the binding with one pass's value ({.apply}). A `while` / `until` body does not enter it: its
71
77
  # fixpoint widens the ivar (issue #1930).
72
- def in_repeating_body
73
- depth = Thread.current[REPEATING_KEY] || 0
74
- Thread.current[REPEATING_KEY] = depth + 1
75
- begin
76
- yield
77
- ensure
78
- Thread.current[REPEATING_KEY] = depth
79
- end
78
+ def in_repeating_body(&)
79
+ state = TypingState.current
80
+ state.with(REPEATING, (state[REPEATING] || 0) + 1, &)
81
+ end
82
+
83
+ # Runs the block at repeat depth zero, restoring the depth on exit. A user-method body re-typed for its return
84
+ # starts from its own scope and reads its own write, so a caller's `for` body does not repeat it
85
+ # ({ExpressionTyper.with_call_site_frame}).
86
+ def outside_repeating_body(&)
87
+ TypingState.current.with(REPEATING, 0, &)
80
88
  end
81
89
 
82
90
  def repeating_body?
83
- (Thread.current[REPEATING_KEY] || 0).positive?
91
+ (TypingState.current[REPEATING] || 0).positive?
84
92
  end
85
93
 
86
94
  # `scope` after `node` runs. A literal name binds that ivar to the value's type (yielded), as `@n = v` does.