rigortype 0.4.6 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (550) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -2
  3. data/data/core_overlay/bigdecimal.rbs +33 -0
  4. data/data/core_overlay/resolv.rbs +30 -0
  5. data/data/gem_overlay/activesupport/core_ext.rbs +45 -13
  6. data/data/gem_overlay/addressable/addressable.rbs +40 -0
  7. data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
  8. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
  9. data/docs/handbook/01-getting-started.md +6 -3
  10. data/docs/handbook/11-sig-gen.md +85 -3
  11. data/docs/handbook/appendix-phpstan.md +4 -4
  12. data/docs/install.md +20 -0
  13. data/docs/manual/01-installation.md +67 -26
  14. data/docs/manual/02-cli-reference.md +455 -113
  15. data/docs/manual/03-configuration.md +76 -5
  16. data/docs/manual/04-diagnostics.md +5 -4
  17. data/docs/manual/06-baseline.md +6 -4
  18. data/docs/manual/08-skills.md +11 -0
  19. data/docs/manual/09-editor-integration.md +21 -2
  20. data/docs/manual/10-mcp-server.md +38 -2
  21. data/docs/manual/12-caching.md +4 -3
  22. data/docs/manual/14-rails-quickstart.md +1 -1
  23. data/docs/manual/19-effect-labels.md +3 -4
  24. data/docs/manual/plugins/rigor-actionpack.md +32 -15
  25. data/docs/manual/plugins/rigor-activesupport-core-ext.md +23 -1
  26. data/docs/manual/plugins/rigor-sorbet.md +17 -3
  27. data/lib/rigor/analysis/baseline.rb +24 -27
  28. data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
  29. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  30. data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
  31. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  32. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +12 -4
  33. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  34. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +85 -13
  35. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +16 -4
  36. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -1
  37. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  38. data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
  39. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +41 -12
  40. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
  41. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +3 -3
  42. data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
  43. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  44. data/lib/rigor/analysis/check_rules/rule_ids.rb +9 -2
  45. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  46. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  47. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  48. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +2 -2
  49. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +2 -2
  50. data/lib/rigor/analysis/check_rules.rb +720 -141
  51. data/lib/rigor/analysis/crash_signature.rb +45 -3
  52. data/lib/rigor/analysis/dependency_recorder.rb +237 -80
  53. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  54. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  55. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  56. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  57. data/lib/rigor/analysis/edit_replay.rb +93 -6
  58. data/lib/rigor/analysis/fact_store.rb +3 -3
  59. data/lib/rigor/analysis/incremental.rb +11 -1
  60. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +4 -3
  61. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  62. data/lib/rigor/analysis/incremental_session.rb +306 -53
  63. data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
  64. data/lib/rigor/analysis/rule_catalog.rb +39 -3
  65. data/lib/rigor/analysis/run_cache_key.rb +27 -3
  66. data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
  67. data/lib/rigor/analysis/run_stats.rb +2 -2
  68. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
  69. data/lib/rigor/analysis/runner/declaration_position.rb +8 -8
  70. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +264 -112
  71. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +4 -9
  72. data/lib/rigor/analysis/runner/pool_coordinator.rb +173 -328
  73. data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
  74. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  75. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  76. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  77. data/lib/rigor/analysis/runner.rb +298 -109
  78. data/lib/rigor/analysis/self_call_resolution_recorder.rb +62 -23
  79. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  80. data/lib/rigor/analysis/template_units.rb +9 -1
  81. data/lib/rigor/analysis/worker_session.rb +65 -71
  82. data/lib/rigor/bleeding_edge.rb +22 -10
  83. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  84. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  85. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  86. data/lib/rigor/cache/descriptor.rb +6 -5
  87. data/lib/rigor/cache/file_digest.rb +39 -31
  88. data/lib/rigor/cache/incremental_snapshot.rb +95 -17
  89. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  90. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  91. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  92. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  93. data/lib/rigor/cache/rbs_descriptor.rb +90 -4
  94. data/lib/rigor/cache/rbs_environment.rb +8 -1
  95. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  96. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  97. data/lib/rigor/cache/store.rb +11 -10
  98. data/lib/rigor/ci_detector.rb +1 -1
  99. data/lib/rigor/cli/annotate_command.rb +24 -19
  100. data/lib/rigor/cli/baseline_command.rb +160 -57
  101. data/lib/rigor/cli/catalogue.rb +256 -0
  102. data/lib/rigor/cli/check_command.rb +112 -104
  103. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  104. data/lib/rigor/cli/command.rb +15 -1
  105. data/lib/rigor/cli/coverage_command.rb +21 -8
  106. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  107. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  108. data/lib/rigor/cli/coverage_scan.rb +3 -5
  109. data/lib/rigor/cli/diff_command.rb +25 -20
  110. data/lib/rigor/cli/docs_command.rb +1 -2
  111. data/lib/rigor/cli/doctor_command.rb +12 -12
  112. data/lib/rigor/cli/effects_command.rb +50 -40
  113. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  114. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  115. data/lib/rigor/cli/effects_renderer.rb +2 -1
  116. data/lib/rigor/cli/effects_snapshot_command.rb +26 -18
  117. data/lib/rigor/cli/explain_command.rb +18 -8
  118. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  119. data/lib/rigor/cli/json_document.rb +41 -0
  120. data/lib/rigor/cli/lens_command.rb +29 -10
  121. data/lib/rigor/cli/lsp_command.rb +250 -23
  122. data/lib/rigor/cli/mcp_command.rb +12 -7
  123. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  124. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  125. data/lib/rigor/cli/options.rb +40 -1
  126. data/lib/rigor/cli/plugin_command.rb +93 -46
  127. data/lib/rigor/cli/plugins_command.rb +17 -18
  128. data/lib/rigor/cli/plugins_renderer.rb +10 -6
  129. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  130. data/lib/rigor/cli/protection_renderer.rb +2 -1
  131. data/lib/rigor/cli/show_bleedingedge_command.rb +21 -15
  132. data/lib/rigor/cli/sig_gen_command.rb +92 -27
  133. data/lib/rigor/cli/skill_command.rb +32 -3
  134. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  135. data/lib/rigor/cli/skill_describe.rb +2 -1
  136. data/lib/rigor/cli/trace_command.rb +14 -9
  137. data/lib/rigor/cli/triage_command.rb +10 -11
  138. data/lib/rigor/cli/triage_renderer.rb +2 -1
  139. data/lib/rigor/cli/type_of_command.rb +16 -11
  140. data/lib/rigor/cli/type_of_project_scope.rb +1 -4
  141. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  142. data/lib/rigor/cli/type_scan_command.rb +14 -9
  143. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  144. data/lib/rigor/cli/unused_command.rb +27 -17
  145. data/lib/rigor/cli/upgrade_command.rb +5 -0
  146. data/lib/rigor/cli.rb +214 -56
  147. data/lib/rigor/config_audit.rb +33 -19
  148. data/lib/rigor/configuration/severity_profile.rb +14 -5
  149. data/lib/rigor/configuration.rb +104 -35
  150. data/lib/rigor/effects/collector.rb +53 -21
  151. data/lib/rigor/effects/definition_context.rb +0 -2
  152. data/lib/rigor/effects/envelope.rb +1 -1
  153. data/lib/rigor/effects/framework_units.rb +1 -1
  154. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  155. data/lib/rigor/effects/propagator.rb +1 -1
  156. data/lib/rigor/effects/registry.rb +2 -2
  157. data/lib/rigor/effects/snapshot.rb +1 -1
  158. data/lib/rigor/effects/taint_cause.rb +0 -2
  159. data/lib/rigor/effects/unit_scan.rb +9 -13
  160. data/lib/rigor/environment/bundle_sig_discovery.rb +64 -33
  161. data/lib/rigor/environment/class_registry.rb +3 -3
  162. data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
  163. data/lib/rigor/environment/core_root.rb +77 -0
  164. data/lib/rigor/environment/default_libraries.rb +18 -1
  165. data/lib/rigor/environment/deferred_signature_plan.rb +381 -0
  166. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  167. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  168. data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
  169. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  170. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  171. data/lib/rigor/environment/member_consistency.rb +56 -7
  172. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  173. data/lib/rigor/environment/project_path_gems.rb +124 -0
  174. data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
  175. data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
  176. data/lib/rigor/environment/rbs_hierarchy.rb +12 -15
  177. data/lib/rigor/environment/rbs_loader.rb +772 -110
  178. data/lib/rigor/environment/reflection.rb +7 -7
  179. data/lib/rigor/environment/required_features.rb +0 -3
  180. data/lib/rigor/environment/ruby_library_roots.rb +1 -1
  181. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  182. data/lib/rigor/environment.rb +169 -42
  183. data/lib/rigor/flow_contribution/merger.rb +4 -4
  184. data/lib/rigor/inference/acceptance.rb +136 -40
  185. data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
  186. data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
  187. data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
  188. data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
  189. data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
  190. data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
  191. data/lib/rigor/inference/block_auto_splat.rb +1 -1
  192. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  193. data/lib/rigor/inference/block_run_count.rb +185 -0
  194. data/lib/rigor/inference/budget_trace.rb +8 -2
  195. data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
  196. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  197. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  198. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  199. data/lib/rigor/inference/callee_mutation_chain.rb +88 -77
  200. data/lib/rigor/inference/callee_write_census.rb +815 -0
  201. data/lib/rigor/inference/captured_locals.rb +2 -0
  202. data/lib/rigor/inference/census_foreign_block.rb +354 -0
  203. data/lib/rigor/inference/clause_widening.rb +1 -1
  204. data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
  205. data/lib/rigor/inference/construction_escape.rb +258 -0
  206. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  207. data/lib/rigor/inference/declaration_walk.rb +4 -0
  208. data/lib/rigor/inference/deep_erasure.rb +39 -7
  209. data/lib/rigor/inference/def_handle.rb +1 -1
  210. data/lib/rigor/inference/def_node_resolver.rb +33 -15
  211. data/lib/rigor/inference/def_return_typer.rb +47 -5
  212. data/lib/rigor/inference/definer_resolution.rb +19 -9
  213. data/lib/rigor/inference/definers_index.rb +162 -0
  214. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  215. data/lib/rigor/inference/element_read_widening.rb +49 -3
  216. data/lib/rigor/inference/error_info.rb +16 -0
  217. data/lib/rigor/inference/expression_typer.rb +1559 -320
  218. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  219. data/lib/rigor/inference/flow_tracer.rb +31 -14
  220. data/lib/rigor/inference/fork_map.rb +3 -0
  221. data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
  222. data/lib/rigor/inference/global_write_census.rb +81 -5
  223. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  224. data/lib/rigor/inference/hkt_body.rb +1 -2
  225. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  226. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  227. data/lib/rigor/inference/hkt_registry.rb +3 -5
  228. data/lib/rigor/inference/in_effect_refinements.rb +7 -0
  229. data/lib/rigor/inference/indexed_narrowing.rb +61 -15
  230. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  231. data/lib/rigor/inference/ivar_writer_census.rb +154 -8
  232. data/lib/rigor/inference/key_presence_guard.rb +51 -24
  233. data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
  234. data/lib/rigor/inference/last_line.rb +5 -2
  235. data/lib/rigor/inference/loop_value_record.rb +66 -14
  236. data/lib/rigor/inference/match_rebinding/frame.rb +57 -0
  237. data/lib/rigor/inference/match_rebinding/yielding_match.rb +101 -0
  238. data/lib/rigor/inference/match_rebinding.rb +28 -5
  239. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  240. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  241. data/lib/rigor/inference/method_dispatcher/block_folding.rb +44 -30
  242. data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
  243. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +3 -3
  244. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  245. data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
  246. data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
  247. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
  248. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
  249. data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
  250. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  251. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +1 -1
  252. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  253. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +51 -27
  254. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  255. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +606 -78
  256. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  257. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  258. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +3 -3
  259. data/lib/rigor/inference/method_dispatcher/set_folding.rb +9 -7
  260. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +467 -84
  261. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  262. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  263. data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
  264. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  265. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  266. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  267. data/lib/rigor/inference/method_dispatcher.rb +123 -25
  268. data/lib/rigor/inference/method_parameter_binder.rb +136 -56
  269. data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
  270. data/lib/rigor/inference/module_function_state.rb +24 -3
  271. data/lib/rigor/inference/multi_target_binder.rb +2 -2
  272. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  273. data/lib/rigor/inference/mutation_widening.rb +199 -29
  274. data/lib/rigor/inference/narrowing.rb +238 -50
  275. data/lib/rigor/inference/operand_effects.rb +43 -2
  276. data/lib/rigor/inference/optimistic_origin.rb +134 -9
  277. data/lib/rigor/inference/origin_lookup.rb +1 -1
  278. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  279. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  280. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  281. data/lib/rigor/inference/precision_scanner.rb +1 -1
  282. data/lib/rigor/inference/project_method_ownership.rb +30 -9
  283. data/lib/rigor/inference/project_patched_methods.rb +65 -13
  284. data/lib/rigor/inference/project_patched_scanner.rb +120 -25
  285. data/lib/rigor/inference/project_patched_visibility.rb +39 -3
  286. data/lib/rigor/inference/project_yielder.rb +11 -17
  287. data/lib/rigor/inference/rbs_type_translator.rb +68 -15
  288. data/lib/rigor/inference/receiver_alias.rb +20 -0
  289. data/lib/rigor/inference/receiver_writer_call.rb +67 -0
  290. data/lib/rigor/inference/recursion_rebinding.rb +157 -0
  291. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +57 -17
  292. data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -16
  293. data/lib/rigor/inference/refined_dispatch.rb +13 -0
  294. data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
  295. data/lib/rigor/inference/remover_holder_scan.rb +287 -0
  296. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  297. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  298. data/lib/rigor/inference/scope_indexer/certainty.rb +0 -1
  299. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
  300. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  301. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +70 -26
  302. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  303. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  304. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +295 -0
  305. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
  306. data/lib/rigor/inference/scope_indexer.rb +2213 -226
  307. data/lib/rigor/inference/self_call_ivar_writes.rb +103 -12
  308. data/lib/rigor/inference/self_preserving_block.rb +87 -0
  309. data/lib/rigor/inference/set_mutation.rb +189 -0
  310. data/lib/rigor/inference/singleton_hook_decline.rb +42 -8
  311. data/lib/rigor/inference/singleton_object_constant.rb +32 -2
  312. data/lib/rigor/inference/statement_evaluator.rb +3268 -358
  313. data/lib/rigor/inference/string_mutation.rb +14 -0
  314. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  315. data/lib/rigor/inference/synthetic_method_scanner.rb +40 -17
  316. data/lib/rigor/inference/toplevel_def_census.rb +67 -0
  317. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  318. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  319. data/lib/rigor/inference/untyped_rejoin.rb +1 -1
  320. data/lib/rigor/inference/void_tail_summary.rb +29 -9
  321. data/lib/rigor/language_server/buffer_table.rb +3 -3
  322. data/lib/rigor/language_server/completion_provider.rb +26 -20
  323. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  324. data/lib/rigor/language_server/diagnostic_publisher.rb +8 -3
  325. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  326. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  327. data/lib/rigor/language_server/hover_provider.rb +1 -7
  328. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  329. data/lib/rigor/language_server/project_context.rb +39 -1
  330. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  331. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  332. data/lib/rigor/language_server/server.rb +7 -3
  333. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  334. data/lib/rigor/lens/claims/comparison.rb +78 -0
  335. data/lib/rigor/lens/claims/request.rb +131 -0
  336. data/lib/rigor/lens/claims/type_reader.rb +216 -0
  337. data/lib/rigor/lens/claims.rb +208 -0
  338. data/lib/rigor/lens/contract_reader.rb +5 -0
  339. data/lib/rigor/lens/document.rb +1 -1
  340. data/lib/rigor/lens/file_reader.rb +21 -8
  341. data/lib/rigor/lens/plugins.rb +1 -1
  342. data/lib/rigor/lens/run.rb +8 -0
  343. data/lib/rigor/lens.rb +1 -1
  344. data/lib/rigor/library_probe.rb +68 -0
  345. data/lib/rigor/mcp/server.rb +73 -2
  346. data/lib/rigor/mcp.rb +1 -1
  347. data/lib/rigor/memo_audit.rb +282 -0
  348. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  349. data/lib/rigor/plugin/base.rb +15 -2
  350. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  351. data/lib/rigor/plugin/io_boundary.rb +26 -4
  352. data/lib/rigor/plugin/loader.rb +25 -8
  353. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  354. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  355. data/lib/rigor/plugin/manifest.rb +16 -16
  356. data/lib/rigor/plugin/registry.rb +16 -5
  357. data/lib/rigor/plugin/services.rb +2 -3
  358. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  359. data/lib/rigor/plugin/trust_policy.rb +48 -2
  360. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  361. data/lib/rigor/plugin.rb +6 -8
  362. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  363. data/lib/rigor/project_environment.rb +16 -2
  364. data/lib/rigor/protection/analysis_guard.rb +1 -1
  365. data/lib/rigor/protection/discovery_seed.rb +44 -5
  366. data/lib/rigor/protection/mutation_cache.rb +4 -1
  367. data/lib/rigor/protection/mutator.rb +1 -1
  368. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  369. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  370. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  371. data/lib/rigor/rbs_extended.rb +2 -3
  372. data/lib/rigor/reflection/constant_ancestors.rb +81 -22
  373. data/lib/rigor/reflection/declaration_collectors.rb +55 -0
  374. data/lib/rigor/reflection/declarations.rb +58 -0
  375. data/lib/rigor/reflection/project_method.rb +177 -0
  376. data/lib/rigor/reflection.rb +2 -0
  377. data/lib/rigor/rooted_glob.rb +32 -1
  378. data/lib/rigor/run_scoped_slots.rb +38 -0
  379. data/lib/rigor/runtime/jit.rb +1 -1
  380. data/lib/rigor/scope/discovery_index.rb +168 -8
  381. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
  382. data/lib/rigor/scope/resolution_chain.rb +901 -54
  383. data/lib/rigor/scope.rb +644 -37
  384. data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
  385. data/lib/rigor/sig_gen/classification.rb +18 -1
  386. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  387. data/lib/rigor/sig_gen/generator.rb +588 -66
  388. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  389. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  390. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  391. data/lib/rigor/sig_gen/method_candidate.rb +37 -4
  392. data/lib/rigor/sig_gen/observation_collector.rb +14 -7
  393. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  394. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  395. data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
  396. data/lib/rigor/sig_gen/renderer.rb +15 -9
  397. data/lib/rigor/sig_gen/skip_reason_catalog.rb +62 -2
  398. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  399. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  400. data/lib/rigor/sig_gen/write_result.rb +23 -12
  401. data/lib/rigor/sig_gen/writer.rb +238 -37
  402. data/lib/rigor/sig_gen.rb +1 -0
  403. data/lib/rigor/source/forwardable_delegators.rb +122 -0
  404. data/lib/rigor/source/node_children.rb +13 -0
  405. data/lib/rigor/type/acceptance_router.rb +2 -2
  406. data/lib/rigor/type/accepts_result.rb +1 -1
  407. data/lib/rigor/type/app.rb +4 -6
  408. data/lib/rigor/type/combinator.rb +173 -21
  409. data/lib/rigor/type/constant.rb +4 -5
  410. data/lib/rigor/type/hash_shape.rb +21 -40
  411. data/lib/rigor/type/interface.rb +60 -0
  412. data/lib/rigor/type/union.rb +1 -1
  413. data/lib/rigor/type.rb +1 -0
  414. data/lib/rigor/type_node/identifier.rb +1 -2
  415. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  416. data/lib/rigor/type_node/string_literal.rb +1 -2
  417. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  418. data/lib/rigor/type_node/union.rb +3 -6
  419. data/lib/rigor/type_of_memo/serving.rb +99 -0
  420. data/lib/rigor/type_of_memo/stats.rb +43 -0
  421. data/lib/rigor/type_of_memo.rb +337 -0
  422. data/lib/rigor/typing_state.rb +1094 -0
  423. data/lib/rigor/value_semantics.rb +1 -10
  424. data/lib/rigor/version.rb +1 -1
  425. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  426. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  427. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  428. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +38 -36
  429. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  430. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -4
  431. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  432. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  433. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  434. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  435. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +3 -3
  436. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +115 -36
  437. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
  438. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
  439. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  440. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
  441. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
  442. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -3
  443. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
  444. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  445. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  446. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  447. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  448. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +132 -33
  449. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -6
  450. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
  451. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +54 -17
  452. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
  453. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  454. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  455. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +11 -14
  456. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  457. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  458. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  459. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +30 -9
  460. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +55 -13
  461. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
  462. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
  463. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  464. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  465. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  466. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  467. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
  468. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  469. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  470. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  471. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  472. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  473. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  474. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  475. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +8 -7
  476. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  477. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  478. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
  479. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  480. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  481. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +8 -2
  482. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  483. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  484. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  485. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  486. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  487. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +200 -4
  488. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  489. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  490. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  491. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +4 -3
  492. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  493. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +5 -5
  494. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  495. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  496. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  497. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +3 -2
  498. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  499. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  500. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +210 -3
  501. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
  502. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  503. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
  504. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +97 -24
  505. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  506. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
  507. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +4 -8
  508. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  509. data/sig/rigor/analysis/baseline.rbs +0 -3
  510. data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
  511. data/sig/rigor/analysis/fact_store.rbs +2 -2
  512. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  513. data/sig/rigor/cache/descriptor.rbs +14 -0
  514. data/sig/rigor/cli/diff_command.rbs +1 -1
  515. data/sig/rigor/cli/explain_command.rbs +1 -1
  516. data/sig/rigor/effects/registry.rbs +2 -2
  517. data/sig/rigor/environment.rbs +4 -1
  518. data/sig/rigor/inference.rbs +4 -1
  519. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  520. data/sig/rigor/plugin/base.rbs +53 -18
  521. data/sig/rigor/plugin/effect_attribution.rbs +3 -3
  522. data/sig/rigor/plugin/effect_edge.rbs +1 -1
  523. data/sig/rigor/plugin/effect_entry_points.rbs +3 -3
  524. data/sig/rigor/plugin/interfaces.rbs +51 -0
  525. data/sig/rigor/plugin/io_boundary.rbs +3 -3
  526. data/sig/rigor/plugin/manifest.rbs +9 -9
  527. data/sig/rigor/plugin/node_context.rbs +21 -0
  528. data/sig/rigor/plugin/protocol_contract.rbs +3 -7
  529. data/sig/rigor/reflection.rbs +37 -0
  530. data/sig/rigor/scope.rbs +43 -4
  531. data/sig/rigor/trinary.rbs +1 -0
  532. data/sig/rigor/type.rbs +9 -3
  533. data/sig/rigor.rbs +3 -2
  534. data/skills/rigor-ask/SKILL.md +1 -0
  535. data/skills/rigor-plugin-author/SKILL.md +4 -4
  536. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  537. data/skills/rigor-plugin-review/SKILL.md +3 -3
  538. data/skills/rigor-project-init/references/01-detect.md +1 -0
  539. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +17 -2
  540. data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
  541. data/skills/rigor-rbs-contract/SKILL.md +147 -0
  542. data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
  543. data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
  544. data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
  545. data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
  546. data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
  547. data/skills/rigor-rbs-setup/SKILL.md +3 -1
  548. data/skills/rigor-type-oracle/SKILL.md +26 -1
  549. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
  550. metadata +82 -3
