rigortype 0.4.0 → 0.4.2

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 (237) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -3
  3. data/data/builtins/ruby_core/io.yml +1 -0
  4. data/data/builtins/ruby_core/proc.yml +4 -0
  5. data/data/builtins/ruby_core/random.yml +4 -0
  6. data/data/builtins/ruby_core/struct.yml +3 -0
  7. data/data/core_overlay/env.rbs +34 -0
  8. data/data/core_overlay/gc.rbs +50 -0
  9. data/data/core_overlay/hash.rbs +45 -0
  10. data/data/core_overlay/integer.rbs +31 -0
  11. data/data/core_overlay/io_buffer.rbs +27 -0
  12. data/data/core_overlay/kernel.rbs +40 -0
  13. data/data/core_overlay/match_data.rbs +28 -0
  14. data/data/core_overlay/module.rbs +51 -0
  15. data/data/core_overlay/object_space.rbs +29 -0
  16. data/data/core_overlay/proc.rbs +45 -0
  17. data/data/core_overlay/range.rbs +40 -0
  18. data/data/core_overlay/source_range.rbs +78 -0
  19. data/data/core_overlay/string.rbs +86 -0
  20. data/data/vendored_gem_sigs/prime/integer-extension.rbs +41 -0
  21. data/data/vendored_gem_sigs/prime/prime.rbs +372 -0
  22. data/docs/handbook/01-getting-started.md +3 -3
  23. data/docs/handbook/06-classes.md +3 -1
  24. data/docs/handbook/08-understanding-errors.md +3 -2
  25. data/docs/handbook/11-sig-gen.md +6 -0
  26. data/docs/handbook/appendix-liskov.md +5 -0
  27. data/docs/install.md +87 -15
  28. data/docs/manual/02-cli-reference.md +2 -2
  29. data/docs/manual/03-configuration.md +6 -1
  30. data/docs/manual/04-diagnostics.md +2 -1
  31. data/docs/manual/06-baseline.md +2 -0
  32. data/docs/manual/07-plugins.md +1 -1
  33. data/docs/manual/08-skills.md +4 -3
  34. data/docs/manual/12-caching.md +43 -4
  35. data/docs/manual/14-rails-quickstart.md +19 -11
  36. data/docs/manual/18-removing-dead-code.md +32 -2
  37. data/docs/manual/plugins/README.md +6 -1
  38. data/docs/manual/plugins/rigor-activerecord.md +21 -0
  39. data/docs/manual/plugins/rigor-alba.md +60 -0
  40. data/docs/manual/plugins/rigor-minitest.md +22 -4
  41. data/docs/manual/plugins/rigor-typelizer.md +71 -0
  42. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
  43. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
  44. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
  45. data/lib/rigor/analysis/check_rules/rule_ids.rb +4 -0
  46. data/lib/rigor/analysis/check_rules/source_arity.rb +184 -61
  47. data/lib/rigor/analysis/check_rules.rb +407 -144
  48. data/lib/rigor/analysis/dependency_recorder.rb +215 -18
  49. data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
  50. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
  51. data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
  52. data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
  53. data/lib/rigor/analysis/erb_template_detector.rb +2 -1
  54. data/lib/rigor/analysis/incremental.rb +29 -9
  55. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +291 -0
  56. data/lib/rigor/analysis/incremental_run_slot.rb +173 -0
  57. data/lib/rigor/analysis/incremental_session.rb +287 -50
  58. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
  59. data/lib/rigor/analysis/reachability/graph.rb +123 -23
  60. data/lib/rigor/analysis/reachability/scan.rb +290 -36
  61. data/lib/rigor/analysis/reachability/scan_cache.rb +2 -1
  62. data/lib/rigor/analysis/rule_catalog.rb +43 -0
  63. data/lib/rigor/analysis/run_cache_key.rb +43 -9
  64. data/lib/rigor/analysis/run_stats.rb +2 -2
  65. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +88 -6
  66. data/lib/rigor/analysis/runner/pool_coordinator.rb +52 -13
  67. data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -13
  68. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  69. data/lib/rigor/analysis/runner.rb +371 -49
  70. data/lib/rigor/analysis/shadow_harness.rb +27 -0
  71. data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
  72. data/lib/rigor/analysis/template_unit_paths.rb +6 -4
  73. data/lib/rigor/analysis/template_units.rb +15 -5
  74. data/lib/rigor/analysis/worker_session.rb +11 -7
  75. data/lib/rigor/cache/descriptor.rb +40 -1
  76. data/lib/rigor/cache/file_digest.rb +28 -0
  77. data/lib/rigor/cache/incremental_snapshot.rb +92 -8
  78. data/lib/rigor/cache/store.rb +63 -3
  79. data/lib/rigor/cache/value_digest.rb +325 -0
  80. data/lib/rigor/cli/annotate_command.rb +5 -0
  81. data/lib/rigor/cli/baseline_command.rb +7 -18
  82. data/lib/rigor/cli/check_command.rb +85 -14
  83. data/lib/rigor/cli/doctor_command.rb +1 -1
  84. data/lib/rigor/cli/effects_report.rb +2 -2
  85. data/lib/rigor/cli/trace_command.rb +5 -0
  86. data/lib/rigor/cli/type_of_command.rb +5 -3
  87. data/lib/rigor/cli/unused_command.rb +16 -10
  88. data/lib/rigor/cli.rb +11 -3
  89. data/lib/rigor/config_audit.rb +3 -2
  90. data/lib/rigor/config_path.rb +29 -0
  91. data/lib/rigor/configuration/severity_profile.rb +11 -0
  92. data/lib/rigor/configuration.rb +52 -7
  93. data/lib/rigor/effects/plugin_facts.rb +6 -6
  94. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
  95. data/lib/rigor/environment/gated_signature_guard.rb +43 -0
  96. data/lib/rigor/environment/gated_signature_plan.rb +339 -0
  97. data/lib/rigor/environment/lockfile_resolver.rb +10 -1
  98. data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
  99. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  100. data/lib/rigor/environment/rbs_loader.rb +284 -15
  101. data/lib/rigor/environment/required_features.rb +206 -0
  102. data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
  103. data/lib/rigor/environment.rb +34 -73
  104. data/lib/rigor/inference/acceptance.rb +28 -3
  105. data/lib/rigor/inference/block_parameter_binder.rb +29 -13
  106. data/lib/rigor/inference/builtins/cruby_definers.rb +61 -0
  107. data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
  108. data/lib/rigor/inference/captured_locals.rb +7 -1
  109. data/lib/rigor/inference/declaration_walk/context.rb +195 -0
  110. data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
  111. data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
  112. data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
  113. data/lib/rigor/inference/declaration_walk.rb +7 -0
  114. data/lib/rigor/inference/def_node_resolver.rb +26 -3
  115. data/lib/rigor/inference/definer_resolution.rb +266 -0
  116. data/lib/rigor/inference/expression_typer.rb +351 -61
  117. data/lib/rigor/inference/external_ancestor_resolution.rb +13 -16
  118. data/lib/rigor/inference/global_write_census.rb +7 -4
  119. data/lib/rigor/inference/guard_rebinding.rb +54 -13
  120. data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
  121. data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
  122. data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +282 -0
  123. data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
  124. data/lib/rigor/inference/in_effect_refinements.rb +256 -0
  125. data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
  126. data/lib/rigor/inference/key_presence_guard.rb +303 -0
  127. data/lib/rigor/inference/macro_block_self_type.rb +73 -14
  128. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  129. data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
  130. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
  131. data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
  132. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
  133. data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
  134. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
  135. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
  136. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +305 -54
  137. data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
  138. data/lib/rigor/inference/method_dispatcher.rb +44 -11
  139. data/lib/rigor/inference/module_function_state.rb +214 -0
  140. data/lib/rigor/inference/multi_target_binder.rb +21 -16
  141. data/lib/rigor/inference/narrowing.rb +166 -22
  142. data/lib/rigor/inference/object_mixins.rb +180 -0
  143. data/lib/rigor/inference/parameter_inference_collector.rb +10 -9
  144. data/lib/rigor/inference/project_patched_methods.rb +10 -5
  145. data/lib/rigor/inference/project_patched_scanner.rb +71 -3
  146. data/lib/rigor/inference/refine_census.rb +239 -0
  147. data/lib/rigor/inference/refine_self.rb +317 -0
  148. data/lib/rigor/inference/refined_dispatch/own_method.rb +403 -0
  149. data/lib/rigor/inference/refined_dispatch.rb +272 -0
  150. data/lib/rigor/inference/scope_indexer/certainty.rb +176 -0
  151. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
  152. data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
  153. data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
  154. data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
  155. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +143 -0
  156. data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
  157. data/lib/rigor/inference/scope_indexer.rb +2129 -354
  158. data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
  159. data/lib/rigor/inference/statement_evaluator.rb +149 -32
  160. data/lib/rigor/inference/string_mutation.rb +9 -3
  161. data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
  162. data/lib/rigor/inference/version_guard.rb +19 -10
  163. data/lib/rigor/language_server/project_context.rb +31 -0
  164. data/lib/rigor/language_server/server.rb +18 -0
  165. data/lib/rigor/plugin/base.rb +28 -8
  166. data/lib/rigor/plugin/inflector.rb +1 -1
  167. data/lib/rigor/plugin/io_boundary.rb +124 -7
  168. data/lib/rigor/plugin/isolation.rb +1 -1
  169. data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
  170. data/lib/rigor/plugin/trust_policy.rb +3 -3
  171. data/lib/rigor/project_environment.rb +29 -6
  172. data/lib/rigor/protection/discovery_seed.rb +18 -9
  173. data/lib/rigor/protection/mutation_cache.rb +4 -4
  174. data/lib/rigor/protection/mutator.rb +1 -1
  175. data/lib/rigor/reflection/constant_ancestors.rb +211 -46
  176. data/lib/rigor/reflection/constant_path.rb +33 -7
  177. data/lib/rigor/reflection.rb +41 -10
  178. data/lib/rigor/scope/discovery_index.rb +258 -3
  179. data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
  180. data/lib/rigor/scope/resolution_chain.rb +1032 -0
  181. data/lib/rigor/scope.rb +480 -328
  182. data/lib/rigor/sig_gen/alias_index.rb +1 -1
  183. data/lib/rigor/sig_gen/classification.rb +4 -1
  184. data/lib/rigor/sig_gen/effect_annotation.rb +2 -2
  185. data/lib/rigor/sig_gen/generator.rb +77 -21
  186. data/lib/rigor/sig_gen/method_candidate.rb +2 -2
  187. data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
  188. data/lib/rigor/source/alias_names.rb +52 -0
  189. data/lib/rigor/source/literals.rb +3 -2
  190. data/lib/rigor/source/symbol_name.rb +20 -0
  191. data/lib/rigor/type/combinator.rb +59 -3
  192. data/lib/rigor/type/float_range.rb +4 -1
  193. data/lib/rigor/type/nominal.rb +6 -1
  194. data/lib/rigor/version.rb +1 -1
  195. data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
  196. data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
  197. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
  198. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
  199. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
  200. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
  201. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
  202. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
  203. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
  204. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
  205. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
  206. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
  207. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
  208. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
  209. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
  210. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
  211. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
  212. data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
  213. data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
  214. data/plugins/rigor-alba/sig/alba.rbs +4 -0
  215. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
  216. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
  217. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +13 -2
  218. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
  219. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
  220. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
  221. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
  222. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
  223. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
  224. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
  225. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
  226. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
  227. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
  228. data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
  229. data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
  230. data/sig/rigor/environment.rbs +2 -0
  231. data/sig/rigor/plugin/io_boundary.rbs +4 -0
  232. data/sig/rigor/scope.rbs +69 -4
  233. data/sig/rigor/source.rbs +4 -0
  234. data/sig/rigor.rbs +3 -1
  235. data/skills/rigor-project-init/SKILL.md +37 -11
  236. data/skills/rigor-project-init/references/02-configure.md +7 -2
  237. metadata +90 -1
