rigortype 0.4.7 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (268) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -1
  3. data/data/core_overlay/bigdecimal.rbs +33 -0
  4. data/data/core_overlay/resolv.rbs +30 -0
  5. data/data/gem_overlay/activesupport/core_ext.rbs +29 -0
  6. data/data/gem_overlay/addressable/addressable.rbs +40 -0
  7. data/docs/handbook/01-getting-started.md +1 -1
  8. data/docs/handbook/11-sig-gen.md +35 -0
  9. data/docs/install.md +20 -0
  10. data/docs/manual/01-installation.md +67 -26
  11. data/docs/manual/02-cli-reference.md +307 -91
  12. data/docs/manual/03-configuration.md +41 -3
  13. data/docs/manual/04-diagnostics.md +5 -4
  14. data/docs/manual/08-skills.md +11 -0
  15. data/docs/manual/09-editor-integration.md +8 -0
  16. data/docs/manual/14-rails-quickstart.md +1 -1
  17. data/docs/manual/19-effect-labels.md +3 -4
  18. data/docs/manual/plugins/rigor-actionpack.md +15 -6
  19. data/docs/manual/plugins/rigor-activesupport-core-ext.md +3 -0
  20. data/docs/manual/plugins/rigor-sorbet.md +17 -3
  21. data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
  22. data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
  23. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +8 -0
  24. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +13 -2
  25. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -0
  26. data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
  27. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +40 -11
  28. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
  29. data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
  30. data/lib/rigor/analysis/check_rules/rule_ids.rb +8 -1
  31. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  32. data/lib/rigor/analysis/check_rules.rb +598 -99
  33. data/lib/rigor/analysis/crash_signature.rb +43 -1
  34. data/lib/rigor/analysis/dependency_recorder.rb +233 -76
  35. data/lib/rigor/analysis/incremental.rb +11 -1
  36. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -1
  37. data/lib/rigor/analysis/incremental_session.rb +279 -37
  38. data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
  39. data/lib/rigor/analysis/rule_catalog.rb +37 -1
  40. data/lib/rigor/analysis/run_cache_key.rb +27 -3
  41. data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
  42. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +4 -0
  43. data/lib/rigor/analysis/runner/declaration_position.rb +7 -7
  44. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +72 -20
  45. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +3 -8
  46. data/lib/rigor/analysis/runner/pool_coordinator.rb +26 -5
  47. data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
  48. data/lib/rigor/analysis/runner.rb +222 -39
  49. data/lib/rigor/analysis/self_call_resolution_recorder.rb +61 -22
  50. data/lib/rigor/analysis/worker_session.rb +42 -41
  51. data/lib/rigor/bleeding_edge.rb +19 -4
  52. data/lib/rigor/cache/file_digest.rb +39 -31
  53. data/lib/rigor/cache/incremental_snapshot.rb +66 -7
  54. data/lib/rigor/cache/rbs_descriptor.rb +37 -2
  55. data/lib/rigor/cache/rbs_environment.rb +5 -1
  56. data/lib/rigor/cli/baseline_command.rb +15 -2
  57. data/lib/rigor/cli/check_command.rb +4 -2
  58. data/lib/rigor/cli/effects_snapshot_command.rb +8 -1
  59. data/lib/rigor/cli/lens_command.rb +17 -2
  60. data/lib/rigor/cli/sig_gen_command.rb +8 -0
  61. data/lib/rigor/cli/skill_describe.rb +1 -0
  62. data/lib/rigor/cli.rb +5 -2
  63. data/lib/rigor/config_audit.rb +32 -18
  64. data/lib/rigor/configuration/severity_profile.rb +9 -0
  65. data/lib/rigor/configuration.rb +32 -5
  66. data/lib/rigor/effects/collector.rb +53 -21
  67. data/lib/rigor/environment/bundle_sig_discovery.rb +63 -28
  68. data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
  69. data/lib/rigor/environment/default_libraries.rb +17 -0
  70. data/lib/rigor/environment/deferred_signature_plan.rb +104 -6
  71. data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
  72. data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
  73. data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
  74. data/lib/rigor/environment/rbs_hierarchy.rb +6 -0
  75. data/lib/rigor/environment/rbs_loader.rb +312 -63
  76. data/lib/rigor/environment.rb +131 -35
  77. data/lib/rigor/inference/acceptance.rb +50 -15
  78. data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
  79. data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
  80. data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
  81. data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
  82. data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
  83. data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
  84. data/lib/rigor/inference/block_auto_splat.rb +1 -1
  85. data/lib/rigor/inference/block_run_count.rb +9 -2
  86. data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
  87. data/lib/rigor/inference/callee_mutation_chain.rb +1 -1
  88. data/lib/rigor/inference/captured_locals.rb +2 -0
  89. data/lib/rigor/inference/census_foreign_block.rb +354 -0
  90. data/lib/rigor/inference/clause_widening.rb +1 -1
  91. data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
  92. data/lib/rigor/inference/construction_escape.rb +56 -15
  93. data/lib/rigor/inference/declaration_walk.rb +4 -0
  94. data/lib/rigor/inference/def_node_resolver.rb +32 -14
  95. data/lib/rigor/inference/def_return_typer.rb +1 -1
  96. data/lib/rigor/inference/definer_resolution.rb +15 -7
  97. data/lib/rigor/inference/definers_index.rb +72 -16
  98. data/lib/rigor/inference/error_info.rb +16 -0
  99. data/lib/rigor/inference/expression_typer.rb +1459 -287
  100. data/lib/rigor/inference/flow_tracer.rb +31 -14
  101. data/lib/rigor/inference/fork_map.rb +3 -0
  102. data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
  103. data/lib/rigor/inference/global_write_census.rb +8 -6
  104. data/lib/rigor/inference/in_effect_refinements.rb +7 -0
  105. data/lib/rigor/inference/indexed_narrowing.rb +57 -8
  106. data/lib/rigor/inference/ivar_writer_census.rb +106 -11
  107. data/lib/rigor/inference/key_presence_guard.rb +51 -25
  108. data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
  109. data/lib/rigor/inference/last_line.rb +5 -2
  110. data/lib/rigor/inference/loop_value_record.rb +63 -15
  111. data/lib/rigor/inference/match_rebinding/frame.rb +11 -0
  112. data/lib/rigor/inference/match_rebinding/yielding_match.rb +74 -18
  113. data/lib/rigor/inference/match_rebinding.rb +21 -8
  114. data/lib/rigor/inference/method_dispatcher/block_folding.rb +43 -29
  115. data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
  116. data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
  117. data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
  118. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
  119. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
  120. data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
  121. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +12 -0
  122. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +409 -67
  123. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +1 -1
  124. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +384 -64
  125. data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
  126. data/lib/rigor/inference/method_dispatcher.rb +66 -18
  127. data/lib/rigor/inference/method_parameter_binder.rb +136 -56
  128. data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
  129. data/lib/rigor/inference/module_function_state.rb +13 -2
  130. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  131. data/lib/rigor/inference/mutation_widening.rb +44 -4
  132. data/lib/rigor/inference/narrowing.rb +201 -27
  133. data/lib/rigor/inference/operand_effects.rb +43 -2
  134. data/lib/rigor/inference/optimistic_origin.rb +133 -8
  135. data/lib/rigor/inference/origin_lookup.rb +1 -1
  136. data/lib/rigor/inference/precision_scanner.rb +1 -1
  137. data/lib/rigor/inference/project_method_ownership.rb +28 -10
  138. data/lib/rigor/inference/project_patched_methods.rb +65 -13
  139. data/lib/rigor/inference/project_patched_scanner.rb +120 -25
  140. data/lib/rigor/inference/project_patched_visibility.rb +39 -3
  141. data/lib/rigor/inference/rbs_type_translator.rb +68 -15
  142. data/lib/rigor/inference/receiver_writer_call.rb +67 -0
  143. data/lib/rigor/inference/recursion_rebinding.rb +157 -0
  144. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +56 -18
  145. data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -17
  146. data/lib/rigor/inference/refined_dispatch.rb +13 -0
  147. data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
  148. data/lib/rigor/inference/remover_holder_scan.rb +76 -22
  149. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
  150. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +12 -11
  151. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +188 -28
  152. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
  153. data/lib/rigor/inference/scope_indexer.rb +2079 -183
  154. data/lib/rigor/inference/self_call_ivar_writes.rb +26 -7
  155. data/lib/rigor/inference/self_preserving_block.rb +87 -0
  156. data/lib/rigor/inference/singleton_hook_decline.rb +41 -6
  157. data/lib/rigor/inference/singleton_object_constant.rb +32 -2
  158. data/lib/rigor/inference/statement_evaluator.rb +970 -250
  159. data/lib/rigor/inference/string_mutation.rb +14 -0
  160. data/lib/rigor/inference/synthetic_method_scanner.rb +35 -11
  161. data/lib/rigor/inference/toplevel_def_census.rb +14 -6
  162. data/lib/rigor/inference/unknown_store_widening.rb +1 -1
  163. data/lib/rigor/inference/untyped_rejoin.rb +1 -1
  164. data/lib/rigor/inference/void_tail_summary.rb +29 -10
  165. data/lib/rigor/lens/claims/comparison.rb +78 -0
  166. data/lib/rigor/lens/claims/request.rb +131 -0
  167. data/lib/rigor/lens/claims/type_reader.rb +216 -0
  168. data/lib/rigor/lens/claims.rb +208 -0
  169. data/lib/rigor/lens/contract_reader.rb +5 -0
  170. data/lib/rigor/lens/document.rb +1 -1
  171. data/lib/rigor/lens/file_reader.rb +21 -8
  172. data/lib/rigor/lens/run.rb +8 -0
  173. data/lib/rigor/lens.rb +1 -1
  174. data/lib/rigor/library_probe.rb +68 -0
  175. data/lib/rigor/memo_audit.rb +282 -0
  176. data/lib/rigor/plugin/base.rb +15 -2
  177. data/lib/rigor/plugin/io_boundary.rb +25 -3
  178. data/lib/rigor/plugin/manifest.rb +7 -3
  179. data/lib/rigor/plugin/registry.rb +13 -2
  180. data/lib/rigor/plugin/trust_policy.rb +46 -0
  181. data/lib/rigor/protection/mutation_cache.rb +4 -1
  182. data/lib/rigor/reflection/constant_ancestors.rb +79 -22
  183. data/lib/rigor/reflection/declaration_collectors.rb +55 -0
  184. data/lib/rigor/reflection/declarations.rb +58 -0
  185. data/lib/rigor/reflection/project_method.rb +177 -0
  186. data/lib/rigor/reflection.rb +2 -0
  187. data/lib/rigor/rooted_glob.rb +18 -1
  188. data/lib/rigor/run_scoped_slots.rb +20 -20
  189. data/lib/rigor/scope/discovery_index.rb +149 -2
  190. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
  191. data/lib/rigor/scope/resolution_chain.rb +900 -55
  192. data/lib/rigor/scope.rb +307 -34
  193. data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
  194. data/lib/rigor/sig_gen/classification.rb +14 -1
  195. data/lib/rigor/sig_gen/generator.rb +222 -13
  196. data/lib/rigor/sig_gen/method_candidate.rb +14 -0
  197. data/lib/rigor/sig_gen/observation_collector.rb +11 -2
  198. data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
  199. data/lib/rigor/sig_gen/skip_reason_catalog.rb +47 -0
  200. data/lib/rigor/sig_gen/writer.rb +21 -0
  201. data/lib/rigor/sig_gen.rb +1 -0
  202. data/lib/rigor/source/forwardable_delegators.rb +122 -0
  203. data/lib/rigor/source/node_children.rb +13 -0
  204. data/lib/rigor/type/combinator.rb +164 -8
  205. data/lib/rigor/type/interface.rb +60 -0
  206. data/lib/rigor/type.rb +1 -0
  207. data/lib/rigor/type_of_memo/serving.rb +99 -0
  208. data/lib/rigor/type_of_memo/stats.rb +43 -0
  209. data/lib/rigor/type_of_memo.rb +337 -0
  210. data/lib/rigor/typing_state.rb +1094 -0
  211. data/lib/rigor/version.rb +1 -1
  212. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +2 -2
  213. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +57 -25
  214. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
  215. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
  216. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
  217. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
  218. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
  219. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +130 -30
  220. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -5
  221. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
  222. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +49 -13
  223. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
  224. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +4 -2
  225. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +13 -5
  226. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +29 -0
  227. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
  228. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
  229. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -2
  230. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
  231. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +6 -0
  232. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +199 -1
  233. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +3 -2
  234. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +4 -4
  235. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +209 -2
  236. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
  237. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
  238. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +80 -3
  239. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
  240. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +1 -6
  241. data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
  242. data/sig/rigor/analysis/fact_store.rbs +2 -3
  243. data/sig/rigor/cache/descriptor.rbs +14 -0
  244. data/sig/rigor/environment.rbs +4 -1
  245. data/sig/rigor/plugin/base.rbs +53 -19
  246. data/sig/rigor/plugin/interfaces.rbs +51 -0
  247. data/sig/rigor/plugin/io_boundary.rbs +2 -3
  248. data/sig/rigor/plugin/node_context.rbs +21 -0
  249. data/sig/rigor/reflection.rbs +37 -0
  250. data/sig/rigor/scope.rbs +22 -3
  251. data/sig/rigor/trinary.rbs +1 -0
  252. data/sig/rigor/type.rbs +9 -3
  253. data/sig/rigor.rbs +3 -2
  254. data/skills/rigor-ask/SKILL.md +1 -0
  255. data/skills/rigor-project-init/references/01-detect.md +1 -0
  256. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +5 -1
  257. data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
  258. data/skills/rigor-rbs-contract/SKILL.md +147 -0
  259. data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
  260. data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
  261. data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
  262. data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
  263. data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
  264. data/skills/rigor-rbs-setup/SKILL.md +3 -1
  265. data/skills/rigor-type-oracle/SKILL.md +26 -1
  266. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
  267. metadata +48 -2
  268. data/lib/rigor/inference/memo_audit.rb +0 -55