@@ -57,12 +57,13 @@ module Rigor
57
57
  last: :tuple_last,
58
58
  size: :tuple_size,
59
59
  length: :tuple_size,
60
- count: :tuple_size,
60
+ count: :tuple_count,
61
61
  empty?: :tuple_empty?,
62
62
  any?: :tuple_any?,
63
63
  all?: :tuple_all?,
64
64
  none?: :tuple_none?,
65
65
  include?: :tuple_include?,
66
+ member?: :tuple_include?,
66
67
  sum: :tuple_sum,
67
68
  min: :tuple_min,
68
69
  max: :tuple_max,
@@ -70,13 +71,23 @@ module Rigor
70
71
  sort: :tuple_sort,
71
72
  reverse: :tuple_reverse,
72
73
  to_a: :tuple_to_a,
74
+ entries: :tuple_to_a,
75
+ to_ary: :tuple_to_a,
73
76
  to_h: :tuple_to_h,
77
+ to_set: :tuple_to_set,
74
78
  zip: :tuple_zip,
75
79
  :[] => :tuple_index,
76
80
  slice: :tuple_index,
77
81
  fetch: :tuple_index,
78
82
  dig: :tuple_dig,
79
83
  values_at: :tuple_values_at,
84
+ fetch_values: :tuple_fetch_values,
85
+ assoc: :tuple_assoc,
86
+ rassoc: :tuple_rassoc,
87
+ tally: :tuple_tally,
88
+ transpose: :tuple_transpose,
89
+ product: :tuple_product,
90
+ pack: :tuple_pack,
80
91
  # `concat` / `<<` are deliberately ABSENT from this table: both mutate the receiver in place
