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
@@ -149,6 +149,38 @@ module Rigor
149
149
  next_step: "Generate signatures for the overriding subclass too — widen the `sig-gen` path set to " \
150
150
  "cover it — and the parent's declaration becomes safe to emit."
151
151
  ),
152
+ "sig.skipped.overridden-bot" => Entry.new(
153
+ id: "sig.skipped.overridden-bot",
154
+ summary: "The method's body never returns, and the analysed code overrides the method.",
155
+ explanation: "The inferred return is `bot`, but the analysed code defines the method again, with `def` " \
156
+ "or `define_method`: a subclass (signed or not), a class that includes the method's module, " \
157
+ "a module included later or prepended, or an extending class's own singleton method. " \
158
+ "`bot` describes only the base body the overrides replace, and every override returns a " \
159
+ "value, so the declaration would contradict each of them (#3029).",
160
+ next_step: "Declare the return every implementation shares (often `untyped` or the overrides' common " \
161
+ "type) by hand in `sig/`."
162
+ ),
163
+ "sig.skipped.abstract-method" => Entry.new(
164
+ id: "sig.skipped.abstract-method",
165
+ summary: "The method is abstract: its body only raises an error asking for an implementation.",
166
+ explanation: "The body ends in `raise NotImplementedError`, in an error class named `Abstract...` or " \
167
+ "`NotImplemented...`, or in a raise whose message asks for the method to be implemented, " \
168
+ "defined or overridden. Its inferred return is `bot`, but the method is meant to be " \
169
+ "replaced, and the replacements may live outside the analysed code, as in a plugin gem " \
170
+ "(#3029).",
171
+ next_step: "Declare the return the implementations must provide by hand in `sig/`."
172
+ ),
173
+ "sig.skipped.template-method" => Entry.new(
174
+ id: "sig.skipped.template-method",
175
+ summary: "The method's `bot` return may come only from calling an overridden or abstract method.",
176
+ explanation: "The inferred return is `bot`, and the body does not end in a direct `raise`, `fail`, " \
177
+ "`throw`, `exit` or `abort`. It calls a method the analysed code overrides or declares " \
178
+ "abstract, directly or through a chain of self-dispatched calls, so the `bot` may be only " \
179
+ "the base body of that call, which an override replaces at run time. The call is matched " \
180
+ "by name, whatever its receiver (#3029).",
181
+ next_step: "Declare the return by hand in `sig/`, usually the return the overridden hook's " \
182
+ "implementations share."
183
+ ),
152
184
  "sig.skipped.top-level-def" => Entry.new(
153
185
  id: "sig.skipped.top-level-def",
154
186
  summary: "A top-level `def` defines a private `Object` method, and sig-gen writes no signature for it.",
@@ -171,6 +203,21 @@ module Rigor
171
203
  "writes neither rather than guess (#1968).",
172
204
  next_step: "Move the visibility call to the module body's own statements, or name the methods it " \
173
205
  "should cover (`public :name`, `module_function :name`), so the reading is certain."
206
+ ),
207
+ "sig.skipped.platform-guarded" => Entry.new(
208
+ id: "sig.skipped.platform-guarded",
209
+ summary: "The method is defined in a file loaded only under a platform guard, so its proposal was withheld.",
210
+ explanation: "The file is `require`d / `require_relative`d only inside a conditional on " \
211
+ "`RUBY_ENGINE`, `RUBY_PLATFORM` or `defined?(…)`, so it is not an ordinary definition " \
212
+ "for every platform — the shape is `if RUBY_ENGINE == 'truffleruby'` guarding a " \
213
+ "pure-Ruby fallback whose real methods a C extension provides elsewhere. sig-gen " \
214
+ "withholds the proposal when another scanned file also defines the method (the " \
215
+ "guarded copy would compete with or duplicate the definition this platform loads), " \
216
+ "or when the inferred return is `bot` — the shape a raising stub takes, and never a " \
217
+ "contract worth writing from a file the current engine may never read (#3035).",
218
+ next_step: "Declare the method for the platform the project targets — hand-write the `sig/` " \
219
+ "entry, or cover the file that defines it on this platform in the `sig-gen` paths — " \
220
+ "rather than taking the guarded file's stub as the contract."
174
221
  )
