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
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../typing_state"
4
+
3
5
  module Rigor
4
6
  module Inference
5
7
  # Thread-local event recorder behind `rigor trace`: while a block runs under {record}, the inference engine
@@ -13,8 +15,14 @@ module Rigor
13
15
  # hot paths (`ExpressionTyper#type_of`, `Scope#with_local`, `Type::Combinator.union`, `MethodDispatcher.dispatch`)
14
16
  # each guard their emit behind {active?}, so a normal (non-tracing) run pays one integer comparison.
15
17
  module FlowTracer
16
- KEY = :__rigor_flow_tracer__
17
- private_constant :KEY
18
+ # ADR-129 WD1 — the installed recorder is a Frame whose contents are a Resource: each emit into it touches. It is
19
+ # an output the engine never reads back, and the activation count below is a process switch, outside the typing
20
+ # state (WD5). G1′: a tracer switch, under which the `type_of` memo stays shut (WD4).
21
+ RECORDER = TypingState.frame(
22
+ :flow_tracer, contents: :resource, reason: "the recorder a trace installs; each emit records into it",
23
+ memo_shut_while_installed: true
24
+ )
25
+ private_constant :RECORDER
18
26
 
19
27
  # One animation-relevant moment.
20
28
  #
@@ -95,15 +103,16 @@ module Rigor
95
103
  # Activates recording on the current thread for the duration of the block and returns the frozen event list.
96
104
  # Nests safely; restores the previous recorder on exit.
97
105
  def record
98
- previous = Thread.current[KEY]
99
106
  recorder = Recorder.new
100
- Thread.current[KEY] = recorder
101
- @mutex.synchronize { @active_count += 1 }
102
- yield
103
- recorder.events.freeze
104
- ensure
105
- Thread.current[KEY] = previous
106
- @mutex.synchronize { @active_count -= 1 }
107
+ TypingState.current.with(RECORDER, recorder) do
108
+ @mutex.synchronize { @active_count += 1 }
109
+ begin
110
+ yield
111
+ recorder.events.freeze
112
+ ensure
113
+ @mutex.synchronize { @active_count -= 1 }
114
+ end
115
+ end
107
116
  end
108
117
 
109
118
  # Plain integer read (GVL-atomic) — the disabled fast path.
@@ -114,20 +123,28 @@ module Rigor
114
123
  # Brackets one expression-typing recursion. Falls through to the bare block when the current thread is not
115
124
  # recording (another thread may have flipped {active?}).
116
125
  def trace_node(node, &)
117
- recorder = Thread.current[KEY]
126
+ recorder = recording
118
127
  return yield unless recorder
119
128
 
120
129
  recorder.node(node, &)
121
130
  end
122
131
 
132
+ # The recorder installed on this thread, touched because the caller is about to record into it, or nil.
133
+ def recording
134
+ state = TypingState.current
135
+ recorder = state[RECORDER]
136
+ state.touch(RECORDER) if recorder
137
+ recorder
138
+ end
139
+
123
140
  # `Scope#with_local` — the moment a local enters the scope.
124
141
  def bind(name, type)
125
- Thread.current[KEY]&.emit(:bind, data: { name: name.to_s, type: describe(type) })
142
+ recording&.emit(:bind, data: { name: name.to_s, type: describe(type) })
126
143
  end
127
144
 
128
145
  # `Type::Combinator.union` — the moment branch types merge (including degenerate collapses like `1 | 1 → 1`).
129
146
  def union(members, result)
