rigortype 0.3.9 → 0.4.0

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 (363) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/core_overlay/enumerable.rbs +51 -0
  4. data/data/core_overlay/enumerator.rbs +84 -0
  5. data/data/core_overlay/hash_rbs3.rbs +41 -0
  6. data/data/core_overlay/process.rbs +40 -0
  7. data/data/core_overlay/string_io.rbs +33 -0
  8. data/data/effects/core.yml +3 -3
  9. data/data/gem_overlay/activesupport/core_ext.rbs +127 -4
  10. data/docs/handbook/03-narrowing.md +95 -10
  11. data/docs/handbook/04-tuples-and-shapes.md +8 -6
  12. data/docs/handbook/07-rbs-and-extended.md +16 -11
  13. data/docs/handbook/10-sorbet.md +9 -10
  14. data/docs/handbook/11-sig-gen.md +454 -13
  15. data/docs/manual/02-cli-reference.md +63 -2
  16. data/docs/manual/03-configuration.md +7 -0
  17. data/docs/manual/04-diagnostics.md +4 -0
  18. data/docs/manual/07-plugins.md +1 -1
  19. data/docs/manual/10-mcp-server.md +3 -2
  20. data/docs/manual/16-rbs-extended-annotations.md +40 -7
  21. data/docs/manual/19-effect-labels.md +10 -0
  22. data/docs/manual/plugins/README.md +7 -0
  23. data/docs/manual/plugins/rigor-actioncable.md +8 -1
  24. data/docs/manual/plugins/rigor-actionmailer.md +7 -0
  25. data/docs/manual/plugins/rigor-actionpack.md +243 -0
  26. data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
  27. data/docs/manual/plugins/rigor-activejob.md +7 -0
  28. data/docs/manual/plugins/rigor-activerecord.md +110 -4
  29. data/docs/manual/plugins/rigor-activestorage.md +8 -1
  30. data/docs/manual/plugins/rigor-activesupport-core-ext.md +11 -5
  31. data/docs/manual/plugins/rigor-grape.md +106 -0
  32. data/docs/manual/plugins/rigor-graphql.md +23 -2
  33. data/docs/manual/plugins/rigor-pundit.md +8 -1
  34. data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
  35. data/docs/manual/plugins/rigor-rails-routes.md +7 -0
  36. data/docs/manual/plugins/rigor-rbs-inline.md +212 -25
  37. data/docs/manual/plugins/rigor-sidekiq.md +8 -1
  38. data/docs/manual/plugins/rigor-sorbet.md +21 -5
  39. data/exe/rigor +19 -4
  40. data/lib/rigor/analysis/baseline.rb +1 -1
  41. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
  42. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
  43. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -3
  44. data/lib/rigor/analysis/check_rules/rule_ids.rb +20 -5
  45. data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
  46. data/lib/rigor/analysis/check_rules/source_arity.rb +323 -0
  47. data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
  48. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +36 -2
  49. data/lib/rigor/analysis/check_rules.rb +520 -49
  50. data/lib/rigor/analysis/dependency_recorder.rb +23 -0
  51. data/lib/rigor/analysis/fact_store.rb +9 -0
  52. data/lib/rigor/analysis/incremental.rb +26 -0
  53. data/lib/rigor/analysis/incremental_session.rb +36 -4
  54. data/lib/rigor/analysis/project_scan.rb +11 -1
  55. data/lib/rigor/analysis/reachability/plugin_roots.rb +0 -1
  56. data/lib/rigor/analysis/reachability/scan_cache.rb +1 -0
  57. data/lib/rigor/analysis/rule_catalog.rb +127 -0
  58. data/lib/rigor/analysis/run_cache_key.rb +20 -11
  59. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +167 -11
  60. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -4
  61. data/lib/rigor/analysis/runner/pool_coordinator.rb +107 -15
  62. data/lib/rigor/analysis/runner/project_pre_passes.rb +26 -7
  63. data/lib/rigor/analysis/runner.rb +278 -20
  64. data/lib/rigor/analysis/template_unit_collector.rb +303 -0
  65. data/lib/rigor/analysis/template_unit_paths.rb +91 -0
  66. data/lib/rigor/analysis/template_unit_positions.rb +293 -0
  67. data/lib/rigor/analysis/template_units.rb +399 -0
  68. data/lib/rigor/analysis/worker_session.rb +53 -10
  69. data/lib/rigor/bleeding_edge.rb +0 -2
  70. data/lib/rigor/builtins/hkt_builtins.rb +1 -0
  71. data/lib/rigor/builtins/imported_refinements.rb +4 -0
  72. data/lib/rigor/builtins/regex_refinement.rb +17 -9
  73. data/lib/rigor/builtins/static_return_refinements.rb +2 -0
  74. data/lib/rigor/cache/descriptor.rb +6 -1
  75. data/lib/rigor/cache/engine_source.rb +29 -1
  76. data/lib/rigor/cache/incremental_snapshot.rb +33 -1
  77. data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
  78. data/lib/rigor/cache/rbs_descriptor.rb +37 -7
  79. data/lib/rigor/cache/store.rb +0 -1
  80. data/lib/rigor/ci_detector.rb +1 -0
  81. data/lib/rigor/cli/doc_links.rb +1 -1
  82. data/lib/rigor/cli/docs_command.rb +4 -4
  83. data/lib/rigor/cli/plugin_command.rb +3 -3
  84. data/lib/rigor/cli/prism_colorizer.rb +0 -1
  85. data/lib/rigor/cli/sig_gen_command.rb +210 -29
  86. data/lib/rigor/cli/skill_command.rb +1 -1
  87. data/lib/rigor/cli/skill_describe.rb +0 -1
  88. data/lib/rigor/cli/type_of_command.rb +19 -5
  89. data/lib/rigor/cli/type_of_renderer.rb +20 -9
  90. data/lib/rigor/cli/type_of_template_probe.rb +189 -0
  91. data/lib/rigor/cli.rb +21 -3
  92. data/lib/rigor/configuration/severity_profile.rb +19 -3
  93. data/lib/rigor/configuration.rb +66 -3
  94. data/lib/rigor/effects/ancestry_recorder.rb +191 -0
  95. data/lib/rigor/effects/attribution.rb +11 -2
  96. data/lib/rigor/effects/callee_rule.rb +368 -0
  97. data/lib/rigor/effects/catalog.rb +7 -4
  98. data/lib/rigor/effects/collector.rb +11 -5
  99. data/lib/rigor/effects/config_envelopes.rb +9 -2
  100. data/lib/rigor/effects/definition_context.rb +179 -0
  101. data/lib/rigor/effects/effect_table.rb +11 -3
  102. data/lib/rigor/effects/envelope_check.rb +1 -1
  103. data/lib/rigor/effects/envelope_index.rb +15 -0
  104. data/lib/rigor/effects/file_collection.rb +59 -5
  105. data/lib/rigor/effects/framework_units.rb +1 -1
  106. data/lib/rigor/effects/identity.rb +16 -0
  107. data/lib/rigor/effects/local_ownership.rb +38 -12
  108. data/lib/rigor/effects/method_key.rb +21 -0
  109. data/lib/rigor/effects/mutation_classifier.rb +23 -12
  110. data/lib/rigor/effects/plugin_facts.rb +43 -31
  111. data/lib/rigor/effects/propagator.rb +295 -16
  112. data/lib/rigor/effects/registry.rb +1 -1
  113. data/lib/rigor/effects/scanner.rb +121 -72
  114. data/lib/rigor/effects/signature_sources.rb +1 -1
  115. data/lib/rigor/effects/snapshot.rb +2 -1
  116. data/lib/rigor/effects/summary.rb +27 -4
  117. data/lib/rigor/effects/unit_scan.rb +385 -30
  118. data/lib/rigor/effects/visibility.rb +101 -0
  119. data/lib/rigor/environment/lockfile_resolver.rb +17 -0
  120. data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
  121. data/lib/rigor/environment/member_consistency.rb +298 -0
  122. data/lib/rigor/environment/rbs_loader.rb +399 -133
  123. data/lib/rigor/environment.rb +103 -38
  124. data/lib/rigor/hashing/xxh3.rb +264 -0
  125. data/lib/rigor/inference/acceptance.rb +139 -9
  126. data/lib/rigor/inference/block_auto_splat.rb +216 -0
  127. data/lib/rigor/inference/block_call_timing.rb +338 -0
  128. data/lib/rigor/inference/block_parameter_binder.rb +73 -27
  129. data/lib/rigor/inference/block_repetition.rb +71 -0
  130. data/lib/rigor/inference/body_fixpoint.rb +2 -1
  131. data/lib/rigor/inference/budget_trace.rb +2 -1
  132. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  133. data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
  134. data/lib/rigor/inference/captured_locals.rb +387 -15
  135. data/lib/rigor/inference/closure_escape_analyzer.rb +157 -13
  136. data/lib/rigor/inference/content_join.rb +200 -27
  137. data/lib/rigor/inference/def_return_typer.rb +11 -7
  138. data/lib/rigor/inference/define_method_block_self.rb +64 -0
  139. data/lib/rigor/inference/element_read_widening.rb +22 -10
  140. data/lib/rigor/inference/error_info.rb +196 -0
  141. data/lib/rigor/inference/expression_typer.rb +1532 -496
  142. data/lib/rigor/inference/external_ancestor_resolution.rb +267 -0
  143. data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
  144. data/lib/rigor/inference/global_write_census.rb +239 -0
  145. data/lib/rigor/inference/guard_rebinding.rb +447 -0
  146. data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
  147. data/lib/rigor/inference/index_write_widening.rb +16 -3
  148. data/lib/rigor/inference/indexed_narrowing.rb +61 -9
  149. data/lib/rigor/inference/jump_targets.rb +82 -0
  150. data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
  151. data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
  152. data/lib/rigor/inference/last_line.rb +340 -0
  153. data/lib/rigor/inference/last_status.rb +144 -0
  154. data/lib/rigor/inference/macro_block_self_type.rb +167 -17
  155. data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
  156. data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
  157. data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
  158. data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
  159. data/lib/rigor/inference/match_rebinding.rb +392 -0
  160. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  161. data/lib/rigor/inference/method_dispatcher/block_folding.rb +85 -24
  162. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
  163. data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
  164. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +4 -0
  165. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
  166. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +113 -73
  167. data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
  168. data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
  169. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +881 -38
  170. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
  171. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +128 -53
  172. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +1 -1
  173. data/lib/rigor/inference/method_dispatcher.rb +214 -13
  174. data/lib/rigor/inference/method_parameter_binder.rb +8 -3
  175. data/lib/rigor/inference/multi_target_binder.rb +340 -52
  176. data/lib/rigor/inference/mutation_rejoin.rb +4 -1
  177. data/lib/rigor/inference/mutation_widening.rb +70 -48
  178. data/lib/rigor/inference/narrowing.rb +540 -120
  179. data/lib/rigor/inference/operand_effects.rb +167 -0
  180. data/lib/rigor/inference/operand_walk.rb +88 -0
  181. data/lib/rigor/inference/optimistic_origin.rb +152 -9
  182. data/lib/rigor/inference/parameter_inference_collector.rb +3 -2
  183. data/lib/rigor/inference/project_method_ownership.rb +136 -0
  184. data/lib/rigor/inference/project_patched_methods.rb +7 -2
  185. data/lib/rigor/inference/project_patched_scanner.rb +7 -3
  186. data/lib/rigor/inference/receiver_alias.rb +90 -1
  187. data/lib/rigor/inference/receiver_blind_block.rb +219 -0
  188. data/lib/rigor/inference/refinement_mutation.rb +15 -11
  189. data/lib/rigor/inference/repeated_or_writes.rb +463 -0
  190. data/lib/rigor/inference/return_barrier.rb +54 -0
  191. data/lib/rigor/inference/rewrite_mutation.rb +120 -0
  192. data/lib/rigor/inference/scope_indexer.rb +4615 -551
  193. data/lib/rigor/inference/statement_evaluator.rb +3176 -490
  194. data/lib/rigor/inference/stored_block_call.rb +54 -0
  195. data/lib/rigor/inference/string_mutation.rb +44 -7
  196. data/lib/rigor/inference/unknown_store_widening.rb +200 -0
  197. data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
  198. data/lib/rigor/language_server/debouncer.rb +0 -1
  199. data/lib/rigor/language_server/diagnostic_publisher.rb +3 -2
  200. data/lib/rigor/language_server/hover_renderer.rb +3 -3
  201. data/lib/rigor/language_server/project_context.rb +5 -3
  202. data/lib/rigor/mcp/server.rb +2 -1
  203. data/lib/rigor/plugin/base.rb +168 -5
  204. data/lib/rigor/plugin/box_probe.rb +91 -0
  205. data/lib/rigor/plugin/bundled_catalog.rb +1 -1
  206. data/lib/rigor/plugin/effect_attribution.rb +58 -4
  207. data/lib/rigor/plugin/loader.rb +2 -1
  208. data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
  209. data/lib/rigor/plugin/manifest.rb +71 -10
  210. data/lib/rigor/plugin/registry.rb +35 -1
  211. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
  212. data/lib/rigor/plugin/template_unit.rb +196 -0
  213. data/lib/rigor/plugin.rb +1 -0
  214. data/lib/rigor/protection/discovery_seed.rb +3 -1
  215. data/lib/rigor/protection/kill_signature.rb +0 -1
  216. data/lib/rigor/protection/mutation_cache.rb +1 -2
  217. data/lib/rigor/rbs_extended.rb +27 -0
  218. data/lib/rigor/reflection/constant_ancestors.rb +97 -0
  219. data/lib/rigor/reflection/constant_path.rb +19 -6
  220. data/lib/rigor/reflection.rb +72 -105
  221. data/lib/rigor/scope/discovery_index.rb +135 -2
  222. data/lib/rigor/scope.rb +859 -49
  223. data/lib/rigor/sig_gen/alias_index.rb +289 -0
  224. data/lib/rigor/sig_gen/classification.rb +22 -5
  225. data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
  226. data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
  227. data/lib/rigor/sig_gen/generator.rb +558 -31
  228. data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
  229. data/lib/rigor/sig_gen/method_candidate.rb +47 -3
  230. data/lib/rigor/sig_gen/observation_collector.rb +1 -1
  231. data/lib/rigor/sig_gen/renderer.rb +124 -9
  232. data/lib/rigor/sig_gen/skip_reason_catalog.rb +44 -1
  233. data/lib/rigor/sig_gen/write_result.rb +19 -3
  234. data/lib/rigor/sig_gen/writer.rb +166 -35
  235. data/lib/rigor/sig_gen.rb +3 -0
  236. data/lib/rigor/signature_path_audit.rb +1 -1
  237. data/lib/rigor/source/node_walker.rb +0 -3
  238. data/lib/rigor/source/parameter_envelope.rb +72 -0
  239. data/lib/rigor/source.rb +1 -0
  240. data/lib/rigor/type/combinator.rb +88 -12
  241. data/lib/rigor/type/difference.rb +1 -0
  242. data/lib/rigor/type/hash_shape.rb +1 -1
  243. data/lib/rigor/type/refined.rb +1 -0
  244. data/lib/rigor/version.rb +1 -1
  245. data/lib/rigor.rb +1 -0
  246. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
  247. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +9 -5
  248. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -4
  249. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +0 -1
  250. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
  251. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
  252. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
  253. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
  254. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
  255. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
  256. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +239 -4
  257. data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
  258. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
  259. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
  260. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
  261. data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
  262. data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
  263. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -0
  264. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +9 -5
  265. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
  266. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
  267. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +380 -3
  268. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
  269. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +281 -35
  270. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
  271. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -14
  272. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
  273. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +171 -5
  274. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +2 -0
  275. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +1 -0
  276. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
  277. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
  278. data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
  279. data/plugins/rigor-grape/sig/grape.rbs +263 -0
  280. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +72 -2
  281. data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
  282. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -0
  283. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +11 -7
  284. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +35 -37
  285. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -1
  286. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -0
  287. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +1 -0
  288. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +9 -17
  289. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
  290. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +217 -36
  291. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +1 -0
  292. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +9 -5
  293. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +0 -1
  294. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +46 -1
  295. data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
  296. data/sig/rigor/analysis/baseline.rbs +69 -7
  297. data/sig/rigor/analysis/fact_store.rbs +1 -1
  298. data/sig/rigor/analysis/project_scan.rbs +74 -0
  299. data/sig/rigor/effects/config_envelopes.rbs +100 -0
  300. data/sig/rigor/effects/effect_table.rbs +60 -0
  301. data/sig/rigor/effects/envelope.rbs +97 -0
  302. data/sig/rigor/effects/envelope_index.rbs +33 -0
  303. data/sig/rigor/effects/file_collection.rbs +81 -0
  304. data/sig/rigor/effects/label.rbs +26 -0
  305. data/sig/rigor/effects/label_set.rbs +46 -0
  306. data/sig/rigor/effects/method_key.rbs +24 -0
  307. data/sig/rigor/effects/origin.rbs +50 -0
  308. data/sig/rigor/effects/plugin_facts.rbs +141 -0
  309. data/sig/rigor/effects/registry.rbs +68 -0
  310. data/sig/rigor/effects/summary.rbs +50 -0
  311. data/sig/rigor/effects/taint_cause.rbs +12 -0
  312. data/sig/rigor/environment.rbs +12 -10
  313. data/sig/rigor/inference/optimistic_origin.rbs +10 -0
  314. data/sig/rigor/inference.rbs +6 -4
  315. data/sig/rigor/plugin/additional_initializer.rbs +36 -0
  316. data/sig/rigor/plugin/base.rbs +30 -7
  317. data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
  318. data/sig/rigor/plugin/effect_attribution.rbs +66 -0
  319. data/sig/rigor/plugin/effect_edge.rbs +33 -0
  320. data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
  321. data/sig/rigor/plugin/io_boundary.rbs +1 -1
  322. data/sig/rigor/plugin/loader.rbs +3 -3
  323. data/sig/rigor/plugin/manifest.rbs +50 -11
  324. data/sig/rigor/plugin/protocol_contract.rbs +68 -0
  325. data/sig/rigor/plugin/registry.rbs +62 -1
  326. data/sig/rigor/plugin.rbs +1 -1
  327. data/sig/rigor/rbs_extended.rbs +1 -1
  328. data/sig/rigor/reflection.rbs +7 -6
  329. data/sig/rigor/scope.rbs +135 -20
  330. data/sig/rigor/sig_gen/skip_reason_catalog.rbs +14 -7
  331. data/sig/rigor/source.rbs +4 -4
  332. data/sig/rigor/testing.rbs +10 -4
  333. data/sig/rigor/type.rbs +6 -0
  334. data/sig/rigor.rbs +38 -20
  335. data/skills/rigor-ask/SKILL.md +8 -5
  336. data/skills/rigor-baseline-reduce/SKILL.md +4 -6
  337. data/skills/rigor-ci-setup/SKILL.md +17 -21
  338. data/skills/rigor-doctor/SKILL.md +24 -22
  339. data/skills/rigor-doctor/references/01-checks.md +97 -33
  340. data/skills/rigor-editor-setup/SKILL.md +6 -4
  341. data/skills/rigor-mcp-setup/SKILL.md +5 -4
  342. data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
  343. data/skills/rigor-next-steps/SKILL.md +4 -2
  344. data/skills/rigor-plugin-author/SKILL.md +19 -23
  345. data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
  346. data/skills/rigor-plugin-author/references/02-walker-and-types.md +12 -25
  347. data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
  348. data/skills/rigor-plugin-review/SKILL.md +6 -4
  349. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
  350. data/skills/rigor-plugin-tune/SKILL.md +4 -2
  351. data/skills/rigor-project-init/SKILL.md +9 -7
  352. data/skills/rigor-project-init/references/01-detect.md +13 -9
  353. data/skills/rigor-project-init/references/02-configure.md +33 -8
  354. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
  355. data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
  356. data/skills/rigor-protection-uplift/SKILL.md +4 -6
  357. data/skills/rigor-rbs-setup/SKILL.md +4 -2
  358. data/skills/rigor-type-oracle/SKILL.md +4 -6
  359. data/skills/rigor-type-oracle/references/01-oracle-commands.md +12 -8
  360. data/skills/rigor-type-oracle/references/03-gap-protocol.md +0 -3
  361. data/skills/rigor-unused-adjudicate/SKILL.md +4 -2
  362. data/skills/rigor-upgrade/SKILL.md +13 -8
  363. metadata +108 -1
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "../type"
4
4
  require_relative "range_constant"