175
222
  }.freeze
176
223
 
@@ -38,6 +38,21 @@ module Rigor
38
38
  :target, keyword_init: true)
39
39
  private_constant :MergeState
40
40
 
41
+ # #2576 — a target the writer cannot write (a permission denial, a directory sitting where the
42
+ # `.rbs` belongs, a parent path that is a regular file) is a bad `--write` invocation, not a
43
+ # crash: the command
44
+ # reports it as the same usage error `rigor init --path` gives and exits 64. Carries the target
45
+ # and the stripped Errno reason so the CLI prints `cannot write PATH: REASON` without
46
+ # re-parsing the message.
47
+ class WriteError < StandardError
48
+ attr_reader :target_path
49
+
50
+ def initialize(target_path, error)
51
+ @target_path = target_path.to_s
52
+ super("cannot write #{@target_path}: #{error.message.sub(/ @ .*\z/m, '')}")
53
+ end
54
+ end
55
+
41
56
  # The target routing this writer applies; the generator maps candidates through it to ask {#accepts_target?}.
42
57
  attr_reader :path_mapper
43
58
 
@@ -186,6 +201,8 @@ module Rigor
186
201
  end
187
202
 
188
203
  target.exist? ? update_existing(source_path, target, candidates) : create_new(source_path, target, candidates)
204
+ rescue SystemCallError => e
205
+ raise WriteError.new(target, e)
189
206
  end
190
207
 
191
208
  # Deliberately NARROWER than {Classification::EMITTABLE}: the write path has never produced a
@@ -222,6 +239,8 @@ module Rigor
222
239
  end
223
240
  WriteResult.new(source_path: source_path, target_path: target,
224
241
  action: :created, applied: candidates)
242
+ rescue SystemCallError => e
243
+ raise WriteError.new(target, e)
225
244
  end
226
245
 
227
246
  # ADR-14 gap-#3 follow-up (c): when one candidate's `class_name` is a strict prefix of another's, emit a
@@ -429,6 +448,8 @@ module Rigor
429
448
  action: :updated, applied: state.applied, skipped: state.skipped,
430
449
  left_unreadable: state.left_unreadable, replaced: state.replaced,
431
450
  repaired_headers: state.repaired_headers)
451
+ rescue SystemCallError => e
452
+ raise WriteError.new(target, e)
432
453
  end
433
454
 
434
455
  # Applies every class group, then every requested shell, then normalises the layout the three steps
data/lib/rigor/sig_gen.rb CHANGED
@@ -5,6 +5,7 @@ require_relative "sig_gen/classification"
5
5
  require_relative "sig_gen/effect_annotation"
6
6
  require_relative "sig_gen/inline_declarations"
7
7
  require_relative "sig_gen/method_candidate"
8
+ require_relative "sig_gen/platform_guarded_files"
8
9
  require_relative "sig_gen/observed_call"
9
10
  require_relative "sig_gen/meta_class_shape"
