rigortype 0.4.7 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (268) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -1
  3. data/data/core_overlay/bigdecimal.rbs +33 -0
  4. data/data/core_overlay/resolv.rbs +30 -0
  5. data/data/gem_overlay/activesupport/core_ext.rbs +29 -0
  6. data/data/gem_overlay/addressable/addressable.rbs +40 -0
  7. data/docs/handbook/01-getting-started.md +1 -1
  8. data/docs/handbook/11-sig-gen.md +35 -0
  9. data/docs/install.md +20 -0
  10. data/docs/manual/01-installation.md +67 -26
  11. data/docs/manual/02-cli-reference.md +307 -91
  12. data/docs/manual/03-configuration.md +41 -3
  13. data/docs/manual/04-diagnostics.md +5 -4
  14. data/docs/manual/08-skills.md +11 -0
  15. data/docs/manual/09-editor-integration.md +8 -0
  16. data/docs/manual/14-rails-quickstart.md +1 -1
  17. data/docs/manual/19-effect-labels.md +3 -4
  18. data/docs/manual/plugins/rigor-actionpack.md +15 -6
  19. data/docs/manual/plugins/rigor-activesupport-core-ext.md +3 -0
  20. data/docs/manual/plugins/rigor-sorbet.md +17 -3
  21. data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
  22. data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
  23. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +8 -0
  24. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +13 -2
  25. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -0
  26. data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
  27. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +40 -11
  28. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
  29. data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
  30. data/lib/rigor/analysis/check_rules/rule_ids.rb +8 -1
  31. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  32. data/lib/rigor/analysis/check_rules.rb +598 -99
  33. data/lib/rigor/analysis/crash_signature.rb +43 -1
  34. data/lib/rigor/analysis/dependency_recorder.rb +233 -76
  35. data/lib/rigor/analysis/incremental.rb +11 -1
  36. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -1
  37. data/lib/rigor/analysis/incremental_session.rb +279 -37
  38. data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
  39. data/lib/rigor/analysis/rule_catalog.rb +37 -1
  40. data/lib/rigor/analysis/run_cache_key.rb +27 -3
  41. data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
  42. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +4 -0
  43. data/lib/rigor/analysis/runner/declaration_position.rb +7 -7
  44. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +72 -20
  45. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +3 -8
  46. data/lib/rigor/analysis/runner/pool_coordinator.rb +26 -5
  47. data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
  48. data/lib/rigor/analysis/runner.rb +222 -39
  49. data/lib/rigor/analysis/self_call_resolution_recorder.rb +61 -22
  50. data/lib/rigor/analysis/worker_session.rb +42 -41
  51. data/lib/rigor/bleeding_edge.rb +19 -4
  52. data/lib/rigor/cache/file_digest.rb +39 -31
  53. data/lib/rigor/cache/incremental_snapshot.rb +66 -7
  54. data/lib/rigor/cache/rbs_descriptor.rb +37 -2
  55. data/lib/rigor/cache/rbs_environment.rb +5 -1
  56. data/lib/rigor/cli/baseline_command.rb +15 -2
  57. data/lib/rigor/cli/check_command.rb +4 -2
  58. data/lib/rigor/cli/effects_snapshot_command.rb +8 -1
  59. data/lib/rigor/cli/lens_command.rb +17 -2
  60. data/lib/rigor/cli/sig_gen_command.rb +8 -0
  61. data/lib/rigor/cli/skill_describe.rb +1 -0
  62. data/lib/rigor/cli.rb +5 -2
  63. data/lib/rigor/config_audit.rb +32 -18
  64. data/lib/rigor/configuration/severity_profile.rb +9 -0
  65. data/lib/rigor/configuration.rb +32 -5
  66. data/lib/rigor/effects/collector.rb +53 -21
  67. data/lib/rigor/environment/bundle_sig_discovery.rb +63 -28
  68. data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
  69. data/lib/rigor/environment/default_libraries.rb +17 -0
  70. data/lib/rigor/environment/deferred_signature_plan.rb +104 -6
  71. data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
  72. data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
  73. data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
  74. data/lib/rigor/environment/rbs_hierarchy.rb +6 -0
  75. data/lib/rigor/environment/rbs_loader.rb +312 -63
  76. data/lib/rigor/environment.rb +131 -35
  77. data/lib/rigor/inference/acceptance.rb +50 -15
  78. data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
  79. data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
  80. data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
  81. data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
  82. data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
  83. data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
  84. data/lib/rigor/inference/block_auto_splat.rb +1 -1
  85. data/lib/rigor/inference/block_run_count.rb +9 -2
  86. data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
  87. data/lib/rigor/inference/callee_mutation_chain.rb +1 -1
  88. data/lib/rigor/inference/captured_locals.rb +2 -0
  89. data/lib/rigor/inference/census_foreign_block.rb +354 -0
  90. data/lib/rigor/inference/clause_widening.rb +1 -1
  91. data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
  92. data/lib/rigor/inference/construction_escape.rb +56 -15
  93. data/lib/rigor/inference/declaration_walk.rb +4 -0
  94. data/lib/rigor/inference/def_node_resolver.rb +32 -14
  95. data/lib/rigor/inference/def_return_typer.rb +1 -1
  96. data/lib/rigor/inference/definer_resolution.rb +15 -7
  97. data/lib/rigor/inference/definers_index.rb +72 -16
  98. data/lib/rigor/inference/error_info.rb +16 -0
  99. data/lib/rigor/inference/expression_typer.rb +1459 -287
  100. data/lib/rigor/inference/flow_tracer.rb +31 -14
  101. data/lib/rigor/inference/fork_map.rb +3 -0
  102. data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
  103. data/lib/rigor/inference/global_write_census.rb +8 -6
  104. data/lib/rigor/inference/in_effect_refinements.rb +7 -0
  105. data/lib/rigor/inference/indexed_narrowing.rb +57 -8
  106. data/lib/rigor/inference/ivar_writer_census.rb +106 -11
  107. data/lib/rigor/inference/key_presence_guard.rb +51 -25
  108. data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
  109. data/lib/rigor/inference/last_line.rb +5 -2
  110. data/lib/rigor/inference/loop_value_record.rb +63 -15
  111. data/lib/rigor/inference/match_rebinding/frame.rb +11 -0
  112. data/lib/rigor/inference/match_rebinding/yielding_match.rb +74 -18
  113. data/lib/rigor/inference/match_rebinding.rb +21 -8
  114. data/lib/rigor/inference/method_dispatcher/block_folding.rb +43 -29
  115. data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
  116. data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
  117. data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
  118. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
  119. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
  120. data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
  121. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +12 -0
  122. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +409 -67
  123. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +1 -1
  124. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +384 -64
  125. data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
  126. data/lib/rigor/inference/method_dispatcher.rb +66 -18
  127. data/lib/rigor/inference/method_parameter_binder.rb +136 -56
  128. data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
  129. data/lib/rigor/inference/module_function_state.rb +13 -2
  130. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  131. data/lib/rigor/inference/mutation_widening.rb +44 -4
  132. data/lib/rigor/inference/narrowing.rb +201 -27
  133. data/lib/rigor/inference/operand_effects.rb +43 -2
  134. data/lib/rigor/inference/optimistic_origin.rb +133 -8
  135. data/lib/rigor/inference/origin_lookup.rb +1 -1
  136. data/lib/rigor/inference/precision_scanner.rb +1 -1
  137. data/lib/rigor/inference/project_method_ownership.rb +28 -10
  138. data/lib/rigor/inference/project_patched_methods.rb +65 -13
  139. data/lib/rigor/inference/project_patched_scanner.rb +120 -25
  140. data/lib/rigor/inference/project_patched_visibility.rb +39 -3
  141. data/lib/rigor/inference/rbs_type_translator.rb +68 -15
  142. data/lib/rigor/inference/receiver_writer_call.rb +67 -0
  143. data/lib/rigor/inference/recursion_rebinding.rb +157 -0
  144. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +56 -18
  145. data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -17
  146. data/lib/rigor/inference/refined_dispatch.rb +13 -0
  147. data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
  148. data/lib/rigor/inference/remover_holder_scan.rb +76 -22
  149. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
  150. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +12 -11
  151. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +188 -28
  152. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
  153. data/lib/rigor/inference/scope_indexer.rb +2079 -183
  154. data/lib/rigor/inference/self_call_ivar_writes.rb +26 -7
  155. data/lib/rigor/inference/self_preserving_block.rb +87 -0
  156. data/lib/rigor/inference/singleton_hook_decline.rb +41 -6
  157. data/lib/rigor/inference/singleton_object_constant.rb +32 -2
  158. data/lib/rigor/inference/statement_evaluator.rb +970 -250
  159. data/lib/rigor/inference/string_mutation.rb +14 -0
  160. data/lib/rigor/inference/synthetic_method_scanner.rb +35 -11
  161. data/lib/rigor/inference/toplevel_def_census.rb +14 -6
  162. data/lib/rigor/inference/unknown_store_widening.rb +1 -1
  163. data/lib/rigor/inference/untyped_rejoin.rb +1 -1
  164. data/lib/rigor/inference/void_tail_summary.rb +29 -10
  165. data/lib/rigor/lens/claims/comparison.rb +78 -0
  166. data/lib/rigor/lens/claims/request.rb +131 -0
  167. data/lib/rigor/lens/claims/type_reader.rb +216 -0
  168. data/lib/rigor/lens/claims.rb +208 -0
  169. data/lib/rigor/lens/contract_reader.rb +5 -0
  170. data/lib/rigor/lens/document.rb +1 -1
  171. data/lib/rigor/lens/file_reader.rb +21 -8
  172. data/lib/rigor/lens/run.rb +8 -0
  173. data/lib/rigor/lens.rb +1 -1
  174. data/lib/rigor/library_probe.rb +68 -0
  175. data/lib/rigor/memo_audit.rb +282 -0
  176. data/lib/rigor/plugin/base.rb +15 -2
  177. data/lib/rigor/plugin/io_boundary.rb +25 -3
  178. data/lib/rigor/plugin/manifest.rb +7 -3
  179. data/lib/rigor/plugin/registry.rb +13 -2
  180. data/lib/rigor/plugin/trust_policy.rb +46 -0
  181. data/lib/rigor/protection/mutation_cache.rb +4 -1
  182. data/lib/rigor/reflection/constant_ancestors.rb +79 -22
  183. data/lib/rigor/reflection/declaration_collectors.rb +55 -0
  184. data/lib/rigor/reflection/declarations.rb +58 -0
  185. data/lib/rigor/reflection/project_method.rb +177 -0
  186. data/lib/rigor/reflection.rb +2 -0
  187. data/lib/rigor/rooted_glob.rb +18 -1
  188. data/lib/rigor/run_scoped_slots.rb +20 -20
  189. data/lib/rigor/scope/discovery_index.rb +149 -2
  190. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
  191. data/lib/rigor/scope/resolution_chain.rb +900 -55
  192. data/lib/rigor/scope.rb +307 -34
  193. data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
  194. data/lib/rigor/sig_gen/classification.rb +14 -1
  195. data/lib/rigor/sig_gen/generator.rb +222 -13
  196. data/lib/rigor/sig_gen/method_candidate.rb +14 -0
  197. data/lib/rigor/sig_gen/observation_collector.rb +11 -2
  198. data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
  199. data/lib/rigor/sig_gen/skip_reason_catalog.rb +47 -0
  200. data/lib/rigor/sig_gen/writer.rb +21 -0
  201. data/lib/rigor/sig_gen.rb +1 -0
  202. data/lib/rigor/source/forwardable_delegators.rb +122 -0
  203. data/lib/rigor/source/node_children.rb +13 -0
  204. data/lib/rigor/type/combinator.rb +164 -8
  205. data/lib/rigor/type/interface.rb +60 -0
  206. data/lib/rigor/type.rb +1 -0
  207. data/lib/rigor/type_of_memo/serving.rb +99 -0
  208. data/lib/rigor/type_of_memo/stats.rb +43 -0
  209. data/lib/rigor/type_of_memo.rb +337 -0
  210. data/lib/rigor/typing_state.rb +1094 -0
  211. data/lib/rigor/version.rb +1 -1
  212. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +2 -2
  213. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +57 -25
  214. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
  215. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
  216. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
  217. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
  218. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
  219. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +130 -30
  220. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -5
  221. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
  222. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +49 -13
  223. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
  224. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +4 -2
  225. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +13 -5
  226. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +29 -0
  227. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
  228. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
  229. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -2
  230. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
  231. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +6 -0
  232. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +199 -1
  233. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +3 -2
  234. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +4 -4
  235. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +209 -2
  236. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
  237. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
  238. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +80 -3
  239. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
  240. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +1 -6
  241. data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
  242. data/sig/rigor/analysis/fact_store.rbs +2 -3
  243. data/sig/rigor/cache/descriptor.rbs +14 -0
  244. data/sig/rigor/environment.rbs +4 -1
  245. data/sig/rigor/plugin/base.rbs +53 -19
  246. data/sig/rigor/plugin/interfaces.rbs +51 -0
  247. data/sig/rigor/plugin/io_boundary.rbs +2 -3
  248. data/sig/rigor/plugin/node_context.rbs +21 -0
  249. data/sig/rigor/reflection.rbs +37 -0
  250. data/sig/rigor/scope.rbs +22 -3
  251. data/sig/rigor/trinary.rbs +1 -0
  252. data/sig/rigor/type.rbs +9 -3
  253. data/sig/rigor.rbs +3 -2
  254. data/skills/rigor-ask/SKILL.md +1 -0
  255. data/skills/rigor-project-init/references/01-detect.md +1 -0
  256. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +5 -1
  257. data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
  258. data/skills/rigor-rbs-contract/SKILL.md +147 -0
  259. data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
  260. data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
  261. data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
  262. data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
  263. data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
  264. data/skills/rigor-rbs-setup/SKILL.md +3 -1
  265. data/skills/rigor-type-oracle/SKILL.md +26 -1
  266. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
  267. metadata +48 -2
  268. data/lib/rigor/inference/memo_audit.rb +0 -55