81
92
  # (`ARRAY_MUTATORS` in `MutationWidening`), and a fold tier has no way to write the folded
82
93
  # result back into the caller's binding — it can only return a value for the call expression.
@@ -194,6 +205,7 @@ module Rigor
194
205
  Type::Tuple => :dispatch_tuple,
195
206
  Type::HashShape => :dispatch_hash_shape,
196
207
  Type::Nominal => :dispatch_nominal_size,
208
+ Type::Constant => :dispatch_constant_set,
197
209
  Type::Difference => :dispatch_difference,
198
210
  Type::Refined => :dispatch_refined,
199
211
  Type::Intersection => :dispatch_intersection,
@@ -215,6 +227,7 @@ module Rigor
215
227
  # `CONST = [...].freeze; CONST.any? { … }`; fixing it at the fold (not the rule) unblocks that
216
228
  # preservation.
217
229
  return nil unless context.block_type.nil?
230
+ return constant_set_first(context) if method_name == :first && receiver.is_a?(Type::Constant)
218
231
 
219
232
  handler = RECEIVER_HANDLERS[receiver.class]
220
233
  return nil unless handler
@@ -302,9 +315,62 @@ module Rigor
302
315
  when :flatten then array_nominal_flatten(nominal, args)
303
316
  when :compact then array_nominal_compact(nominal, args)
304
317
  end