10
11
  require_relative "sig_gen/type_elaborator"
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+ require_relative "literals"
5
+
6
+ module Rigor
7
+ module Source
8
+ # Issue #2793 — the methods a `Forwardable` / `SingleForwardable` delegator macro defines, read off its syntax
9
+ # alone. Ruby 4.0's `forwardable.rb` is the oracle for each row:
10
+ #
11
+ # - `def_instance_delegator(acc, m, ali = m)` defines `ali` on the instance side;
12
+ # - `def_instance_delegators(acc, *ms)` defines each `m` but `__send__` and `__id__`, instance side (the two are
13
+ # recorded anyway: `BasicObject` declares both, so a recorded entry changes no answer);
14
+ # - `instance_delegate(ms => acc)` defines each key name (a name or an array of them), instance side;
15
+ # - `def_single_delegator(s)` and `single_delegate` are the same on the singleton side;
16
+ # - `def_delegator(s)` and `delegate` are the same on the side the `extend` decides.
17
+ #
18
+ # `def_delegator(s)` and `delegate` are `Forwardable`'s instance aliases and `SingleForwardable`'s singleton
19
+ # ones, so their side is {AMBIGUOUS} and the caller settles it from the `extend` it has seen. `def_delegator :@x,
20
+ # :last, :final` defines `final` and never `last`.
21
+ #
22
+ # A name the syntax does not spell (`def_delegators :@x, *NAMES`, `delegate names => :@x`) makes the delegation
23
+ # {Delegation#open?}: the macro defines methods no reader can enumerate. `delegate` takes Forwardable's
24
+ # single-hash form only, so ActiveSupport's `delegate :a, to: :b` (positional names before a `to:` key) is
25
+ # not read here.
26
+ module ForwardableDelegators
27
+ AMBIGUOUS = :ambiguous
28
+
29
+ # The names a delegator macro defines, on which side, and whether it also defines names its syntax hides.
30
+ Delegation = Data.define(:side, :names, :open) do
31
+ def open? = open
32
+ end
33
+
34
+ # `[side, shape]` per macro: `:alias` takes `(accessor, method, alias = method)`, `:list` takes `(accessor,
35
+ # *methods)`, `:hash` takes `(methods => accessor, …)`.
36
+ MACROS = Ractor.make_shareable(
37
+ {
38
+ def_instance_delegator: %i[instance alias], def_instance_delegators: %i[instance list],
39
+ instance_delegate: %i[instance hash],
40
+ def_single_delegator: %i[singleton alias], def_single_delegators: %i[singleton list],
41
+ single_delegate: %i[singleton hash],
42
+ def_delegator: [AMBIGUOUS, :alias], def_delegators: [AMBIGUOUS, :list], delegate: [AMBIGUOUS, :hash]
43
+ }
44
+ )
45
+
46
+ private_constant :MACROS
47
+
48
+ module_function
49
+
50
+ # The {Delegation} of an implicit-self (or `self.`) delegator macro call, or nil for any other call, and for
51
+ # one whose arguments are not a shape the macro accepts.
52
+ def of(call_node)
53
+ return nil unless call_node.is_a?(Prism::CallNode)
54
+ return nil unless call_node.receiver.nil? || call_node.receiver.is_a?(Prism::SelfNode)
55
+
56
+ side, shape = MACROS[call_node.name]
57
+ return nil if side.nil?
58
+
59
+ arguments = call_node.arguments&.arguments || []
60
+ return Delegation.new(side: side, names: [].freeze, open: true) if unplaceable_splat?(shape, arguments)
61
+
62
+ names, open = case shape
63
+ when :alias then alias_names(arguments)
64
+ when :list then list_names(arguments)
65
+ else hash_names(arguments)
66
+ end
67
+ return nil if names.nil?
68
+
69
+ Delegation.new(side: side, names: names.freeze, open: open)
70
+ end
71
+
72
+ # A splat (`def_delegator(*ARGS)`, `def_delegators(*ARGS)`, `delegate(*pairs)`) hides which argument is the
73
+ # accessor and which a name, so the macro may define any name. A splat after the accessor of the list form is
74
+ # only a hidden name, and {#list_names} reads the literal names beside it.
75
+ def unplaceable_splat?(shape, arguments)
76
+ splats = arguments.each_index.select { |index| arguments[index].is_a?(Prism::SplatNode) }
77
+ return false if splats.empty?
78
+
79
+ shape != :list || splats.include?(0)
80
+ end
81
+
82
+ def alias_names(arguments)
83
+ return nil unless (2..3).cover?(arguments.size)
84
+
85
+ name = Literals.symbol_or_string(arguments.last)
86
+ name.nil? ? [[], true] : [[name], false]
87
+ end
88
+
89
+ def list_names(arguments)
90
+ return nil if arguments.empty?
91
+
92
+ rest = arguments.drop(1)
93
+ names = rest.filter_map { |argument| Literals.symbol_or_string(argument) }
94
+ [names, names.size != rest.size]
95
+ end
96
+
97
+ def hash_names(arguments)
98
+ return nil unless arguments.size == 1
99
+
100
+ hash = arguments.first
101
+ return nil unless hash.is_a?(Prism::KeywordHashNode) || hash.is_a?(Prism::HashNode)
102
+
103
+ names = []
104
+ open = false
105
+ hash.elements.each do |element|
106
+ return [[], true] unless element.is_a?(Prism::AssocNode)
107
+
108
+ keys = key_names(element.key)
109
+ keys.nil? ? (open = true) : names.concat(keys)
110
+ end
111
+ [names, open]
112
+ end
113
+
114
+ # The names a hash key spells — a literal name, or an array of them — or nil when any is computed.
115
+ def key_names(key)
116
+ elements = key.is_a?(Prism::ArrayNode) ? key.elements : [key]
117
+ names = elements.map { |element| Literals.symbol_or_string(element) }
118
+ names.include?(nil) ? nil : names
119
+ end
120
+ end
121
+ end
122
+ end
@@ -109,6 +109,19 @@ module Rigor
109
109
  def each_child(node, &)