@@ -243,7 +243,8 @@ module Rigor
243
243
  method_name: context.method_name,
244
244
  args: context.args,
245
245
  environment: environment,
246
- scope: context.scope
246
+ scope: context.scope,
247
+ call_node: context.call_node
247
248
  )
248
249
  end
249
250
 
@@ -277,6 +278,11 @@ module Rigor
277
278
 
278
279
  def dispatch_one(receiver, method_name, args, environment, block_type, self_type_override = nil, # rubocop:disable Metrics/ParameterLists
279
280
  public_only: false, scope: nil, call_node: nil)
281
+ # Issue #1794 — a sequence's inherited Enumerable surface reads its Rigor-private element argument, with
282
+ # `self` still the sequence.
283
+ element = ArithmeticSequenceElements.element_receiver(receiver, method_name, environment)
284
+ self_type_override ||= receiver if element
285
+ receiver = element || receiver
280
286
  descriptor = receiver_descriptor(receiver)
281
287
  return nil unless descriptor
282
288
 
@@ -564,11 +570,11 @@ module Rigor
564
570
 
565
571
  memo = core_stdlib_memo(environment, scope)
566
572
  key = [class_name.to_s, method_name.to_sym]
567
- return memo[key] if memo&.key?(key)
573
+ return memo[key] if memo.key?(key)
568
574
 