318
+ when "Set"
319
+ set_nominal_algebra(nominal, method_name, args)
305
320
  end
306
321
  end
307
322
 
323
+ # The Set algebra that returns a new Set holding both operands' elements (issue #2294). RBS declares each
324
+ # as `(_Each[A]) -> Set[A]`, the receiver's element alone, so `ARGV.to_set | [1]` read `Set[String]` and a
325
+ # read of its `1` reported on correct code. The result is `Set[E | F]`, `F` the argument's elements with
326
+ # their value pins dropped ({SetMutation.element_types}).
327
+ SET_UNION_ALGEBRA = %i[| union + ^].to_set.freeze
328
+ private_constant :SET_UNION_ALGEBRA
329
+
330
+ def set_nominal_algebra(nominal, method_name, args)
331
+ return nil unless SET_UNION_ALGEBRA.include?(method_name) && args.size == 1
332
+
333
+ receiver = nominal.type_args.size == 1 ? nominal.type_args.first : Type::Combinator.untyped
334
+ added = Inference::SetMutation.element_types(args.first).map { |t| MethodDispatcher.array_new_element(t) }
335
+ Type::Combinator.nominal_of("Set", type_args: [Type::Combinator.union(receiver, *added)])
336
+ end
337
+
338
+ # The same algebra on a constant-folded Set whose argument is not constant (two constants fold upstream in
339
+ # {ConstantFolding}): the receiver's members and the argument's elements, typed as #2290 types a
340
+ # `Set[...]` literal over non-constant members ({SetFolding.typed_set}).
341
+ def dispatch_constant_set(constant, method_name, args)
342
+ return nil unless SET_UNION_ALGEBRA.include?(method_name) && args.size == 1
343
+ return nil unless Inference::SetMutation.constant?(constant)
344
+
345
+ members = constant.value.map { |v| Type::Combinator.constant_of(v) }
346
+ SetFolding.typed_set(members + Inference::SetMutation.element_types(args.first)) ||
347
+ Type::Combinator.nominal_of("Set", type_args: [Type::Combinator.untyped])
348
+ end
349
+
350
+ # Issue #2657 — `Set#first` (no argument) on a constant-folded Set held in a LOCAL that provably nobody else
351
+ # can reach. RBS alone answers `E?` over a constant receiver whose `E` is unbound, so `Set["r"].first` read
352
+ # `Dynamic[top]?`. The answer is the literal's first member (insertion order), `nil` for the empty set.
353
+ #
354
+ # What is proved: the receiver is a bare local read whose binding carries the `:synthesised` construction
355
+ # fact, which {Inference::ConstructionEscape} withholds from a local that is referenced inside any block or
356
+ # lambda, passed as an argument, held in a container or an alias, receives a call outside its reader /
357
+ # iterator / modelled-mutator lists (`method`, `instance_eval`, `send`, a project-defined name), or lives in a
358
+ # body that calls `binding` / `eval`; and the modelled mutators (`clear`, `delete`, `<<`, ...) widen the
359
+ # carrier off `Constant[Set]` at the call itself.
360
+ # What is NOT covered, and keeps the RBS answer: a receiver that is not a local (an ivar, a global, a
361
+ # constant, a call result, a literal), and whatever the escape scan itself does not see.
362
+ def constant_set_first(context)
363
+ constant = context.receiver
364
+ return nil unless context.args.nil? || context.args.empty?
365
+ return nil unless Inference::SetMutation.constant?(constant)
366
+
367
+ node = context.call_node&.receiver
368
+ return nil unless node.is_a?(Prism::LocalVariableReadNode)
369
+ return nil unless context.scope&.construction_fact?(node.name)
370
+
371
+ Type::Combinator.constant_of(constant.value.first)
372
+ end
373
+
308
374
  # `Integer#to_s` / `Integer#inspect` (#993) on a bare `Nominal[Integer]` receiver, ahead of the
309
375
  # existing `:*` binary fold. `#inspect` takes no argument and matches `#to_s()` byte for byte, so
310
376
  # it routes through the same no-arg branch.
@@ -317,7 +383,7 @@ module Rigor
317
383
  end
318
384
 
319
385
  # `decimal-int-string`'s predicate (`/\A-?\d+\z/`) admits the optional sign, so — unlike the
320
- # `IntegerRange#to_s` tier above, which declines a signed range because it cannot yet prove the
386
+ # `IntegerRange#to_s` tier below, which declines a signed range because it cannot yet prove the
321
387
  # sign is absent — a bare `Integer` (sign completely unknown) still reaches it for the base-10
322
388
  # case: base 10 is Ruby's default sign-and-digits grammar for every Integer. Any other base,
323
389
  # known or not statically known, drops to `non-empty-string`: `to_s(base)` never returns `""`,
@@ -422,14 +488,29 @@ module Rigor
422
488
  # Resolves the element type of a flattened `Array[element]`. Each `Array[U]` nesting level
423
489
  # contributes `U`; the per-level element types are unioned. `depth < 0` recurses without bound;
424
490
  # `depth == 0` stops (Ruby's `flatten(0)` is a no-op copy and returns the element unchanged).
491
+ #
492
+ # Issue #2427 — a `Tuple` element is an Array too and contributes the union of its own elements, each
493
+ # flattened one level deeper, and so does a `Tuple` member of a `Union` element. `ARGV.map { |x| [x] }`
494
+ # is `Array[[String]]`, and returning its `[String]` element unchanged made `.flatten.first.upcase`
495
+ # report `undefined method 'upcase' for [String]` on code Ruby runs.
496
+ #
497
+ # A nominal `Array[U]` member of a `Union` element is kept. RBS's `String#scan(Regexp)` answers
498
+ # `Array[String | Array[String?]]`, the `?` standing for an optional group, and flattening that member
499
+ # turned the idiomatic `str.scan(/k = (.+)/).flatten.map(&:strip)` into a `possible-nil-receiver` for a
500
+ # group that always matches.
425
501
  def flatten_nominal_element(element, depth)
426
502
  return element if depth.zero?
427
- return element unless array_nominal?(element)
428
-
429
- inner = element.type_args.first
430
- return element if inner.nil?
431
503
 
432
- flatten_nominal_element(inner, depth - 1)
504
+ case element
505
+ when Type::Tuple
506
+ Type::Combinator.union(*element.elements.map { |inner| flatten_nominal_element(inner, depth - 1) })
507
+ when Type::Union
508
+ Type::Combinator.union(*element.members.map do |member|
509
+ member.is_a?(Type::Tuple) ? flatten_nominal_element(member, depth) : member
510
+ end)
511
+ else
512
+ array_nominal?(element) ? flatten_nominal_element(element.type_args.first, depth - 1) : element
513
+ end
433
514
  end
434
515
 
435
516
  def array_nominal?(type)
@@ -472,7 +553,7 @@ module Rigor
472
553
  # leading sign. Base 10 is Ruby's decimal-literal grammar, so the result lifts to
473
554
  # `decimal-int-string`. Signed ranges fall through entirely (the result could carry a `-` sign
474
555
  # that `decimal-int-string` alone would need a sign proof for, matching the
475
- # `Nominal[Integer]` tier's own signed-base-10 carve-out below).
556
+ # `Nominal[Integer]` tier's own signed-base-10 carve-out above).
476
557
  #
477
558
  # #1004 — every other base (a different literal, such as 8 or 16, or a value that is not
478
559
  # statically known) does NOT lift to `octal-int-string` / `hex-int-string`: those refinements'
@@ -621,10 +702,10 @@ module Rigor
621
702
  Type::Combinator.positive_int
622
703
  end
623
704
 
624
- # Predicate-subset projections over a `Refined[base, predicate]` receiver. Today the catalogue is
625
- # the String case-normalisation pair: `s.downcase` over a `lowercase-string` receiver folds to
626
- # the same carrier (already lowercase), and `s.upcase` lifts a `lowercase-string` to
627
- # `uppercase-string`. Symmetric rules apply with the predicates swapped. Numeric-string
705
+ # Predicate-subset projections over a `Refined[base, predicate]` receiver. The catalogue is
706
+ # `REFINED_STRING_PROJECTIONS`, led by the String case-normalisation pair: `s.downcase` over a
707
+ # `lowercase-string` receiver folds to the same carrier (already lowercase), and `s.upcase` lifts a
708
+ # `lowercase-string` to `uppercase-string`. Symmetric rules apply with the predicates swapped. Numeric-string
628
709
  # idempotence over `#downcase` / `#upcase` is also recognised because a numeric string equals its
629
710
  # own case-normalisation.
630
711
  #
@@ -747,7 +828,7 @@ module Rigor
747
828
  # Compute the bounded-integer meet of two or more `IntegerRange` carriers. We compare via the
748
829
  # numeric `lower` / `upper` accessors (`-Float::INFINITY` / `Float::INFINITY` for the symbolic
749
830
  # ends), then map back to the symbolic-bound representation `IntegerRange.new` expects. The
