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
@@ -3,28 +3,64 @@
3
3
  require "prism"
4
4
 
5
5
  require_relative "../type"
6
+ require_relative "optimistic_origin"
7
+ require_relative "method_dispatcher/rbs_dispatch"
6
8
 
7
9
  module Rigor
8
10
  module Inference
9
11
  # Slice 5 phase 2 sub-phase 2 destructuring binder.
10
12
  #
11
- # `Rigor::Inference::MultiTargetBinder` decomposes a tuple-shaped right-hand side type
13
+ # `Rigor::Inference::MultiTargetBinder` decomposes a tuple- or array-shaped right-hand side type
12
14
  # against a Prism multi-target tree and produces a `name -> Rigor::Type` binding map. The
13
- # binder is shared between two surfaces:
15
+ # binder is shared between four surfaces:
14
16
  #
15
17
  # 1. `Rigor::Inference::StatementEvaluator#eval_multi_write` for the statement-level `a, b =
16
18
  # rhs` form (`Prism::MultiWriteNode`).
17
19
  # 2. `Rigor::Inference::BlockParameterBinder` for nested destructuring inside block parameter
18
20
  # lists (`Prism::MultiTargetNode` under `BlockParametersNode#requireds`).
21
+ # 3. `Rigor::Inference::StatementEvaluator#bind_for_index` for `for a, b in pairs`.
22
+ # 4. `Rigor::Inference::ScopeIndexer`'s class-ivar pre-pass, for the ivar targets of `@a, @b = rhs`
23
+ # (issue #1110), which records {Result#ivars} and drops the marks, as it does for `@x = xs.first`,
24
+ # with `soften_slots: false` (see {.bind_marked}).
25
+ # 5. `Rigor::Inference::StatementEvaluator`'s `case/in` pattern binding (issue #1122), through
26
+ # {.decompose_slots}: a pattern binds its names against the same carriers, minus the two
27
+ # statement-only properties that method documents.
19
28
  #
20
29
  # Both Prism nodes share the same `lefts` / `rest` (a `Prism::SplatNode`) / `rights` triple,
21
30
  # so the binder treats them uniformly. The binder is pure: it MUST NOT mutate its inputs and
22
31
  # MUST return a fresh `Hash` on every call.
23
32
  #
24
- # The binder threads `Type::Tuple` decompositions when the right-hand side carrier is a
25
- # known-arity tuple. Other carriers (`Nominal[Array]`, `Dynamic[Top]`, `Top`, `Bot`, ...)
26
- # collapse to `Dynamic[Top]` per slot — Slice 5 phase 2 sub-phase 2 stays conservative on
27
- # dynamic-arity right-hand sides until the narrower receiver-shape lattice lands.
33
+ # Per-carrier rule for the right-hand side:
34
+ #
35
+ # - `Type::Tuple` (known arity) decomposes element-wise; see {slot_type} for the softening.
36
+ # - `Nominal[Array, [T]]`, and a `Refined` / `Difference` over one (`non-empty-array[T]`
37
+ # after an `empty?` guard), binds every fixed front/back slot to `T` and a named rest to
38
+ # `Array[T]` (issue #1093). A short array pads the fixed slots with `nil` at runtime, so
39
+ # `T` is the same optimistic read `Array#[]` / `Array#first` get past core RBS's
40
+ # `%a{implicitly-returns-nil}`: the binder reports those names in {Result#optimistic}, and
41
+ # every caller that applies the bindings to a scope records them through
42
+ # `Scope#with_optimistic_local` ({Result#apply_to}) so the ADR-101 branch elision
43
+ # declines on them. A nested target under an optimistic slot inherits the mark (`nil`
44
+ # destructures to `nil` for every inner name); the rest never carries it.
45
+ # - A `Type::Union` distributes (issue #1094): each member decomposes on its own against the
46
+ # same target tree, and each name binds the join of its per-member types. A member that binds
47
+ # a name to `Dynamic[Top]` — every name, for a member no rule here decomposes — makes that
48
+ # name `Dynamic[Top]` for the whole union rather than dropping out of the join. A member that
49
+ # binds a name to bare `nil` while another binds a value drops out of that name's join, and
50
+ # the name is marked optimistic: the per-slot softening of {slot_type}, applied across
51
+ # members (see {join_member_bindings}). So `a, b = ints[1..]` (`Array[Integer] | nil`) binds
52
+ # `Integer` to each slot, marked, exactly as a short `ints` would. A name is also optimistic
53
+ # when any member marked it, the rule `Scope#join` applies to the same mark at a merge.
54
+ # - A value that provably has no implicit `to_ary` conversion
55
+ # ({MethodDispatcher::RbsDispatch.array_conversion_free?}: `Integer`, `String`, `Hash`,
56
+ # `nil`, ... and RBS-known classes whose ancestry neither defines `to_ary` nor overrides
57
+ # `method_missing` / `respond_to_missing?`) is what Ruby wraps as `[rhs]`, so it decomposes
58
+ # as that one-element `Tuple` (issue #1094): `a, b = 1` binds `1` and `nil`. The wrap is
59
+ # exact rather than a bet, so it adds no mark.
60
+ # - Everything else — raw `Array`, `Array[Dynamic[top]]`, `Dynamic[Array[T]]` (whose static
61
+ # facet must not surface as a bare `T`), a value that may convert (`SimpleDelegator`, a
62
+ # Ruby-source class, a module, `Object`), `Top`, `Bot` — collapses to `Dynamic[Top]` per
63
+ # slot.
28
64
  #