569
- answer = compute_core_stdlib_ancestor_method(environment, class_name, method_name, scope)
570
- memo[key] = answer if memo
571
- answer
575
+ store_unless_recorded(memo, key) do
576
+ compute_core_stdlib_ancestor_method(environment, class_name, method_name, scope)
577
+ end
572
578
  end
573
579
 
574
580
  # Issue #1173 — the include-edge sibling of {#core_stdlib_ancestor_method}: a discovered
@@ -596,11 +602,11 @@ module Rigor
596
602
 
597
603
  memo = mixin_ancestor_memo(environment, scope)
598
604
  key = [class_name.to_s, method_name.to_sym]
599
- return memo[key] if memo&.key?(key)
605
+ return memo[key] if memo.key?(key)
600
606
 
601
- answer = compute_included_module_method(environment, class_name, method_name, scope)
602
- memo[key] = answer if memo
603
- answer
607
+ store_unless_recorded(memo, key) do
608
+ compute_included_module_method(environment, class_name, method_name, scope)
609
+ end
604
610
  end
605
611
 
606
612
  # The declines are conjunctive and ordered as {compute_core_stdlib_ancestor_method}'s are:
@@ -626,20 +632,12 @@ module Rigor
626
632
  end
627
633
 
628
634
  # The same one-slot memo shape as {#core_stdlib_memo}; see there for why it is one slot keyed
629
- # on the discovery index's identity, and why a recording run bypasses it.
635
+ # on the discovery index's identity, and what a recording run may store in it.
630
636
  MIXIN_ANCESTOR_MEMO_KEY = :__rigor_mixin_ancestor_dispatch__
631
637
  private_constant :MIXIN_ANCESTOR_MEMO_KEY
632
638
 
633
639
  def mixin_ancestor_memo(environment, scope)
634
- return nil if Rigor::Analysis::DependencyRecorder.active?
635
-
636
- discovery = scope.discovery
637
- slot = Thread.current[MIXIN_ANCESTOR_MEMO_KEY]
638
- unless slot && slot[0].equal?(discovery) && slot[1].equal?(environment)
639
- slot = [discovery, environment, {}]
640
- Thread.current[MIXIN_ANCESTOR_MEMO_KEY] = slot
641
- end
642
- slot[2]
640
+ ancestor_memo_slot(MIXIN_ANCESTOR_MEMO_KEY, environment, scope)
643
641
  end
644
642
 
645
643
  # The declines are conjunctive, so their ORDER is free — and it is chosen so the two that walk
@@ -800,8 +798,10 @@ module Rigor
800
798
  # Memo for the whole decision. Every call site of a class asks the same `(class, method)`
801
799
  # question, and the answer is a pure function of the frozen discovery index and the
802
800
  # environment, so it is cacheable on their identity. A run that is RECORDING ADR-46
803
- # dependency edges bypasses it: the shadow probes above read the project's method tables, and
804
- # a memo would swallow that edge for every file after the first.
801
+ # dependency edges stores only the answers whose computation recorded nothing
802
+ # ({#store_unless_recorded}): the shadow probes above read the project's method tables, and
803
+ # serving an answer that read them from the memo would swallow those edges for every later
804
+ # caller.
805
805
  #
806
806
  # ONE slot, replaced rather than accumulated — see {ExternalAncestorResolution}'s twin for
807
807
  # the measurement. A `Scope` hands each analysed file its own discovery index, so an
@@ -811,15 +811,38 @@ module Rigor
811
811
  private_constant :CORE_STDLIB_ANCESTOR_MEMO_KEY
812
812
 
813
813
  def core_stdlib_memo(environment, scope)
814
- return nil if Rigor::Analysis::DependencyRecorder.active?
814
+ ancestor_memo_slot(CORE_STDLIB_ANCESTOR_MEMO_KEY, environment, scope)
815
+ end
815
816
 
817
+ # The slot behind both memos. Whether this thread is recording is part of its key, so a slot
818
+ # filled with every answer is never read by a recording caller, which may only reuse the
819
+ # answers {#store_unless_recorded} lets through.
820
+ def ancestor_memo_slot(memo_key, environment, scope)
816
821
  discovery = scope.discovery
817
- slot = Thread.current[CORE_STDLIB_ANCESTOR_MEMO_KEY]
818
- unless slot && slot[0].equal?(discovery) && slot[1].equal?(environment)
819
- slot = [discovery, environment, {}]
820
- Thread.current[CORE_STDLIB_ANCESTOR_MEMO_KEY] = slot
822
+ recording = Rigor::Analysis::DependencyRecorder.recording?
823
+ slot = Thread.current[memo_key]
824
+ unless slot && slot[0].equal?(discovery) && slot[1].equal?(environment) && slot[2] == recording
825
+ slot = [discovery, environment, recording, {}]
826
+ Thread.current[memo_key] = slot
821
827
  end