5
+ require_relative "content_join"
5
6
 
6
7
  module Rigor
7
8
  module Inference
@@ -74,10 +75,64 @@ module Rigor
74
75
  }.freeze
75
76
  private_constant :TYPE_HANDLERS
76
77
 
78
+ UNREAD_HASH_REFUSED_KEY = :__rigor_acceptance_unread_hash_refused__
79
+ private_constant :UNREAD_HASH_REFUSED_KEY
80
+
81
+ # Whether a `maybe` for `param` against `arg` rests on a record that could not read a hash's entries: a
82
+ # `Hash` with a gradual arm ({#gradual_hash?}) or an open `HashShape`. The record cannot prove the key set
83
+ # of either, and its `maybe` climbs through whatever holds the record. `OverloadSelector`'s strict pass
84
+ # reads that `maybe` as no evidence for the overload, so this re-asks with each such record answering
85
+ # `no` and reports whether the pair then fails. A `maybe` from anywhere else (a subclass only the Ruby
86
+ # source declares, a splat under a parameter that is no record) survives the re-ask and still counts.
87
+ def maybe_rests_on_unread_hash?(param, arg)
88
+ return false unless contains_type?(param) { |type| type.is_a?(Type::HashShape) }
89
+ return false unless contains_type?(arg) { |type| unread_hash?(type) }
90
+
91
+ refusing_unread_hashes { accepts(param, arg, mode: :gradual).no? }
92
+ end
93
+
77
94
  # rubocop:disable-next Metrics/ClassLength