29
65
  # Targets the binder recognises:
30
66
  #
@@ -37,65 +73,299 @@ module Rigor
37
73
  # same observable semantics (binding a fresh local in the block-entry scope).
38
74
  # - `Prism::MultiTargetNode` — recurses with the slot's type as the new right-hand side.
39
75
  # - `Prism::SplatNode` (used for `rest`) — its `expression` MUST be a
40
- # `Prism::LocalVariableTargetNode` or a `Prism::RequiredParameterNode` to be observable;
41
- # an anonymous `*` splat or a non-local target is skipped.
76
+ # `Prism::LocalVariableTargetNode`, a `Prism::RequiredParameterNode`, a
77
+ # `Prism::InstanceVariableTargetNode` or a `Prism::IndexTargetNode` to be observable; an
78
+ # anonymous `*` splat or any other target is skipped.
79
+ # - `Prism::InstanceVariableTargetNode` (issue #1110), as a fixed slot, a rest
80
+ # (`*@rest`), or inside a nested target. It decomposes by the same carrier rules as a
81
+ # local and is reported apart, in {Result#ivars} / {Result#optimistic_ivars}, so a caller
82
+ # that threads only locals never sees it. The binder keys it internally by its `:@name`,
83
+ # which no local name can collide with.
84
+ # - `Prism::IndexTargetNode` (`h[:a], x = rhs`), in the same three positions. It binds no
85
+ # name: it stores its slot through `[]=` on its receiver. The binder reports the value it
86
+ # stores in {Result#index_targets}, keyed by the target node itself, so
87
+ # `StatementEvaluator#eval_multi_write` and `#bind_for_index` (`for h[:a], w in pairs`) can
88
+ # widen the receiver's literal shape with that value as content evidence, exactly as a plain
89
+ # `h[:a] = 1` does. It carries no optimistic mark: there is no binding for one to qualify.
90
+ # Its slot is softened as a local's is; `eval_multi_write` documents why that stays honest
91
+ # without the mark.
42
92
  #