822
- slot[2]
828
+ slot[3]
829
+ end
830
+
831
+ # Computes an answer, stores it under `key` when it may be served again, and returns it.
832
+ # Outside recording every answer is stored. Under ADR-46 recording the block runs in an
833
+ # ADR-84 WD2 capture window, which sees every edge the block files for the current consumer
834
+ # (the reads it withholds are not among them). An answer whose window stayed empty is stored:
835
+ # computing it again would read the same frozen tables and file nothing again, so serving it
836
+ # from the memo records exactly what recomputing does. That is most of them, since both
837
+ # computations withhold their ancestry walk and return before any recorded probe unless an
838
+ # RBS declaration answered. Any other answer is recomputed on every call, which files its
839
+ # edges each time, as a recording run always did.
840
+ def store_unless_recorded(memo, key, &)
841
+ return memo[key] = yield unless Rigor::Analysis::DependencyRecorder.recording?
842
+
843
+ answer, read_set = Rigor::Analysis::DependencyRecorder.capture(&)
844
+ memo[key] = answer if read_set.reads.empty? && read_set.missing.empty?
845
+ answer
823
846
  end
824
847
 
825
848
  # BFS over the scope's as-written ancestry tables — include edges first, then the
@@ -889,6 +912,7 @@ module Rigor
889
912
  # The class's own `def self.x` sits ahead of every `extend` — once it has run.
890
913
  # `singleton_def_shadows_call?` orders the def against the call site, so a `def self.sig`
891
914
  # written AFTER this `sig {}` does not suppress the bridge.
915
+ # ADR-119: shadowing already withholds the bridge, so a possible def withholds too.
892
916
  return nil if scope.singleton_def_shadows_call?(current, method_name, call_node)
893
917
 