78
95
  class << self
79
96
  private
80
97
 
98
+ # The flag lives on the thread, not on a parameter, because the record may sit any number of `accepts`
99
+ # calls below the one asked (a tuple element, a `Hash` value, a union member), and those calls go
100
+ # through each carrier's `Type#accepts`. It is set only for the duration of the block.
101
+ def refusing_unread_hashes
102
+ previous = Thread.current[UNREAD_HASH_REFUSED_KEY]
103
+ Thread.current[UNREAD_HASH_REFUSED_KEY] = true
104
+ yield
105
+ ensure
106
+ Thread.current[UNREAD_HASH_REFUSED_KEY] = previous
107
+ end
108
+
109
+ # The `maybe` a record answers for a hash whose entries it cannot read, or `no` inside
110
+ # {#refusing_unread_hashes}.
111
+ def unread_hash_maybe(mode, reason)
112
+ return hash_shape_no(mode, "#{reason} (refused as evidence)") if Thread.current[UNREAD_HASH_REFUSED_KEY]
113
+
114
+ Type::AcceptsResult.maybe(mode: mode, reasons: reason)
115
+ end
116
+
117
+ def unread_hash?(type)
118
+ gradual_hash?(type) || (type.is_a?(Type::HashShape) && type.open?)
119
+ end
120
+
121
+ def contains_type?(type, &)
122
+ return true if yield(type)
123
+
124
+ components =
125
+ case type
126
+ when Type::Union then type.members
127
+ when Type::Nominal then type.type_args
128
+ when Type::Tuple then type.elements
129
+ when Type::HashShape then type.pairs.values
130
+ when Type::Difference then [type.base]
131
+ else return false
132
+ end
133
+ components.any? { |component| contains_type?(component, &) }
134
+ end
135
+
81
136
  def accepts_one(self_type, other_type, mode)