110
110
  node.rigor_each_child(&) if node.is_a?(Prism::Node)
111
111
  end
112
+
113
+ # #2455 — whether `root` nests deeper than `bound` levels, measured without recursion (the question is asked after
114
+ # the recursive walkers ran out of stack). Stops as soon as one path passes the bound.
115
+ def self.nested_deeper_than?(root, bound)
116
+ stack = [[root, 1]]
117
+ until stack.empty?
118
+ node, depth = stack.pop
119
+ return true if depth > bound
120
+
121
+ node.rigor_each_child { |child| stack << [child, depth + 1] }
122
+ end
123
+ false
124
+ end
112
125
  end
113
126
  end
114
127
  end
@@ -46,6 +46,27 @@ module Rigor
46
46
  @untyped
47
47
  end
48
48
 
49
+ # ADR-130 WD2 — the parity predicate. Every site that keys a leniency on `untyped` asks this instead of testing
50
+ # a `Dynamic`'s facet for `Top` or comparing with {untyped}, so a value whose facet parity erases
51
+ # ({erased_facet?}) reads as `untyped` everywhere but member dispatch. `spec/rigor/type/untyped_parity_spec.rb`
52
+ # flags the raw spellings it knows outside this module. True for `Dynamic[top]` and for a plugin-interface
53
+ # value, `Dynamic[Rigor::Plugin::_Scope]`.
54
+ def untyped?(type)
55
+ return false unless type.is_a?(Dynamic)
56
+
57
+ facet = type.static_facet
58
+ facet.is_a?(Top) || erased_facet?(facet)
59
+ end
60
+
61
+ # ADR-130 WD1 — true for a `Dynamic` facet that parity reads as `top`: the `Rigor::Plugin` interface carrier
62
+ # ({Interface}). {untyped?} answers true for it, and {merge_erased_members} merges it with `Dynamic[top]`.
63
+ # `spec/rigor/type/combinator_facet_erasure_spec.rb` also drives the merge through a stub facet. An engine
64
+ # hook, not public API.
65
+ def erased_facet?(facet)
66
+ facet.is_a?(Interface)
67
+ end
68
+ private_class_method :erased_facet?
69
+
49
70
  # Wraps the static facet in a Dynamic[T] carrier. Idempotent on the static facet so
50
71
  # Dynamic[Dynamic[T]] collapses to Dynamic[T] per the value-lattice algebra.
51
72
  def dynamic(static_facet)
@@ -821,7 +842,104 @@ module Rigor
821
842
 
822
843
  return [top] if flattened.any?(Top)
823
844
 