43
- # Other target kinds (`InstanceVariableTargetNode`, `ConstantTargetNode`,
44
- # `IndexTargetNode`, `CallTargetNode`, `ConstantPathTargetNode`, `ImplicitRestNode`, ...)
45
- # MUST be silently skipped: they have no observable contribution to the local-variable scope
46
- # the StatementEvaluator threads.
93
+ # Other target kinds (`ClassVariableTargetNode`, `GlobalVariableTargetNode`,
94
+ # `ConstantTargetNode`, `CallTargetNode`, `ConstantPathTargetNode`, `ImplicitRestNode`, ...)
95
+ # MUST be silently skipped: they have no observable contribution to the scope the
96
+ # StatementEvaluator threads.
47
97
  #
48
98
  # See docs/internal-spec/inference-engine.md for the binding contract and
49
99
  # docs/adr/4-type-inference-engine.md for the slice rationale.
50
100
  module MultiTargetBinder
101
+ NO_BINDINGS = {}.freeze
102
+ NO_NAMES = [].freeze
103
+ private_constant :NO_BINDINGS, :NO_NAMES
104
+
105
+ # `types` is the local `name -> Rigor::Type` map {.bind} returns; `optimistic` the frozen list
106
+ # of local names whose nil-freeness is the short-array bet described in the module comment.
107
+ # `ivars` / `optimistic_ivars` are the same pair for instance-variable targets.
108
+ # `index_targets` maps each `Prism::IndexTargetNode` to the value it stores; {#apply_to}
109
+ # binds nothing for it.
110
+ Result = Data.define(:types, :optimistic, :ivars, :optimistic_ivars, :index_targets) do
111
+ def initialize(types:, optimistic:, ivars: NO_BINDINGS, optimistic_ivars: NO_NAMES,
112
+ index_targets: NO_BINDINGS)
113
+ super
114
+ end
115
+
116
+ # Binds every name into `scope` and records the optimistic mark after the binding, since
117
+ # `Scope#with_local` / `Scope#with_ivar` drop any mark the name carried before. `miss` is what
118
+ # each marked slot answers on a miss (issue #1302): `nil` for every mark the binder makes itself —
119
+ # a short array pads with `nil`, and a `nil` destructures to `nil` — so only a caller whose
120
+ # right-hand side a miss can make something else passes another answer
121
+ # ({OptimisticOrigin.destructuring_miss}).
122
+ def apply_to(scope, miss: nil)
123
+ bound = types.reduce(scope) { |acc, (name, type)| acc.with_local(name, type) }
124
+ bound = ivars.reduce(bound) { |acc, (name, type)| acc.with_ivar(name, type) }
125
+ bound = optimistic.reduce(bound) do |acc, name|
126
+ acc.with_optimistic_local(name, OptimisticOrigin::IMPLICITLY_RETURNS_NIL, miss: miss)
127
+ end
128
+ optimistic_ivars.reduce(bound) do |acc, name|
129
+ acc.with_optimistic_ivar(name, OptimisticOrigin::IMPLICITLY_RETURNS_NIL, miss: miss)
130
+ end
131
+ end
132
+ end
133
+
51
134
  module_function
52
135
 
53
136
  # @param rhs_type — type of the right-hand side
54
- def bind(target_node, rhs_type)
137
+ # @param scope — the scope the destructure is evaluated in, which answers the `to_ary`
138
+ # question for a class outside the core list; without one only the core list wraps.
139
+ def bind(target_node, rhs_type, scope: nil)
140
+ bind_marked(target_node, rhs_type, scope: scope).types
141
+ end
142
+
143
+ # @param optimistic — whether `rhs_type` itself is an optimistic slot of an enclosing
144
+ # decomposition (a block's `|(g, h)|` fed from an auto-splatted `Array[T]`) or an
145
+ # optimistically nil-free value (`k, v = xs.first`), in which case every name bound under it
146
+ # inherits the mark. It may instead be the per-element Array
147
+ # {OptimisticOrigin.destructuring_marks} builds for a literal right-hand side
148
+ # (`x, y = xs.first, 1`), which marks the names under each fixed slot by that slot's element.
149
+ # @param soften_slots — false keeps a present `X | nil` tuple slot and a union member's bare
150
+ # `nil` in the binding instead of applying the ADR-57 softening of {slot_type} /
151
+ # {join_member_bindings}. That softening is only honest together with the optimistic mark
152
+ # it records, so a consumer that drops the marks (the class-ivar seed, issue #1110) turns it
153
+ # off; the `Array[T]` bet is unaffected, matching what `@x = xs.first` records.
154
+ def bind_marked(target_node, rhs_type, optimistic: false, scope: nil, soften_slots: true)
55
155
  bindings = {}