82
137
  handler = TYPE_HANDLERS[self_type.class]
83
138
  return send(handler, self_type, other_type, mode) if handler
@@ -755,10 +810,15 @@ module Rigor
755
810
  # `Difference[base, removed]` accepts another type X when the base accepts X *and* X's value set is
756
811
  # provably disjoint from `removed`. The disjointness test is the subtle part — it is NOT the same as
757
812
  # `removed.accepts(X)`, because `Nominal[String]` includes `""` even though `Constant[""]` does not
758
- # "accept" `Nominal[String]`. The conservative rule here: we can prove disjointness only when X is
759
- # itself a `Constant` carrier (compare values directly) or another `Difference` with the same
760
- # removed value (already exhibits the disjointness). Any other shape — Nominal, Union, IntegerRange
761
- # — could overlap the removed value, so the difference rejects it under gradual mode.
813
+ # "accept" `Nominal[String]`. The conservative rule here: we can prove disjointness only when X is a
814
+ # carrier whose value set is visibly apart from `removed` — a `Constant` (compare values directly),
815
+ # another `Difference` with the same removed value (already exhibits the disjointness), an
816
+ # `IntegerRange` that skips an Integer removed value, a `Tuple` whose fixed arity differs from a
817
+ # removed `Tuple`, or a `HashShape` with a required key a closed removed `HashShape` cannot hold. The
818
+ # last three cover the empty witnesses of `non-zero-int`, `non-empty-array[T]` and
819
+ # `non-empty-hash[K, V]`. Any other carrier — a Nominal above all — is no proof and the difference
820
+ # rejects it under gradual mode, even where its values happen to exclude `removed`. A Union or
821
+ # Intersection argument never reaches this rule whole: `accepts` splits it into members first.
762
822
  def accepts_difference(self_type, other_type, mode)