824
- absorb_contained_members(unique_members(flattened))
845
+ absorb_contained_members(merge_erased_members(unique_members(flattened)))
846
+ end
847
+
848
+ # ADR-130 WD1 — a facet parity erases ({erased_facet?}) must not give a join a shape master's dedup would
849
+ # not: `Dynamic[_A] | Dynamic[top]` is `Dynamic[top]`, and so are the same members anywhere inside a carrier
850
+ # that holds a type ({TYPE_CHILDREN}). Members equal after {erase_facets} merge into the erased form, at the
851
+ # first one's position; a member no other matches keeps its facet, and {absorb_contained_members} compares
852
+ # the erased forms too. The facet scan runs on every join and allocates nothing (no enumerator, no array),
853
+ # so the merge costs only where a facet is present, which is nowhere until ADR-130 slice 1b.
854
+ def merge_erased_members(members)
855
+ return members if members.size < 2
856
+ return members unless members.any? { |member| facet_bearing?(member) }
857
+
858
+ erased = members.map { |member| erase_facets(member) }
859
+ counts = Hash.new(0)
860
+ erased.each { |type| counts[type] += 1 }
861
+ merged = {}
862
+ result = []
863
+ members.each_with_index do |member, index|
864
+ type = erased[index]
865
+ if counts[type] == 1
866
+ result << member
867
+ elsif !merged.key?(type)
868
+ merged[type] = true
869
+ result << type
870
+ end
871
+ end
872
+ result
873
+ end
874
+
875
+ def facet_bearing?(type)
876
+ return erased_facet?(type.static_facet) || facet_bearing?(type.static_facet) if type.is_a?(Dynamic)
877
+
878
+ readers = TYPE_CHILDREN[type.class]
879
+ return false if readers.nil?
880
+
881
+ readers.each { |reader| return true if child_facet_bearing?(type.public_send(reader)) }
882
+ false
883
+ end
884
+
885
+ def child_facet_bearing?(child)
886
+ case child
887
+ when Array then child.any? { |type| facet_bearing?(type) }
888
+ when Hash
889
+ child.each_value { |type| return true if facet_bearing?(type) }
890
+ false
891
+ else facet_bearing?(child)
892
+ end
893
+ end
894
+
895
+ # `type` with every facet {erased_facet?} accepts rewritten to `top`, through every carrier
896
+ # {TYPE_CHILDREN} lists. The identity, by reference, on a type that bears none. A facet-bearing `Dynamic`
897
+ # is never `Dynamic[top]`, so {untyped?} answers the hook for it here.
898
+ def erase_facets(type)
899
+ return type unless facet_bearing?(type)
900
+
901
+ case type
902
+ when Dynamic then untyped?(type) ? untyped : dynamic(erase_facets(type.static_facet))
903
+ when Union then union(*erase_each(type.members))
904
+ when Intersection then intersection(*erase_each(type.members))
905
+ when Tuple then tuple_of(*erase_each(type.elements))
906
+ when Nominal then nominal_of(type.class_name, type_args: erase_each(type.type_args))
907
+ when HashShape then erase_shape_facets(type)
908
+ else erase_wrapper_facets(type)
909
+ end
910
+ end
911
+
912
+ def erase_wrapper_facets(type)
913
+ case type
914
+ when Difference then Difference.new(erase_facets(type.base), erase_facets(type.removed))
915
+ when Refined then Refined.new(erase_facets(type.base), type.predicate_id)
916
+ when Maybe then Maybe.new(erase_facets(type.value_type))
917
+ when Result then Result.new(erase_facets(type.ok_type), erase_facets(type.err_type))
918
+ else erase_bearer_facets(type)
919
+ end
920
+ end
921
+
922
+ def erase_bearer_facets(type)
923
+ case type
924
+ when App then App.new(type.uri, erase_each(type.args), bound: erase_facets(type.bound))
925
+ when BoundMethod
926
+ BoundMethod.new(receiver_type: erase_facets(type.receiver_type), method_name: type.method_name)
927
+ when DataInstance then DataInstance.new(erase_values(type.members), type.class_name)
928
+ when StructInstance then StructInstance.new(erase_values(type.members), type.class_name)
929
+ # The facet itself, erased apart from its `Dynamic` (which erases whole, to `untyped`).
930
+ when Interface then Interface.new(type.name, erase_each(type.type_args))
931
+ else type
932
+ end
933
+ end
934
+
935
+ def erase_each(types) = types.map { |type| erase_facets(type) }
936
+
937
+ def erase_values(pairs) = pairs.transform_values { |type| erase_facets(type) }
938
+
939
+ def erase_shape_facets(shape)
940
+ HashShape.new(erase_values(shape.pairs),
941
+ required_keys: shape.required_keys, optional_keys: shape.optional_keys,
942
+ read_only_keys: shape.read_only_keys, extra_keys: shape.extra_keys)
825
943
  end
826
944
 
827
945
  # Drops every member another member already contains, so the join after a guard names one set
@@ -835,15 +953,30 @@ module Rigor
835
953
  # The `!absorbed_by?(other, member)` re-check costs one predicate call per pair that absorbed