56
- visit(target_node, rhs_type, bindings)
57
- bindings
156
+ marked = []
157
+ visit(target_node, rhs_type, optimistic, bindings, marked, [scope, soften_slots])
158
+ split_result(bindings, marked)
159
+ end
160
+
161
+ # The per-slot types of a positional pattern (`in [i, s]`, `in [*pre, m, *post]`), sharing
162
+ # every carrier rule {.bind_marked} applies to a multi-write target except two statement-only
163
+ # properties (issue #1122):
164
+ #
165
+ # - the multi-assign `[rhs]` wrap is NOT applied. `a, b = 1` binds `a` to `1` because Ruby wraps
166
+ # a right-hand side with no implicit `to_ary`; an array pattern instead matches through the
167
+ # subject's `deconstruct` / `to_ary`, and a subject with neither raises
168
+ # `NoMatchingPatternError` — no body is reached, so binding `1` there would type dead code.
169
+ # - no optimistic mark is reported. The ADR-101 short-array bet exists because a SHORT
170
+ # right-hand side pads the fixed slots with `nil`; a pattern that matched has every fixed slot
171
+ # it named, so there is nothing to bet on and `Result#optimistic` has no counterpart here.
172
+ #
173
+ # A `Type::Union` is the caller's to distribute: a pattern matches SOME member, and only the
174
+ # caller can tell which members can match it at all. Returns `[fronts, rest_type, backs]`, with
175
+ # every carrier no rule decomposes answering `Dynamic[Top]` per slot — the floor.
176
+ def decompose_slots(rhs_type, front_count:, back_count:, rest_present:, scope: nil, soften_slots: true)
177
+ fronts, rest_type, backs, = decompose(
178
+ rhs_type, front_count, back_count, rest_present: rest_present,
179
+ context: [scope, soften_slots], wrap_single: false
180
+ )
181
+ [fronts, rest_type, backs]
182
+ end
183
+
184
+ # The `T` of an `Array[T]` carrier the binder may decompose, or nil. Declines raw `Array`
185
+ # and an untyped / top element, which bind `Dynamic[Top]` per slot as before, and every
186
+ # `Dynamic` wrapper: `Dynamic[Array[T]]` is gradual, and projecting its static facet would
187
+ # hand the slot a bare `T` that licenses the negative rules (ADR-5). Shared with
188
+ # {BlockParameterBinder}'s auto-splat so both surfaces accept the same carriers.
189
+ def array_element_type(type)
190
+ case type
191
+ when Type::Nominal
192
+ return nil unless type.class_name == "Array" && type.type_args.size == 1
193
+
194
+ element = type.type_args.first
195
+ element.is_a?(Type::Dynamic) || element.is_a?(Type::Top) ? nil : element
196
+ when Type::Refined, Type::Difference
197
+ array_element_type(type.base)
198
+ end
199
+ end
200
+
201
+ # The class whose instances `type` describes, for the `to_ary` question, or nil when `type`
202
+ # names no single class: `Dynamic`, `Top`, `Singleton`, `Intersection` and the rest decline.
203
+ # A union never reaches here; the binder distributes it first.
204
+ def conversion_class_name(type)
205
+ case type
206
+ when Type::Constant then type.value.class.name
207
+ when Type::Nominal then type.class_name
208
+ when Type::HashShape then "Hash"
209
+ when Type::IntegerRange then "Integer"
210
+ when Type::Refined, Type::Difference then conversion_class_name(type.base)
211
+ end
58
212
  end