763
823
  base_result = accepts(self_type.base, other_type, mode: mode)
764
824
  return base_result if base_result.no?
@@ -787,9 +847,27 @@ module Rigor
787
847
  # Disjointness is monotonic over Intersection: if any member is provably disjoint from
788
848
  # `removed`, the meet is too.
789
849
  other_type.members.any? { |m| provably_disjoint_from_removed?(m, removed) }
850
+ when Type::IntegerRange
851
+ removed.is_a?(Type::Constant) && removed.value.is_a?(Integer) && !other_type.covers?(removed.value)
852
+ when Type::Tuple
853
+ # A Tuple's arity is fixed, so it shares no inhabitant with a Tuple of another arity — the
854
+ # zero-arity witness `non-empty-array[T]` removes included.
855
+ removed.is_a?(Type::Tuple) && removed.elements.size != other_type.elements.size
856
+ when Type::HashShape
857
+ hash_shape_excludes_removed?(other_type, removed)
790
858
  end
791
859
  end
792
860
 
861
+ # Every inhabitant of `shape` holds each of its required keys, and a closed `removed` shape holds no
862
+ # key outside its pairs, so one required key `removed` does not list makes the two disjoint. Against
863
+ # the closed `{}` witness `non-empty-hash[K, V]` removes, that is any required key; an optional key
864
+ # proves nothing, since the shape is inhabited without it.
865
+ def hash_shape_excludes_removed?(shape, removed)
866
+ return false unless removed.is_a?(Type::HashShape) && removed.closed?
867
+
868
+ shape.required_keys.any? { |key| !removed.pairs.key?(key) }
869
+ end
870
+
793
871
  # `Refined[base, predicate]` accepts another type X when the base accepts the *base* of X *and* X is
794
872
  # provably contained in the predicate's value set. The base check is delegated to
795
873
  # `accepts(self.base, X.base)` so handlers like `accepts_nominal` see Nominal-vs-Nominal and return
@@ -924,11 +1002,15 @@ module Rigor
924
1002
 
925
1003
  # HashShape{k1: T1, ...} accepts another HashShape when every required key of self is required on
926
1004
  # the other side and Ti accepts Ui (depth covariant). Optional keys may be absent on the other side;
927
- # when present, their values are checked. A closed self rejects known or possible extra keys. Other
928
- # types are rejected; the converse direction (a Nominal accepting a HashShape) is handled by
1005
+ # when present, their values are checked. A closed self rejects known extra keys, and answers at most
1006
+ # `maybe` for an open source: `h = { a: 1 }; h.default = 0` reopens a shape whose key set is still
1007
+ # exactly the literal's. Other types are rejected, except a `Hash` with a gradual arm (see
1008
+ # {#gradual_hash?}); the converse direction (a Nominal accepting a HashShape) is handled by
929
1009
  # `accepts_nominal` via projection.
930
1010
  def accepts_hash_shape(self_type, other_type, mode)
931
1011
  unless other_type.is_a?(Type::HashShape)