836
954
  # at all, and it is what makes "both arms dropped" unrepresentable should a future clause make
837
955
  # the relation symmetric on a pair of distinct members.
956
+ #
957
+ # ADR-130 WD1 — the relation reads each member's erased form ({erase_facets}), so a facet does not keep an
958
+ # arm master absorbs: `[Dynamic[_A]] | [Dynamic[top] | Integer]` drops the first arm as master's
959
+ # `[Dynamic[top]] | [Dynamic[top] | Integer]` does. Without a facet the readings are the members themselves.
838
960
  def absorb_contained_members(members)
839
961
  return members unless members.any? { |m| absorbable_member?(m) }
840
962
 
841
963
  members = join_sequence_elements(members)
842
- members.reject do |member|
843
- absorbable_member?(member) &&
844
- members.any? do |other|
845
- !other.equal?(member) && absorbed_by?(member, other) && !absorbed_by?(other, member)
846
- end
964
+ readings = members.any? { |member| facet_bearing?(member) } ? members.map { |m| erase_facets(m) } : members
965
+ # `each_index`, not `each_with_index`: the Enumerable form allocates an iterator object on every join.
966
+ kept = []
967
+ members.each_index do |index|
968
+ member = members[index]
969
+ kept << member unless absorbable_member?(member) && absorbed_reading?(readings, index)
970
+ end
971
+ kept
972
+ end
973
+
974
+ # `any?` rather than an early `return` from an `each` block: a non-local return allocates on every absorbed
975
+ # member. Readings are distinct objects (the members were made unique, and an erased form is a new object).
976
+ def absorbed_reading?(readings, index)
977
+ reading = readings[index]
978
+ readings.any? do |wider|
979
+ !wider.equal?(reading) && absorbed_by?(reading, wider) && !absorbed_by?(wider, reading)
847
980
  end
848
981
  end
849
982
 
@@ -1040,10 +1173,18 @@ module Rigor
1040
1173
  # need not render alike (`Constant[1..2]` and `Constant[1..2.0]`), so equality is not a safe key. A
1041
1174
  # non-main Ractor may not read the module's ivar, so a pool worker renders the key every time, as every
1042
1175
  # run did before.
1176
+ #
1177
+ # ADR-130 WD1 — a member bearing a facet sorts by its erased form's rendering, so a facet does not move it:
1178
+ # `Dynamic[_A] | Dynamic[String]` keeps master's `Dynamic[String] | Dynamic[top]` order. The facet scan runs
1179
+ # only when a key is rendered, and a member without a facet renders as it always did.
1043
1180
  def sort_key(member)
1044
- return member.describe(:short) unless Ractor.main?
1181
+ return render_sort_key(member) unless Ractor.main?
1045
1182
 
1046
- @sort_keys[member] ||= member.describe(:short)
1183
+ @sort_keys[member] ||= render_sort_key(member)
1184
+ end
1185
+
1186
+ def render_sort_key(member)
1187
+ (facet_bearing?(member) ? erase_facets(member) : member).describe(:short)
1047
1188
  end
1048
1189
  end
1049
1190
 
@@ -1052,6 +1193,21 @@ module Rigor
1052
1193
  # The main Ractor's {sort_key} memo, allocated here for the same reason.
1053
1194
  @sort_keys = ObjectSpace::WeakMap.new
1054
1195
 
1196
+ # ADR-130 WD1 — the readers of every type a carrier holds, which facet erasure walks. A leaf lists none.
1197
+ # `spec/rigor/type/combinator_facet_erasure_spec.rb` fails on a `Rigor::Type` carrier missing here, and on a
1198
+ # listed carrier whose erasure leaves a facet in any of its type slots.
1199
+ TYPE_CHILDREN = Ractor.make_shareable(
1200
+ {
1201
+ Top => [], Bot => [], Constant => [], IntegerRange => [], FloatRange => [], Singleton => [],
1202
+ DataClass => [], StructClass => [],
1203
+ Dynamic => %i[static_facet], Nominal => %i[type_args], Tuple => %i[elements], HashShape => %i[pairs],
1204
+ Union => %i[members], Intersection => %i[members], Difference => %i[base removed], Refined => %i[base],
1205
+ Maybe => %i[value_type], Result => %i[ok_type err_type], App => %i[args bound],
1206
+ BoundMethod => %i[receiver_type], DataInstance => %i[members], StructInstance => %i[members],
1207
+ Interface => %i[type_args]
1208
+ }
1209
+ )
1210
+
1055
1211
  NIL_CONSTANT = Constant.new(nil)