130
- Thread.current[KEY]&.emit(
147
+ recording&.emit(
131
148
  :union,
132
149
  data: { members: members.map { |m| describe(m) }.freeze, type: describe(result) }
133
150
  )
@@ -136,7 +153,7 @@ module Rigor
136
153
  # `MethodDispatcher.dispatch` — resolution or the fail-soft `nil` ("no rule matched"; the caller will widen
137
154
  # to `Dynamic[Top]`).
138
155
  def dispatch(receiver:, method_name:, args:, result:, location: nil)
139
- recorder = Thread.current[KEY]
156
+ recorder = recording
140
157
  return unless recorder
141
158
 
142
159
  recorder.emit(
@@ -3,6 +3,7 @@
3
3
  require "tmpdir"
4
4
 
5
5
  require_relative "../runtime/jit"
6
+ require_relative "../type_of_memo"
6
7
 
7
8
  module Rigor
8
9
  module Inference
@@ -95,6 +96,7 @@ module Rigor
95
96
  # what is left of the parent's window rather than a fresh one; {Runtime::Jit.rearm_after_fork}
96
97
  # carries the mechanism.
97
98
  Runtime::Jit.rearm_after_fork
99
+ TypeOfMemo.forked
98
100
  # Published before the block runs, so anything it lazily creates for this process alone lands in the
99
101
  # parent's tmpdir and dies with it. See {.child_scratch_dir}.
100
102
  @child_scratch_dir = File.dirname(out_path)
@@ -105,6 +107,7 @@ module Rigor
105
107
  ensure
106
108
  @child_scratch_dir = nil
107
109
  end
110
+ TypeOfMemo.write_report
108
111
  exit!(status)
109
112
  end
110
113
 
@@ -23,7 +23,8 @@ module Rigor
23
23
  # what it reads. Unlike a root block it shares that slot, so a match in it still rebinds the definer's `$~`.
24
24
  #
25
25
  # `Enumerator.new` is neither: its block runs on an internal fiber whose root is not the block, so it reads and
26
- # rebinds the creator's slot like any other block.
26
+ # rebinds the creator's slot. It reads that slot when the enumerator is read, though, not where the block is
27
+ # written ({.deferred_body?}, issue #3195).
27
28
  #
28
29
  # The receiver of a root call must be the literal constant, and must resolve to the core class where a scope is
29
30
  # at hand, so a project's own `Thread` (`module Pool; class Thread; …`) keeps the ordinary block rules.
@@ -115,6 +116,56 @@ module Rigor
115
116
  scope.forget_error_info.forget_last_status
116
117
  end
117
118
 
119
+ # The core constructors that keep their block and run it on a schedule of their own: an enumerator's generator
120
+ # runs when the enumerator is first read, and a hash's default proc on a missing key.
121
+ DEFERRED_CONSTRUCTORS = Set[:Enumerator, :Hash].freeze
122
+ private_constant :DEFERRED_CONSTRUCTORS
123
+
124
+ # Issue #1371, #3195 — true when `block_node` is a body that runs later than where it is written, on the slot of
125
+ # the frame that wrote it: a proc's ({.proc_body?}), or the block of `Enumerator.new` or `Hash.new` on the core
126
+ # class ({.deferred_constructor?}). Such a body runs whenever the proc is called, the enumerator is read or a
127
+ # key is missing, so the frame-local specials narrowed where it is written prove nothing about the slot it
128
+ # reads then. It enters with `$_` unbound ({LastLine.block_entry}), and with the match globals as
129
+ # {MatchRebinding.block_entry} gives each kind. It still shares the slot, so a match or a reader it runs
130
+ # rebinds the frame's as any block's does. The other calls that keep their block ({StoredBlockCall}) are a
131
+ # definer's or a root's ({.entry}).
132
+ def deferred_body?(block_node, call_node, scope)
133
+ proc_body?(block_node, call_node, scope) || deferred_constructor?(block_node, call_node, scope)
134
+ end
135
+
136
+ # Issue #1371 — true when `block_node` is the body of a proc made where it is written: a lambda literal, or the
137
+ # block of Kernel's `lambda` / `proc` or of `Proc.new` on the core class. It enters with the match globals
138
+ # unbound; a project's own `Proc` keeps the ordinary entry.
139
+ # Only a lambda literal the statement evaluator enters gets this entry: one written as an operand is typed as a
140
+ # value, and the rule walker reads its body in the enclosing scope.
141
+ def proc_body?(block_node, call_node, scope)
142
+ return true if block_node.is_a?(Prism::LambdaNode)
143
+ return false unless call_node.is_a?(Prism::CallNode) && call_node.block.equal?(block_node)
144
+
145
+ receiver = call_node.receiver
146
+ case call_node.name
147
+ when :lambda, :proc then StoredBlockCall.kernel_spelled?(receiver)
148
+ when :new then StoredBlockCall.root_constant_name(receiver) == :Proc && core_class?(receiver, :Proc, scope)
149
+ else false
150
+ end
151
+ end
152
+
153
+ # Issue #3195 — true when `block_node` is the block of `Enumerator.new` or `Hash.new` on the literal constant
154
+ # naming the core class. The block often runs right away, in the statement that makes it
155
+ # (`Enumerator.new { … }.to_a`, `Hash.new { … }[key]`), and reads the narrowing then; it may as well run after a
156
+ # later match. The analyzer does not follow which, so the body enters with a narrowed match global untyped, as
157
+ # a definer body does ({.entry}): neither narrowed nor flagged. A project's own `Enumerator` or `Hash` keeps
158
+ # the ordinary entry, and so does every other spelling of the core class: a subclass (`Class.new(Hash)`), an
159
+ # alias (`E = Enumerator`), `Object::Enumerator`, or a receiver that is not a constant (`klass.new { … }`).
160
+ def deferred_constructor?(block_node, call_node, scope)
161
+ return false unless call_node.is_a?(Prism::CallNode) && call_node.name == :new &&
162
+ call_node.block.equal?(block_node)
163
+
164
+ receiver = call_node.receiver
165
+ constant = StoredBlockCall.root_constant_name(receiver)
166
+ DEFERRED_CONSTRUCTORS.include?(constant) && core_class?(receiver, constant, scope)
167
+ end
168
+
118
169
  def core_class?(receiver, constant, scope)
119
170
  type = scope.type_of(receiver)
120
171
  type.is_a?(Type::Singleton) && type.class_name == constant.name
@@ -29,8 +29,8 @@ module Rigor
29
29
  # - `[:main_mixin, name]` — a module a top-level `include`, `prepend` or `extend` mixes into `Object` (or
30
30
  # `main`), as written. A module RBS does not know may carry any method, so it reaches every class.
31
31
  # - the instance-variable writer entries of {IvarWriterCensus} (issue #1154): `[:ivar_set, name]`,
32
- # `[:ivar_set_class, class_name]`, `[:own_write, class_name, name, path]` and, issue #2079,
33
- # `[:own_write_type, class_name, name, value, path]`.
32
+ # `[:ivar_set_class, class_name]`, `[:own_write, class_name, name, path]`, issue #2079's
33
+ # `[:own_write_type, class_name, name, value, path]` and issue #2983's `[:binds_class, class_key, constant]`.
34
34
  # - `[:global_writer, name, method]` — issue #2314: a write of the global `name` inside a method body, keyed by
35
35
  # the `def`'s name (or the literal name a `define_method` block defines), or by nil inside a lambda or a block
36
36
  # its call keeps to run later ({StoredBlockCall}). A write in a block its call runs at once belongs to the
@@ -87,7 +87,7 @@ module Rigor
87
87
  end
88
88
 
89
89
  # #1154 — the {IvarWriterCensus} entries of `census`, for the incremental diff and its readers.
90
- IVAR_KINDS = %i[ivar_set ivar_set_class own_write own_write_type].to_set.freeze
90
+ IVAR_KINDS = %i[ivar_set ivar_set_class own_write own_write_type binds_class].to_set.freeze
91
91
 
92
92
  def ivar_set_entries(census) = census.select { |entry| IVAR_KINDS.include?(entry.first) }.to_set
93
93
 
@@ -97,6 +97,7 @@ module Rigor
97
97
  when :ivar_set then "ivar_set:#{entry[1]}"
98
98
  when :ivar_set_class then "ivar_set_class:#{entry[1]}"
99
99
  when :own_write_type then "own_write_type:#{entry[1]}##{entry[2]}"
100
+ when :binds_class then "binds_class:#{entry[1].split(CensusForeignBlock::CANDIDATE_SEPARATOR).last.split('::').last}"
100
101
  else "own_write:#{entry[1]}##{entry[2]}"
101
102
  end
102
103
  end
@@ -156,9 +157,10 @@ module Rigor
156
157
  # A node the collector cannot read must not fail the analysis of its file, nor of every file the pre-pass
157
158
  # feeds: the census then counts it as a definition of every name, which only declines a report.
158
159
  # `owner` is the qualified name of the class whose body the node sits in, for {IvarWriterCensus} (nil where
159
- # the host walk does not track it, or no class owns an instance there).
160
- def visit(node, top_level:, owner: nil)
161
- @ivar_writers.visit(node, owner)
160
+ # the host walk does not track it, or no class owns an instance there); `receiver_owner` the class an
161
+ # explicit-receiver writer call names (issue #2488).
162
+ def visit(node, top_level:, owner: nil, receiver_owner: nil)
163
+ @ivar_writers.visit(node, owner, receiver_owner)
162
164
  visit_global_writer(node)
163
165
  case node
164
166
  when Prism::AliasGlobalVariableNode then visit_global_alias(node)
@@ -6,6 +6,7 @@ require_relative "../source/constant_path"
6
6
  require_relative "../source/node_children"
7
7
  require_relative "refine_census"
8
8
  require_relative "refine_self"
9
+ require_relative "../typing_state"
9
10
  require_relative "in_effect_refinements/proc_literals"
10
11
  require_relative "in_effect_refinements/scope_reads"
11
12
  require_relative "in_effect_refinements/refine_defs"
@@ -53,6 +54,12 @@ module Rigor
53
54
  #
54
55
  # Built lazily: the walk runs the first time a consumer asks, so a file nobody asks about pays one small object.
55
56
  class InEffectRefinements
57
+ # ADR-129 G4 — {#build} fills every one of these once, from the file's root node alone.
58
+ TypingState.memo_field(self, :@built, :@activations, :@refinement_defs, :@refine_defs, :@chained_refined_calls,
59
+ :@class_body_refines, :@refine_contexts, :@literal_bindings, :@refine_offsets,
60
+ :@unresolved_using, :@nesting,
61
+ reason: "built once by #build from the file's root node alone")
62
+
56
63
  # The marker an activation contributes when it may put any refinement in effect: a non-constant `using`, a
57
64
  # `using` whose include expansion is unknown, or a `refine` block whose module this walk cannot name. A list
58
65
  # that carries it answers "any refinement may be in effect"; its position carries no meaning.
@@ -33,8 +33,10 @@ module Rigor
33
33
  #
34
34
  # **Invalidation.** Three conditions drop a recorded narrowing:
35
35
  # - The receiver variable is rebound (handled inside `Scope#with_local` / `Scope#with_ivar`).
36
- # - An intervening `receiver[key] = value` writes the same slot — `:[]=` could rebind the
37
- # slot to nil; conservative drop.
36
+ # - An intervening store writes the same slot — `receiver[key] = value`, an index target, or a
37
+ # compound `receiver[key] op= value` / `&&=` (issue #1218) — since it could rebind the slot to
38
+ # nil; conservative drop. A store whose index is not one literal key drops every narrowing
39
+ # on the receiver.
38
40
  # - An intervening mutator from {MutationWidening::SHAPE_MUTATORS} runs against the receiver
39
41
  # (e.g. `params.delete(:f)`, `params.clear`, `params.default = 0`, `buf.delete_prefix!("x")`).
40
42
  #
@@ -138,14 +140,61 @@ module Rigor
138
140
  MutationWidening::SHAPE_MUTATORS.include?(method_name)
139
141
  end
140
142
 
141
- def invalidate_indexed_write(call_node, current_scope)
142
- args = call_node.arguments&.arguments
143
- return current_scope if args.nil? || args.empty?
143
+ # Drops the narrowing on the slot a store through `[]=` overwrote. `node` is the `[]=` call, an index target
144
+ # (a multi-assign slot, a `for` index, a rescue reference) or a compound index write (`h[k] += v`,
145
+ # `h[k] &&= v`, `h[k] ||= v`); every form reads only `receiver` and `arguments`, and only a `[]=` call's last
146
+ # argument is the stored value rather than an index.
147
+ #
148
+ # Issue #1218 — a literal single index drops that one slot and leaves the others. Any other index list on a
149
+ # stable receiver — a key that is not a literal (`h[k] = 1` may be `h[:e] = 1`), a multi-index splice
150
+ # (`s[0, 1] = [1, 2]` shifts every later slot), a splat — may write any slot, so it drops every narrowing on
151
+ # the receiver, as {.invalidate_mutator} does. So does a negative Integer index on a receiver not shown to be a
152
+ # `Hash`: on an `Array` `a[-1]` is the slot some non-negative index also names. An unstable receiver records
153
+ # no narrowing to drop.
154
+ def invalidate_indexed_write(node, current_scope)
155
+ receiver = stable_receiver(node.receiver)
156
+ return current_scope if receiver.nil?
144
157
 
145
- address = stable_address(call_node.receiver, args.first)
146
- return current_scope if address.nil?
158
+ indexes = index_arguments(node)
159
+ address = indexes.size == 1 ? stable_address(node.receiver, indexes.first) : nil
160
+ if address && !(negative_index?(address.last) && !hash_receiver?(current_scope.type_of(node.receiver)))
161
+ return current_scope.without_indexed_narrowing(*address)
162
+ end
163
+
164
+ current_scope.without_indexed_narrowings_for(*receiver)
165
+ end
166
+
167
+ # Whether the literal `key` may name the same slot as another literal key: a negative Integer, unless the
168
+ # receiver is a `Hash`, which tells `-1` from every other key.
169
+ def aliasing_index?(key, receiver_type)
170
+ negative_index?(key) && !hash_receiver?(receiver_type)
171
+ end
172
+
173
+ def negative_index?(key) = key.is_a?(Integer) && key.negative?
174
+
175
+ def hash_receiver?(receiver_type) = carrier_receiver_type?(receiver_type, HASH_CARRIER)
176
+
177
+ VERBATIM_STORE_CARRIERS = %w[Hash Array].freeze
178
+ HASH_CARRIER = %w[Hash].freeze
179
+ private_constant :VERBATIM_STORE_CARRIERS, :HASH_CARRIER
180
+
181
+ # Whether a store through `[]=` on a receiver of `receiver_type` at the literal `key` leaves exactly the stored
182
+ # value at that slot, so a recorded narrowing may claim it. Only a `Hash` or `Array` carrier (a shape, a tuple,
183
+ # a `Hash` / `Array` nominal, refined or not) does, for every member of a union. A `String`'s `[]=` splices
184
+ # (`s[0] = "xy"` leaves `s[0] == "x"`), and a project class's `[]=` may store anything. A negative Integer key
185
+ # aliases a non-negative one on an `Array` ({.aliasing_index?}), so it records only on a `Hash`.
186
+ def verbatim_slot?(receiver_type, key)
187
+ carrier_receiver_type?(receiver_type, VERBATIM_STORE_CARRIERS) && !aliasing_index?(key, receiver_type)
188
+ end
189
+
190
+ def carrier_receiver_type?(type, classes)
191
+ members = type.is_a?(Type::Union) ? type.members : [type]
192
+ members.all? { |member| classes.include?(UnknownStoreWidening.carrier_class(member)) }
193
+ end
147
194
 
148
- current_scope.without_indexed_narrowing(*address)
195
+ def index_arguments(node)
196
+ args = node.arguments&.arguments || []
197
+ node.is_a?(Prism::CallNode) ? args[0...-1] : args
149
198
  end
150
199
 
151
200
  def invalidate_mutator(call_node, current_scope)
@@ -2,6 +2,9 @@
2
2
 
3
3
  require "prism"
4
4
 
5
+ require_relative "census_foreign_block"
6
+ require_relative "receiver_writer_call"
7
+
5
8
  module Rigor
6
9
  module Inference
7
10
  # Issue #1154 — the instance-variable writers {GlobalWriteCensus} records for the declared-parameter credit
@@ -24,9 +27,15 @@ module Rigor
24
27
  # file writes the same ivar of the same class.
25
28
  # - `[:own_write_type, class_name, name, value]` — issues #2079 / #2170: the same write with its coarse value, a
26
29
  # literal's as `[:lit, value]` and anything else as {DYNAMIC}, and a class-body `attr_writer` /
27
- # `attr_accessor` of the name as {DYNAMIC}. The path is appended as for `:own_write`. The class-ivar census
28
- # of a file joins another file's entries, and those of the modules and superclasses on the class's chain,
29
- # into the ivars it already holds ({ScopeIndexer::IvarContributions}).
30
+ # `attr_accessor` of the name as {DYNAMIC}. The path is appended as for `:own_write`. Issue #2488: an
31
+ # explicit-receiver `E.attr_writer` / `E.attr_accessor` (or one through `send`) records the same {DYNAMIC}
32
+ # entry under `E`, and an `E.define_method` block is an instance body of `E` ({ReceiverWriterCall}). The
33
+ # class-ivar census of a file joins another file's entries, and those of the modules and superclasses on the
34
+ # class's chain, into the ivars it already holds ({ScopeIndexer::IvarContributions}).
35
+ # Issue #2983: a barred `class_eval`-family block on a receiver rooted at another constant files the code in it
36
+ # that runs on an instance under that constant's candidate key ({#remember_root_constant}), a String
37
+ # `class_eval` records a {DYNAMIC} write of {ANY_IVAR} ({#record_string_eval}), and a constant write `C = K` /
38
+ # `C = K.new` records `[:binds_class, key, "C"]` ({#record_binding}), so the join can take `C`'s entries for `K`.
30
39
  class IvarWriterCensus
31
40
  SEND_CALLS = %i[send __send__ public_send].to_set.freeze
32
41
  # The methods a `send` reaching which is an open-world writer.
@@ -41,6 +50,10 @@ module Rigor
41
50
  NO_ARGUMENTS = [].freeze
42
51
  # Issue #2079 — the coarse value of a write that is not a literal.
43
52
  DYNAMIC = :dynamic
53
+ # Issue #2983 — the ivar name of a write whose ivar no literal spells: a string `class_eval`'s code may write any.
54
+ ANY_IVAR = :*
55
+ # The `class_eval`-family calls that take code in a String.
56
+ STRING_EVALS = %i[class_eval module_eval].to_set.freeze
44
57
  # The writer declarations a class body records as a {DYNAMIC} write of each name it spells.
45
58
  WRITER_DECLARATIONS = %i[attr_writer attr_accessor].to_set.freeze
46
59
 
@@ -54,16 +67,22 @@ module Rigor
54
67
  @owner_names = {}.compare_by_identity
55
68
  @own_writes = {}
56
69
  @typed_writes = Set.new
70
+ @rooted = Set.new.compare_by_identity
57
71
  end
58
72
 
59
73
  # Fed each node of a pre-order descent. `owner` is the class whose body the node sits in, as its qualified name
60
74
  # or as the host walk's prefix Array (joined here only when an entry needs it), or nil where no class owns an
61
- # instance (`class << self`, the top level, an opaque block).
62
- def visit(node, owner)
75
+ # instance (`class << self`, the top level, an opaque block). `receiver_owner` is the class an explicit-receiver
76
+ # writer call ({ReceiverWriterCall}) names, resolved by the host walk as it resolves a `class_eval` receiver.
77
+ def visit(node, owner, receiver_owner = nil)
63
78
  case node
64
- when Prism::DefNode then remember(node.location, owner) if owner && node.receiver.nil?
79
+ when Prism::DefNode then remember_host(node, owner) if node.receiver.nil?
80
+ when Prism::ConstantWriteNode, Prism::ConstantPathWriteNode then record_binding(node, owner)
65
81
  when Prism::SingletonClassNode then visit_singleton_class(node)
66
- when Prism::CallNode then visit_call(node, owner) if WATCHED_CALLS.include?(node.name)
82
+ when Prism::CallNode
83
+ bar_foreign_block(node)
84
+ visit_call(node, owner) if WATCHED_CALLS.include?(node.name)
85
+ record_receiver_writers(node, receiver_owner) if receiver_owner
67
86
  when Prism::InstanceVariableWriteNode, Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode,
68
87
  Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableTargetNode
69
88
  record_own_write(node) unless @instance_ranges.empty?
@@ -111,6 +130,74 @@ module Rigor
111
130
  [:lit, node.is_a?(Prism::SymbolNode) ? text.to_sym : text.dup.freeze].freeze
112
131
  end
113
132
 
133
+ # Issue #2490 — a block inside an instance body whose `self` is another object ({CensusForeignBlock}) is
134
+ # remembered with no owner, so its writes credit nothing. Pre-order, so an instance body nested in it (an
135
+ # `E.define_method` block, remembered after) still answers for its own class.
136
+ def bar_foreign_block(node)
137
+ return if @instance_ranges.empty? || !CensusForeignBlock::CALLS.include?(node.name)
138
+
139
+ instance = instance_owner(node)
140
+ return unless instance && CensusForeignBlock.foreign?(node, owner_name(instance))
141
+
142
+ remember(node.block.location, nil)
143
+ remember_reentry(node, instance)
144
+ remember_root_constant(node, instance)
145
+ end
146
+
147
+ # Issue #2983 — the code a barred `class_eval`-family block on a receiver rooted at another constant reenters
148
+ # (`Anc.class_eval`, `INST.class.class_eval`, `AliasK.new.class.class_eval`), remembered under that constant's
149
+ # candidate key ({CensusForeignBlock.root_constant}), in place of the host walk's as-written owner. A class
150
+ # whose census joins the entries of the constant the key resolves to, an ancestor's, an alias's or an instance
151
+ # constant's ({ScopeIndexer::IvarContributions}), then sees the write.
152
+ def remember_root_constant(node, instance)
153
+ root, blocks = CensusForeignBlock.rooted_reentry(node, owner_name(instance))
154
+ blocks&.each { |block| remember(block.location, root) }
155
+ @rooted.merge(blocks) if blocks
156
+ end
157
+
158
+ # The host walk's `owner` of a `def` or `define_method` block, unless {#remember_root_constant} filed it already.
159
+ def remember_host(node, owner) = (remember(node.location, owner) if owner && !@rooted.include?(node))
160
+
161
+ # Issue #2983 — `C = K` or `C = K.new` (`.allocate`) records `[:binds_class, key, "C"]`, `K`'s candidate key and
162
+ # `C`'s qualified name ({CensusForeignBlock.binding_entry}), so a class's census also joins the entries filed
163
+ # under an alias or an instance constant of it (`AliasK.class_eval { … }`, `INST.class.class_eval { … }`).
164
+ def record_binding(node, owner)
165
+ entry = CensusForeignBlock.binding_entry(node, nesting(owner))
166
+ @census << entry if entry
167
+ end
168
+
169
+ # The lexical class and module names `owner` spells: the host walk's prefix Array, or a resolved name.
170
+ def nesting(owner) = owner.nil? ? NO_ARGUMENTS : owner_name(owner).split("::")
171
+
172
+ # Issue #2983 — `class_eval` / `module_eval` given a String runs code the census cannot read, which may define
173
+ # a method writing any ivar: a {DYNAMIC} write of {ANY_IVAR} is recorded for the module the receiver may be. That
174
+ # is the class owning the body for an implicit or `self` receiver in a class body, the instance's class for a
175
+ # receiver in an instance body that {CensusForeignBlock.foreign_module?} does not bar, and otherwise the constant
176
+ # the receiver chain is rooted at.
177
+ def record_string_eval(node, owner)
178
+ return unless STRING_EVALS.include?(node.name) && node.block.nil? && node.arguments
179
+
180
+ instance = instance_owner(node) unless @instance_ranges.empty?
181
+ target = CensusForeignBlock.string_eval_target(node, owner_name(instance), owner_name(owner))
182
+ record_typed_write(target, ANY_IVAR, DYNAMIC) if target
183
+ end
184
+
185
+ # The `define_method` blocks in a barred body that run on the instance again, remembered after the bar so they
186
+ # answer for the instance's class ({CensusForeignBlock.reentry_blocks}).
187
+ def remember_reentry(node, instance)
188
+ CensusForeignBlock.reentry_blocks(node, owner_name(instance)).each do |block|
189
+ remember(block.location, instance)
190
+ end
191
+ end
192
+
193
+ # Issue #2488 — `E.attr_writer :w` declares a writer of `@w` on `E`, and `E.define_method(:m) { … }` installs an
194
+ # instance body of `E`, wherever the call sits.
195
+ def record_receiver_writers(node, class_name)
196
+ ReceiverWriterCall.writer_ivars(node).each { |ivar| record_typed_write(class_name, ivar, DYNAMIC) }
197
+ block = ReceiverWriterCall.define_method_block(node)
198
+ remember(block.location, class_name) if block
199
+ end
200
+
114
201
  # Issue #2079 — `attr_writer :x` / `attr_accessor :x` in a class body writes `@x` with whatever the caller hands.
115
202
  def record_writer_declaration(node, owner)
116
203
  return unless owner && WRITER_DECLARATIONS.include?(node.name) && self_receiver?(node)
@@ -128,7 +215,8 @@ module Rigor
128
215
 
129
216
  def visit_call(node, owner)
130
217
  record_writer_declaration(node, owner)
131
- remember(node.block.location, owner) if owner && define_method_block?(node)
218
+ record_string_eval(node, owner)
219
+ remember_host(node.block, owner) if define_method_block?(node)
132
220
  if self_receiver?(node) || own_singleton_class_call?(node)
133
221
  return if @instance_ranges.empty? || !(own_singleton_class_call?(node) || open_world?(node))
134
222
 
@@ -139,12 +227,19 @@ module Rigor
139
227
  end
140
228
  end
141
229
 
142
- # `class << self` inside an instance `def` opens the instance's singleton class.
230
+ # `class << self` inside an instance `def` opens the instance's singleton class. Its body's `self` is that
231
+ # singleton class, so an `@x` write there is the singleton's own ivar (#2490), whatever the expression.
143
232
  def visit_singleton_class(node)
144
- return unless node.expression.is_a?(Prism::SelfNode) && !@instance_ranges.empty?
233
+ return if @instance_ranges.empty?
145
234
 
146
235
  instance = instance_owner(node)
147
- record_class(instance) if instance
236
+ return unless instance
237
+
238
+ record_class(instance) if node.expression.is_a?(Prism::SelfNode)
239
+ return unless node.body
240
+
241
+ remember(node.body.location, nil)
242
+ remember_reentry(node, instance)
148
243
  end
149
244
 
150
245
  def record_class(owner) = @census << [:ivar_set_class, owner_name(owner)].freeze
@@ -5,7 +5,8 @@ require "prism"
5
5
  require_relative "../type"
6
6
  require_relative "mutation_widening"
7
7
  require_relative "key_presence_guard/scan"
8
- require_relative "../run_scoped_slots"
8
+ require_relative "../typing_state"
9
+ require_relative "../memo_audit"
9
10
 
10
11
  module Rigor
11
12
  module Inference
@@ -58,9 +59,22 @@ module Rigor
58
59
  ] + MutationWidening::SHAPE_MUTATORS.to_a
59
60
  ).to_set.freeze