@@ -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.
@@ -216,6 +227,7 @@ module Rigor
216
227
  # `CONST = [...].freeze; CONST.any? { … }`; fixing it at the fold (not the rule) unblocks that
217
228
  # preservation.
218
229
  return nil unless context.block_type.nil?
230
+ return constant_set_first(context) if method_name == :first && receiver.is_a?(Type::Constant)
219
231
 
220
232
  handler = RECEIVER_HANDLERS[receiver.class]
221
233
  return nil unless handler
@@ -335,6 +347,30 @@ module Rigor
335
347
  Type::Combinator.nominal_of("Set", type_args: [Type::Combinator.untyped])
336
348
  end
337
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
+
338
374
  # `Integer#to_s` / `Integer#inspect` (#993) on a bare `Nominal[Integer]` receiver, ahead of the
339
375
  # existing `:*` binary fold. `#inspect` takes no argument and matches `#to_s()` byte for byte, so
340
376
  # it routes through the same no-arg branch.
@@ -843,6 +879,25 @@ module Rigor
843
879
  Type::Combinator.constant_of(tuple.elements.size)
844
880
  end
845
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
+
846
901
  # `tuple.empty?` — folds to a precise bool from the tuple's known arity.
847
902
  # rubocop:disable Style/ReturnNilInPredicateMethodDefinition