1056
1212
  TRUE_CONSTANT = Constant.new(true)
1057
1213
  FALSE_CONSTANT = Constant.new(false)
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../trinary"
4
+ require_relative "../value_semantics"
5
+ require_relative "acceptance_router"
6
+ require_relative "plain_lattice"
7
+
8
+ module Rigor
9
+ module Type
10
+ # ADR-130 WD1 — an RBS interface under `Rigor::Plugin` (`_Scope`, `_Environment`, ...), as the facet of the
11
+ # `Dynamic` that carries a plugin-interface value: `Dynamic[Rigor::Plugin::_Scope]`. It appears only there.
12
+ # `Combinator.untyped?` reads such a `Dynamic` as `untyped`, and union normalisation erases it to `Dynamic[top]`
13
+ # wherever a join would otherwise keep an arm master does not, so the facet changes one behaviour: a call of a
14
+ # member the interface declares resolves through that member's RBS (`RbsDispatch`, ADR-130 WD3).
15
+ #
16
+ # `name` is spelled as `Nominal#class_name` is (`Rigor::Plugin::_Scope`), and `type_args` are the interface's
17
+ # type arguments, translated in the caller's context. Every other RBS interface still translates to
18
+ # `Dynamic[top]` (`RbsTypeTranslator`, ADR-130 WD4).
19
+ class Interface
20
+ attr_reader :name, :type_args
21
+
22
+ EMPTY_TYPE_ARGS = [].freeze
23
+ private_constant :EMPTY_TYPE_ARGS
24
+
25
+ def initialize(name, type_args = EMPTY_TYPE_ARGS)
26
+ raise ArgumentError, "name must be a String, got #{name.class}" unless name.is_a?(String)
27
+
28
+ @name = name.frozen? ? name : name.dup.freeze
29
+ @type_args = type_args.frozen? ? type_args : type_args.dup.freeze
30
+ freeze
31
+ end
32
+
33
+ def describe(verbosity = :short)
34
+ return name if type_args.empty?
35
+
36
+ "#{name}[#{type_args.map { |arg| arg.describe(verbosity) }.join(', ')}]"
37
+ end
38
+
39
+ # A facet never reaches erasure on its own (`Dynamic#erase_to_rbs` is `untyped`); spelled as RBS for a
40
+ # caller that erases the carrier directly.
41
+ def erase_to_rbs
42
+ return name if type_args.empty?
43
+
44
+ "#{name}[#{type_args.map(&:erase_to_rbs).join(', ')}]"
45
+ end
46
+
47
+ include Rigor::Type::PlainLattice
48
+
49
+ include Rigor::Type::AcceptanceRouter
50
+
51
+ include Rigor::ValueSemantics
52
+
53
+ value_fields :name, :type_args
54
+
55
+ def inspect
56
+ "#<Rigor::Type::Interface #{describe(:short)}>"
57
+ end
58
+ end
59
+ end
60
+ end
data/lib/rigor/type.rb CHANGED
@@ -28,6 +28,7 @@ require_relative "type/union"
28
28
  require_relative "type/difference"
29
29
  require_relative "type/refined"
30
30
  require_relative "type/intersection"
31
+ require_relative "type/interface"
31
32
  require_relative "type/app"
32
33
  require_relative "type/result"