60
61
 
61
- STATE_KEY = :__rigor_key_presence_guard
62
- CACHE_KEY = RunScopedSlots.register(:__rigor_key_presence_guard_rewalk)
63
- private_constant :STATE_KEY, :CACHE_KEY
62
+ # ADR-129 WD1 — the walk {.with_guards} installs is a Frame, and {.record}, which rewrites its `guard_ends` in
63
+ # place, is a Resource touch that also takes a fresh id ({TypingState::State#rewrite}), so a read of the walk's
64
+ # guards is fixed while the id is. The walk's `defs`, `frozen_constants` and `memo` are extensional caches of
65
+ # `root`, filled in place without a touch.
66
+ WALK = TypingState.frame(
67
+ :key_presence_walk,
68
+ contents: :resource, read_exemption: :id_determined,
69
+ reason: "installed per guarded re-walk; each recorded guard rewrites its `guard_ends`",
70
+ witness: "spec/fixtures/typing_state_witnesses/key_presence_walk.rb"
71
+ )
72
+ REWALK = TypingState.memo(
73
+ :key_presence_rewalk,
74
+ run_scoped: true, audit: "key presence rewalk",
75
+ reason: "the last file's guarded index, keyed by the file's root"
76
+ )
77
+ private_constant :WALK, :REWALK
64
78
 