750
- # disjoint-meet case cannot arise from sound member-wise projections in v0.0.4 but is guarded
831
+ # disjoint-meet case cannot arise from sound member-wise projections but is guarded
751
832
  # defensively to keep the carrier total.
752
833
  def narrow_integer_ranges(ranges)
753
834
  numeric_low = ranges.map(&:lower).max
@@ -798,6 +879,25 @@ module Rigor
798
879
  Type::Combinator.constant_of(tuple.elements.size)
799
880
  end
800
881
 
882
+ # `tuple.count` / `tuple.count(needle)` — 0-arg mirrors `#size`; 1-arg counts elements equal to
883
+ # `needle` (`==`) when the needle and every element are constant values or shapes (or folds to `0`
884
+ # on `[]`).
885
+ def tuple_count(tuple, _method_name, args)
886
+ return Type::Combinator.constant_of(tuple.elements.size) if args.empty?
887
+ return nil unless args.size == 1
888
+
889
+ needle = unwrap_constant_type(args.first)
890
+ return nil if needle.equal?(NOT_CONSTANT)
891
+ return Type::Combinator.constant_of(0) if tuple.elements.empty?
892
+
893
+ values = unwrap_constant_elements(tuple.elements)
894
+ return nil if values.nil?
895
+
896
+ Type::Combinator.constant_of(values.count { |v| v == needle })
897
+ rescue StandardError
898
+ nil
899
+ end
900
+
801
901
  # `tuple.empty?` — folds to a precise bool from the tuple's known arity.
802
902
  # rubocop:disable Style/ReturnNilInPredicateMethodDefinition
803
903
  def tuple_empty?(tuple, _method_name, args)
@@ -839,27 +939,29 @@ module Rigor
839
939
  Type::Combinator.constant_of(decision)
840
940
  end
841
941
 
842
- # `tuple.include?(needle)` — folds to a precise bool when the needle is a `Constant` and the
843
- # tuple's elements are all `Constant` (so disjointness is checkable). If any element matches the
844
- # needle's value the answer is `Constant[true]`; if every element is a Constant whose value is
845
- # structurally distinct from the needle the answer is `Constant[false]`.
942
+ # `tuple.include?(needle)` — folds to a precise bool when the needle and elements are constant
943
+ # scalars or constant nested Tuple / HashShape values. If any element matches the needle's value
944
+ # the answer is `Constant[true]`; if every element is constant and structurally distinct from the
945
+ # needle the answer is `Constant[false]`.
846
946
  def tuple_include?(tuple, _method_name, args)
847
947
  return nil unless args.size == 1
848
948
 
849
- needle = args.first
850
- return nil unless needle.is_a?(Type::Constant)
949
+ needle = unwrap_constant_type(args.first)
950
+ return nil if needle.equal?(NOT_CONSTANT)
851
951
  return Type::Combinator.constant_of(false) if tuple.elements.empty?
852
952
 
853
- return Type::Combinator.constant_of(true) if any_element_matches?(tuple.elements, needle.value)
854
- return Type::Combinator.constant_of(false) if tuple.elements.all?(Type::Constant)
953
+ return Type::Combinator.constant_of(true) if any_element_matches?(tuple.elements, needle)
954
+ return Type::Combinator.constant_of(false) unless unwrap_constant_elements(tuple.elements).nil?
855
955
 
856
956
  nil
857
957
  end
858
958
  # rubocop:enable Style/ReturnNilInPredicateMethodDefinition
859
959
 
860
- # `tuple.sum` — when every element is a numeric Constant, fold to `Constant[sum]`. Mixed /
861
- # non-numeric elements decline so RBS widens.
960
+ # `tuple.sum` / `tuple.sum(init)` — when every element is a numeric Constant (and optional `init`
961
+ # is a numeric Constant), fold to `Constant[sum]`; when every element and `init` are String
962
+ # Constants, fold to `Constant[sum]` within `TUPLE_JOIN_BYTE_LIMIT`.
862
963
  def tuple_sum(tuple, _method_name, args)
964
+ return tuple_sum_with_init(tuple, args.first) if args.size == 1
863
965
  return nil unless args.empty?
864
966
  return Type::Combinator.constant_of(0) if tuple.elements.empty?
865
967
 
@@ -867,16 +969,45 @@ module Rigor
867
969
  return nil if values.nil?
868
970
 
869
971
  Type::Combinator.constant_of(values.sum)
972
+ rescue StandardError
973
+ nil
974
+ end
975
+
976
+ def tuple_sum_with_init(tuple, init)
977
+ return nil unless init.is_a?(Type::Constant)
978
+ return tuple_sum_with_string_init(tuple, init.value) if init.value.is_a?(String)
979
+ return nil unless init.value.is_a?(Numeric)
980
+ return init if tuple.elements.empty?
981
+
982
+ values = constant_numeric_values(tuple.elements)
983
+ return nil if values.nil?
984
+
985
+ Type::Combinator.constant_of(values.sum(init.value))
986
+ rescue StandardError
987
+ nil
988
+ end
989
+
990
+ def tuple_sum_with_string_init(tuple, init_str)
991
+ values = constant_values(tuple.elements)
992
+ return nil if values.nil? || !values.all?(String)
993
+ return nil if values.sum(&:bytesize) + init_str.bytesize > TUPLE_JOIN_BYTE_LIMIT
994
+
995
+ result = values.sum(init_str)
996
+ return nil if result.bytesize > TUPLE_JOIN_BYTE_LIMIT
997
+
998
+ Type::Combinator.constant_of(result)
999
+ rescue StandardError
1000
+ nil
870
1001
  end
871
1002
 
872
1003
  # `tuple.join(sep = "")` — fold to the joined `Constant[String]` when every element is a
873
- # `Constant` (its `to_s` is deterministic for the scalar value classes) and the separator is
874
- # absent or a `Constant[String]`. Capped at `TUPLE_JOIN_BYTE_LIMIT`.
1004
+ # constant scalar or nested constant Tuple and the separator is absent or a `Constant[String]`.
1005
+ # Capped at `TUPLE_JOIN_BYTE_LIMIT`.
875
1006
  def tuple_join(tuple, _method_name, args)
876
1007
  sep = tuple_join_separator(args)
877
1008
  return nil if sep.nil?
878
1009
 
879
- values = constant_values(tuple.elements)
1010
+ values = unwrap_constant_elements(tuple.elements)
880
1011
  return nil if values.nil?
881
1012
 
882
1013
  result = values.join(sep)
@@ -900,14 +1031,13 @@ module Rigor
900
1031
  end
901
1032
 
902
1033
  # `tuple.inspect` / `tuple.to_s` (`Array#to_s` is an exact alias of `Array#inspect`) — folds when
903
- # every element is a `Constant`, by reconstructing the Ruby Array and calling the REAL `inspect`
904
- # so the analyzer's Ruby (4.0.x's `Array#inspect` format) is matched rather than re-derived.
905
- # Capped at `TUPLE_JOIN_BYTE_LIMIT` like `#join`, since a large tuple's `inspect` string is
906
- # similarly unbounded.
1034
+ # every element is a constant scalar or nested constant Tuple / HashShape, by reconstructing the
1035
+ # Ruby Array and calling the REAL `inspect` so the analyzer's Ruby (4.0.x's `Array#inspect` format)
1036
+ # is matched rather than re-derived. Capped at `TUPLE_JOIN_BYTE_LIMIT` like `#join`.
907
1037
  def tuple_inspect(tuple, _method_name, args)
908
1038
  return nil unless args.empty?
909
1039
 
910
- values = constant_values(tuple.elements)
1040
+ values = unwrap_constant_elements(tuple.elements)
911
1041
  return nil if values.nil?
912
1042
 
913
1043
  result = values.inspect
@@ -952,7 +1082,7 @@ module Rigor
952
1082
  Type::Combinator.tuple_of(*(tuple.elements * count))
953
1083
  end
954
1084
 
955
- # `tuple.min` / `tuple.max` — fold when every element is a `Constant` whose values share a
1085
+ # `tuple.min` / `tuple.max` — fold when every element is a constant whose values share a
956
1086
  # Ruby-comparable domain. Empty tuples fold to `Constant[nil]`. The 1-arg `min(n)` / `max(n)`
957
1087
  # form folds to a `Tuple` of the n edge-most values in Ruby's order (`min(n)` ascending, `max(n)`
958
1088
  # descending) — the n-arg sibling of `first(n)`.
@@ -968,11 +1098,11 @@ module Rigor
968
1098
  return tuple_minmax_n(tuple, args.first, edge) unless args.empty?
969
1099
  return Type::Combinator.constant_of(nil) if tuple.elements.empty?
970
1100
 
971
- values = constant_values(tuple.elements)
1101
+ values = unwrap_constant_elements(tuple.elements)
972
1102
  return nil if values.nil?
973
1103
 
974
1104
  result = values.public_send(edge)
975
- Type::Combinator.constant_of(result)
1105
+ lift_constant_value(result)
976
1106
  rescue StandardError