894
918
  resolved = rbs_complete_extended_module_for(current, extends, environment, scope,
@@ -927,6 +951,8 @@ module Rigor
927
951
  nil
928
952
  end
929
953
 
954
+ # ADR-119: "defines" here stops the allow-list walk (a withholding answer), so a possible def stays
955
+ # a definer — treating it as absent would let a later extend bridge a call the def may own.
930
956
  def extend_owner_defines?(owner, method_name, call_node, scope, environment)
931
957
  return true if scope.instance_def_shadows_call?(owner, method_name, call_node)
932
958
 
@@ -986,41 +1012,147 @@ module Rigor
986
1012
  return override if override
987
1013
 
988
1014
  instance_type = Type::Combinator.nominal_of(class_name)
989
- resolved_self_type =
990
- case kind
991
- when :singleton then Type::Combinator.singleton_of(class_name)
992
- else instance_type
993
- end
994
1015
  # `self_type_override` lets the user-class fallback path preserve the ORIGINAL receiver as the
995
1016
  # substitute for `Bases::Self` — so `Kernel#dup: () -> self` resolved through the Object
996
1017
  # fallback returns the caller's type, not Object. `dispatch_one` also routes the receiver's
997
1018
  # type-argument-bearing projection through it ({SelfSubstitute}, #1092).
998
- self_type = self_type_override || resolved_self_type
1019
+ self_type = self_type_override ||
1020
+ (kind == :singleton ? Type::Combinator.singleton_of(class_name) : instance_type)
1021
+
1022
+ keywords_last = keyword_arguments_last?(call_node, args)
1023
+ call_site = [class_name, method_name, kind]
1024
+ if keywords_last && args.last.is_a?(Type::HashShape)
1025
+ distributed = distributed_keyword_return(method_definition, self_type, instance_type, type_vars, args,
1026
+ block_type, scope, call_node, call_site, environment)
1027
+ return distributed unless distributed.equal?(NOT_DISTRIBUTED)
1028
+ end
999
1029
 
1000
1030
  candidates = OverloadSelector.select_candidates(
1001
1031
  method_definition,
1002
1032
  arg_types: args,
1033
+ keywords_last: keywords_last, splats: splat_indices(call_node, args),
1003
1034
  # A `Dynamic` self (#1092) is a return-side answer; overload selection and ReceiverAffinity
1004
1035
  # read the static facet, as they did before the substitute carried the wrapping.
1005
1036
  self_type: self_type.is_a?(Type::Dynamic) ? self_type.static_facet : self_type,
1006
- instance_type: instance_type,
1007
- type_vars: type_vars,
1037
+ instance_type: instance_type, type_vars: type_vars,
1008
1038
  block_required: !block_type.nil?,
1009
1039
  environment: environment
1010
1040
  )
1011
1041
  return nil if candidates.empty?
1012
1042
 
1013
- call_site = [class_name, method_name, kind]
1014
1043
  record_dispatch_provenance(method_definition, candidates.first, scope, call_node, call_site)
1015
1044
  join_candidate_returns(
1016
1045
  candidates,
1017
1046
  method_definition: method_definition,
1018
1047
  self_type: self_type, instance_type: instance_type, type_vars: type_vars,
1019
1048
  args: args, block_type: block_type, scope: scope, call_node: call_node, call_site: call_site,
1020
- alias_expander: environment.rbs_loader
1049
+ alias_expander: environment.rbs_loader, keywords_last: keywords_last
1021
1050
  )
1022
1051
  end
1023
1052
 
1053
+ # Issue #1746 — a keyword hash holding a union value (`headers: flag` with `flag : bool`) selects per member
1054
+ # at runtime, so the return reads one overload per {KeywordArguments.distributions} list and joins their
1055
+ # returns, as the block probe does for its parameters (#1737). Selecting once for the whole value took one
1056
+ # member's overload (`Symbol` for `(headers: true) -> Symbol | (?headers: false) -> Integer`). Each list
1057
+ # counts only a genuine match: a member no overload takes, or more lists than the limit, answers
1058
+ # `Dynamic[top]` rather than lend the first-overload fallback to the join. A precise union value joins the
1059
+ # returns as a union; a `Dynamic` value keeps the answer `Dynamic`, as the #521 join does, since the split
1060
+ # dropped the value's `nil` and the untyped input behind it. A keyword hash with no union value answers
1061
+ # {NOT_DISTRIBUTED}, and the call selects as before; so does one whose values do not split (#1779) and that no
1062
+ # overload genuinely takes.
1063
+ NOT_DISTRIBUTED = Object.new.freeze
1064
+ private_constant :NOT_DISTRIBUTED
1065
+
1066
+ # rubocop:disable Metrics/ParameterLists
1067
+ def distributed_keyword_return(method_definition, self_type, instance_type, type_vars, args, block_type,
1068
+ scope, call_node, call_site, environment)
1069
+ # rubocop:enable Metrics/ParameterLists
1070
+ # One overload is the only one any member can select, and a wide union value (`receiver_type: Type::t`)
1071
+ # spelled out member by member would only pass the distribution limit.
1072
+ method_types = method_definition.method_types
1073
+ return NOT_DISTRIBUTED if method_types.size < 2
1074
+ # A keyword hash no overload takes as keywords is a positional `Hash` throughout (#1737).
1075
+ return NOT_DISTRIBUTED unless KeywordArguments.any_declares?(method_types)
1076
+
1077
+ keywords = args.last
1078
+ return NOT_DISTRIBUTED if keywords.pairs.none? { |_name, value| KeywordArguments.union_members(value) }
1079
+
1080
+ selector_self = self_type.is_a?(Type::Dynamic) ? self_type.static_facet : self_type
1081
+ per_list = distributed_keyword_matches(method_definition, args, selector_self, instance_type, type_vars,
1082
+ !block_type.nil?, environment, splat_indices(call_node, args))
1083
+ return NOT_DISTRIBUTED if per_list.equal?(NOT_DISTRIBUTED)
1084
+ return Type::Combinator.untyped if per_list.nil?
1085
+
1086
+ overloads = per_list.flat_map(&:last).uniq
1087
+ if overloads.size == 1
1088
+ record_dispatch_provenance(method_definition, overloads.first, scope, call_node, call_site)
1089
+ end
1090
+ returns = per_list.map do |arg_types, matches|
1091
+ join_candidate_returns(
1092
+ matches,
1093
+ method_definition: method_definition,
1094
+ self_type: self_type, instance_type: instance_type, type_vars: type_vars,
1095
+ args: arg_types, block_type: block_type, scope: scope, call_node: call_node, call_site: call_site,
1096
+ alias_expander: environment.rbs_loader, keywords_last: true
1097
+ )
1098
+ end
1099
+ join_distributed_returns(returns, keywords)
1100
+ end
1101
+
1102
+ # Each distribution list with its genuine matches, or nil past the distribution limit or when a list has
1103
+ # none. When no value split (no union value's key discriminates between the overloads, #1779), the one list
1104
+ # is the call as given, and one with no genuine match answers {NOT_DISTRIBUTED}: the call then selects as it
1105
+ # did before #1746, first-overload fallback included (`1.step(10, by: (c ? 1 : 2.0))` is
1106
+ # `Enumerator::ArithmeticSequence` through the incomplete stdlib RBS).
1107
+ # rubocop:disable Metrics/ParameterLists
1108
+ def distributed_keyword_matches(method_definition, args, selector_self, instance_type, type_vars,
1109
+ block_required, environment, splats)
1110
+ # rubocop:enable Metrics/ParameterLists
1111
+ distributions = keyword_distributions(method_definition, args, selector_self, instance_type, type_vars,
1112
+ environment, splats)
1113
+ return nil if distributions.nil?
1114
+
1115
+ distributions.map do |arg_types|
1116
+ matches = keyword_matches(method_definition, arg_types, selector_self, instance_type, type_vars,
1117
+ block_required, environment, splats)
1118
+ return arg_types.equal?(args) ? NOT_DISTRIBUTED : nil if matches.empty?
1119
+
1120
+ [arg_types, matches]
1121
+ end
1122
+ end
1123
+
1124
+ def join_distributed_returns(returns, keywords)
1125
+ return nil if returns.any?(&:nil?)
1126
+
1127
+ joined = Type::Combinator.union(*returns.uniq)
1128
+ dynamic_value = keywords.pairs.any? { |_name, value| value.is_a?(Type::Dynamic) }
1129
+ dynamic_value && !joined.is_a?(Type::Dynamic) ? Type::Combinator.dynamic(joined) : joined
1130
+ end
1131
+
1132
+ # Issue #1727 — whether `args`' last entry is the call's keyword hash (`f(a, k: 1)`), which the overload
1133
+ # selector then matches against an overload's keywords. Only the AST tells a keyword hash from a
1134
+ # braced positional one (`f(a, { k: 1 })`), so a dispatch without the call node, or one whose
1135
+ # arguments are not the node's own (a forwarded `...`, a `send` re-dispatch), answers false and keeps
1136
+ # reading every argument positionally.
1137
+ def keyword_arguments_last?(call_node, args)
1138
+ return false if args.empty? || !call_node.respond_to?(:arguments)
1139
+
1140
+ arguments = call_node.arguments&.arguments
1141
+ !arguments.nil? && arguments.size == args.size && arguments.last.is_a?(Prism::KeywordHashNode)
1142
+ end
1143
+
1144
+ # Issue #1801 — the indices of `args` the call passes as splat arguments (`f(*xs, k: 1)`), whose positional
1145
+ # count is not known statically, or nil without one. As for {#keyword_arguments_last?}, only a call whose
1146
+ # node's arguments are exactly `args` answers.
1147
+ def splat_indices(call_node, args)
1148
+ return nil if args.empty? || !call_node.respond_to?(:arguments)
1149
+
1150
+ arguments = call_node.arguments&.arguments
1151
+ return nil if arguments.nil? || arguments.size != args.size || arguments.none?(Prism::SplatNode)
1152
+
1153
+ arguments.each_index.select { |index| arguments[index].is_a?(Prism::SplatNode) }
1154
+ end
1155
+
1024
1156
  # The two provenance side-tables the return-typing tier is the last place able to populate, recorded
1025
1157
  # together because they share one gate (`scope` && `call_node`) and one call site.
1026
1158
  #
@@ -1054,10 +1186,16 @@ module Rigor
1054
1186
  # A candidate whose return does not translate leaves the join incomplete — decline (fail-soft
1055
1187
  # to Dynamic downstream) rather than answer a join missing an arm the runtime can take.
1056
1188
  # rubocop:disable-next Metrics/ParameterLists
1189
+ # rubocop:disable Metrics/ParameterLists
1057
1190
  def join_candidate_returns(candidates, method_definition:, self_type:, instance_type:, type_vars:, args:,
1058
- block_type:, scope:, call_node:, call_site:, alias_expander: nil)
1191
+ block_type:, scope:, call_node:, call_site:, alias_expander: nil,
1192
+ keywords_last: false)
1193
+ # rubocop:enable Metrics/ParameterLists
1059
1194
  returns = candidates.map do |method_type|