65
79
  # The re-walk's state: the file's root, the end offset of each recorded guard, and the per-file scans.
66
80
  Walk = Struct.new(:root, :guard_ends, :defs, :frozen_constants, :memo) do
@@ -88,25 +102,21 @@ module Rigor
88
102
 
89
103
  # Runs the block with guards recorded and read, for the re-walk of `root`. Thread-local, so it holds under a
90
104
  # Ractor worker too.
91
- def with_guards(root)
92
- previous = Thread.current[STATE_KEY]
93
- Thread.current[STATE_KEY] = Walk.new(root, {}, nil, nil, {})
94
- yield
95
- ensure
96
- Thread.current[STATE_KEY] = previous
105
+ def with_guards(root, &)
106
+ TypingState.current.with(WALK, Walk.new(root, {}, nil, nil, {}), &)
97
107
  end
98
108
 
99
109
  # Runs the block with guards off again: a callee's return summary, memoised for the whole run, must be the one
100
110
  # the file's analysis reads.
101
- def without_guards
102
- previous = Thread.current[STATE_KEY]
103
- Thread.current[STATE_KEY] = nil
104
- yield
105
- ensure
106
- Thread.current[STATE_KEY] = previous
111
+ def without_guards(&)
112
+ TypingState.current.with(WALK, nil, &)
107
113
  end