59
213
 
60
214
  class << self
61
215
  private
62
216
 
63
- def visit(node, rhs_type, bindings)
217
+ # Partitions the walk's single map: an ivar name carries its `@` sigil, which a local name
218
+ # never does, and an index target is keyed by its node rather than by a name.
219
+ def split_result(bindings, marked)
220
+ return Result.new(types: bindings, optimistic: marked.freeze) if bindings.each_key.all? { |k| local_key?(k) }
221
+
222
+ grouped = bindings.group_by { |key, _| key_kind(key) }.transform_values(&:to_h)
223
+ ivar_marked, local_marked = marked.partition { |name| key_kind(name) == :ivar }
224
+ Result.new(types: grouped.fetch(:local, {}), optimistic: local_marked.freeze,
225
+ ivars: grouped.fetch(:ivar, NO_BINDINGS), optimistic_ivars: ivar_marked.freeze,
226
+ index_targets: grouped.fetch(:index, NO_BINDINGS))
227
+ end
228
+
229
+ def local_key?(key) = key.is_a?(Symbol) && !key.start_with?("@")
230
+
231
+ def key_kind(key)
232
+ return :index unless key.is_a?(Symbol)
233
+
234
+ key.start_with?("@") ? :ivar : :local
235
+ end
236
+
237
+ # `context` is the `[scope, soften_slots]` pair every step of the walk shares.
238
+ def visit(node, rhs_type, optimistic, bindings, marked, context)
239
+ if rhs_type.is_a?(Type::Union)
240
+ return visit_union(node, rhs_type.members, optimistic, bindings, marked, context)
241
+ end
242
+
64
243
  lefts = node.lefts || []
65
244
  rest = node.rest
66
245
  rights = node.rights || []
246
+ rest_present = !rest.nil?
67
247
 