1060
- full_type_vars = compose_type_vars(method_type, type_vars, args, block_type, scope, call_node, call_site)
1195
+ # An overload that takes the keyword hash as its keywords binds no positional parameter to it.
1196
+ positional = KeywordArguments.positional(method_type, args, keywords_last)
1197
+ full_type_vars = compose_type_vars(method_type, type_vars, args, positional, block_type, scope,
1198
+ call_node, call_site)
1061
1199
  returned = RbsTypeTranslator.translate(
1062
1200
  method_type.type.return_type,
1063
1201
  self_type: self_type,
@@ -1067,7 +1205,7 @@ module Rigor
1067
1205
  )
1068
1206
  next returned unless combining_overload?(method_definition, method_type, call_site)
1069
1207
 
1070
- class_level_sum(returned, method_type, args)
1208
+ class_level_sum(returned, method_type, positional)
1071
1209
  end
1072
1210
  return returns.first if returns.size == 1
1073
1211
  return nil if returns.any?(&:nil?)
@@ -1078,11 +1216,17 @@ module Rigor
1078
1216
  Type::Combinator.dynamic(Type::Combinator.union(*distinct))
1079
1217
  end
1080
1218
 
1081
- def compose_type_vars(method_type, type_vars, args, block_type, scope, call_node, call_site)
1219
+ # `positional` is `args` less a keyword hash the overload takes as its keywords (#1727). The block-return
1220
+ # rule reads every argument, the keyword hash included: a keyword parameter naming the block's variable
1221
+ # (`[T] (?default: T) { () -> T } -> T`) is an argument reaching it. The positional binding reads only the
1222
+ # arguments that land in positional parameters.
1223
+ # rubocop:disable Metrics/ParameterLists
1224
+ def compose_type_vars(method_type, type_vars, args, positional, block_type, scope, call_node, call_site)
1225
+ # rubocop:enable Metrics/ParameterLists
1082
1226
  vars = compose_block_type_vars(method_type, type_vars, block_type, args,
1083
1227
  scope: scope, call_node: call_node, call_site: call_site)
1084
- compose_arg_type_vars(method_type, vars, args, scope: scope, call_node: call_node,
1085
- call_site: call_site)
1228
+ compose_arg_type_vars(method_type, vars, positional, scope: scope, call_node: call_node,
1229
+ call_site: call_site)
1086
1230
  end
1087
1231
 
1088
1232
  # Whether the overload is one `Enumerable` declares for `sum`, whose declared return the tier reads
@@ -1248,7 +1392,8 @@ module Rigor
1248
1392
  # Whether an argument the call passes may land in a parameter whose type names `name`, anywhere
1249
1393
  # inside it (`hash[_Key, K2]` names `K2`). The argument count is not matched against the
1250
1394
  # parameter list: a `*splat` argument stands for any number of arguments, and keyword arguments
1251
- # arrive as one more entry in `args`, so any argument counts as reaching every parameter.
1395
+ # arrive as one more entry in `args` (untrimmed, #1727), so any argument counts as reaching every
1396
+ # parameter.
1252
1397
  def argument_reaches_variable?(method_type, name, args)
1253
1398
  return false if args.empty?
1254
1399
 
@@ -1582,11 +1727,15 @@ module Rigor
1582
1727
 
1583
1728
  # ----- block parameter probe (Phase C sub-phase 1) -----
1584
1729
 
1585
- def probe_block_param_types(receiver:, method_name:, args:, environment:, scope: nil)
1730
+ def probe_block_param_types(receiver:, method_name:, args:, environment:, scope: nil, call_node: nil)
1586
1731
  args ||= []
1732
+ keywords_last = keyword_arguments_last?(call_node, args)
1733
+ splat = splat_indices(call_node, args)
1587
1734
  case receiver
1588
- when Type::Union then probe_block_param_types_union(receiver, method_name, args, environment, scope)
1589
- else probe_block_param_types_one(receiver, method_name, args, environment, scope)
1735
+ when Type::Union
1736
+ probe_block_param_types_union(receiver, method_name, args, environment, scope, keywords_last, splat)
1737
+ else
1738
+ probe_block_param_types_one(receiver, method_name, args, environment, scope, keywords_last, splat)
1590
1739
  end
1591
1740
  end
1592
1741
 
@@ -1594,9 +1743,9 @@ module Rigor
1594
1743
  # member resolves the same arity and types (otherwise the call sites would have to thread
1595
1744
  # per-member binders, which the slice does not support yet). Mismatches degrade to the empty
1596
1745
  # array so the binder defaults all params to Dynamic[Top].
1597
- def probe_block_param_types_union(receiver, method_name, args, environment, scope)
1746
+ def probe_block_param_types_union(receiver, method_name, args, environment, scope, keywords_last, splat)
1598
1747
  results = receiver.members.map do |member|
1599
- probe_block_param_types_one(member, method_name, args, environment, scope)
1748
+ probe_block_param_types_one(member, method_name, args, environment, scope, keywords_last, splat)
1600
1749
  end
1601
1750
  return [] if results.empty?
1602
1751
  return [] unless results.all? { |r| r == results.first }
@@ -1604,7 +1753,9 @@ module Rigor
1604
1753
  results.first
1605
1754
  end
1606
1755
 