108
114
 
109
- def active? = !Thread.current[STATE_KEY].nil?
115
+ def active? = !TypingState.current[WALK].nil?
116
+
117
+ # {.active?}, read only to decide whether to turn the walk off for the extent that follows ({.without_guards}):
118
+ # that extent reads it off either way, so the key audit does not count the read (ADR-129 WD1).
119
+ def forcing_off? = !TypingState.current.forcing_read(WALK).nil?
110
120
 
111
121
  # Whether `call.possible-nil-receiver` should withhold its report on `call_node`, whose receiver the file's
112
122
  # analysis types `receiver_type`: true only when the guarded re-walk types that receiver as exactly
@@ -130,19 +140,33 @@ module Rigor
130
140
  # The guarded scope index of `root`, built once per file (the cache holds the last file only), or nil when the
131
141
  # file holds no guard-shaped call.
132
142
  def rewalk(root, scope_index)
133
- cached = Thread.current[CACHE_KEY]
134
- return cached.last if cached&.first.equal?(root)
143
+ return guarded_index(root, scope_index) if MemoAudit.enabled? && MemoAudit.suspended?
144
+
145
+ state = TypingState.current
146
+ cached = state[REWALK]
147
+ if cached&.first.equal?(root)
148
+ # A re-walk that raised is stored as nil, and its audit recomputation answers nil the same way.
149
+ return MemoAudit.check("key presence rewalk", cached.last, root) do
150
+ guarded_index(root, scope_index)
151
+ rescue StandardError
152
+ nil
153
+ end
154
+ end
135
155
 