1012
+ return gradual_hash_verdict(self_type, other_type, mode) if gradual_hash?(other_type)
1013
+
932
1014
  return Type::AcceptsResult.no(
933
1015
  mode: mode,
934
1016
  reasons: "HashShape does not accept #{other_type.class}"
@@ -939,8 +1021,6 @@ module Rigor
939
1021
  return hash_shape_no(mode, "HashShape missing required keys: #{missing.inspect}") unless missing.empty?
940
1022
 
941
1023
  if self_type.closed?
942
- return hash_shape_no(mode, "HashShape closed target rejects open source") if other_type.open?
943
-
944
1024
  extra = other_type.pairs.keys - self_type.pairs.keys
945
1025
  unless extra.empty?
946
1026
  return hash_shape_no(mode, "HashShape closed target rejects extra keys: #{extra.inspect}")
@@ -948,9 +1028,59 @@ module Rigor
948
1028
  end
949
1029
 
950
1030
  per_entry = hash_shape_entry_results(self_type, other_type, mode)
951
- combine_arg_results(per_entry, mode)
1031
+ result = combine_arg_results(per_entry, mode)
1032
+ return result unless result.yes? && self_type.closed? && other_type.open?
1033
+
1034
+ unread_hash_maybe(mode, "HashShape closed target cannot check open source")
1035
+ end
1036
+
1037
+ # A `Hash` nominal — directly or as the base of a `Difference` (`non-empty-hash[K, V]`) — that is raw or
1038
+ # carries a `Dynamic` arm in a type argument. A hash literal with a `**splat` entry carries the arm by
1039
+ # design, `Hash.new` filled key by key reads as the raw `Hash`, and a declared `Hash[Symbol, untyped]`
1040
+ # is the author saying the hash may hold anything (the reading `MutationRejoin#regrowable_carrier?`
1041
+ # gives a declared gradual arm). The runtime value may then hold exactly the record's keys, so a shape
1042
+ # cannot reject it on the arm's account, the way `Hash[K, V]` accepts a raw `Hash` as maybe. A `Hash`
1043
+ # subclass is not read: its entries need not be the record's (`HashWithIndifferentAccess` stores String
1044
+ # keys).
1045
+ def gradual_hash?(type)
1046
+ nominal = type.is_a?(Type::Difference) ? type.base : type
1047
+ nominal.is_a?(Type::Nominal) && nominal.class_name == "Hash" &&
1048
+ (nominal.type_args.empty? || nominal.type_args.any? { |arg| gradual_arm?(arg) })
1049
+ end
1050
+
1051
+ # `maybe`, unless an argument without a gradual arm already rules the record out: a precise `K` that
1052
+ # rejects a required key (`Hash[String, untyped]` never holds `:a`), or a precise `V` none of whose
1053
+ # members a required key's type accepts.
1054
+ def gradual_hash_verdict(self_type, other_type, mode)
1055
+ nominal = other_type.is_a?(Type::Difference) ? other_type.base : other_type
1056
+ key_type, value_type = nominal.type_args
1057
+ missing = unholdable_keys(self_type, key_type, mode)
1058
+ return hash_shape_no(mode, "Hash key type cannot hold required keys: #{missing.inspect}") if missing.any?
1059
+
1060
+ refused = refused_value_keys(self_type, value_type, mode)
1061
+ return hash_shape_no(mode, "Hash value type refused for required keys: #{refused.inspect}") if refused.any?
1062
+
1063
+ unread_hash_maybe(mode, "HashShape cannot check #{other_type.describe(:short)}")
952
1064
  end
953
1065
 
1066
+ def unholdable_keys(self_type, key_type, mode)
1067
+ return [] if key_type.nil? || gradual_arm?(key_type)
1068
+
1069
+ self_type.required_keys.select { |key| key_type.accepts(Type::Combinator.constant_of(key), mode: mode).no? }
1070
+ end
1071
+
1072
+ def refused_value_keys(self_type, value_type, mode)
1073
+ return [] if value_type.nil? || gradual_arm?(value_type)
1074
+
1075
+ members = ContentJoin.union_members(value_type)
1076
+ self_type.required_keys.select do |key|
1077
+ formal = self_type.pairs.fetch(key)
1078
+ members.all? { |member| accepts(formal, member, mode: mode).no? }
1079
+ end
1080
+ end
1081
+
1082
+ def gradual_arm?(type) = ContentJoin.union_members(type).any?(Type::Dynamic)
1083
+
954
1084
  def hash_shape_entry_results(self_type, other_type, mode)
955
1085
  self_type.pairs.filter_map do |key, formal|
956
1086
  next unless other_type.pairs.key?(key)
@@ -0,0 +1,216 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../type"
4
+ require_relative "multi_target_binder"
5
+
6
+ module Rigor
7
+ module Inference
8
+ # The auto-splat half of {BlockParameterBinder}: given a block's positional parameter shape and the one
9
+ # value the receiver yields, it answers the positional table the block's names are bound from.
10
+ #
11
+ # Ruby blocks (NOT lambdas) spread a single yielded array-ish value across their positionals, and CRuby
12
+ # decides that on the PARAMETER LIST alone ({ParameterShape#splats?}) — never on the value. So the
13
+ # carrier only decides how precisely each position can be filled, and a carrier this module cannot
14
+ # decompose lowers the positions to `Dynamic[Top]` rather than cancelling the spread (issue #1116).
15
+ #
16
+ # Each member of the carrier (a union distributes; anything else is its own single member) contributes
17
+ # one {Table}, and {join} folds them into the answer:
18
+ #
19
+ # - a `Type::Tuple` spreads element-wise, leading positionals (required, then optional) from the head
20
+ # and, with a rest present, trailing positionals from the tail with the rest absorbing the middle —
21
+ # the split `MultiTargetBinder` applies to `a, *r, b = tuple`, so `|*r, v|` over `[K, V]` binds `v` to
22
+ # `V`. A slot past the tuple is left unfilled and lands on `Dynamic[Top]`;
23
+ # - an `Array[T]` ({MultiTargetBinder.array_element_type}) binds `T` to every required and trailing
24
+ # positional, `Dynamic[Top]` to every optional one (a short array hands it its default, not `nil`),
25
+ # and `Array[T]` to a named rest. A short array pads the fixed positions with `nil` at runtime, so
26
+ # those positions are reported optimistic (issue #1093);
27
+ # - an opaque array carrier — a raw `Array`, an `Array[untyped]` / `Array[top]`, a `Refined` /
28
+ # `Difference` over either, or a `Dynamic` over any `Array` however precise — binds `Dynamic[Top]` to
29
+ # every position: the floor `MultiTargetBinder` binds for the same undecomposable right-hand side, and
30
+ # the reason `[1, 2].tap { |a, b| a.succ }` no longer reads `a` as the whole `Array` (issue #1116);
31
+ # - a bare `nil` binds `nil` to every position. `nil` has no `to_ary`, so CRuby passes it as the one
32
+ # argument and `|a, b|` sees `nil, nil` — which is also what the wrap `[nil]` pads to, so the two
33
+ # readings agree here, and {join} takes that `nil` back out of any position another member fills;
34
+ # - every other member takes the same `Dynamic[Top]` floor as the opaque one, and does NOT license the
35
+ # spread on its own. It is the member the union knows least about, and joining it as a value would put
36
+ # a nil-bearing element beside a nil-free one: `String#scan`'s `String | Array[String?]` yield joins a
37
+ # capture's `String?` against the whole match's `String` and fires `call.possible-nil-receiver` on the
38
+ # correlated-invariant `refinements[name.to_sym]`, which ADR-5 ranks above the precision.
39
+ #
40
+ # The spread needs at least one Tuple, `Array[T]` or opaque array member; a carrier with none of them
41
+ # ({#for} answers nil) leaves the binder's declared types alone, which binds the value to the first
42
+ # parameter and `Dynamic[Top]` after. So a lone value with no `to_ary` is NOT wrapped as `[value]` the
43
+ # way `a, b = value` wraps it: the one-value premise of auto-splat is the RBS block signature's arity,
44
+ # which nothing here can check against the actual yield, and a wrong wrap binds `nil` to a parameter the
45
+ # block receives a value in.
46
+ #
47
+ # See docs/internal-spec/inference-engine.md for the binding contract.
48
+ module BlockAutoSplat
49
+ # The positional part of a parameter list, as counts: all the auto-splat rule reads. An explicit
50
+ # `BlockParametersNode` list and a numbered-parameter block (`maximum` required positionals) both
51
+ # reduce to it, so the two spellings cannot drift.
52
+ ParameterShape = Data.define(:required, :optional, :post, :rest) do
53
+ def self.of(params_node)
54
+ new(required: params_node.requireds.size, optional: params_node.optionals.size,
55
+ post: params_node.posts.size, rest: !params_node.rest.nil?)
56
+ end
57
+
58
+ # The `_1` / `_2` form, read as the explicit list of `arity` required positionals.
59
+ def self.of_arity(arity)
60
+ new(required: arity, optional: 0, post: 0, rest: false)
61
+ end
62
+
63
+ def positional_count = required + optional + post
64
+
65
+ # CRuby's own condition (`vm_callee_setup_block_arg` / `setup_parameters_complex`): a block splats a
66
+ # lone array argument when it has a mandatory positional (`lead + post > 0`) or more than one
67
+ # optional, except a bare `|a|` (the iseq's `ambiguous_param0`). So `|k, *r|`, `|*r, v|`,
68
+ # `|a = 1, b = 2|` and the trailing-comma `|k,|` (Prism's `ImplicitRestNode`) splat, while `|*r|`,
69
+ # `|a = 1, *r|` and `|a, &b|` / `|a, k: 1|` do not.
70
+ def splats?
71
+ mandatory = required + post
72
+ return false unless mandatory.positive? || optional > 1
73
+
74
+ !(mandatory == 1 && optional.zero? && !rest)
75
+ end
76
+ end
77
+
78
+ # A positional table: the type per position, the positions bound optimistically, and the named rest's
79
+ # type (nil for the `Array[Dynamic[Top]]` default). One arm produces one; {join} folds them into one.
80
+ # Private as a name — {#for} hands the folded value out, and nothing outside constructs one.
81
+ Table = Data.define(:types, :optimistic_positions, :rest_type)
82
+ private_constant :Table
83
+
84
+ # The arm kinds that make a member an array as far as the spread is concerned, so that a union
85
+ # carrying one of them spreads even where its other members do not decompose.
86
+ ARRAY_CARRIER_ARMS = %i[tuple array opaque].freeze
87
+ private_constant :ARRAY_CARRIER_ARMS
88
+
89
+ module_function
90
+
91
+ # @param shape — the block's {ParameterShape}.
92
+ # @param carrier — the one value the receiver yields.
93
+ # @return the folded {Table}, or nil when no member is an array carrier and the
94
+ # spread must leave the declared types alone.
95
+ def for(shape, carrier)
96
+ arms = arms_of(carrier)
97
+ return nil if arms.nil?
98
+
99
+ join(arms.map { |member, kind| table_for(shape, member, kind) })
100
+ end
101
+
102
+ def arms_of(carrier)
103
+ members = carrier.is_a?(Type::Union) ? carrier.members : [carrier]
104
+ arms = members.map { |member| [member, arm_of(member)] }
105
+ arms.any? { |_, kind| ARRAY_CARRIER_ARMS.include?(kind) } ? arms : nil
106
+ end
107
+
108
+ def arm_of(member)
109
+ return :tuple if member.is_a?(Type::Tuple)
110
+ return :array if MultiTargetBinder.array_element_type(member)
111
+ return :opaque if opaque_array_carrier?(member)
112
+ return :nil_value if nil_literal?(member)
113
+
114
+ :unknown
115
+ end
116
+
117
+ # A carrier that is an array the module may not decompose into per-position types: the raw `Array`, an
118
+ # `Array[untyped]` / `Array[top]`, a `Refined` / `Difference` over either, and a `Dynamic` over ANY
119
+ # carrier that would license the spread — `Dynamic[Array[Integer]]`, `Dynamic[[Integer, String]]`,
120
+ # `Dynamic[Array[Integer] | nil]` alike. {MultiTargetBinder.array_element_type} declines every
121
+ # `Dynamic` wrapper on purpose: the value is gradual, and projecting its static facet would hand each
122
+ # position a bare `T` that licenses the negative rules (issue #1093, ADR-5). That is a reason to floor
123
+ # the positions, not a reason to leave the whole value on the first parameter, so the facet decides
124
+ # only whether CRuby would splat.
125
+ def opaque_array_carrier?(type)
126
+ case type
127
+ when Type::Nominal then type.class_name == "Array"
128
+ when Type::Refined, Type::Difference then opaque_array_carrier?(type.base)
129
+ when Type::Dynamic then !arms_of(type.static_facet).nil?
130
+ else false
131
+ end
132
+ end
133
+
134
+ def table_for(shape, member, kind)
135
+ case kind
136
+ when :tuple then tuple_table(shape, member.elements)
137
+ when :array then array_table(shape, MultiTargetBinder.array_element_type(member))
138
+ when :nil_value then uniform_table(shape, member)
139
+ else uniform_table(shape, Type::Combinator.untyped)
140
+ end
141
+ end
142
+
143
+ # The split is mirrored rather than delegated to `MultiTargetBinder.decompose_tuple` because that one
144
+ # pads a missing slot with `Constant[nil]` and softens `X | nil`; a block slot past the tuple has
145
+ # always bound `Dynamic[Top]`. Without a rest the trailing positionals continue from the head (Ruby
146
+ # fills `|a, b = 1, c|` from the head when the tuple is long enough).
147
+ def tuple_table(shape, elements)
148
+ head = shape.required + shape.optional
149
+ posts = shape.post
150
+ tail_start = shape.rest ? [elements.size - posts, head].max : head
151
+ types = Array.new(head) { |i| elements[i] } + Array.new(posts) { |j| elements[tail_start + j] }
152
+ Table.new(types: types, optimistic_positions: [], rest_type: nil)
153
+ end
154
+
155
+ def array_table(shape, element)
156
+ required = shape.required
157
+ optional = shape.optional
158
+ posts = shape.post
159
+ Table.new(
160
+ types: Array.new(required) { element } + Array.new(optional) { Type::Combinator.untyped } +
161
+ Array.new(posts) { element },
162
+ optimistic_positions: (0...required).to_a + ((required + optional)...(required + optional + posts)).to_a,
163
+ rest_type: Type::Combinator.nominal_of("Array", type_args: [element])
164
+ )
165
+ end
166
+
167
+ # One type for every position: the `Dynamic[Top]` floor of a member the module cannot decompose, or
168
+ # the `nil` a `nil` member hands each of them. The `nil` rest_type leaves a named rest on the binder's
169
+ # `Array[Dynamic[Top]]` default, which is right for both — CRuby gives `nil.tap { |a, *r| }` an empty
170
+ # `r`, and an opaque carrier says nothing about the leftovers.
171
+ def uniform_table(shape, type)
172
+ Table.new(types: Array.new(shape.positional_count) { type }, optimistic_positions: [], rest_type: nil)
173
+ end
174
+
175
+ # A single member's table is used as is. Across members, a position any member leaves unfilled (a slot
176
+ # past a short Tuple) or fills with `Dynamic[Top]` stays `Dynamic[Top]`, and a member's bare `nil`
177
+ # drops out of a position another member fills with a value, that position then being optimistic —
178
+ # `MultiTargetBinder`'s cross-member softening. A named rest joins only when every member supplies
179
+ # one; otherwise the binder's `Array[Dynamic[Top]]` default stands.
180
+ def join(tables)
181
+ types, softened = join_types(tables.map(&:types))
182
+ rests = tables.map(&:rest_type)
183
+ Table.new(
184
+ types: types,
185
+ optimistic_positions: (tables.flat_map(&:optimistic_positions) + softened).uniq,
186
+ rest_type: rests.include?(nil) ? nil : Type::Combinator.union(*rests)
187
+ )
188
+ end
189
+
190
+ # @return the joined table and the positions it softened.
191
+ def join_types(tables)
192
+ return [tables.first, []] if tables.size == 1
193
+
194
+ softened = []
195
+ joined = Array.new(tables.map(&:size).max) do |i|
196
+ column = tables.map { |table| table[i] }
197
+ next Type::Combinator.untyped if column.any? { |t| t.nil? || t == Type::Combinator.untyped }
198
+
199
+ firm = column.reject { |t| nil_literal?(t) }
200
+ next Type::Combinator.union(*column) if firm.empty? || firm.size == column.size
201
+
202
+ softened << i
203
+ Type::Combinator.union(*firm)
204
+ end
205
+ [joined, softened]
206
+ end
207
+
208
+ def nil_literal?(type) = type.is_a?(Type::Constant) && type.value.nil?
209
+
210
+ # {#for} is the whole surface; the arms and the fold are this module's own business, and keeping them
211
+ # off the singleton stops a caller reaching past the fold for one member's table.
212
+ private_class_method :arms_of, :arm_of, :opaque_array_carrier?, :table_for, :tuple_table, :array_table,
213
+ :uniform_table, :join, :join_types, :nil_literal?
214
+ end
215
+ end
216
+ end