977
1107
  nil
978
1108
  end
@@ -985,19 +1115,18 @@ module Rigor
985
1115
  return nil unless arg.is_a?(Type::Constant) && arg.value.is_a?(Integer)
986
1116
  return nil if arg.value.negative?
987
1117
 
988
- values = constant_values(tuple.elements)
1118
+ values = unwrap_constant_elements(tuple.elements)
989
1119
  return nil if values.nil?
990
1120
 
991
1121
  picked = values.public_send(edge, arg.value)
992
- Type::Combinator.tuple_of(*picked.map { |v| Type::Combinator.constant_of(v) })
1122
+ Type::Combinator.tuple_of(*picked.map { |v| lift_constant_value(v) })
993
1123
  rescue StandardError
994
1124
  nil
995
1125
  end
996
1126
 
997
- # `tuple.minmax` — the `[min, max]` pair as a 2-slot `Tuple[Constant[min], Constant[max]]`,
998
- # mirroring the `Range#minmax` fold. Every element must be a `Constant` and the values must
999
- # Ruby-compare; an empty tuple folds to `Tuple[nil, nil]` (Ruby's `[].minmax`), incomparable
1000
- # mixed-class values decline.
1127
+ # `tuple.minmax` — the `[min, max]` pair as a 2-slot `Tuple`, mirroring the `Range#minmax` fold.
1128
+ # Every element must be constant and the values must Ruby-compare; an empty tuple folds to
1129
+ # `Tuple[nil, nil]` (Ruby's `[].minmax`), incomparable mixed-class values decline.
1001
1130
  def tuple_minmax_pair(tuple, _method_name, args)
1002
1131
  return nil unless args.empty?
1003
1132
 
@@ -1006,30 +1135,30 @@ module Rigor
1006
1135
  return Type::Combinator.tuple_of(nil_const, nil_const)
1007
1136
  end
1008
1137
 
1009
- values = constant_values(tuple.elements)
1138
+ values = unwrap_constant_elements(tuple.elements)
1010
1139
  return nil if values.nil?
1011
1140
 
1012
1141
  low, high = values.minmax
1013
1142
  Type::Combinator.tuple_of(
1014
- Type::Combinator.constant_of(low),
1015
- Type::Combinator.constant_of(high)
1143
+ lift_constant_value(low),
1144
+ lift_constant_value(high)
1016
1145
  )
1017
1146
  rescue StandardError
1018
1147
  nil
1019
1148
  end
1020
1149
 
1021
- # `tuple.sort` — every element must be a `Constant` and the values must Ruby-compare. The result
1150
+ # `tuple.sort` — every element must be constant and the values must Ruby-compare. The result
1022
1151
  # is a Tuple with the same elements in sorted order. Comparison failures (mixed-class
1023
1152
  # incomparable values) decline.
1024
1153
  def tuple_sort(tuple, _method_name, args)
1025
1154
  return nil unless args.empty?
1026
1155
  return tuple if tuple.elements.size <= 1
1027
1156
 
1028
- values = constant_values(tuple.elements)
1157
+ values = unwrap_constant_elements(tuple.elements)
1029
1158
  return nil if values.nil?
1030
1159
 
1031
1160
  sorted = values.sort
1032
- Type::Combinator.tuple_of(*sorted.map { |v| Type::Combinator.constant_of(v) })
1161
+ Type::Combinator.tuple_of(*sorted.map { |v| lift_constant_value(v) })
1033
1162
  rescue StandardError
1034
1163
  nil
1035
1164
  end
@@ -1056,7 +1185,7 @@ module Rigor
1056
1185
  # runtime semantics). Declines when any `other_k` is not a Tuple, since the arity is then unknown
1057
1186
  # and the result would be `Array[Array[…]]` — RBS already gives that answer.
1058
1187
  def tuple_zip(tuple, _method_name, args)
1059
- return nil if args.empty? || args.size > MAX_ZIP_ARITY
1188
+ return nil if args.size > MAX_ZIP_ARITY
1060
1189
  return nil unless args.all?(Type::Tuple)
1061
1190
 
1062
1191
  zipped = tuple.elements.each_with_index.map do |elem, i|
@@ -1094,23 +1223,180 @@ module Rigor
1094
1223
  [key.value, value]
1095
1224
  end
1096
1225
 
1097
- # `tuple.values_at(i1, i2, ...)` — returns a Tuple of per-index elements. Each argument must be a
1098
- # `Constant[Integer]`. Out-of-range indices fill with `Constant[nil]`, mirroring Ruby's runtime
1099
- # behaviour. Declines when any argument is non-static.
1226
+ # `tuple.to_set` — folds to `Constant[Set]` when every element is a `Constant`, matching
1227
+ # `Set[...]` / `Set.new([...])`. Non-constant elements decline so RBS keeps `Set[Elem]`.
1228
+ def tuple_to_set(tuple, _method_name, args)
1229
+ return nil unless args.empty?
1230
+
1231
+ values = constant_values(tuple.elements)
1232
+ return nil if values.nil?
1233
+
1234
+ Type::Combinator.constant_of(::Set.new(values))
1235
+ rescue StandardError
1236
+ nil
1237
+ end
1238
+
1239
+ # `tuple.values_at(i1, i2, ...)` — returns a Tuple of per-index / per-range elements. Each
1240
+ # argument must be a `Constant[Integer]` or a `Constant[Range]` with integer/endless/beginless
1241
+ # endpoints. Out-of-range positive indices fill with `Constant[nil]`, mirroring Ruby's runtime
1242
+ # behaviour. Declines when any argument is non-static, raises `RangeError`, or exceeds
1243
+ # `MAX_SET_OPERATION_SIZE`.
1100
1244
  def tuple_values_at(tuple, _method_name, args)
1101
- return nil if args.empty?
1245
+ return nil if args.empty? || args.size > MAX_SET_OPERATION_SIZE
1246
+
1247
+ raw_args = args.map do |arg|
1248
+ return nil unless arg.is_a?(Type::Constant) && values_at_selector?(arg.value)
1102
1249
 
1250
+ arg.value
1251
+ end
1252
+ picked = tuple.elements.values_at(*raw_args)
1253
+ return nil if picked.size > MAX_SET_OPERATION_SIZE
1254
+
1255
+ Type::Combinator.tuple_of(*picked.map { |elem| elem || Type::Combinator.constant_of(nil) })
1256
+ rescue StandardError
1257
+ nil
1258
+ end
1259
+
1260
+ def values_at_selector?(value)
1261
+ return true if value.is_a?(Integer)
1262
+ return false unless value.is_a?(Range) && integer_range?(value)
1263
+
1264
+ !(value.end.is_a?(Integer) && value.end > MAX_SET_OPERATION_SIZE)
1265
+ end
1266
+
1267
+ # `tuple.fetch_values(i1, i2, ...)` — returns a Tuple of per-index elements when every argument
1268
+ # is an in-bounds `Constant[Integer]`; an out-of-range or non-static index declines so RBS handles
1269
+ # the possible `IndexError`.
1270
+ def tuple_fetch_values(tuple, _method_name, args)
1103
1271
  values = args.map do |arg|
1104
- return nil unless arg.is_a?(Type::Constant)
1105
- return nil unless arg.value.is_a?(Integer)
1272
+ return nil unless arg.is_a?(Type::Constant) && arg.value.is_a?(Integer)
1106
1273
 
1107
1274
  idx = normalise_index(arg.value, tuple.elements.size)
1108
- idx ? tuple.elements[idx] : Type::Combinator.constant_of(nil)
1275
+ return nil unless idx
1276
+
1277
+ tuple.elements[idx]
1109
1278
  end
1110
1279
 
1111
1280
  Type::Combinator.tuple_of(*values)
1112
1281
  end
1113
1282
 