68
- fronts, rest_type, backs = decompose(rhs_type, lefts.size, rights.size, rest_present: !rest.nil?)
69
- lefts.each_with_index { |t, i| bind_target(t, fronts[i], bindings) }
70
- bind_rest_target(rest, rest_type, bindings) if rest
71
- rights.each_with_index { |t, i| bind_target(t, backs[i], bindings) }
248
+ fronts, rest_type, backs, slots_optimistic =
249
+ decompose(rhs_type, lefts.size, rights.size, rest_present: rest_present, context: context)
250
+ rest_type = arity_free_rest(rest_type) if optimistic == true
251
+ front_marks, back_marks =
252
+ slot_marks(optimistic, slots_optimistic, rhs_type, [lefts.size, rights.size], rest_present: rest_present)
253
+ lefts.each_with_index { |t, i| bind_target(t, fronts[i], front_marks[i], bindings, marked, context) }
254
+ bind_rest_target(rest, rest_type, bindings, marked) if rest
255
+ rights.each_with_index { |t, i| bind_target(t, backs[i], back_marks[i], bindings, marked, context) }
256
+ end
257
+
258
+ # The `[front_marks, back_marks]` pair a visit hands its fixed slots. A boolean `optimistic`
259
+ # applies to every slot, as does the decomposition's own short-array bet (`fallback`, the
260
+ # issue #1093 `Array[T]` mark). A per-element Array applies only to the `Type::Tuple` a literal
261
+ # right-hand side types as, of the same arity, and each slot picks its element at the offset
262
+ # {decompose_tuple} reads ({slot_offsets}); any other carrier falls back to `fallback`, and a
263
+ # slot past the literal's end binds an exact `nil` that no mark qualifies.
264
+ def slot_marks(optimistic, fallback, rhs_type, (front_count, back_count), rest_present:)
265
+ literal = optimistic.is_a?(Array) && rhs_type.is_a?(Type::Tuple) && rhs_type.elements.size == optimistic.size
266
+ uniform = optimistic.is_a?(Array) ? fallback : optimistic || fallback
267
+ return [[uniform] * front_count, [uniform] * back_count] unless literal
268
+
269
+ slot_offsets(optimistic.size, front_count, back_count, rest_present: rest_present)
270
+ .map { |offsets| offsets.map { |i| optimistic.fetch(i, false) } }
271
+ end
272
+
273
+ # Every member walks the same target tree, so each binds the same names; the first member's
274
+ # key order is the declaration order.
275
+ def visit_union(node, members, optimistic, bindings, marked, context)
276
+ walks = members.map do |member|
277
+ member_bindings = {}
278
+ member_marked = []
279
+ visit(node, member, optimistic, member_bindings, member_marked, context)
280
+ [member_bindings, member_marked]
281
+ end
282
+ walks.first.first.each_key do |name|
283
+ types = walks.map { |member_bindings, _| member_bindings[name] }
284
+ joined, softened = join_member_bindings(types, context.last)
285
+ mark = softened || walks.any? { |_, member_marked| member_marked.include?(name) }
286
+ bind_name(name, joined, mark, bindings, marked)
287
+ end
288
+ end
289
+
290
+ # Returns `[type, softened]`. `Dynamic[Top]` from any member is the whole answer: the join
291
+ # must not let a decomposable member's precise type stand for a member nothing is known
292
+ # about. A member that binds the name to bare `nil` — a `nil` slot, a slot past a short
293
+ # member, or a `nil` member wrapped as `[nil]` — is left out of the join when another member
294
+ # binds a value, and `softened` reports it so the caller marks the name optimistic. This is
295
+ # {slot_type}'s ADR-57 softening across members: which member arrived is correlated with the
296
+ # other slots (`k, v = hash.find { ... }; v.x if k`, `status, value = ok ? [:ok, v] : [:err]`),
297
+ # and a per-slot `T | nil` fires `call.possible-nil-receiver` on the guarded read.
298
+ def join_member_bindings(types, soften_slots)
299
+ untyped = Type::Combinator.untyped
300
+ return [untyped, false] if types.any? { |type| type == untyped }
301
+
302
+ firm = types.reject { |type| nil_literal?(type) }
303
+ return [Type::Combinator.union(*types), false] if !soften_slots || firm.empty? || firm.size == types.size
304
+
305
+ [Type::Combinator.union(*firm), true]
72
306
  end
73
307
 
74
308
  # Decomposes the right-hand side type into the per-slot types. Returns a `[fronts,
75
- # rest_type, backs]` triple, with `fronts` and `backs` each an ordered array of length
76
- # `front_count`/`back_count`, and `rest_type` either a `Rigor::Type` (when
77
- # `rest_present:` is true) or `nil`.
78
- def decompose(rhs_type, front_count, back_count, rest_present:)
309
+ # rest_type, backs, optimistic]` quadruple, with `fronts` and `backs` each an ordered
310
+ # array of length `front_count`/`back_count`, `rest_type` either a `Rigor::Type` (when
311
+ # `rest_present:` is true) or `nil`, and `optimistic` true when the fixed slots are the
312
+ # short-array bet rather than known elements.
313
+ def decompose(rhs_type, front_count, back_count, rest_present:, context:, wrap_single: true)
314
+ scope, soften = context
79
315
  if rhs_type.is_a?(Type::Tuple)
80
- decompose_tuple(rhs_type, front_count, back_count, rest_present: rest_present)
316
+ [*decompose_tuple(rhs_type, front_count, back_count, rest_present: rest_present, soften: soften), false]
317
+ elsif (element = array_element_type(rhs_type))
318
+ [*decompose_array(element, front_count, back_count, rest_present: rest_present), true]
319
+ elsif wrap_single && wraps_as_single_element?(rhs_type, scope)
320
+ wrapped = Type::Combinator.tuple_of(rhs_type)
321
+ [*decompose_tuple(wrapped, front_count, back_count, rest_present: rest_present, soften: soften), false]
81
322
  else