@@ -0,0 +1,1094 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ # ADR-129 WD1–WD3 — the one owner of the mutable state typing reads and writes.
5
+ #
6
+ # Every piece of that state is a **slot** declared here with one of four kinds, where its owner names it
7
+ # (`RETURN_SINK = TypingState.frame(:return_sink, …)`), and a deny-by-default static spec
8
+ # (`spec/rigor/typing_state_closure_spec.rb`, ADR-129's G3) rejects any other state in a file it
9
+ # covers. The kind decides how a future `type_of` memo treats the slot, so the memo needs no knowledge of any
10
+ # particular one:
11
+ #
12
+ # - **Frame** ({.frame}): an input installed for a dynamic extent with {State#with}, restored exactly on the way
13
+ # out, and frozen while installed. A frame whose installed object is a container mutated in place (a jump sink,
14
+ # the key-presence walk) declares `contents: :resource`: the installation is the Frame, and each mutation of
15
+ # the contents is a Resource touch ({State#touch}, {State#rewrite}). The installed value may also hold
16
+ # extensional caches its owner fills in place: those are Memos by kind and take no touch.
17
+ # - **Table** ({.table}): a keyed store typing writes and may read back. Its reads and writes are logged to the
18
+ # journal ({State#log_read}, {State#log_write}) while a capture is open. A table that answers what reading
19
+ # ABSENT would have computed declares `read_through: true` (WD1). A table whose stores are held elsewhere, one per
20
+ # owner (the advisory tables each file's scopes share, an installed recorder), declares `bound: true` and logs
21
+ # through {State#log_bound_read} / {State#log_bound_write}, which name the store beside the key.
22
+ # - **Resource** ({.resource}): state consumed or rewritten in place, or read as a delta. A write refuses every
23
+ # open capture (the `any` rule, WD3); so does a read, unless the slot declares one of WD3's two read exemptions
24
+ # (`:id_determined`, `:delta_internal`).
25
+ # - **Memo** ({.memo}): an extensional cache keyed on all its inputs. Unlogged.
26
+ #
27
+ # Typing also reaches state that is not a slot value: the instance variables of the objects a scope holds. ADR-129's
28
+ # G4 (`spec/rigor/typing_state_reachability_spec.rb`) walks everything reachable from a scope and the slot values,
29
+ # and accepts a mutable object only where a declaration names it: an extensional cache an object fills in place
30
+ # ({.memo_field}), or the store of a bound Table ({.store_field}).
31
+ #
32
+ # The values live in one {State} per thread (per fiber, as `Thread.current[]` is), cached by the typer and the
33
+ # evaluator, so a slot read is an ivar and an array index. Each slot constant is the slot's integer index, which
34
+ # keeps the declaration Ractor-shareable and the read an `Array#[]`.
35
+ #
36
+ # Stage 1 (ADR-129 WD6) adds no caching: no capture is opened outside the specs, so the journal stays empty and
37
+ # every touch is a size check.
38
+ module TypingState
39
+ KINDS = %i[frame table resource memo].freeze
40
+ READ_EXEMPTIONS = %i[id_determined delta_internal].freeze
41
+ # ADR-129 G1′ — the four things a Frame declares about how typing meets it, exactly one each ({.frame}).
42
+ G1_KINDS = %i[witness typing_unreachable typing_never_reads memo_shut_while_installed].freeze
43
+
44
+ # One declared slot: its name, kind and index, why it is the kind it is, and its kind's options.
45
+ # - `run_scoped`: a Table, Memo or Resource whose contents belong to one analysis run, emptied by
46
+ # {.release_run_scoped} when the run ends ({RunScopedSlots.release}).
47
+ # - `read_exemption`: a Resource, or a `contents: :resource` Frame's contents, whose reads do not refuse (WD3).
48
+ # - `contents`: `:resource` for a Frame whose installed container is mutated in place, `:table` for one whose
49
+ # installed container is a store of a bound Table, whose entries are journaled rather than touched.
50
+ # - `read_through`: a Table that is a read-through cache of the computation's own value (WD1).
51
+ # - `bound`: a Table whose stores are not the slot's value but objects its owner holds, several at once (a store
52
+ # per file's scopes, per installed recorder); its journal records key on `[store, key]`.
53
+ # - `audit`: a Memo's `MemoAudit.check` labels, the hit sites that recompute and compare (WD1, G2).
54
+ # - `memos`: a Frame's members or keys that hold Memos its owner fills in place (G4 accepts them).
55
+ # - `computed`: a Table filled by a computation (the ADR-84 return memo), the `MemoAudit.key_audit` label of its
56
+ # miss (WD1's key audit, G2).
57
+ # - `entries`: a Table's entry protocol ({StoreEntries}), how the journal reads and writes one of its entries.
58
+ # - `declaration`: a Frame's G1′ declaration (ADR-129 WD5), a frozen `[kind, argument]` pair: `[:witness, path]` (a
59
+ # repo-relative fixture on which G1 must fail with the slot ignored), or `[:typing_unreachable, true]`,
60
+ # `[:typing_never_reads, true]` or `[:memo_shut_while_installed, true]` ({FrameAudit} asserts the first two under
61
+ # `RIGOR_MEMO_AUDIT`).
62
+ Slot = Data.define(:name, :kind, :index, :reason, :run_scoped, :read_exemption, :contents, :read_through, :bound,
63
+ :audit, :memos, :computed, :entries, :declaration)
64
+
65
+ # A field declared outside the slots: `kind` is `:memo` ({.memo_field}) or `:store` ({.store_field}, whose `slot`
66
+ # names the bound Table).
67
+ Field = Data.define(:owner, :ivar, :kind, :slot, :reason)
68
+
69
+ # A Table read of a key the table does not hold logs this rather than nil, so a stored nil and a miss differ.
70
+ ABSENT = Object.new.freeze
71
+
72
+ # ADR-129 WD3 — a Table's entry protocol: how the journal reaches one entry of a store, to validate a logged read
73
+ # ({State#stale_read}), replay a logged write ({State#replay}) and note the value a write replaces
74
+ # ({State#open_priors}). A Table names its protocol as `entries:`; this one is the default, for a store that answers
75
+ # `fetch(key, default)` and `[]=` (a Hash, or a recorder's accumulator built to that shape). A protocol module
76
+ # answers:
77
+ # - `entry(store, key)`: the value at `key`, or {ABSENT}, for a nil store too (an unbound table not created yet);
78
+ # - `put(store, key, value)`: stores `value` at `key` the way the table's own writer does;
79
+ # - `delete(store, key)`: removes the entry at `key`, undoing a first write (G1's stage-4 form);
80
+ # - `container(state, slot)`: the store an unbound table's replay writes into, the one `state` holds in `slot` or
81
+ # one it creates there the way the table's writer does.
82
+ #
83
+ # Records index their stores by identity and their keys by `eql?`, which is identity for the Prism nodes most
84
+ # tables key on (`Prism::Node` defines neither `==` nor `hash`) and what each store's own lookup uses for the rest.
85
+ module StoreEntries
86
+ module_function
87
+
88
+ def entry(store, key) = store.nil? ? ABSENT : store.fetch(key, ABSENT)
89
+
90
+ def put(store, key, value)
91
+ store[key] = value
92
+ end
93
+
94
+ def delete(store, key)
95
+ store.delete(key)
96
+ end
97
+
98
+ def container(state, slot) = state[slot] || (state[slot] = {})
99
+ end
100
+
101
+ # The entry protocol of a Table whose slot holds an `ObjectSpace::WeakKeyMap` ({State#weak_key_map}), which has no
102
+ # `fetch`.
103
+ module WeakKeyMapEntries
104
+ module_function
105
+
106
+ def entry(store, key) = store&.key?(key) ? store[key] : ABSENT
107
+
108
+ def put(store, key, value)
109
+ store[key] = value
110
+ end
111
+
112
+ def delete(store, key)
113
+ store&.delete(key)
114
+ end
115
+
116
+ def container(state, slot) = state.weak_key_map(slot)
117
+ end
118
+
119
+ # The read and write slices of a computation that logged nothing.
120
+ EMPTY_SLICES = [[].freeze, [].freeze].freeze
121
+
122
+ # A read slice's fourth element when the read is not a read-through cache's ABSENT read of a key the computation
123
+ # then wrote; a write's prior when the writer did not pass one ({State#log_write}).
124
+ NONE = Object.new.freeze
125
+ private_constant :NONE
126
+
127
+ STATE_KEY = :__rigor_typing_state__
128
+ private_constant :STATE_KEY
129
+
130
+ # The registry and the run-scoped indices, each replaced by a frozen copy on every declaration so a non-main
131
+ # Ractor can read them. Declarations run at require time, on the main Ractor.
132
+ @slots = [].freeze
133
+ @run_scoped = [].freeze
134
+ @fields = [].freeze
135
+
136
+ class << self
137
+ # Declares a Frame slot and returns its index. A `contents: :resource` frame may declare a read exemption for
138
+ # reads of its contents. `memos` names the members (a Data or Struct value) or keys (a Hash value) of the
139
+ # installed value that hold extensional caches its owner fills in place, Memos by kind, which `reason` explains.
140
+ #
141
+ # A Frame declares exactly one of ADR-129 G1′'s four things about how typing meets it (WD5):
142
+ # - `witness:` a repo-relative fixture path, a program whose typing installs the Frame and reads it inside
143
+ # `type_of`, and on which G1 must fail with the slot ignored;
144
+ # - `typing_unreachable: true`: no `type_of` runs while the Frame is installed, which audit mode asserts;
145
+ # - `typing_never_reads: true`: typing wraps no read of the Frame, only code outside `type_of` reads it, which
146
+ # audit mode asserts;
147
+ # - `memo_shut_while_installed: true`: the `type_of` memo never runs while the Frame is installed (the tracer
148
+ # switches).
149
+ def frame(name, reason:, contents: nil, read_exemption: nil, memos: nil, witness: nil, # rubocop:disable Metrics/ParameterLists
150
+ typing_unreachable: false, typing_never_reads: false, memo_shut_while_installed: false)
151
+ check_unclaimed(name)
152
+ unless [nil, :resource, :table].include?(contents)
153
+ raise ArgumentError, "#{name}: contents must be :resource, :table or nil"
154
+ end
155
+ if read_exemption && contents != :resource
156
+ raise ArgumentError, "#{name}: only a contents: :resource frame declares a read exemption"
157
+ end
158
+
159
+ check_read_exemption(name, read_exemption)
160
+ declare(name, :frame, reason, contents: contents, read_exemption: read_exemption,
161
+ memos: memos && Array(memos).freeze,
162
+ declaration: g1_declaration(name, witness, typing_unreachable, typing_never_reads,
163
+ memo_shut_while_installed))
164
+ end
165
+
166
+ # Declares a Table slot and returns its index. A `bound` table's stores live with their owners, so the slot holds
167
+ # nothing a run could release. A table a computation fills on a miss names the miss's `MemoAudit.key_audit` label
168
+ # as `computed`. `entries` names the table's entry protocol ({StoreEntries}, the default).
169
+ def table(name, reason:, run_scoped: false, read_through: false, bound: false, computed: nil,
170
+ entries: StoreEntries)
171
+ raise ArgumentError, "#{name}: a bound table holds no run-scoped value" if bound && run_scoped
172
+ unless %i[entry put delete container].all? { |method| entries.respond_to?(method) }
173
+ raise ArgumentError, "#{name}: entries must answer entry, put, delete and container"
174
+ end
175
+
176
+ declare(name, :table, reason, run_scoped: run_scoped, read_through: read_through, bound: bound,
177
+ computed: computed, entries: entries)
178
+ end
179
+
180
+ # Declares a Resource slot and returns its index.
181
+ def resource(name, reason:, read_exemption: nil, run_scoped: false)
182
+ check_read_exemption(name, read_exemption)
183
+ declare(name, :resource, reason, read_exemption: read_exemption, run_scoped: run_scoped)
184
+ end
185
+
186
+ # Declares a Memo slot and returns its index. `audit` names the `MemoAudit.check` labels its hits go through;
187
+ # a spec (`spec/rigor/typing_state_memo_audit_spec.rb`) holds every Memo to them, or to its stage-2 ratchet.
188
+ def memo(name, reason:, run_scoped: false, audit: nil)
189
+ labels = Array(audit)
190
+ declare(name, :memo, reason, run_scoped: run_scoped, audit: labels.empty? ? nil : labels)
191
+ end
192
+
193
+ # Declares that `owner`'s instances fill each of `ivars` in place, or assign it lazily, as an extensional cache
194
+ # keyed on all its inputs: a Memo that lives outside a slot. `reason` says why it is extensional. G4 accepts
195
+ # what such a field holds without walking it, and accepts an unfrozen `owner` whose other fields keep their
196
+ # objects.
197
+ def memo_field(owner, *ivars, reason:)
198
+ raise ArgumentError, "#{owner}: memo_field names at least one field" if ivars.empty?
199
+
200
+ ivars.each { |ivar| declare_field(owner, ivar, :memo, reason, nil) }
201
+ nil
202
+ end
203
+
204
+ # Declares that `owner`'s instance variable `ivar` holds a store of the bound Table `slot` (an index from
205
+ # {.table} with `bound: true`), whose entries are journaled through the slot.
206
+ def store_field(owner, ivar, slot)
207
+ declared = @slots.fetch(slot)
208
+ raise ArgumentError, "#{declared.name}: only a bound table has stores in fields" unless declared.bound
209
+
210
+ declare_field(owner, ivar, :store, declared.reason, declared.name)
211
+ end
212
+
213
+ # Every declared slot, in declaration order (a slot's index is its position).
214
+ attr_reader :slots
215
+
216
+ # The indices of the Frames that declare the G1′ kind `kind` ({G1_KINDS}), for {FrameAudit}.
217
+ def g1_slots(kind) = @slots.filter_map { |slot| slot.index if slot.declaration&.first == kind }.freeze
218
+
219
+ # Every field declared with {.memo_field} or {.store_field}, as `Field` values.
220
+ attr_reader :fields
221
+
222
+ # The slot declared as `name`. For specs and diagnostics; the hot path reads by index.
223
+ def slot(name)
224
+ @slots.find { |slot| slot.name == name } || raise(KeyError, "no typing-state slot #{name.inspect}")
225
+ end
226
+
227
+ # The calling thread's state, created on first use. The `is_a?` test types the answer as a {State} for Rigor's
228
+ # own analysis of its callers, where a bare `Thread.current[]` read is `Dynamic`.
229
+ def current
230
+ state = Thread.current[STATE_KEY]
231
+ return state if state.is_a?(State)
232
+
233
+ Thread.current[STATE_KEY] = State.new
234
+ end
235
+
236
+ # Empties every run-scoped slot on the calling thread, the TypingState half of {RunScopedSlots.release}.
237
+ def release_run_scoped
238
+ state = Thread.current[STATE_KEY]
239
+ state&.release(@run_scoped)
240
+ nil
241
+ end
242
+
243
+ private
244
+
245
+ def check_unclaimed(name)
246
+ return unless @slots.any? { |slot| slot.name == name }
247
+
248
+ raise ArgumentError, "typing-state slot #{name.inspect} is already declared"
249
+ end
250
+
251
+ def g1_declaration(name, witness, typing_unreachable, typing_never_reads, memo_shut_while_installed)
252
+ declared = []
253
+ declared << [:witness, witness] unless witness.nil?
254
+ declared << [:typing_unreachable, true] if typing_unreachable
255
+ declared << [:typing_never_reads, true] if typing_never_reads
256
+ declared << [:memo_shut_while_installed, true] if memo_shut_while_installed
257
+ unless declared.size == 1
258
+ raise ArgumentError, "#{name}: a Frame declares exactly one of #{G1_KINDS.inspect} (ADR-129 G1′), " \
259
+ "not #{declared.map(&:first).inspect}"
260
+ end
261
+ if witness && !(witness.is_a?(String) && witness.end_with?(".rb") && !witness.start_with?("/"))
262
+ raise ArgumentError, "#{name}: witness is a repo-relative path to a .rb fixture"
263
+ end
264
+
265
+ declared.first.freeze
266
+ end
267
+
268
+ def check_read_exemption(name, read_exemption)
269
+ return if read_exemption.nil? || READ_EXEMPTIONS.include?(read_exemption)
270
+
271
+ raise ArgumentError, "#{name}: read_exemption must be one of #{READ_EXEMPTIONS.inspect} or nil"
272
+ end
273
+
274
+ def declare_field(owner, ivar, kind, reason, slot_name)
275
+ raise ArgumentError, "#{owner}##{ivar}: a field is an instance variable" unless ivar.to_s.start_with?("@")
276
+ raise ArgumentError, "#{owner}##{ivar}: a field names why it is its kind" if reason.to_s.strip.empty?
277
+ if @fields.any? { |field| field.owner.equal?(owner) && field.ivar == ivar }
278
+ raise ArgumentError, "#{owner}##{ivar} is already declared"
279
+ end
280
+
281
+ field = Field.new(owner: owner, ivar: ivar, kind: kind, slot: slot_name, reason: reason.dup.freeze)
282
+ @fields = Ractor.make_shareable([*@fields, field])
283
+ nil
284
+ end
285
+
286
+ def declare(name, kind, reason, run_scoped: false, read_exemption: nil, contents: nil, read_through: false, # rubocop:disable Metrics/ParameterLists
287
+ bound: false, audit: nil, memos: nil, computed: nil, entries: nil, declaration: nil)
288
+ check_unclaimed(name)
289
+ raise ArgumentError, "#{name}: a slot names why it is its kind" if reason.to_s.strip.empty?
290
+
291
+ index = @slots.size
292
+ slot = Slot.new(name: name, kind: kind, index: index, reason: reason.dup.freeze, run_scoped: run_scoped,
293
+ read_exemption: read_exemption, contents: contents, read_through: read_through, bound: bound,
294
+ audit: audit, memos: memos, computed: computed, entries: entries, declaration: declaration)
295
+ @slots = Ractor.make_shareable([*@slots, slot])
296
+ @run_scoped = [*@run_scoped, index].freeze if run_scoped
297
+ index
298
+ end
299
+ end
300
+
301
+ # ADR-129 WD3 — a {State}'s reach into the Table entries its journal names: reading one ({#entry}), splitting a
302
+ # capture's records into the slices an entry keeps ({#slices}), validating a read slice ({#stale_read}), replaying a
303
+ # write slice ({#replay}), and the priors windows that note what each write replaced ({#open_priors}).
304
+ module Slices
305
+ # The value the Table `slot` holds at `key`, a journal record's key (`[store, key]` for a bound table), or
306
+ # {ABSENT}.
307
+ def entry(slot, key)
308
+ declared = TypingState.slots[slot]
309
+ return declared.entries.entry(key[0], key[1]) if declared.bound
310
+
311
+ declared.entries.entry(@values[slot], key)
312
+ end
313
+
314
+ # Splits a capture's records ({#close_capture}) into the slices a stored entry keeps (WD3), as a frozen pair:
315
+ # - the read slice, four entries per read `(slot, key, observed, own)`: every read of a key the computation had
316
+ # not written yet (a read of its own write is internal). `own` is the value the computation last wrote at that
317
+ # key when the table is a read-through cache and the read observed {ABSENT}, else an internal marker;
318
+ # - the write slice, three entries per write `(slot, key, value)`, in order.
319
+ def slices(records)
320
+ return EMPTY_SLICES if records.empty?
321
+
322
+ reads = []
323
+ writes = []
324
+ written = nil
325
+ index = 0
326
+ while index < records.size
327
+ slot = records[index + 1]
328
+ key = records[index + 2]
329
+ value = records[index + 3]
330
+ if records[index] == :write
331
+ writes.push(slot, key, value)
332
+ (written ||= KeyIndex.new).store(slot, key, value)
333
+ elsif written.nil? || !written.key?(slot, key)
334
+ reads.push(slot, key, value, NONE)
335
+ end
336
+ index += 4
337
+ end
338
+ attach_own_writes(reads, written) if written
339
+ [reads.freeze, writes.freeze].freeze
340
+ end
341
+
342
+ # The slot of the first read in `reads` (a read slice, {#slices}) that no longer holds: the table holds a value
343
+ # other than the one observed, by identity, {ABSENT} included, unless the table is a read-through cache, the
344
+ # read observed {ABSENT} and the table now holds what the computation itself wrote there. nil when all hold.
345
+ def stale_read(reads)
346
+ index = 0
347
+ while index < reads.size
348
+ return reads[index] unless read_holds?(reads, index)
349
+
350
+ index += 4
351
+ end
352
+ nil
353
+ end
354
+
355
+ # Every slot with a read in `reads` that no longer holds ({#stale_read}), each once, in first-failure order.
356
+ def stale_slots(reads)
357
+ slots = []
358
+ index = 0
359
+ while index < reads.size
360
+ slot = reads[index]
361
+ slots << slot unless slots.include?(slot) || read_holds?(reads, index)
362
+ index += 4
363
+ end
364
+ slots
365
+ end
366
+
367
+ # Replays `writes` (a write slice, {#slices}) in order, through each table's entry protocol, journaling each write
368
+ # as its writer would (WD3: the replay is itself journaled).
369
+ def replay(writes)
370
+ index = 0
371
+ while index < writes.size
372
+ slot = writes[index]
373
+ key = writes[index + 1]
374
+ value = writes[index + 2]
375
+ note_prior(slot, key, NONE) if @priors
376
+ put(slot, key, value)
377
+ @journal.push(:write, slot, key, value) unless @marks.empty?
378
+ index += 3
379
+ end
380
+ nil
381
+ end
382
+
383
+ # Opens a priors window: until {#close_priors}, the first write of each Table entry notes the value it replaced.
384
+ # Windows nest, and a write is noted in every open window that has not seen its key. A window opens only inside
385
+ # a capture, since some writers log only while one is open, and closes before that capture does.
386
+ def open_priors
387
+ raise ArgumentError, "a priors window opens inside a capture" unless capturing?
388
+
389
+ (@priors ||= []) << [KeyIndex.new, []]
390
+ nil
391
+ end
392
+
393
+ # Closes the innermost priors window and answers what it noted, three entries per entry `(slot, key, prior)` in
394
+ # first-write order, {ABSENT} for an entry the table did not hold.
395
+ def close_priors
396
+ _, noted = @priors.pop
397
+ @priors = nil if @priors.empty?
398
+ noted.freeze
399
+ end
400
+
401
+ # The journal's length now, a mark for {#truncate_journal}.
402
+ def journal_size = @journal.size
403
+
404
+ # Drops the journal's records past `mark`, which no capture opened since `mark` is still open to own (G1's
405
+ # stage-4 form drops a recomputation's records, which the served entry's replace).
406
+ def truncate_journal(mark)
407
+ @journal.slice!(mark..)
408
+ nil
409
+ end
410
+
411
+ # Appends `reads` (a read slice, {#slices}) to the journal as the reads they record, so every open capture
412
+ # inherits them: a served hit's upward merge (WD3 step 2).
413
+ def merge_reads(reads)
414
+ return if @marks.empty?
415
+
416
+ index = 0
417
+ while index < reads.size
418
+ @journal.push(:read, reads[index], reads[index + 1], reads[index + 2])
419
+ index += 4
420
+ end
421
+ nil
422
+ end
423
+
424
+ # Puts back, last first, the values `priors` ({#close_priors}) noted, removing an entry noted {ABSENT}: undoes
425
+ # the writes of the computation the window was open around.
426
+ def undo(priors)
427
+ index = priors.size - 3
428
+ while index >= 0
429
+ slot = priors[index]
430
+ key = priors[index + 1]
431
+ prior = priors[index + 2]
432
+ ABSENT.equal?(prior) ? remove(slot, key) : put(slot, key, prior)
433
+ index -= 3
434
+ end
435
+ nil
436
+ end
437
+
438
+ private
439
+
440
+ # Stores `value` at `key` (a journal record's key) in the Table `slot`, creating an unbound table's store.
441
+ def put(slot, key, value)
442
+ declared = TypingState.slots[slot]
443
+ return declared.entries.put(key[0], key[1], value) if declared.bound
444
+
445
+ entries = declared.entries
446
+ entries.put(entries.container(self, slot), key, value)
447
+ end
448
+
449
+ # Removes the entry at `key` (a journal record's key) from the Table `slot`.
450
+ def remove(slot, key)
451
+ declared = TypingState.slots[slot]
452
+ return declared.entries.delete(key[0], key[1]) if declared.bound
453
+
454
+ declared.entries.delete(@values[slot], key)
455
+ end
456
+
457
+ def read_holds?(reads, index)
458
+ current = entry(reads[index], reads[index + 1])
459
+ return true if current.equal?(reads[index + 2])
460
+
461
+ own = reads[index + 3]
462
+ !NONE.equal?(own) && current.equal?(own)
463
+ end
464
+
465
+ # Gives each read-through table's ABSENT read in `reads` the value the computation last wrote at its key.
466
+ def attach_own_writes(reads, written)
467
+ index = 0
468
+ while index < reads.size
469
+ slot = reads[index]
470
+ key = reads[index + 1]
471
+ if ABSENT.equal?(reads[index + 2]) && TypingState.slots[slot].read_through && written.key?(slot, key)
472
+ reads[index + 3] = written.fetch(slot, key)
473
+ end
474
+ index += 4
475
+ end
476
+ end
477
+
478
+ # Notes in every open priors window that has not seen `key` the value `slot` holds there now, or `prior` when the
479
+ # writer passed it.
480
+ def note_prior(slot, key, prior)
481
+ @priors.each do |seen, noted|
482
+ next if seen.key?(slot, key)
483
+
484
+ prior = entry(slot, key) if NONE.equal?(prior)
485
+ seen.store(slot, key, true)
486
+ noted.push(slot, key, prior)
487
+ end
488
+ end
489
+ end
490
+
491
+ # One thread's typing state: the slot values, the state id (WD2), and the journal (WD3).
492
+ #
493
+ # **The state id** is an integer that names the installed Frame values. Every install takes a fresh one, and the
494
+ # frame restores the saved one on the way out. Because Frame values are frozen, equal ids mean equal frames: no
495
+ # interning or hashing. An id-determined Resource (WD3) keeps that property: the guard stack's push and pop each
496
+ # take a fresh id ({#push}, {#pop}), and a rewrite in place ({#rewrite}) also moves the **epoch**, so a frame
497
+ # that saw one exits to a fresh id rather than to its saved one, which named the contents before the rewrite.
498
+ #
499
+ # **The journal** is one flat array of Table reads and writes, four entries per record (`:read` or `:write`, the
500
+ # slot, the key, the value or {ABSENT}). A capture is a start mark in it plus a refusal flag. Records are kept
501
+ # only while a capture is open, and the journal empties when the outermost capture closes.
502
+ class State
503
+ include Slices
504
+
505
+ attr_reader :id
506
+
507
+ def initialize
508
+ @values = []
509
+ @id = 0
510
+ @last_id = 0
511
+ @epoch = 0
512
+ @journal = []
513
+ @marks = []
514
+ @refused = []
515
+ @priors = nil
516
+ end
517
+
518
+ # A Frame or Memo read, or a Table's container.
519
+ def [](slot) = @values[slot]
520
+
521
+ # A Frame read only to decide whether to install it at a constant for the extent that follows (the return memo
522
+ # turning the key-presence walk off): whichever way it goes, that extent reads the constant, so the key audit
523
+ # ({KeyAudit}) does not note it. Any other read goes through {#[]}.
524
+ def forcing_read(slot) = @values[slot]
525
+
526
+ # A Frame read as a term of a memo key (ADR-129 WD1's key audit): the value {#[]} answers. Under the key audit
527
+ # ({KeyAudit}) the read is also noted, so the audit compares what the key carries with what its miss read.
528
+ def key_frame(slot) = @values[slot]
529
+
530
+ # The state id read as a term of a memo key: it names every installed Frame value (WD2), so a key carrying it
531
+ # carries every Frame.
532
+ def key_state_id = @id
533
+
534
+ # A Memo write, or a Table's container (an entry's own write is {#log_write}).
535
+ def []=(slot, value)
536
+ @values[slot] = value
537
+ end
538
+
539
+ # Runs the block with `value` installed in the Frame `slot`, under a fresh state id, and restores both.
540
+ def with(slot, value)
541
+ values = @values
542
+ previous = values[slot]
543
+ saved_id = @id
544
+ saved_epoch = @epoch
545
+ values[slot] = value
546
+ @id = (@last_id += 1)
547
+ begin
548
+ yield
549
+ ensure
550
+ values[slot] = previous
551
+ @id = @epoch == saved_epoch ? saved_id : (@last_id += 1)
552
+ end
553
+ end
554
+
555
+ # {#with} for two Frames installed together, under one fresh id.
556
+ def with_pair(slot, value, other, other_value)
557
+ values = @values
558
+ previous = values[slot]
559
+ other_previous = values[other]
560
+ saved_id = @id
561
+ saved_epoch = @epoch
562
+ values[slot] = value
563
+ values[other] = other_value
564
+ @id = (@last_id += 1)
565
+ begin
566
+ yield
567
+ ensure
568
+ values[slot] = previous
569
+ values[other] = other_previous
570
+ @id = @epoch == saved_epoch ? saved_id : (@last_id += 1)
571
+ end
572
+ end
573
+
574
+ # {#with} for nil installed in each Frame of `slots` (a frozen Array), under one fresh id.
575
+ def without(slots)
576
+ values = @values
577
+ previous = slots.map { |slot| values[slot] }
578
+ saved_id = @id
579
+ saved_epoch = @epoch
580
+ slots.each { |slot| values[slot] = nil }
581
+ @id = (@last_id += 1)
582
+ begin
583
+ yield
584
+ ensure
585
+ index = 0
586
+ while index < slots.size
587
+ values[slots[index]] = previous[index]
588
+ index += 1
589
+ end
590
+ @id = @epoch == saved_epoch ? saved_id : (@last_id += 1)
591
+ end
592
+ end
593
+
594
+ # A Resource write, a read the slot does not exempt, or a write to a `contents: :resource` Frame's container:
595
+ # refuses every open capture (the `any` rule, WD3).
596
+ def touch(_slot) = refuse_captures
597
+
598
+ # Refuses every open capture: a touch, or a miss that wrote a Table ({#memo_miss}).
599
+ def refuse_captures
600
+ refused = @refused
601
+ refused.fill(true) unless refused.empty?
602
+ nil
603
+ end
604
+
605
+ # A Resource read the slot does not exempt.
606
+ def read(slot)
607
+ touch(slot)
608
+ @values[slot]
609
+ end
610
+
611
+ # A Resource write.
612
+ def write(slot, value)
613
+ touch(slot)
614
+ @values[slot] = value
615
+ end
616
+
617
+ # The Resource container in `slot`, created by the block on first use: a read the slot does not exempt, and a
618
+ # write when it creates. Callers mutate the container in place after this touch.
619
+ def acquire(slot)
620
+ touch(slot)
621
+ values = @values
622
+ values[slot] || (values[slot] = yield)
623
+ end
624
+
625
+ # Appends `value` to the append-only Resource log in `slot`, creating it on first use: a write.
626
+ def append(slot, value)
627
+ touch(slot)
628
+ (@values[slot] ||= []) << value
629
+ end
630
+
631
+ # The id-determined Resource stack in `slot`, created empty on first use. Reading it is exempt (WD3: its value
632
+ # is fixed while the id is), and creating it changes no contents; {#push} and {#pop} are its writes.
633
+ def stack(slot) = @values[slot] ||= []
634
+
635
+ # Pushes `value` onto the id-determined Resource stack in `slot`, creating it on first use: a write, under a
636
+ # fresh id.
637
+ def push(slot, value)
638
+ touch(slot)
639
+ @id = (@last_id += 1)
640
+ (@values[slot] ||= []) << value
641
+ end
642
+
643
+ # Pops the id-determined Resource stack in `slot`: a write, under a fresh id. The stack holds what it held
644
+ # before the matching {#push}, so the epoch stays.
645
+ def pop(slot)
646
+ touch(slot)
647
+ @id = (@last_id += 1)
648
+ @values[slot].pop
649
+ end
650
+
651
+ # The contents of the id-determined `slot` (a Resource, or a `contents: :resource` Frame's container) were
652
+ # rewritten in place: a write, under a fresh id, and a new epoch, so no frame open now restores an id that
653
+ # named the old contents.
654
+ def rewrite(slot)
655
+ touch(slot)
656
+ @epoch += 1
657
+ @id = (@last_id += 1)
658
+ nil
659
+ end
660
+
661
+ # The `ObjectSpace::WeakKeyMap` a Table or Memo `slot` holds, created on first use.
662
+ def weak_key_map(slot) = @values[slot] ||= ObjectSpace::WeakKeyMap.new
663
+
664
+ # Logs a Table read of `key` in `slot` that observed `value` ({ABSENT} for a miss), while a capture is open.
665
+ def log_read(slot, key, value)
666
+ @journal.push(:read, slot, key, value) unless @marks.empty?
667
+ nil
668
+ end
669
+
670
+ # Logs a Table write of `value` at `key` in `slot`, while a capture is open. A writer logs before it stores, so
671
+ # while a priors window is open ({#open_priors}) the value the write replaces is still there to note; a writer
672
+ # that knows it (a set-only add, whose prior is {ABSENT}) passes it as `prior` and may log after.
673
+ def log_write(slot, key, value, prior = NONE)
674
+ note_prior(slot, key, prior) if @priors
675
+ @journal.push(:write, slot, key, value) unless @marks.empty?
676
+ nil
677
+ end
678
+
679
+ # Logs a read of `key` in `store`, one of the bound Table `slot`'s stores, that observed `value` ({ABSENT} for a
680
+ # miss), while a capture is open. The record's key is the frozen pair `[store, key]`, built only then, so a run
681
+ # with no capture open allocates nothing.
682
+ def log_bound_read(slot, store, key, value)
683
+ @journal.push(:read, slot, [store, key].freeze, value) unless @marks.empty?
684
+ nil
685
+ end
686
+
687
+ # Logs a write of `value` at `key` in `store`, one of the bound Table `slot`'s stores, while a capture is open,
688
+ # before the store changes or with the replaced value as `prior` ({#log_write}).
689
+ def log_bound_write(slot, store, key, value, prior = NONE)
690
+ return if @marks.empty?
691
+
692
+ pair = [store, key].freeze
693
+ note_prior(slot, pair, prior) if @priors
694
+ @journal.push(:write, slot, pair, value)
695
+ nil
696
+ end
697
+
698
+ # Opens a capture over the journal from here; the innermost open capture closes first.
699
+ def open_capture
700
+ @marks << @journal.size
701
+ @refused << false
702
+ nil
703
+ end
704
+
705
+ # Closes the innermost capture and returns `[refused, records]`: whether a touch refused it, and the journal
706
+ # records logged while it was open, nested captures' included, as a frozen flat array.
707
+ def close_capture
708
+ mark = @marks.pop
709
+ refused = @refused.pop
710
+ records = @journal[mark..].freeze
711
+ @journal.clear if @marks.empty?
712
+ [refused, records]
713
+ end
714
+
715
+ # Whether a capture is open.
716
+ def capturing? = !@marks.empty?
717
+
718
+ # ADR-129 WD1's two Memo rules: runs a miss computation and refuses every open capture when, while a capture was
719
+ # open, it wrote what a later hit of an enclosing entry would replay stale:
720
+ # - a Memo's miss (`own` nil) that wrote an entry of any Table: the Memo, now filled, no longer makes the write,
721
+ # and a newer typing may have overwritten it;
722
+ # - a read-through Table's miss (`own`, its slot) that wrote its own table and another one: reading back its
723
+ # own write validates (WD1's read-through rule) while the other write may have been overwritten (the inject
724
+ # record's miss writes `dynamic_origins` causes). A miss that did not fill its own table leaves its ABSENT
725
+ # read to validate strictly, so its other writes replay only while nothing has changed.
726
+ # The refusal is made on the way out, a raise included, since the journal keeps what a raising computation wrote.
727
+ def memo_miss(own = nil)
728
+ return yield if @marks.empty?
729
+
730
+ mark = @journal.size
731
+ begin
732
+ yield
733
+ ensure
734
+ refuse_captures if wrote_since?(mark, own)
735
+ end
736
+ end
737
+
738
+ # Whether a priors window is open ({#open_priors}).
739
+ def noting_priors? = !@priors.nil?
740
+
741
+ # Whether the journal holds, past `mark`, a write to a Table other than `own` and, when `own` is a slot, a write
742
+ # to `own` too. The journal empties when the outermost capture closes, which a miss computation cannot do to a
743
+ # capture open at its start.
744
+ def wrote_since?(mark, own)
745
+ journal = @journal
746
+ other = false
747
+ filled = own.nil?
748
+ index = mark
749
+ while index < journal.size
750
+ if journal[index] == :write
751
+ journal[index + 1].equal?(own) ? (filled = true) : (other = true)
752
+ return true if other && filled
753
+ end
754
+ index += 4
755
+ end
756
+ false
757
+ end
758
+ private :wrote_since?
759
+
760
+ # Empties the slots at `indices` ({TypingState.release_run_scoped}).
761
+ def release(indices)
762
+ values = @values
763
+ indices.each { |index| values[index] = nil }
764
+ nil
765
+ end
766
+
767
+ # The names of the slots holding a value other than nil or an empty collection: what a finished run left
768
+ # behind. For specs.
769
+ def holding
770
+ TypingState.slots.filter_map do |slot|
771
+ value = @values[slot.index]
772
+ slot.name unless value.nil? || (value.respond_to?(:empty?) && value.empty?)
773
+ end
774
+ end
775
+ end
776
+
777
+ # A set of Table entries named by journal record keys, with a value each: the stores of a bound table by
778
+ # identity (`[store, key]` pairs are never hashed whole, since `Array#hash` would hash a Hash store by its
779
+ # contents), the keys by `eql?` ({StoreEntries}).
780
+ class KeyIndex
781
+ def initialize
782
+ @tables = {}
783
+ end
784
+
785
+ def key?(slot, key)
786
+ keys = keys_for(slot, key, false)
787
+ !keys.nil? && keys.key?(entry_key(slot, key))
788
+ end
789
+
790
+ def fetch(slot, key) = keys_for(slot, key, false).fetch(entry_key(slot, key))
791
+
792
+ def store(slot, key, value)
793
+ keys_for(slot, key, true)[entry_key(slot, key)] = value
794
+ end
795
+
796
+ private
797
+
798
+ def entry_key(slot, key) = TypingState.slots[slot].bound ? key[1] : key
799
+
800
+ def keys_for(slot, key, create)
801
+ tables = @tables
802
+ if TypingState.slots[slot].bound
803
+ stores = tables[slot] || (create ? (tables[slot] = {}.compare_by_identity) : nil)
804
+ return nil if stores.nil?
805
+
806
+ stores[key[0]] || (create ? (stores[key[0]] = {}) : nil)
807
+ else
808
+ tables[slot] || (create ? (tables[slot] = {}) : nil)
809
+ end
810
+ end
811
+ end
812
+
813
+ # ADR-129 G1′'s audit of the Frame declarations, on one {State}: `MemoAudit` extends a thread's state with this
814
+ # module the first time an audited run enters `ExpressionTyper#type_of` ({MemoAudit.enter_type_of}), so a run with
815
+ # the audit off reads a slot through the plain {State#[]}. It asserts three of the four declarations, each as a
816
+ # {MemoAudit::Mismatch} on the thread's list (not raised):
817
+ # - `typing_unreachable`: a `type_of` entered while such a Frame holds a value;
818
+ # - `typing_never_reads`: a read of such a Frame ({State#[]}, {State#key_frame}, {State#forcing_read}) while a
819
+ # `type_of` is running;
820
+ # - `memo_shut_while_installed`: the `type_of` memo's shadow ({TypeOfMemo.shadow}) reaching its table while such a
821
+ # Frame holds a value ({MemoAudit.note_memo_lookup}).
822
+ #
823
+ # **What this module hooks.** The three Frame accessors are the reads a typed answer can come through: {State#[]},
824
+ # {State#key_frame} and {State#forcing_read}. A new accessor that reads `@values` of a Frame must hook here too, or
825
+ # a `typing_never_reads` Frame can be read past the audit. The declared slot lists are a snapshot taken when the
826
+ # state is extended: every Frame is declared at require time, before any audited `type_of`, so a Frame declared
827
+ # later (a spec's probe) is not asserted.
828
+ module FrameAudit
829
+ # How many `type_of`s are running on this thread, for specs that observe the reads inside them.
830
+ attr_reader :type_of_depth
831
+
832
+ def self.extended(state) = state.send(:start_frame_audit)
833
+
834
+ def [](slot)
835
+ note_typing_read(slot) if @type_of_depth.positive?
836
+ super
837
+ end
838
+
839
+ def key_frame(slot)
840
+ note_typing_read(slot) if @type_of_depth.positive?
841
+ super
842
+ end
843
+
844
+ def forcing_read(slot)
845
+ note_typing_read(slot) if @type_of_depth.positive?
846
+ super
847
+ end
848
+
849
+ # A `type_of` begins: fails on an installed `typing_unreachable` Frame, and opens the extent in which reads of a
850
+ # `typing_never_reads` Frame fail.
851
+ def enter_type_of
852
+ @unreachable.each do |slot|
853
+ next if @values[slot].nil?
854
+
855
+ MemoAudit.report_frame_declaration(
856
+ self, "frame audit: type_of ran while the typing_unreachable Frame #{TypingState.slots[slot].name} " \
857
+ "is installed"
858
+ )
859
+ end
860
+ @type_of_depth += 1
861
+ end
862
+
863
+ # The `type_of` memo's shadow reached its table: fails on an installed `memo_shut_while_installed` Frame.
864
+ def note_memo_lookup
865
+ @memo_shut.each do |slot|
866
+ next if @values[slot].nil?
867
+
868
+ MemoAudit.report_frame_declaration(
869
+ self, "frame audit: the type_of memo ran while the memo_shut_while_installed Frame " \
870
+ "#{TypingState.slots[slot].name} is installed"
871
+ )
872
+ end
873
+ nil
874
+ end
875
+
876
+ # The matching `type_of` ends.
877
+ def leave_type_of
878
+ @type_of_depth -= 1
879
+ end
880
+
881
+ private
882
+
883
+ def start_frame_audit
884
+ @type_of_depth = 0
885
+ @unreachable = TypingState.g1_slots(:typing_unreachable)
886
+ @never_reads = TypingState.g1_slots(:typing_never_reads)
887
+ @memo_shut = TypingState.g1_slots(:memo_shut_while_installed)
888
+ end
889
+
890
+ def note_typing_read(slot)
891
+ return unless @never_reads.include?(slot)
892
+
893
+ MemoAudit.report_frame_declaration(
894
+ self, "frame audit: the typing_never_reads Frame #{TypingState.slots[slot].name} was read inside type_of"
895
+ )
896
+ end
897
+ end
898
+
899
+ # ADR-129 WD2's ELIDE, measured by stage 3's shadow only: a second id that, unlike the state id, stays put when an
900
+ # install puts back the value the slot already holds (`equal?`), so two frames that differ only by such an install
901
+ # share it. Every other install, every id-determined Resource change and every rewrite moves it exactly as the
902
+ # state id moves, so equal ELIDE ids still name equal Frame values. `TypeOfMemo` extends a thread's state with this
903
+ # module when the measurement is on and keys on {#elide_id}; the state id, which other memos key on, is unchanged.
904
+ module ElideId
905
+ attr_reader :elide_id
906
+
907
+ def self.extended(state) = state.restart_elide_id
908
+
909
+ # A fresh ELIDE id, naming the Frame values installed now.
910
+ def restart_elide_id
911
+ @last_elide_id = (@last_elide_id || 0) + 1
912
+ @elide_id = @last_elide_id
913
+ nil
914
+ end
915
+
916
+ def with(slot, value)
917
+ values = @values
918
+ elide_install(values[slot].equal?(value)) { super }
919
+ end
920
+
921
+ def with_pair(slot, value, other, other_value)
922
+ values = @values
923
+ elide_install(values[slot].equal?(value) && values[other].equal?(other_value)) { super }
924
+ end
925
+
926
+ def without(slots)
927
+ values = @values
928
+ elide_install(slots.all? { |slot| values[slot].nil? }) { super }
929
+ end
930
+
931
+ def push(slot, value)
932
+ result = super
933
+ @elide_id = (@last_elide_id += 1)
934
+ result
935
+ end
936
+
937
+ def pop(slot)
938
+ result = super
939
+ @elide_id = (@last_elide_id += 1)
940
+ result
941
+ end
942
+
943
+ def rewrite(slot)
944
+ super
945
+ @elide_id = (@last_elide_id += 1)
946
+ nil
947
+ end
948
+
949
+ private
950
+
951
+ # Runs the block, an install, under a fresh ELIDE id unless the install puts back what the slots hold, and
952
+ # restores the id on the way out unless an id-determined Resource was rewritten inside it.
953
+ def elide_install(same)
954
+ saved = @elide_id
955
+ saved_epoch = @epoch
956
+ @elide_id = (@last_elide_id += 1) unless same
957
+ begin
958
+ yield
959
+ ensure
960
+ @elide_id = @epoch == saved_epoch ? saved : (@last_elide_id += 1)
961
+ end
962
+ end
963
+ end
964
+
965
+ # ADR-129 WD1's key audit, on one {State}: `MemoAudit` extends a thread's state with this module the first time an
966
+ # audited run reaches a memo miss, so a run with the audit off reads a slot through the plain {State#[]}.
967
+ #
968
+ # While a miss computation runs ({#open_key_audit} .. {#close_key_audit}, nested for a miss inside a miss) every
969
+ # Frame read is noted on each open audit, except a read of a Frame installed since that audit opened: the
970
+ # computation installed it itself, so it is internal (the return memo's `with_return_sink`). A read an inner audit
971
+ # declares a known gap reaches the audits around it as that gap, named through the inner audit's label: the outer
972
+ # value is computed from the inner one. {#collect_key_terms} notes the Frames a key builder reads through
973
+ # {State#key_frame} and {State#key_state_id}.
974
+ module KeyAudit
975
+ # The key term {State#key_state_id} notes: the id names every installed Frame.
976
+ STATE_ID = :state_id
977
+
978
+ def self.extended(state) = state.send(:start_key_audit)
979
+
980
+ def [](slot)
981
+ note_frame_read(slot) unless @key_audits.empty?
982
+ super
983
+ end
984
+
985
+ def key_frame(slot)
986
+ @key_terms&.push(slot)
987
+ note_frame_read(slot) unless @key_audits.empty?
988
+ super
989
+ end
990
+
991
+ def key_state_id
992
+ @key_terms&.push(STATE_ID)
993
+ super
994
+ end
995
+
996
+ def with(slot, value)
997
+ @key_installs.push(slot)
998
+ begin
999
+ super
1000
+ ensure
1001
+ @key_installs.pop
1002
+ end
1003
+ end
1004
+
1005
+ def with_pair(slot, value, other, other_value)
1006
+ installs = @key_installs
1007
+ installs.push(slot, other)
1008
+ begin
1009
+ super
1010
+ ensure
1011
+ installs.pop(2)
1012
+ end
1013
+ end
1014
+
1015
+ def without(slots)
1016
+ installs = @key_installs
1017
+ installs.concat(slots)
1018
+ begin
1019
+ super
1020
+ ensure
1021
+ installs.pop(slots.size)
1022
+ end
1023
+ end
1024
+
1025
+ # Runs the block, a key builder, and remembers the key it answers with the Frame terms it read, for the
1026
+ # {#key_terms_for} of the miss that follows. Answers the key.
1027
+ def collect_key_terms
1028
+ saved = @key_terms
1029
+ terms = @key_terms = []
1030
+ key = yield
1031
+ @last_key = [key, terms.uniq.freeze]
1032
+ key
1033
+ ensure
1034
+ @key_terms = saved
1035
+ end
1036
+
1037
+ # The Frame terms (slot indices, or {STATE_ID}) the key builder of `key` read, or an empty list when `key` is not
1038
+ # the last key {#collect_key_terms} built (a memo whose key reads no Frame passes nil).
1039
+ def key_terms_for(key)
1040
+ last = @last_key
1041
+ last && !key.nil? && last[0].equal?(key) ? last[1] : EMPTY_TERMS
1042
+ end
1043
+
1044
+ # Opens the audit of the miss computation `label` names, whose site declares `gaps` (slot name => reason).
1045
+ def open_key_audit(label, gaps)
1046
+ @key_audits.push([@key_installs.size, {}, label, gaps])
1047
+ nil
1048
+ end
1049
+
1050
+ # Closes the innermost audit and answers what its computation read from outside it: `slot => true` for a read of
1051
+ # its own, or `slot => [inner label, reason]` for one an audit nested in it declared a gap.
1052
+ def close_key_audit
1053
+ @key_audits.pop[1]
1054
+ end
1055
+
1056
+ EMPTY_TERMS = [].freeze
1057
+ private_constant :EMPTY_TERMS
1058
+
1059
+ private
1060
+
1061
+ def start_key_audit
1062
+ @key_audits = []
1063
+ @key_installs = []
1064
+ @key_terms = nil
1065
+ @last_key = nil
1066
+ end
1067
+
1068
+ # Innermost audit first: an audit whose computation installed the slot, and every audit around it, read it
1069
+ # internally.
1070
+ def note_frame_read(slot)
1071
+ declared = TypingState.slots[slot]
1072
+ return unless declared.kind == :frame
1073
+
1074
+ at = @key_installs.rindex(slot)
1075
+ audits = @key_audits
1076
+ gap = nil
1077
+ index = audits.size - 1
1078
+ while index >= 0
1079
+ mark, reads, label, gaps = audits[index]
1080
+ break if at && at >= mark
1081
+
1082
+ if gap
1083
+ reads[slot] ||= gap
1084
+ else
1085
+ reads[slot] = true
1086
+ reason = gaps[declared.name]
1087
+ gap = [label, reason].freeze if reason
1088
+ end
1089
+ index -= 1
1090
+ end
1091
+ end
1092
+ end
1093
+ end
1094
+ end