1283
+ # `tuple.assoc(needle)` / `tuple.rassoc(needle)` — scans inner Tuples in declaration order for a
1284
+ # constant match at slot 0 (`assoc`) or slot 1 (`rassoc`). Known non-array scalars/hashes/ranges
1285
+ # are skipped, while an earlier non-constant slot or opaque element declines.
1286
+ def tuple_assoc(tuple, _method_name, args)
1287
+ tuple_slot_lookup(tuple, args, slot_index: 0)
1288
+ end
1289
+
1290
+ def tuple_rassoc(tuple, _method_name, args)
1291
+ tuple_slot_lookup(tuple, args, slot_index: 1)
1292
+ end
1293
+
1294
+ def tuple_slot_lookup(tuple, args, slot_index:)
1295
+ return nil unless args.size == 1
1296
+
1297
+ needle = unwrap_constant_type(args.first)
1298
+ return nil if needle.equal?(NOT_CONSTANT)
1299
+
1300
+ tuple.elements.each do |elem|
1301
+ next if non_array_shape?(elem)
1302
+ return nil unless elem.is_a?(Type::Tuple)
1303
+
1304
+ match = tuple_slot_match(elem, needle, slot_index)
1305
+ return nil if match == :decline
1306
+ return elem if match
1307
+ end
1308
+
1309
+ Type::Combinator.constant_of(nil)
1310
+ rescue StandardError
1311
+ nil
1312
+ end
1313
+
1314
+ def non_array_shape?(elem)
1315
+ elem.is_a?(Type::Constant) || elem.is_a?(Type::HashShape) ||
1316
+ elem.is_a?(Type::IntegerRange) || elem.is_a?(Type::FloatRange)
1317
+ end
1318
+
1319
+ def tuple_slot_match(elem, needle, slot_index)
1320
+ return false if elem.elements.size <= slot_index
1321
+
1322
+ slot = unwrap_constant_type(elem.elements[slot_index])
1323
+ return :decline if slot.equal?(NOT_CONSTANT)
1324
+
1325
+ slot == needle
1326
+ end
1327
+
1328
+ # `tuple.tally` — returns a closed `HashShape` of `value => Constant[count]` when every element
1329
+ # is a `Constant` whose value is a valid static HashShape key.
1330
+ def tuple_tally(tuple, _method_name, args)
1331
+ return nil unless args.empty?
1332
+
1333
+ values = constant_values(tuple.elements)
1334
+ return nil if values.nil?
1335
+ return nil unless values.all? { |v| static_shape_key?(v) }
1336
+
1337
+ counts = values.tally.transform_values { |n| Type::Combinator.constant_of(n) }
1338
+ Type::Combinator.hash_shape_of(counts)
1339
+ end
1340
+
1341
+ # `tuple.transpose` — transposes a Tuple of equal-length Tuples into a Tuple of Tuples.
1342
+ def tuple_transpose(tuple, _method_name, args)
1343
+ return nil unless args.empty?
1344
+ return tuple if tuple.elements.empty?
1345
+ return nil unless tuple.elements.all?(Type::Tuple)
1346
+
1347
+ width = tuple.elements.first.elements.size
1348
+ return nil unless tuple.elements.all? { |row| row.elements.size == width }
1349
+
1350
+ transposed = tuple.elements.map(&:elements).transpose
1351
+ Type::Combinator.tuple_of(*transposed.map { |col| Type::Combinator.tuple_of(*col) })
1352
+ end
1353
+
1354
+ # `tuple.product(other_1, ...)` — Cartesian product of Tuples, bounded by
1355
+ # `MAX_SET_OPERATION_ARITY` and `MAX_SET_OPERATION_SIZE`.
1356
+ def tuple_product(tuple, _method_name, args)
1357
+ return nil if args.size > MAX_SET_OPERATION_ARITY
1358
+ return nil unless args.all?(Type::Tuple)
1359
+
1360
+ total = args.reduce(tuple.elements.size) { |acc, other| acc * other.elements.size }
1361
+ return nil if total > MAX_SET_OPERATION_SIZE
1362
+ return Type::Combinator.tuple_of if total.zero?
1363
+
1364
+ pairs = tuple.elements.product(*args.map(&:elements))
1365
+ Type::Combinator.tuple_of(*pairs.map { |row| Type::Combinator.tuple_of(*row) })
1366
+ end
1367
+
1368
+ # `tuple.pack(template)` — folds to `Constant[String]` when all elements and template are
1369
+ # `Constant` (excluding non-deterministic pointer directives `p`/`P` and numeric counts or offsets
1370
+ # whose sum exceeds `TUPLE_JOIN_BYTE_LIMIT`), capped at `TUPLE_JOIN_BYTE_LIMIT`.
1371
+ def tuple_pack(tuple, _method_name, args)
1372
+ return nil unless args.size == 1
1373
+
1374
+ template = args.first
1375
+ return nil unless template.is_a?(Type::Constant) && template.value.is_a?(String)
1376
+ return nil if template.value.bytesize > TUPLE_JOIN_BYTE_LIMIT
1377
+ return nil if template.value.match?(/[pP]/)
1378
+ return nil if pack_count_exceeds_limit?(template.value)
1379
+
1380
+ values = constant_values(tuple.elements)
1381
+ return nil if values.nil?
1382
+
1383
+ result = values.pack(template.value)
1384
+ return nil if result.bytesize > TUPLE_JOIN_BYTE_LIMIT
1385
+
1386
+ Type::Combinator.constant_of(result)
1387
+ rescue StandardError
1388
+ nil
1389
+ end
1390
+
1391
+ def pack_count_exceeds_limit?(template)
1392
+ total = 0
1393
+ template.scan(/\d+/) do |digits|
1394
+ total += digits.to_i
1395
+ return true if total > TUPLE_JOIN_BYTE_LIMIT
1396
+ end
1397
+ false
1398
+ end
1399
+
1114
1400
  # `tuple + other` — concatenates two Tuples. Both sides must be `Type::Tuple`. Returns a new
1115
1401
  # Tuple whose elements are those of the receiver followed by those of the argument.
1116
1402
  # `at(n)` — the strict single-Integer accessor. Deliberately NOT an alias of {#tuple_index}: `[]`
@@ -1133,15 +1419,33 @@ module Rigor
1133
1419
  # Every set operation is evaluated by running Ruby's own operator over the unwrapped values, which
1134
1420
  # is the only way to reproduce its `eql?` / `hash` semantics exactly — `[1] & [1.0]` is empty even
1135
1421
  # though `1 == 1.0`, so an equality-based reimplementation would fold the wrong answer.
1136
- def tuple_intersection(tuple, _method_name, args)
1422
+ def tuple_intersection(tuple, method_name, args)
1423
+ return tuple if method_name == :intersection && args.empty?
1424
+
1137
1425
  set_operation(tuple, args) { |values, others| others.reduce(values, :&) }
1138
1426
  end
1139
1427
 
1140
- def tuple_union(tuple, _method_name, args)
1428
+ def tuple_union(tuple, method_name, args)
1429
+ return tuple_zero_arg_union(tuple) if method_name == :union && args.empty?
1430
+
1141
1431
  set_operation(tuple, args) { |values, others| others.reduce(values, :|) }
1142
1432
  end
1143
1433
 
1144
- def tuple_difference(tuple, _method_name, args)
1434
+ def tuple_zero_arg_union(tuple)
1435
+ return tuple if tuple.elements.size <= 1
1436
+
1437
+ values = unwrap_constant_elements(tuple.elements)
1438
+ return nil if values.nil?
1439
+
1440
+ result = values.union
1441
+ return nil if result.size > MAX_SET_OPERATION_SIZE
1442
+
1443
+ Type::Combinator.tuple_of(*result.map { |value| lift_constant_value(value) })
1444
+ end
1445
+
1446
+ def tuple_difference(tuple, method_name, args)
1447
+ return tuple if method_name == :difference && args.empty?
1448
+
1145
1449
  set_operation(tuple, args) { |values, others| others.reduce(values, :-) }
1146
1450
  end
1147
1451
 
@@ -1158,23 +1462,23 @@ module Rigor
1158
1462
 
1159
1463
  # Shared body for the set operations: unwrap the receiver and every Tuple argument, hand the raw
1160
1464
  # values to `block`, and lift the result back. Declines unless every element on both sides is a
1161
- # `Constant` (an unknown element makes membership undecidable) and every argument is a `Tuple` —
1162
- # `Array#&` also accepts anything that responds to `to_ary`, which a shape cannot prove.
1465
+ # constant scalar or constant nested Tuple / HashShape (an unknown element makes membership
1466
+ # undecidable) and every argument is a `Tuple`.
1163
1467
  def set_operation(tuple, args)
1164
1468
  return nil if args.empty? || args.size > MAX_SET_OPERATION_ARITY
1165
1469
 
1166
- values = constant_values(tuple.elements)
1470
+ values = unwrap_constant_elements(tuple.elements)
1167
1471
  return nil if values.nil?
1168
1472
 
1169
1473
  others = args.map do |arg|
1170
1474
  return nil unless arg.is_a?(Type::Tuple)
1171
1475
 
1172
- constant_values(arg.elements) || (return nil)
1476
+ unwrap_constant_elements(arg.elements) || (return nil)
1173
1477
  end
1174
1478
  result = yield(values, others)
1175
1479
  return nil if result.size > MAX_SET_OPERATION_SIZE
1176
1480
 
1177
- Type::Combinator.tuple_of(*result.map { |value| Type::Combinator.constant_of(value) })
1481
+ Type::Combinator.tuple_of(*result.map { |value| lift_constant_value(value) })
1178
1482
  end
1179
1483
 
1180
1484
  # An argument list longer than this, or a result wider than this, declines to the RBS tier rather
@@ -1204,28 +1508,48 @@ module Rigor
1204
1508
  Type::Combinator.tuple_of(*tuple.elements, *other.elements)
1205
1509
  end
1206
1510
 