1607
- def probe_block_param_types_one(receiver, method_name, args, environment, scope)
1756
+ def probe_block_param_types_one(receiver, method_name, args, environment, scope, keywords_last, splat)
1757
+ receiver = ArithmeticSequenceElements.element_receiver(receiver, method_name, environment, block: true) ||
1758
+ receiver
1608
1759
  descriptor = receiver_descriptor(receiver)
1609
1760
  return [] unless descriptor
1610
1761
 
@@ -1622,7 +1773,8 @@ module Rigor
1622
1773
  environment: environment,
1623
1774
  receiver: receiver,
1624
1775
  receiver_args: receiver_args,
1625
- method_name: method_name
1776
+ method_name: method_name,
1777
+ keywords_last: keywords_last, positional_splat: splat
1626
1778
  )
1627
1779
  rescue StandardError
1628
1780
  []
@@ -1631,7 +1783,7 @@ module Rigor
1631
1783
  # rubocop:disable Metrics/ParameterLists
1632
1784
  def extract_block_param_types(method_definition, class_name:, kind:, args:, type_vars:,
1633
1785
  environment: nil, receiver: nil, receiver_args: [],
1634
- method_name: nil)
1786
+ method_name: nil, keywords_last: false, positional_splat: nil)
1635
1787
  # rubocop:enable Metrics/ParameterLists
1636
1788
  instance_type = Type::Combinator.nominal_of(class_name)
1637
1789
  self_type =
@@ -1652,12 +1804,25 @@ module Rigor
1652
1804
  substitute = SelfSubstitute.for(receiver, receiver_args, method_name, args, nil)
1653
1805
  self_type = Type::Combinator.nominal_of(class_name, type_args: receiver_args) if substitute
1654
1806
 
1807
+ selector_self = self_type.is_a?(Type::Dynamic) ? self_type.static_facet : self_type
1808
+ if keywords_last
1809
+ return agreed_keyword_block_params(method_definition, args, self_type, selector_self, instance_type,
1810
+ type_vars, environment, positional_splat)
1811
+ end
1812
+
1813
+ # A splat (#1801) leaves the positional count open, so every overload some count reaches must agree.
1814
+ if positional_splat || FacetDistribution.faceted?(args)
1815
+ matches = facet_matches(method_definition, args, selector_self, instance_type, type_vars, true,
1816
+ environment, false, positional_splat)
1817
+ return agreed_block_params(matches, self_type, instance_type, type_vars, environment)
1818
+ end
1819
+
1655
1820
  method_type = OverloadSelector.select(
1656
1821
  method_definition,
1657
1822
  arg_types: args,
1658
1823
  # Overload selection reads a `Dynamic` self's static facet, mirroring the return path;
1659
1824
  # the substitution verdict here is built from the receiver's type arguments alone.
1660
- self_type: self_type.is_a?(Type::Dynamic) ? self_type.static_facet : self_type,
1825
+ self_type: selector_self,
1661
1826
  instance_type: instance_type,
1662
1827
  type_vars: type_vars,
1663
1828
  block_required: true,
@@ -1665,8 +1830,20 @@ module Rigor
1665
1830
  )
1666
1831
  return [] unless method_type
1667
1832
 
1833
+ block_params_of(method_type, self_type, instance_type, type_vars, environment) || []
1834
+ end
1835
+
1836
+ # The block parameters every overload in `matches` declares alike, or no information (`[]`).
1837
+ def agreed_block_params(matches, self_type, instance_type, type_vars, environment)
1838
+ answers = matches.uniq.map do |method_type|
1839
+ block_params_of(method_type, self_type, instance_type, type_vars, environment)
1840
+ end
1841
+ answers.uniq.size == 1 && answers.first ? answers.first : []
1842
+ end
1843
+
1844
+ def block_params_of(method_type, self_type, instance_type, type_vars, environment)
1668
1845
  block = method_type.respond_to?(:block) ? method_type.block : nil
1669
- return [] unless block
1846
+ return nil unless block
1670
1847
 
1671
1848
  translate_block_positional_params(
1672
1849
  block,
@@ -1677,6 +1854,80 @@ module Rigor
1677
1854
  )
1678
1855
  end
1679
1856
 