82
- decompose_default(front_count, back_count, rest_present: rest_present)
323
+ [*decompose_default(front_count, back_count, rest_present: rest_present), false]
83
324
  end
84
325
  end
85
326
 
86
- def decompose_tuple(tuple, front_count, back_count, rest_present:)
327
+ # Ruby's `[rhs]` wrap for a value with no implicit `to_ary`; see the module comment.
328
+ def wraps_as_single_element?(rhs_type, scope)
329
+ class_name = conversion_class_name(rhs_type)
330
+ !class_name.nil? && MethodDispatcher::RbsDispatch.array_conversion_free?(class_name, scope)
331
+ end
332
+
333
+ # Under an inherited mark the whole right-hand side may be the `nil` a short outer array padded in, and
334
+ # `(p, *q) = nil` binds `q = []`. A Tuple rest would claim the middle elements are present
335
+ # (`q.first.nil?` folding to `false`), so it widens to `Array[union of the middle]`, which admits the
336
+ # empty array; an empty middle has no element type to offer and stays `Array[Dynamic[top]]`.
337
+ def arity_free_rest(rest_type)
338
+ return rest_type unless rest_type.is_a?(Type::Tuple)
339
+
340
+ element = rest_type.elements.empty? ? Type::Combinator.untyped : Type::Combinator.union(*rest_type.elements)
341
+ Type::Combinator.nominal_of("Array", type_args: [element])
342
+ end
343
+
344
+ def decompose_array(element, front_count, back_count, rest_present:)
345
+ [
346
+ Array.new(front_count) { element },
347
+ rest_present ? Type::Combinator.nominal_of("Array", type_args: [element]) : nil,
348
+ Array.new(back_count) { element }
349
+ ]
350
+ end
351
+
352
+ def decompose_tuple(tuple, front_count, back_count, rest_present:, soften:)
87
353
  elements = tuple.elements
88
- fronts = Array.new(front_count) { |i| slot_type(elements, i) }
89
- if rest_present
90
- middle_end = [elements.size - back_count, front_count].max
91
- middle = elements[front_count...middle_end] || []
92
- rest_type = Type::Combinator.tuple_of(*middle)
93
- backs = Array.new(back_count) { |i| slot_type(elements, middle_end + i) }
94
- else
95
- rest_type = nil
96
- backs = Array.new(back_count) { |i| slot_type(elements, front_count + i) }
97
- end
98
- [fronts, rest_type, backs]
354
+ fronts, backs = slot_offsets(elements.size, front_count, back_count, rest_present: rest_present)
355
+ .map { |offsets| offsets.map { |i| slot_type(elements, i, soften) } }
356
+ # The end is clamped at the fronts, as {slot_offsets} clamps the backs: an unclamped negative end
357
+ # (more back slots than elements) would count from the end instead. A range starting past the end
358
+ # slices to `nil`.
359
+ middle = elements[front_count...[elements.size - back_count, front_count].max] || []
360
+ [fronts, rest_present ? Type::Combinator.tuple_of(*middle) : nil, backs]
361
+ end
362
+
363
+ # The element offsets a `[fronts, backs]` pair of fixed slots reads out of `size` elements: the
364
+ # back slots follow the middle a rest absorbs, and follow the fronts directly without one. An
365
+ # offset past `size` is a slot the source is too short to fill.
366
+ def slot_offsets(size, front_count, back_count, rest_present:)
367
+ back_start = rest_present ? [size - back_count, front_count].max : front_count
368
+ [Array.new(front_count) { |i| i }, Array.new(back_count) { |i| back_start + i }]
99
369
  end
100
370
 
101
371
  # The per-slot type for index `i` of a tuple decomposition, FP-safely softened: a
@@ -116,11 +386,11 @@ module Rigor
116
386
  # outranks the worst-case per-slot reading, so we drop the `nil` from a destructured slot
117
387
  # and keep the non-`nil` constituent (a bare `nil` slot stays `nil` — there is nothing to