848
903
  def tuple_empty?(tuple, _method_name, args)
@@ -884,27 +939,29 @@ module Rigor
884
939
  Type::Combinator.constant_of(decision)
885
940
  end
886
941
 
887
- # `tuple.include?(needle)` — folds to a precise bool when the needle is a `Constant` and the
888
- # tuple's elements are all `Constant` (so disjointness is checkable). If any element matches the
889
- # needle's value the answer is `Constant[true]`; if every element is a Constant whose value is
890
- # 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]`.
891
946
  def tuple_include?(tuple, _method_name, args)
892
947
  return nil unless args.size == 1
893
948
 
894
- needle = args.first
895
- return nil unless needle.is_a?(Type::Constant)
949
+ needle = unwrap_constant_type(args.first)
950
+ return nil if needle.equal?(NOT_CONSTANT)
896
951
  return Type::Combinator.constant_of(false) if tuple.elements.empty?
897
952
 
898
- return Type::Combinator.constant_of(true) if any_element_matches?(tuple.elements, needle.value)
899
- 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?
900
955
 
901
956
  nil
902
957
  end
903
958
  # rubocop:enable Style/ReturnNilInPredicateMethodDefinition
904
959
 
905
- # `tuple.sum` — when every element is a numeric Constant, fold to `Constant[sum]`. Mixed /
906
- # 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`.
907
963
  def tuple_sum(tuple, _method_name, args)