1857
+ # {KeywordArguments.distributions} for the call's keyword hash. An overload that declares no keywords may read
1858
+ # the hash as a positional `Hash`, and then every union value splits: with
1859
+ # `(a: Integer) -> Integer | (Hash[Symbol, String]) -> String`, `a: Integer | String`'s `String` member takes
1860
+ # the second overload and its `Integer` member the first. A parameter that cannot take a `Hash` (`(String)`)
1861
+ # leaves the split to the keyword declarations.
1862
+ # `splats`: a splat argument hides how many positional arguments precede the hash.
1863
+ def keyword_distributions(method_definition, args, selector_self, instance_type, type_vars, environment,
1864
+ splats)
1865
+ hash = Type::Combinator.nominal_of("Hash")
1866
+ count = splats ? nil : args.size
1867
+ KeywordArguments.distributions(args, true, method_definition.method_types, count) do |param|
1868
+ param_type = RbsTypeTranslator.translate(
1869
+ param.type, self_type: selector_self, instance_type: instance_type, type_vars: type_vars,
1870
+ alias_expander: environment.rbs_loader
1871
+ )
1872
+ !param_type.accepts(hash, mode: :gradual).no?
1873
+ end
1874
+ end
1875
+
1876
+ # rubocop:disable Metrics/ParameterLists
1877
+ def agreed_keyword_block_params(method_definition, args, self_type, selector_self, instance_type, type_vars,
1878
+ environment, splats)
1879
+ # rubocop:enable Metrics/ParameterLists
1880
+ distributions = keyword_distributions(method_definition, args, selector_self, instance_type, type_vars,
1881
+ environment, splats)
1882
+ return [] if distributions.nil?
1883
+
1884
+ candidates = []
1885
+ distributions.each do |arg_types|
1886
+ # Genuine matches only (`member` true): a member no overload takes must not lend the first-overload
1887
+ # fallback to the agreement, which bound `headers: (true | nil)`'s block to the `true` arm.
1888
+ matches = keyword_matches(method_definition, arg_types, selector_self, instance_type, type_vars, true,
1889
+ environment, splats)
1890
+ return [] if matches.empty?
1891
+
1892
+ candidates.concat(matches)
1893
+ end
1894
+ agreed_block_params(candidates, self_type, instance_type, type_vars, environment)
1895
+ end
1896
+
1897
+ # One distribution's genuine matches. A faceted positional argument (a `Dynamic` with sealed facet members) is
1898
+ # read member-wise as `OverloadSelector.select_candidates` reads it, but every list answers only a genuine
1899
+ # match: the first-overload fallback must not count toward the block probe's agreement or the return path's
1900
+ # join.
1901
+ # rubocop:disable Metrics/ParameterLists
1902
+ def keyword_matches(method_definition, arg_types, selector_self, instance_type, type_vars, block_required,
1903
+ environment, splats)
1904
+ # rubocop:enable Metrics/ParameterLists
1905
+ unless FacetDistribution.faceted?(arg_types)
1906
+ return OverloadSelector.select_declared(method_definition, arg_types, selector_self, instance_type,
1907
+ type_vars, block_required, environment, true, true, splats)
1908
+ end
1909
+
1910
+ facet_matches(method_definition, arg_types, selector_self, instance_type, type_vars, block_required,
1911
+ environment, true, splats)
1912
+ end
1913
+
1914
+ # Issue #1750 — the overloads a faceted argument list may reach, read member-wise with genuine matches only.
1915
+ # Where `FacetDistribution.select` falls back to the arguments as given (overloads not provable, more lists
1916
+ # than its cap, or a member no overload takes), the bare `Dynamic` gradually matches every arm and the
1917
+ # strict pass's pick is the first by position, so the answer is every gradual match
1918
+ # (`OverloadSelector.select_declared`'s `:gradual`) and the caller binds only what they all agree on.
1919
+ # rubocop:disable Metrics/ParameterLists
1920
+ def facet_matches(method_definition, arg_types, selector_self, instance_type, type_vars, block_required,
1921
+ environment, keywords_last, splats = nil)
1922
+ # rubocop:enable Metrics/ParameterLists
1923
+ FacetDistribution.select(arg_types, method_definition, member_wise: true, environment:) do |list, member|
1924
+ # The fallback list (`member` false) answers every gradual match rather than the strict pass's pick.
1925
+ OverloadSelector.select_declared(method_definition, list, selector_self, instance_type, type_vars,
1926
+ block_required, environment, member || :gradual, keywords_last,
1927
+ splats)
1928
+ end
1929
+ end
1930
+
1680
1931
  # `RBS::Types::Block#type` is normally an `RBS::Types::Function` carrying the block's parameter
1681
1932
  # list; some signatures use `RBS::Types::UntypedFunction` (a `(?)` block) which exposes no
1682
1933
  # parameter types -- we treat it as "no information" and return an empty array so the binder
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module Inference
5
+ module MethodDispatcher
6
+ # How `OverloadSelector` reads a call whose positional arguments include a splat (`f(*xs, a: v)`), #1801.
7
+ #
8
+ # A splat stands for any number of arguments, so the call's positional count is not known statically. The
9
+ # selector sees each splat as one untyped entry of the argument list, and counted that way it ruled out every
10
+ # overload whose arity the splat may still meet: `ph(*xs, a: "s")` skipped `(Hash[Symbol, String]) -> String`,
11
+ # which takes `{ a: "s" }` when `xs` is empty (`def ph(h) = h; ph(*[], a: "s")` is `{ a: "s" }`). Instead, an
12
+ # overload matches when some count of the splats' elements fits its arity and every argument, the splats'
13
+ # untyped elements included, is accepted at the parameter that count binds it to.
14
+ module SplatArity
15
+ # The most argument lists {.expansions} spells out; past it, the selector only asks that every other argument
16
+ # fit some positional parameter of the overload.
17
+ LIMIT = 32
18
+ private_constant :LIMIT
19
+
20
+ module_function
21
+
22
+ # The argument lists `arg_types` stands for against `fun`, each splat entry (at `splats`, indices into
23
+ # `arg_types`) repeated as often as `fun`'s positional parameters can take, from none upward; nil past
24
+ # {LIMIT}. An untyped `(?)` parameter list takes any count, so the list as given stands for all of them.
25
+ def expansions(fun, arg_types, splats)
26
+ return [arg_types] unless fun.respond_to?(:required_positionals)
27
+
28
+ indices = splats.select { |index| index < arg_types.size }
29
+ return [arg_types] if indices.empty?
30
+
31
+ bound = element_bound(fun, arg_types.size - indices.size)
32
+ counts = counts(indices.size, bound)
33
+ return nil if counts.nil?
34
+
35
+ counts.map { |count| expand(arg_types, indices, count) }
36
+ end
37
+
38
+ # The most splat elements a call with `fixed` other positional arguments can pass to `fun`: up to its maximum
39
+ # arity, or, with a `*rest`, one past filling every other parameter, since further elements land in the rest
40
+ # parameter alike.
41
+ def element_bound(fun, fixed)
42
+ declared = fun.required_positionals.size + fun.optional_positionals.size + fun.trailing_positionals.size
43
+ return declared + 1 if fun.rest_positionals
44
+
45
+ [declared - fixed, 0].max
46
+ end
47
+
48
+ # Every way to give `splats` splats at most `bound` elements in all, or nil past {LIMIT}.
49
+ def counts(splats, bound)
50
+ combinations = [[]]
51
+ splats.times do
52
+ combinations = combinations.flat_map do |partial|
53
+ (0..(bound - partial.sum)).map { |count| partial + [count] }
54
+ end
55
+ return nil if combinations.size > LIMIT
56
+ end
57
+ combinations
58
+ end
59
+
60
+ def expand(arg_types, indices, counts)
61
+ expanded = []
62
+ arg_types.each_with_index do |arg, index|
63
+ position = indices.index(index)
64
+ position ? expanded.concat([arg] * counts[position]) : expanded << arg
65
+ end
66
+ expanded
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end