118
388
  # soften). A pure non-optional element keeps its precise type unchanged.
119
- def slot_type(elements, index)
389
+ def slot_type(elements, index, soften)
120
390
  element = elements[index]
121
391
  return Type::Combinator.constant_of(nil) if element.nil?
122
392
 
123
- soften_optional_slot(element)
393
+ soften ? soften_optional_slot(element) : element
124
394
  end
125
395
 
126
396
  def soften_optional_slot(element)
@@ -145,24 +415,42 @@ module Rigor
145
415
  ]
146
416
  end
147
417
 
148
- def bind_target(target, type, bindings)
149
- case target
150
- when Prism::LocalVariableTargetNode, Prism::RequiredParameterNode
151
- bindings[target.name] = type
152
- when Prism::MultiTargetNode
153
- visit(target, type, bindings)
154
- end
418
+ # A per-element Array mark reaches a name only through a nested target: a name bound to a
419
+ # whole literal array holds an Array, which is never `nil`. Nor does any mark reach a name bound
420
+ # to an exact `nil` — a slot past a Tuple's end or a wrap's empty slot — which is `nil` whether
421
+ # or not the source missed, so the mark could only withhold an honest verdict.
422
+ def bind_target(target, type, optimistic, bindings, marked, context)
423
+ return visit(target, type, optimistic, bindings, marked, context) if target.is_a?(Prism::MultiTargetNode)
424
+
425
+ key = binding_key(target)
426
+ bind_name(key, type, optimistic == true && !nil_literal?(type), bindings, marked) if key
155
427
  end
156
428
 
157
- def bind_rest_target(splat_node, type, bindings)
429
+ # The rest is an `Array` even when the right-hand side is short, so it is never marked.
430
+ def bind_rest_target(splat_node, type, bindings, marked)
158
431
  return unless splat_node.is_a?(Prism::SplatNode)
159
432
 
160
- expression = splat_node.expression
161
- case expression
162
- when Prism::LocalVariableTargetNode, Prism::RequiredParameterNode
163
- bindings[expression.name] = type
433
+ key = binding_key(splat_node.expression)
434
+ bind_name(key, type, false, bindings, marked) if key
435
+ end
436
+
437
+ # A named target's name, an index target's node (it binds no name, see {bind_name}), or nil for
438
+ # a target that contributes nothing to the scope.
439
+ def binding_key(target)
440
+ case target
441
+ when Prism::LocalVariableTargetNode, Prism::RequiredParameterNode, Prism::InstanceVariableTargetNode
442
+ target.name
443
+ when Prism::IndexTargetNode then target
164
444
  end
165
445
  end
446
+
447
+ # A later binding of the same name (`a, a = ints`) wins, mark included. `name` is the
448
+ # target node for an index target, which binds no name and so is never marked.
449
+ def bind_name(name, type, optimistic, bindings, marked)
450
+ bindings[name] = type
451
+ marked.delete(name)
452
+ marked << name if optimistic && name.is_a?(Symbol)
453
+ end
166
454
  end
167
455
  end
168
456
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "../type"
4
4
  require_relative "content_join"
5
+ require_relative "rewrite_mutation"
5
6
 
6
7
  module Rigor
7
8
  module Inference
@@ -48,7 +49,9 @@ module Rigor
48
49
  MutationWidening.join_added_pairs(pre_state, method_name, arg_types,
49
50
  [pinned_evidence(seed_keys), pinned_evidence(seed_values)])
50
51
  end
51
- rejoined = keep_precise_parameters(pre_state, rejoined)
52
+ # The arm lands after the precise parameters are restored: a rewriter falsifies a proven side too
53
+ # (`Hash.new(0)` under `transform_values!(&:to_s)` holds Strings), which an adder's one store cannot.
54
+ rejoined = RewriteMutation.arm(keep_precise_parameters(pre_state, rejoined), method_name)
52
55
  rejoined == nominal ? nil : rejoined
53
56
  end
54
57