964
+ return tuple_sum_with_init(tuple, args.first) if args.size == 1
908
965
  return nil unless args.empty?
909
966
  return Type::Combinator.constant_of(0) if tuple.elements.empty?
910
967
 
@@ -912,16 +969,45 @@ module Rigor
912
969
  return nil if values.nil?
913
970
 
914
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
915
1001
  end
916
1002
 
917
1003
  # `tuple.join(sep = "")` — fold to the joined `Constant[String]` when every element is a
918
- # `Constant` (its `to_s` is deterministic for the scalar value classes) and the separator is
919
- # 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`.
920
1006
  def tuple_join(tuple, _method_name, args)
921
1007
  sep = tuple_join_separator(args)
922
1008
  return nil if sep.nil?
923
1009
 
924
- values = constant_values(tuple.elements)
1010
+ values = unwrap_constant_elements(tuple.elements)
925
1011
  return nil if values.nil?
926
1012
 
927
1013
  result = values.join(sep)
@@ -945,14 +1031,13 @@ module Rigor
945
1031
  end
946
1032
 
947
1033
  # `tuple.inspect` / `tuple.to_s` (`Array#to_s` is an exact alias of `Array#inspect`) — folds when
948
- # every element is a `Constant`, by reconstructing the Ruby Array and calling the REAL `inspect`
949
- # so the analyzer's Ruby (4.0.x's `Array#inspect` format) is matched rather than re-derived.
950
- # Capped at `TUPLE_JOIN_BYTE_LIMIT` like `#join`, since a large tuple's `inspect` string is
951
- # 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`.
952
1037
  def tuple_inspect(tuple, _method_name, args)