1207
- # `tuple.compact` — removes every element that is `Constant[nil]`. Folds only when every element
1208
- # is a `Constant` (so the nil set is decidable). Mixed-shape elements decline so the RBS tier
1209
- # widens.
1511
+ # `tuple.compact` — removes every element that is `Constant[nil]`. When every element is a
1512
+ # `Constant` the nil set is decidable and the result is the exact Tuple of the kept elements.
1513
+ # Otherwise an element that may be nil leaves the arity unknown, so the result is `Array[E]` with
1514
+ # `E` the elements' union minus its `nil` constituent (#2519) — never a fixed-arity Tuple. When
1515
+ # no element carries a `nil` constituent there is nothing to strip and the method declines so
1516
+ # the RBS tier answers (an element such as `Dynamic[top]` may still be nil at runtime, so the
1517
+ # Tuple is not kept either).
1210
1518
  def tuple_compact(tuple, _method_name, args)
1211
1519
  return nil unless args.empty?
1212
- return nil unless tuple.elements.all?(Type::Constant)
1213
1520
 
1214
- kept = tuple.elements.reject { |e| e.is_a?(Type::Constant) && e.value.nil? }
1215
- Type::Combinator.tuple_of(*kept)
1521
+ if tuple.elements.all?(Type::Constant)
1522
+ kept = tuple.elements.reject { |e| e.value.nil? }
1523
+ return Type::Combinator.tuple_of(*kept)
1524
+ end
1525
+
1526
+ tuple_compact_widened(tuple.elements)
1527
+ end
1528
+
1529
+ def tuple_compact_widened(elements)
1530
+ stripped = elements.map { |e| strip_nil_constituent(e) }
1531
+ return nil if stripped.zip(elements).all? { |s, e| s.equal?(e) }
1532
+
1533
+ element = Type::Combinator.union(*stripped)
1534
+ return Type::Combinator.tuple_of if element.is_a?(Type::Bot)
1535
+
1536
+ Type::Combinator.nominal_of("Array", type_args: [element])
1216
1537
  end
1217
1538
 
1218
1539
  # `uniq` (no block) → `Tuple` of the first occurrence of each distinct value. Folds only when
1219
- # every element is a `Constant` so value equality is decidable; the block form defers.
1540
+ # every element is a constant scalar or constant nested Tuple / HashShape so `eql?` identity is
1541
+ # decidable; the block form defers.
1220
1542
  def tuple_uniq(tuple, _method_name, args)
1221
1543
  return nil unless args.empty?
1222
- return nil unless tuple.elements.all?(Type::Constant)
1223
1544
 
1224
- seen = []
1225
- kept = tuple.elements.each_with_object([]) do |element, acc|
1226
- next if seen.include?(element.value)
1545
+ values = unwrap_constant_elements(tuple.elements)
1546
+ return nil if values.nil?
1547
+
1548
+ seen = {}
1549
+ kept = tuple.elements.zip(values).each_with_object([]) do |(element, value), acc|
1550
+ next if seen.key?(value)
1227
1551
 
1228
- seen << element.value
1552
+ seen[value] = true
1229
1553
  acc << element
1230
1554
  end
1231
1555
  Type::Combinator.tuple_of(*kept)
@@ -1233,9 +1557,9 @@ module Rigor
1233
1557
 
1234
1558
  # `index(obj)` / `find_index(obj)` → `Constant[Integer]` of the first element equal to `obj`,
1235
1559
  # `Constant[nil]` when none match. Folds only for the argument form (the block form defers) when
1236
- # every element AND the argument are `Constant` (decidable equality).
1560
+ # every element AND the argument are constant (decidable equality).
1237
1561
  def tuple_find_index(tuple, _method_name, args)
1238
- constant_index(tuple, args) { |elements, value| elements.index { |e| e.value == value } }
1562
+ constant_index(tuple, args) { |values, needle| values.index { |v| v == needle } }
1239
1563
  end
1240
1564
 
1241
1565
  # `tuple.flatten` / `tuple.flatten(depth)` — recursively flattens nested Tuple elements into a
@@ -1279,21 +1603,24 @@ module Rigor
1279
1603
 
1280
1604
  # `rindex(obj)` → the LAST matching index, same decidability gate.
1281
1605
  def tuple_rindex(tuple, _method_name, args)
1282
- constant_index(tuple, args) { |elements, value| elements.rindex { |e| e.value == value } }
1606
+ constant_index(tuple, args) { |values, needle| values.rindex { |v| v == needle } }
1283
1607
  end
1284
1608
 
1285
1609
  def constant_index(tuple, args)
1286
1610
  return nil unless args.size == 1
1287
1611
 
1288
- needle = args.first
1289
- return nil unless needle.is_a?(Type::Constant)
1290
- return nil unless tuple.elements.all?(Type::Constant)
1612
+ needle = unwrap_constant_type(args.first)
1613
+ values = unwrap_constant_elements(tuple.elements)
1614
+ return nil if needle.equal?(NOT_CONSTANT) || values.nil?
1291
1615
 
1292
- Type::Combinator.constant_of(yield(tuple.elements, needle.value))
1616
+ Type::Combinator.constant_of(yield(values, needle))
1617
+ rescue StandardError
1618
+ nil
1293
1619
  end
1294
1620
 
1295
1621
  # `tuple.take(n)` — returns the first n elements as a new Tuple. The argument must be a
1296
- # `Constant[Integer]`. n <= 0 returns the empty Tuple; n >= size returns the full receiver.
1622
+ # `Constant[Integer]`. `n == 0` returns the empty Tuple, a negative `n` declines, and `n >= size`
1623
+ # returns the full receiver.
1297
1624
  def tuple_take(tuple, _method_name, args)
1298
1625
  n = non_negative_count_arg(args)
1299
1626
  return nil if n.nil?
@@ -1359,7 +1686,63 @@ module Rigor
1359
1686
  end
1360
1687
 
1361
1688
  def any_element_matches?(elements, value)
1362
- elements.any? { |e| e.is_a?(Type::Constant) && e.value == value }
1689
+ elements.any? do |e|
1690
+ unwrapped = unwrap_constant_type(e)
1691
+ !unwrapped.equal?(NOT_CONSTANT) && unwrapped == value
1692
+ end
1693
+ end
1694
+
1695
+ NOT_CONSTANT = Object.new.freeze
1696
+ private_constant :NOT_CONSTANT
1697
+
1698
+ # Unwraps a `Type::Constant`, a `Type::Tuple` of recursively constant elements, or a closed
1699
+ # required-only `Type::HashShape` of recursively constant values into its plain Ruby value, or
1700
+ # `NOT_CONSTANT` when any part is non-constant.
1701
+ def unwrap_constant_type(type)
1702
+ case type
1703
+ when Type::Constant
1704
+ type.value
1705
+ when Type::Tuple
1706
+ values = unwrap_constant_elements(type.elements)
1707
+ values.nil? ? NOT_CONSTANT : values
1708
+ when Type::HashShape
1709
+ unwrap_constant_hash_shape(type)
1710
+ else
1711
+ NOT_CONSTANT
1712
+ end
1713
+ end
1714
+
1715
+ def unwrap_constant_hash_shape(shape)
1716
+ return NOT_CONSTANT unless shape.closed? && shape.optional_keys.empty?
1717
+
1718
+ pairs = {}
1719
+ shape.pairs.each do |k, v|
1720
+ unwrapped = unwrap_constant_type(v)
1721
+ return NOT_CONSTANT if unwrapped.equal?(NOT_CONSTANT)
1722
+
1723
+ pairs[k] = unwrapped
1724
+ end
1725
+ pairs
1726
+ end
1727
+
1728
+ def unwrap_constant_elements(elements)
1729
+ elements.map do |elem|
1730
+ val = unwrap_constant_type(elem)
1731
+ return nil if val.equal?(NOT_CONSTANT)
1732
+
1733
+ val
1734
+ end
1735
+ end
1736
+
1737
+ def lift_constant_value(value)
1738
+ case value
1739
+ when Array
1740
+ Type::Combinator.tuple_of(*value.map { |v| lift_constant_value(v) })
1741
+ when Hash
1742
+ Type::Combinator.hash_shape_of(value.transform_values { |v| lift_constant_value(v) })
1743
+ else
1744
+ Type::Combinator.constant_of(value)
1745
+ end
1363
1746
  end
1364
1747
 
1365
1748
  # Per-element Constant value extraction. Returns nil when any element is non-Constant, so the
@@ -1919,7 +2302,7 @@ module Rigor
1919
2302
  # `shape.values_at(:a, :b, ...)` with a list of static keys. Returns a `Tuple` whose per-position
1920
2303
  # values are the per-key value types (`Constant[nil]` for missing keys, mirroring Ruby's runtime
1921
2304
  # behaviour). A computed key takes the nil-bearing step {#hash_dig_step} gives it; the call falls
1922
- # through when any step does (a computed key on an open shape).
2305
+ # through when any step does (a computed key on an open or empty shape).
1923
2306
  def hash_values_at(shape, _method_name, args)
1924
2307
  return nil if args.empty?
1925
2308