136
156
  # Cached before the walk, so a re-walk that raises is attempted once per file and then answers nil.
137
- Thread.current[CACHE_KEY] = [root, nil]
157
+ state[REWALK] = [root, nil]
158
+ index = MemoAudit.key_audit("key presence rewalk") { guarded_index(root, scope_index) }
159
+ state[REWALK] = [root, index]
160
+ index
161
+ end
162
+
163
+ def guarded_index(root, scope_index)
138
164
  return nil unless guard_shaped_call?(root)
139
165
 
140
166
  base = scope_index[root]
141
- index = base && with_guards(root) do
167
+ base && with_guards(root) do
142
168
  ScopeIndexer.index(root, default_scope: base.with_isolated_side_tables)
143
169
  end
144
- Thread.current[CACHE_KEY] = [root, index]
145
- index
146
170
  end
147
171
 
148
172
  def guard_shaped_call?(node)
@@ -206,7 +230,8 @@ module Rigor
206
230
  # The truthy edge of `receiver.key?(key)` with a non-literal key, inside the re-walk: `scope` with the guard
207
231
  # recorded, or nil when the guard does not apply.
208
232
  def record(call_node, scope)
209
- walk = Thread.current[STATE_KEY]
233
+ state = TypingState.current
234
+ walk = state[WALK]
210
235
  return nil if walk.nil?
211
236
 
212
237
  args = call_node.arguments&.arguments
@@ -217,6 +242,7 @@ module Rigor
217
242
  return nil unless receiver_type?(scope.type_of(call_node.receiver))
218
243
  return nil unless Scan.eligible?(walk, call_node, guard)
219
244
 
245
+ state.rewrite(WALK)
220
246
  walk.guard_ends[guard] = call_node.location.end_offset
221
247
  scope.with_indexed_narrowing(*guard, PRESENT)
222
248
  end
@@ -226,7 +252,7 @@ module Rigor
226
252
  # read may have changed it ({.intervening?}), and no value of the receiver can be `nil` (the read cannot tell a
227
253
  # value's own `nil` from the miss). Nil when no guard applies or nothing would change.
228
254
  def guarded_read(node, type, scope)
229
- walk = Thread.current[STATE_KEY]
255
+ walk = TypingState.current[WALK]
230
256
  guard = walk && held_guard(node, scope)
231
257
  return nil if guard.nil? || Scan.intervening?(walk, guard, node)
232
258