953
1038
  return nil unless args.empty?
954
1039
 
955
- values = constant_values(tuple.elements)
1040
+ values = unwrap_constant_elements(tuple.elements)
956
1041
  return nil if values.nil?
957
1042
 
958
1043
  result = values.inspect
@@ -997,7 +1082,7 @@ module Rigor
997
1082
  Type::Combinator.tuple_of(*(tuple.elements * count))
998
1083
  end
999
1084
 
1000
- # `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
1001
1086
  # Ruby-comparable domain. Empty tuples fold to `Constant[nil]`. The 1-arg `min(n)` / `max(n)`
1002
1087
  # form folds to a `Tuple` of the n edge-most values in Ruby's order (`min(n)` ascending, `max(n)`
1003
1088
  # descending) — the n-arg sibling of `first(n)`.
@@ -1013,11 +1098,11 @@ module Rigor
1013
1098
  return tuple_minmax_n(tuple, args.first, edge) unless args.empty?
1014
1099
  return Type::Combinator.constant_of(nil) if tuple.elements.empty?
1015
1100
 
1016
- values = constant_values(tuple.elements)
1101
+ values = unwrap_constant_elements(tuple.elements)
1017
1102
  return nil if values.nil?
1018
1103
 
1019
1104
  result = values.public_send(edge)
1020
- Type::Combinator.constant_of(result)
1105
+ lift_constant_value(result)
1021
1106
  rescue StandardError
1022
1107
  nil
1023
1108
  end
@@ -1030,19 +1115,18 @@ module Rigor
1030
1115
  return nil unless arg.is_a?(Type::Constant) && arg.value.is_a?(Integer)
1031
1116
  return nil if arg.value.negative?
1032
1117
 
1033
- values = constant_values(tuple.elements)
1118
+ values = unwrap_constant_elements(tuple.elements)
1034
1119
  return nil if values.nil?
1035
1120
 
1036
1121
  picked = values.public_send(edge, arg.value)
1037
- Type::Combinator.tuple_of(*picked.map { |v| Type::Combinator.constant_of(v) })
1122
+ Type::Combinator.tuple_of(*picked.map { |v| lift_constant_value(v) })
1038
1123
  rescue StandardError
1039
1124
  nil
1040
1125
  end
1041
1126
 
1042
- # `tuple.minmax` — the `[min, max]` pair as a 2-slot `Tuple[Constant[min], Constant[max]]`,
1043
- # mirroring the `Range#minmax` fold. Every element must be a `Constant` and the values must
1044
- # Ruby-compare; an empty tuple folds to `Tuple[nil, nil]` (Ruby's `[].minmax`), incomparable
1045
- # 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.
1046
1130
  def tuple_minmax_pair(tuple, _method_name, args)
1047
1131
  return nil unless args.empty?
1048
1132
 
@@ -1051,30 +1135,30 @@ module Rigor
1051
1135
  return Type::Combinator.tuple_of(nil_const, nil_const)
1052
1136
  end
1053
1137
 
1054
- values = constant_values(tuple.elements)
1138
+ values = unwrap_constant_elements(tuple.elements)
1055
1139
  return nil if values.nil?
1056
1140
 
1057
1141
  low, high = values.minmax