33
34
  require_relative "type/maybe"
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module TypeOfMemo
5
+ # G1's stage-4 form (ADR-129 WD5), mixed into {TypeOfMemo}'s singleton: on a would-be hit with
6
+ # `TypeOfMemo.enable!(serve: true)`, the memo serves the entry and checks it against a recomputation. For specs
7
+ # and audit tools; the CLI's `RIGOR_TYPE_OF_SHADOW=1` never serves.
8
+ module Serving
9
+ EMPTY = [].freeze
10
+ private_constant :EMPTY
11
+
12
+ private
13
+
14
+ # G1's stage-4 form (ADR-129 WD5) on one would-be hit:
15
+ # 1. recompute on the same scope objects with the memo suspended, in a capture and a priors window, and record
16
+ # the final state of every entry either side touches; a recomputation that touched a Resource is a mismatch
17
+ # and its answer stands;
18
+ # 2. undo the recomputation's Table writes and drop its journal records, which the served entry replaces;
19
+ # 3. serve the entry: validate its reads, merge them into the enclosing captures, replay its writes, and compare
20
+ # the touched entries and the type with the recorded ones. A recomputation that raises where an entry exists
21
+ # is a mismatch too.
22
+ def serve_hit(state, table, phase, scope, node, entry, &)
23
+ stored, reads, writes = entry.is_a?(Array) ? entry : [entry, EMPTY, EMPTY]
24
+ count(phase, :checked)
25
+ mark = state.journal_size
26
+ fresh, priors, refused = recompute(state, table, phase, scope, node, stored, &)
27
+ if refused
28
+ judge(state, phase, scope, node, stored, fresh, writes, priors, refused)
29
+ return fresh
30
+ end
31
+
32
+ serve(state, phase, scope, node, stored, fresh, [reads, writes, priors, mark])
33
+ end
34
+
35
+ # Steps 2 and 3 of {#serve_hit}. An error in them (a replay into a store the entry outlived) is a mismatch,
36
+ # counted rather than raised into the typing path's rescues. The recomputation's type is answered; the tables may
37
+ # hold a partial replay by then, which the counted mismatch already fails.
38
+ def serve(state, phase, scope, node, stored, fresh, slices)
39
+ reads, writes, priors, mark = slices
40
+ live = touched(writes, priors).map { |slot, key| [slot, key, state.entry(slot, key)] }
41
+ state.undo(priors)
42
+ state.truncate_journal(mark)
43
+ kinds = []
44
+ kinds << :revalidate if state.stale_read(reads)
45
+ state.merge_reads(reads)
46
+ state.replay(writes)
47
+ served(state, phase, scope, node, kinds, stored, fresh, live)
48
+ stored
49
+ rescue StandardError => e
50
+ report(phase, scope, node, [:serve_error, "#{e.class}: #{e.message[0, 120]}"], stored, fresh, [])
51
+ fresh
52
+ end
53
+
54
+ def recompute(state, table, phase, scope, node, stored)
55
+ state.open_capture
56
+ state.open_priors
57
+ table.suspended += 1
58
+ returned = false
59
+ begin
60
+ fresh = yield
61
+ returned = true
62
+ ensure
63
+ table.suspended -= 1
64
+ priors = state.close_priors
65
+ refused, = state.close_capture
66
+ report(phase, scope, node, [:raised], stored, nil, []) unless returned
67
+ end
68
+ [fresh, priors, refused]
69
+ end
70
+
71
+ # Every entry the served writes or the recomputation's writes touch, each once.
72
+ def touched(writes, priors)
73
+ seen = TypingState::KeyIndex.new
74
+ keys = []
75
+ [writes, priors].each do |slice|
76
+ slice.each_slice(3) do |slot, key, _|
77
+ next if seen.key?(slot, key)
78
+
79
+ seen.store(slot, key, true)
80
+ keys << [slot, key]
81
+ end
82
+ end
83
+ keys
84
+ end
85
+
86
+ def served(state, phase, scope, node, kinds, stored, fresh, live) # rubocop:disable Metrics/ParameterLists
87
+ kinds << :type unless same?(fresh, stored)
88
+ diffs = live.filter_map do |slot, key, want|
89
+ now = state.entry(slot, key)
90
+ [slot, key, want, now] unless same?(want, now)
91
+ end
92
+ kinds << :table unless diffs.empty?
93
+ report(phase, scope, node, kinds, stored, fresh, diffs) unless kinds.empty?
94
+ rescue StandardError => e
95
+ report(phase, scope, node, [:judge_error, "#{e.class}: #{e.message[0, 120]}"], stored, fresh, [])
96
+ end
97
+ end
98
+ end
99
+ end