1058
1142
  Type::Combinator.tuple_of(
1059
- Type::Combinator.constant_of(low),
1060
- Type::Combinator.constant_of(high)
1143
+ lift_constant_value(low),
1144
+ lift_constant_value(high)
1061
1145
  )
1062
1146
  rescue StandardError
1063
1147
  nil
1064
1148
  end
1065
1149
 
1066
- # `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
1067
1151
  # is a Tuple with the same elements in sorted order. Comparison failures (mixed-class
1068
1152
  # incomparable values) decline.
1069
1153
  def tuple_sort(tuple, _method_name, args)
1070
1154
  return nil unless args.empty?
1071
1155
  return tuple if tuple.elements.size <= 1
1072
1156
 
1073
- values = constant_values(tuple.elements)
1157
+ values = unwrap_constant_elements(tuple.elements)
1074
1158
  return nil if values.nil?
1075
1159
 
1076
1160
  sorted = values.sort
1077
- Type::Combinator.tuple_of(*sorted.map { |v| Type::Combinator.constant_of(v) })
1161
+ Type::Combinator.tuple_of(*sorted.map { |v| lift_constant_value(v) })
1078
1162
  rescue StandardError
1079
1163
  nil
1080
1164
  end
@@ -1101,7 +1185,7 @@ module Rigor
1101
1185
  # runtime semantics). Declines when any `other_k` is not a Tuple, since the arity is then unknown
1102
1186
  # and the result would be `Array[Array[…]]` — RBS already gives that answer.
1103
1187
  def tuple_zip(tuple, _method_name, args)
1104
- return nil if args.empty? || args.size > MAX_ZIP_ARITY
1188
+ return nil if args.size > MAX_ZIP_ARITY
1105
1189
  return nil unless args.all?(Type::Tuple)
1106
1190
 
1107
1191
  zipped = tuple.elements.each_with_index.map do |elem, i|
@@ -1139,23 +1223,180 @@ module Rigor
1139
1223
  [key.value, value]
1140
1224
  end
1141
1225
 
1142
- # `tuple.values_at(i1, i2, ...)` — returns a Tuple of per-index elements. Each argument must be a
1143
- # `Constant[Integer]`. Out-of-range indices fill with `Constant[nil]`, mirroring Ruby's runtime
1144
- # 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`.
1145
1244
  def tuple_values_at(tuple, _method_name, args)
1146
- 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)
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)
1147
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)
1148
1271
  values = args.map do |arg|
1149
- return nil unless arg.is_a?(Type::Constant)
1150
- return nil unless arg.value.is_a?(Integer)
1272
+ return nil unless arg.is_a?(Type::Constant) && arg.value.is_a?(Integer)
1151
1273
 
1152
1274
  idx = normalise_index(arg.value, tuple.elements.size)
1153
- idx ? tuple.elements[idx] : Type::Combinator.constant_of(nil)
1275
+ return nil unless idx
1276
+
1277
+ tuple.elements[idx]
1154
1278
  end
1155
1279
 
1156
1280
  Type::Combinator.tuple_of(*values)
1157
1281
  end
1158
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
+
1159
1400
  # `tuple + other` — concatenates two Tuples. Both sides must be `Type::Tuple`. Returns a new
1160
1401
  # Tuple whose elements are those of the receiver followed by those of the argument.
1161
1402
  # `at(n)` — the strict single-Integer accessor. Deliberately NOT an alias of {#tuple_index}: `[]`
@@ -1178,15 +1419,33 @@ module Rigor
1178
1419
  # Every set operation is evaluated by running Ruby's own operator over the unwrapped values, which
1179
1420
  # is the only way to reproduce its `eql?` / `hash` semantics exactly — `[1] & [1.0]` is empty even
1180
1421
  # though `1 == 1.0`, so an equality-based reimplementation would fold the wrong answer.
1181
- 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
+
1182
1425
  set_operation(tuple, args) { |values, others| others.reduce(values, :&) }
1183
1426
  end
1184
1427
 
1185
- 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
+
1186
1431
  set_operation(tuple, args) { |values, others| others.reduce(values, :|) }
1187
1432
  end
1188
1433
 
1189
- 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
+
1190
1449
  set_operation(tuple, args) { |values, others| others.reduce(values, :-) }
1191
1450
  end
1192
1451
 
@@ -1203,23 +1462,23 @@ module Rigor
1203
1462
 
1204
1463
  # Shared body for the set operations: unwrap the receiver and every Tuple argument, hand the raw
1205
1464
  # values to `block`, and lift the result back. Declines unless every element on both sides is a
1206
- # `Constant` (an unknown element makes membership undecidable) and every argument is a `Tuple` —
1207
- # `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`.
1208
1467
  def set_operation(tuple, args)
1209
1468
  return nil if args.empty? || args.size > MAX_SET_OPERATION_ARITY
1210
1469
 
1211
- values = constant_values(tuple.elements)
1470
+ values = unwrap_constant_elements(tuple.elements)
1212
1471
  return nil if values.nil?
1213
1472
 
1214
1473
  others = args.map do |arg|
1215
1474
  return nil unless arg.is_a?(Type::Tuple)
1216
1475
 
1217
- constant_values(arg.elements) || (return nil)
1476
+ unwrap_constant_elements(arg.elements) || (return nil)
1218
1477
  end
1219
1478
  result = yield(values, others)
1220
1479
  return nil if result.size > MAX_SET_OPERATION_SIZE
1221
1480
 
1222
- Type::Combinator.tuple_of(*result.map { |value| Type::Combinator.constant_of(value) })
1481
+ Type::Combinator.tuple_of(*result.map { |value| lift_constant_value(value) })
1223
1482
  end
1224
1483
 
1225
1484
  # An argument list longer than this, or a result wider than this, declines to the RBS tier rather
@@ -1278,16 +1537,19 @@ module Rigor
1278
1537
  end
1279
1538
 
1280
1539
  # `uniq` (no block) → `Tuple` of the first occurrence of each distinct value. Folds only when
1281
- # 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.
1282
1542
  def tuple_uniq(tuple, _method_name, args)
1283
1543
  return nil unless args.empty?
1284
- return nil unless tuple.elements.all?(Type::Constant)
1285
1544
 
1286
- seen = []
1287
- kept = tuple.elements.each_with_object([]) do |element, acc|
1288
- 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)
1289
1551
 
1290
- seen << element.value
1552
+ seen[value] = true
1291
1553
  acc << element
1292
1554
  end
1293
1555
  Type::Combinator.tuple_of(*kept)
@@ -1295,9 +1557,9 @@ module Rigor
1295
1557
 
1296
1558
  # `index(obj)` / `find_index(obj)` → `Constant[Integer]` of the first element equal to `obj`,
1297
1559
  # `Constant[nil]` when none match. Folds only for the argument form (the block form defers) when
1298
- # every element AND the argument are `Constant` (decidable equality).
1560
+ # every element AND the argument are constant (decidable equality).
1299
1561
  def tuple_find_index(tuple, _method_name, args)
1300
- constant_index(tuple, args) { |elements, value| elements.index { |e| e.value == value } }
1562
+ constant_index(tuple, args) { |values, needle| values.index { |v| v == needle } }
1301
1563
  end
1302
1564
 
1303
1565
  # `tuple.flatten` / `tuple.flatten(depth)` — recursively flattens nested Tuple elements into a
@@ -1341,17 +1603,19 @@ module Rigor
1341
1603
 
1342
1604
  # `rindex(obj)` → the LAST matching index, same decidability gate.
1343
1605
  def tuple_rindex(tuple, _method_name, args)
1344
- constant_index(tuple, args) { |elements, value| elements.rindex { |e| e.value == value } }
1606
+ constant_index(tuple, args) { |values, needle| values.rindex { |v| v == needle } }
1345
1607
  end
1346
1608
 
1347
1609
  def constant_index(tuple, args)
1348
1610
  return nil unless args.size == 1
1349
1611
 
1350
- needle = args.first
1351
- return nil unless needle.is_a?(Type::Constant)
1352
- 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?
1353
1615
 
1354
- Type::Combinator.constant_of(yield(tuple.elements, needle.value))
1616
+ Type::Combinator.constant_of(yield(values, needle))
1617
+ rescue StandardError
1618
+ nil
1355
1619
  end
1356
1620
 
1357
1621
  # `tuple.take(n)` — returns the first n elements as a new Tuple. The argument must be a
@@ -1422,7 +1686,63 @@ module Rigor
1422
1686
  end
1423
1687
 
1424
1688
  def any_element_matches?(elements, value)
1425
- 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
1426
1746
  end
1427
1747
 
1428
1748
  # Per-element Constant value extraction. Returns nil when any element is non-Constant, so the