rigortype 0.0.4 → 0.0.6

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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +215 -134
  3. data/data/builtins/ruby_core/comparable.yml +87 -0
  4. data/data/builtins/ruby_core/complex.yml +505 -0
  5. data/data/builtins/ruby_core/date.yml +1737 -0
  6. data/data/builtins/ruby_core/enumerable.yml +557 -0
  7. data/data/builtins/ruby_core/file.yml +9 -0
  8. data/data/builtins/ruby_core/pathname.yml +1067 -0
  9. data/data/builtins/ruby_core/rational.yml +365 -0
  10. data/data/builtins/ruby_core/string.yml +9 -0
  11. data/data/builtins/ruby_core/time.yml +6 -4
  12. data/lib/rigor/cli.rb +1 -1
  13. data/lib/rigor/inference/builtins/comparable_catalog.rb +27 -0
  14. data/lib/rigor/inference/builtins/complex_catalog.rb +41 -0
  15. data/lib/rigor/inference/builtins/date_catalog.rb +98 -0
  16. data/lib/rigor/inference/builtins/enumerable_catalog.rb +27 -0
  17. data/lib/rigor/inference/builtins/pathname_catalog.rb +35 -0
  18. data/lib/rigor/inference/builtins/rational_catalog.rb +38 -0
  19. data/lib/rigor/inference/expression_typer.rb +285 -23
  20. data/lib/rigor/inference/method_dispatcher/block_folding.rb +322 -0
  21. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +197 -12
  22. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +99 -0
  23. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +95 -0
  24. data/lib/rigor/inference/method_dispatcher.rb +20 -8
  25. data/lib/rigor/inference/narrowing.rb +210 -1
  26. data/lib/rigor/inference/scope_indexer.rb +87 -11
  27. data/lib/rigor/inference/statement_evaluator.rb +5 -1
  28. data/lib/rigor/rbs_extended.rb +11 -6
  29. data/lib/rigor/type/integer_range.rb +4 -2
  30. data/lib/rigor/version.rb +1 -1
  31. data/sig/rigor/environment.rbs +4 -6
  32. data/sig/rigor/inference.rbs +2 -1
  33. data/sig/rigor/type.rbs +41 -41
  34. metadata +15 -1
@@ -500,39 +500,99 @@ module Rigor
500
500
  # rebinding is the StatementEvaluator's job (Slice 3 phase 2).
501
501
  # Without an else clause the branch's implicit value is nil, which
502
502
  # is included in the union.
503
+ #
504
+ # v0.0.6 — when the predicate folds to a `Type::Constant` whose
505
+ # value is Ruby-truthy (resp. Ruby-falsey), the unreachable
506
+ # branch is elided so the if-expression's type is the live
507
+ # branch alone. Statement-level branch elision lives in
508
+ # `StatementEvaluator#eval_if`; this handler covers the
509
+ # expression-position ternary form (`a ? b : c`) and any
510
+ # `if`/`unless` reached through `type_of`.
503
511
  def type_of_if(node)
504
512
  then_type = statements_or_nil(node.statements)
505
- else_type =
506
- if node.subsequent
507
- type_of(node.subsequent)
508
- else
509
- Type::Combinator.constant_of(nil)
510
- end
511
- Type::Combinator.union(then_type, else_type)
513
+ else_type = if_else_type(node.subsequent)
514
+ elide_or_union(node.predicate, then_type, else_type)
512
515
  end
513
516
 
514
517
  # `unless c; t; else; e; end`. Prism uses `else_clause` here (no
515
- # `elsif` chain).
518
+ # `elsif` chain). Branch-elision logic mirrors `type_of_if`,
519
+ # inverted: a truthy predicate selects the else branch.
516
520
  def type_of_unless(node)
517
521
  then_type = statements_or_nil(node.statements)
518
- else_type =
519
- if node.else_clause
520
- type_of(node.else_clause)
521
- else
522
- Type::Combinator.constant_of(nil)
523
- end
524
- Type::Combinator.union(then_type, else_type)
522
+ else_type = if_else_type(node.else_clause)
523
+ elide_or_union(node.predicate, else_type, then_type)
524
+ end
525
+
526
+ def if_else_type(subsequent)
527
+ return Type::Combinator.constant_of(nil) if subsequent.nil?
528
+
529
+ type_of(subsequent)
530
+ end
531
+
532
+ # Routes the predicate's typed value through branch elision.
533
+ # `live_when_truthy` and `live_when_falsey` are the branch
534
+ # types selected by the predicate's polarity; the names
535
+ # match `IfNode` semantics directly and invert at the
536
+ # `type_of_unless` call site.
537
+ def elide_or_union(predicate, live_when_truthy, live_when_falsey)
538
+ case constant_predicate_polarity(predicate)
539
+ when :truthy then live_when_truthy
540
+ when :falsey then live_when_falsey
541
+ else Type::Combinator.union(live_when_truthy, live_when_falsey)
542
+ end
543
+ end
544
+
545
+ # Returns `:truthy`, `:falsey`, or `nil` for an arbitrary
546
+ # predicate expression. Only `Type::Constant` answers
547
+ # decisively — `Union[true, false]`, `Nominal[bool]`, and
548
+ # `Dynamic[T]` keep both branches live.
549
+ def constant_predicate_polarity(predicate)
550
+ return nil if predicate.nil?
551
+
552
+ type = type_of(predicate)
553
+ return nil unless type.is_a?(Type::Constant)
554
+
555
+ type.value ? :truthy : :falsey
525
556
  end
526
557
 
527
558
  def type_of_else(node)
528
559
  statements_or_nil(node.statements)
529
560
  end
530
561
 
531
- # `a && b` and `a || b` short-circuit. Without a truthy/falsy
532
- # narrowing model (Slice 6), the result of either side is reachable
533
- # so the type is the union of the operand types.
562
+ # `a && b` and `a || b` short-circuit at the value level:
563
+ # `a && b` returns `a` when `a` is falsey, else `b`.
564
+ # `a || b` returns `a` when `a` is truthy, else `b`.
565
+ #
566
+ # v0.0.6 — when the left operand folds to a `Type::Constant`,
567
+ # we know which side actually flows through, so the result
568
+ # is one operand's type instead of a union. Otherwise the
569
+ # union-of-both-operands fallback is preserved.
534
570
  def type_of_and_or(node)
535
- Type::Combinator.union(type_of(node.left), type_of(node.right))
571
+ left_type = type_of(node.left)
572
+ polarity = constant_value_polarity(left_type)
573
+ return short_circuit_for(node, left_type, polarity) if polarity
574
+
575
+ Type::Combinator.union(left_type, type_of(node.right))
576
+ end
577
+
578
+ def short_circuit_for(node, left_type, polarity)
579
+ and_node = node.is_a?(Prism::AndNode)
580
+ if polarity == :truthy
581
+ and_node ? type_of(node.right) : left_type
582
+ else
583
+ and_node ? left_type : type_of(node.right)
584
+ end
585
+ end
586
+
587
+ # Returns `:truthy` / `:falsey` for a `Type::Constant`,
588
+ # nil otherwise. Mirrors `constant_predicate_polarity` but
589
+ # operates on a typed value (already-type-of'd) rather
590
+ # than a Prism node, so the same predicate analysis can
591
+ # be reused in both contexts.
592
+ def constant_value_polarity(type)
593
+ return nil unless type.is_a?(Type::Constant)
594
+
595
+ type.value ? :truthy : :falsey
536
596
  end
537
597
 
538
598
  def type_of_case(node)
@@ -701,12 +761,17 @@ module Rigor
701
761
  # when every element is a non-splat value. Splatted entries
702
762
  # (`[*xs, 1]`) preserve the Slice 4 phase 2d behavior: we union
703
763
  # the contributed element types and emit
704
- # `Nominal[Array, [union]]`. An empty literal stays as the raw
705
- # `Array` (no element evidence to lock either an arity or an
706
- # element type).
764
+ # `Nominal[Array, [union]]`.
765
+ #
766
+ # v0.0.6 — the empty literal `[]` resolves to the empty
767
+ # `Tuple[]` carrier rather than the raw `Nominal[Array]`.
768
+ # Both carriers erase to RBS `Array`, but `Tuple[]` pins
769
+ # the literal's known arity (zero), which lets the
770
+ # per-element block fold concatenate across all-empty
771
+ # positions like `[1, 2].flat_map { |_| [] }`.
707
772
  def array_type_for(node)
708
773
  elements = node.elements
709
- return Type::Combinator.nominal_of(Array) if elements.empty?
774
+ return Type::Combinator.tuple_of if elements.empty?
710
775
 
711
776
  if elements.any?(Prism::SplatNode)
712
777
  element_types = elements.map { |e| type_of(e) }
@@ -739,6 +804,7 @@ module Rigor
739
804
  # for the CallNode itself (the inner type_of calls already record
740
805
  # their own fallbacks for unrecognised receivers/args, so the tracer
741
806
  # captures both the immediate dispatch miss and the deeper cause).
807
+ # rubocop:disable Metrics/CyclomaticComplexity
742
808
  def call_type_for(node)
743
809
  receiver = call_receiver_type_for(node)
744
810
  arg_types = call_arg_types(node)
@@ -771,6 +837,18 @@ module Rigor
771
837
  return dynamic_top
772
838
  end
773
839
 
840
+ # v0.0.6 phase 2 — per-element block fold for Tuple
841
+ # receivers. When `[a, b, c].map { |x| f(x) }` and the
842
+ # receiver is a `Tuple` carrier with finite elements,
843
+ # type the block body once per position with the
844
+ # corresponding element bound to the block parameter
845
+ # and assemble the results into a `Tuple[U_1..U_n]`.
846
+ # This sits ahead of `MethodDispatcher.dispatch` so
847
+ # the RBS tier does not re-widen the answer back to
848
+ # `Array[union]`.
849
+ per_element = try_per_element_block_fold(node, receiver)
850
+ return per_element if per_element
851
+
774
852
  result = MethodDispatcher.dispatch(
775
853
  receiver_type: receiver,
776
854
  method_name: node.name,
@@ -797,6 +875,7 @@ module Rigor
797
875
 
798
876
  fallback_for(node, family: :prism)
799
877
  end
878
+ # rubocop:enable Metrics/CyclomaticComplexity
800
879
 
801
880
  # v0.0.2 #5 — re-types the body of a user-defined
802
881
  # instance method with the call site's argument types
@@ -976,6 +1055,189 @@ module Rigor
976
1055
 
977
1056
  block_scope.type_of(body)
978
1057
  end
1058
+
1059
+ # v0.0.6 phase 2 — per-element block fold for Tuple
1060
+ # receivers under `:map` / `:collect`. Walks every Tuple
1061
+ # position, binds the block parameter to that element's
1062
+ # type, and re-types the block body. The per-position
1063
+ # results are assembled into `Tuple[U_1..U_n]`, strictly
1064
+ # tighter than the RBS-projected `Array[union]`.
1065
+ #
1066
+ # Declines (returns nil) when the receiver is not a
1067
+ # `Tuple` with at least one element, when the call has
1068
+ # no `Prism::BlockNode`, when the method is outside the
1069
+ # supported set, when block typing raises mid-loop, or
1070
+ # when the block has no body. The decline path leaves
1071
+ # the dispatch chain untouched.
1072
+ PER_ELEMENT_TUPLE_METHODS = Set[
1073
+ :map, :collect, :filter_map, :flat_map,
1074
+ :find, :detect, :find_index, :index
1075
+ ].freeze
1076
+ private_constant :PER_ELEMENT_TUPLE_METHODS
1077
+
1078
+ # Cardinality cap for per-element block fold over
1079
+ # finite-bound `Constant<Range>` receivers. Walking
1080
+ # `(1..1_000_000).map { … }` element-wise would balloon
1081
+ # block-typing cost and explode the resulting Tuple, so
1082
+ # only short ranges expand into per-position folds.
1083
+ # Larger ranges decline so the RBS tier widens.
1084
+ PER_ELEMENT_RANGE_LIMIT = 8
1085
+ private_constant :PER_ELEMENT_RANGE_LIMIT
1086
+
1087
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
1088
+ def try_per_element_block_fold(call_node, receiver_type)
1089
+ return nil unless PER_ELEMENT_TUPLE_METHODS.include?(call_node.name)
1090
+ return nil if find_family_with_args?(call_node)
1091
+
1092
+ element_types = per_element_elements_of(receiver_type)
1093
+ return nil if element_types.nil? || element_types.empty?
1094
+
1095
+ block_node = call_node.block
1096
+ return nil unless block_node.is_a?(Prism::BlockNode)
1097
+
1098
+ per_position = element_types.map do |element_type|
1099
+ type_block_body_with_param(block_node, [element_type])
1100
+ end
1101
+ return nil if per_position.any?(&:nil?)
1102
+
1103
+ assemble_per_element_result(call_node.name, per_position, element_types)
1104
+ end
1105
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
1106
+
1107
+ # Returns the per-position element types for a finite,
1108
+ # statically-known receiver shape — or nil when the
1109
+ # receiver does not pin a finite element list.
1110
+ #
1111
+ # `Tuple[A, B, …]` → [A, B, …]
1112
+ # `Constant<a..b>` → [Constant[a], …, Constant[b]]
1113
+ # everything else → nil
1114
+ #
1115
+ # Note: `Type::IntegerRange` is the bounded-Integer
1116
+ # carrier (`int<a, b>` represents "an Integer between
1117
+ # a and b"), not a Range value. Calls like `.map` /
1118
+ # `.find` on an `IntegerRange` receiver would resolve
1119
+ # to `Integer#map` / `Integer#find` — neither exists —
1120
+ # so IntegerRange does NOT participate in this fold.
1121
+ def per_element_elements_of(receiver_type)
1122
+ case receiver_type
1123
+ when Type::Tuple then receiver_type.elements
1124
+ when Type::Constant then constant_range_elements(receiver_type.value)
1125
+ end
1126
+ end
1127
+
1128
+ # rubocop:disable Metrics/CyclomaticComplexity
1129
+ def constant_range_elements(value)
1130
+ return nil unless value.is_a?(Range)
1131
+ return nil unless value.begin.is_a?(Integer) && value.end.is_a?(Integer)
1132
+
1133
+ cardinality = value.exclude_end? ? value.end - value.begin : value.end - value.begin + 1
1134
+ return nil if cardinality <= 0 || cardinality > PER_ELEMENT_RANGE_LIMIT
1135
+
1136
+ value.to_a.map { |v| Type::Combinator.constant_of(v) }
1137
+ end
1138
+ # rubocop:enable Metrics/CyclomaticComplexity
1139
+
1140
+ # `index(value)` and `find_index(value)` carry a positional
1141
+ # argument and search by `==` rather than running the block.
1142
+ # Decline so the RBS tier owns those forms.
1143
+ def find_family_with_args?(call_node)
1144
+ return false unless %i[find_index index].include?(call_node.name)
1145
+
1146
+ args = call_node.arguments
1147
+ !args.nil? && !args.arguments.empty?
1148
+ end
1149
+
1150
+ def assemble_per_element_result(method_name, per_position, element_types)
1151
+ case method_name
1152
+ when :map, :collect then Type::Combinator.tuple_of(*per_position)
1153
+ when :filter_map then assemble_filter_map_result(per_position)
1154
+ when :flat_map then assemble_flat_map_result(per_position)
1155
+ when :find, :detect then assemble_find_result(per_position, element_types)
1156
+ when :find_index, :index then assemble_find_index_result(per_position)
1157
+ end
1158
+ end
1159
+
1160
+ # `filter_map` folds tightly only when every per-position
1161
+ # result is a `Constant`: positions whose value is `nil`
1162
+ # or `false` drop, the rest survive in declaration order.
1163
+ # When any position is non-Constant the dispatcher
1164
+ # declines (returns nil) so the RBS tier widens to
1165
+ # `Array[U]`.
1166
+ def assemble_filter_map_result(per_position)
1167
+ return nil unless per_position.all?(Type::Constant)
1168
+
1169
+ kept = per_position.reject { |type| type.value.nil? || type.value == false }
1170
+ Type::Combinator.tuple_of(*kept)
1171
+ end
1172
+
1173
+ # `flat_map` flattens a single level: if the per-position
1174
+ # result is a `Tuple`, its elements are concatenated; if
1175
+ # it's a non-Array scalar carrier (`Constant<…>` over a
1176
+ # non-Array literal) it contributes one element. We fold
1177
+ # tightly only when every per-position result is one of
1178
+ # those two recognisable shapes — `Nominal[Array[T]]`,
1179
+ # `Union[…]`, and other opaque carriers decline so the
1180
+ # RBS tier widens to `Array[U]`.
1181
+ #
1182
+ # `Type::Constant` only ever holds non-Array scalars (the
1183
+ # carrier rejects Array literals), so a single `Constant`
1184
+ # safely contributes itself as a single Tuple element.
1185
+ def assemble_flat_map_result(per_position)
1186
+ flattened = per_position.flat_map { |type| flat_map_contribution(type) }
1187
+ return nil if flattened.nil? || flattened.any?(&:nil?)
1188
+
1189
+ Type::Combinator.tuple_of(*flattened)
1190
+ end
1191
+
1192
+ def flat_map_contribution(type)
1193
+ case type
1194
+ when Type::Tuple then type.elements
1195
+ when Type::Constant then [type]
1196
+ else [nil]
1197
+ end
1198
+ end
1199
+
1200
+ # `find` / `detect`: returns the first receiver element
1201
+ # whose block result is Ruby-truthy, or `nil` when no
1202
+ # position folds to truthy.
1203
+ #
1204
+ # Folds tightly only when every per-position block result
1205
+ # is a `Type::Constant` — otherwise we cannot decide which
1206
+ # position (if any) is "the first matching one". When the
1207
+ # first decisive truthy position is found, the answer is
1208
+ # the corresponding receiver element. When every position
1209
+ # folds to falsey, the answer is `Constant[nil]`.
1210
+ def assemble_find_result(per_position, element_types)
1211
+ return nil unless per_position.all?(Type::Constant)
1212
+
1213
+ first_truthy_index = per_position.index { |type| truthy_constant?(type) }
1214
+ return Type::Combinator.constant_of(nil) if first_truthy_index.nil?
1215
+
1216
+ element_types[first_truthy_index]
1217
+ end
1218
+
1219
+ # `find_index` / `index`: returns the index of the first
1220
+ # truthy position, or `Constant[nil]` when nothing matches.
1221
+ def assemble_find_index_result(per_position)
1222
+ return nil unless per_position.all?(Type::Constant)
1223
+
1224
+ first_truthy_index = per_position.index { |type| truthy_constant?(type) }
1225
+ return Type::Combinator.constant_of(nil) if first_truthy_index.nil?
1226
+
1227
+ Type::Combinator.constant_of(first_truthy_index)
1228
+ end
1229
+
1230
+ def truthy_constant?(type)
1231
+ type.is_a?(Type::Constant) && type.value && type.value != false
1232
+ end
1233
+
1234
+ def type_block_body_with_param(block_node, expected_param_types)
1235
+ bindings = BlockParameterBinder.new(expected_param_types: expected_param_types).bind(block_node)
1236
+ block_scope = bindings.reduce(scope) { |acc, (name, type)| acc.with_local(name, type) }
1237
+ type_block_body(block_node, block_scope)
1238
+ rescue StandardError
1239
+ nil
1240
+ end
979
1241
  end
980
1242
  # rubocop:enable Metrics/ClassLength
981
1243
  end
@@ -0,0 +1,322 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../type"
4
+
5
+ module Rigor
6
+ module Inference
7
+ module MethodDispatcher
8
+ # Block-shaped fold dispatch (v0.0.6 phase 1).
9
+ #
10
+ # Sits ahead of `RbsDispatch.try_dispatch` and folds a small
11
+ # set of block-taking Enumerable methods when the inferred
12
+ # block return type is a Ruby-truthy or Ruby-falsey
13
+ # `Type::Constant`. The block-parameter typing for the same
14
+ # methods continues to be answered by `IteratorDispatch`
15
+ # (this module concerns the *return* of the call, not the
16
+ # block-param binding).
17
+ #
18
+ # The methods covered fall in two families:
19
+ #
20
+ # - **Filter-shaped** (`select` / `filter` / `reject` /
21
+ # `take_while` / `drop_while`): the block's truthiness
22
+ # selects the all-or-nothing endpoints — either the
23
+ # receiver's full shape (when every element is kept) or
24
+ # the empty-tuple carrier (when every element is dropped).
25
+ # - **Predicate-shaped** (`all?` / `any?` / `none?`): the
26
+ # block's truthiness combined with the receiver's
27
+ # emptiness collapses the call to a `Constant[bool]` in
28
+ # the cases where Ruby's actual semantics make it
29
+ # unconditional. Non-empty + truthy `any?` is `true`;
30
+ # non-empty + falsey `all?` is `false`; the empty-receiver
31
+ # "vacuous" answers (`[].all? { false } == true`,
32
+ # `[].any? { true } == false`, `[].none? { true } == true`)
33
+ # are likewise honoured.
34
+ #
35
+ # The dispatcher returns `nil` for any case that cannot be
36
+ # decided from the (receiver-shape, method, block-truthiness)
37
+ # tuple — element-wise block re-evaluation against
38
+ # `Constant<Array>` receivers (the `map` / `filter_map` /
39
+ # `flat_map` precision tier) is reserved for a later slice.
40
+ module BlockFolding # rubocop:disable Metrics/ModuleLength
41
+ module_function
42
+
43
+ FILTER_KEEP_ON_TRUTHY = Set[:select, :filter, :take_while].freeze
44
+ FILTER_KEEP_ON_FALSEY = Set[:reject, :drop_while].freeze
45
+
46
+ PREDICATE_METHODS = Set[:all?, :any?, :none?].freeze
47
+
48
+ # Methods whose answer is `nil` when the block always
49
+ # returns Ruby-falsey — `find` / `detect` short-circuit
50
+ # to nil when nothing matches, `find_index` / `index`
51
+ # likewise. These methods only fold on the falsey side
52
+ # for now; the truthy-block side requires per-position
53
+ # analysis (the index of the first kept element, or the
54
+ # element itself, depend on the receiver's shape and on
55
+ # which positions actually evaluate to truthy).
56
+ FALSEY_BLOCK_NIL_METHODS = Set[:find, :detect, :find_index, :index].freeze
57
+
58
+ # Block-taking `count` returns the number of elements
59
+ # for which the block is truthy. With a Constant-falsey
60
+ # block the answer is unconditionally `Constant[0]`;
61
+ # with a Constant-truthy block on a finitely-sized
62
+ # receiver it is `Constant[size]`.
63
+ COUNT_METHOD = :count
64
+
65
+ # @param receiver [Rigor::Type, nil]
66
+ # @param method_name [Symbol]
67
+ # @param args [Array<Rigor::Type>]
68
+ # @param block_type [Rigor::Type, nil] inferred return type of
69
+ # the call's block. `nil` means "no block at the call site"
70
+ # and disqualifies every rule here.
71
+ # @return [Rigor::Type, nil]
72
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
73
+ def try_fold(receiver:, method_name:, args:, block_type:)
74
+ return nil if receiver.nil? || block_type.nil?
75
+
76
+ truthiness = constant_truthiness(block_type)
77
+ return nil if truthiness.nil?
78
+
79
+ if PREDICATE_METHODS.include?(method_name)
80
+ fold_predicate(receiver, method_name, truthiness)
81
+ elsif filter_method?(method_name)
82
+ fold_filter(receiver, method_name, truthiness)
83
+ elsif FALSEY_BLOCK_NIL_METHODS.include?(method_name)
84
+ fold_falsey_nil_short_circuit(method_name, truthiness, args)
85
+ elsif method_name == COUNT_METHOD
86
+ fold_count(receiver, truthiness, args)
87
+ end
88
+ end
89
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
90
+
91
+ def filter_method?(method_name)
92
+ FILTER_KEEP_ON_TRUTHY.include?(method_name) ||
93
+ FILTER_KEEP_ON_FALSEY.include?(method_name)
94
+ end
95
+
96
+ # Maps the block return type to `:truthy`, `:falsey`, or
97
+ # `nil` (inconclusive). Only `Type::Constant` answers
98
+ # decisively — `Union[true, false]`, `Nominal[…]`, or
99
+ # `Dynamic[T]` keep the dispatcher silent so the RBS
100
+ # tier still owns the call.
101
+ def constant_truthiness(block_type)
102
+ return nil unless block_type.is_a?(Type::Constant)
103
+
104
+ block_type.value ? :truthy : :falsey
105
+ end
106
+
107
+ # Filter-shaped methods collapse to either the receiver
108
+ # (every element kept) or the empty tuple (every element
109
+ # dropped). Tuple-shaped receivers widen to
110
+ # `Array[union of elements]` on the all-kept side because
111
+ # we cannot prove WHICH positional subset survives —
112
+ # Tuple's per-position semantics do not carry over to a
113
+ # filtered Array.
114
+ def fold_filter(receiver, method_name, truthiness)
115
+ return nil unless filter_receiver_known?(receiver)
116
+
117
+ keep_all = filter_keeps_all?(method_name, truthiness)
118
+ keep_all ? receiver_as_kept_array(receiver) : Type::Combinator.tuple_of
119
+ end
120
+
121
+ def filter_keeps_all?(method_name, truthiness)
122
+ (FILTER_KEEP_ON_TRUTHY.include?(method_name) && truthiness == :truthy) ||
123
+ (FILTER_KEEP_ON_FALSEY.include?(method_name) && truthiness == :falsey)
124
+ end
125
+
126
+ def receiver_as_kept_array(receiver)
127
+ case receiver
128
+ when Type::Tuple then tuple_to_array(receiver)
129
+ else receiver
130
+ end
131
+ end
132
+
133
+ def tuple_to_array(tuple)
134
+ return Type::Combinator.tuple_of if tuple.elements.empty?
135
+ return Type::Combinator.nominal_of("Array", type_args: [tuple.elements.first]) if tuple.elements.size == 1
136
+
137
+ element = Type::Combinator.union(*tuple.elements)
138
+ Type::Combinator.nominal_of("Array", type_args: [element])
139
+ end
140
+
141
+ # Predicate folds. The decision table mirrors Ruby's
142
+ # actual semantics on `Enumerable#all?` / `#any?` /
143
+ # `#none?` — see the table at the top of the module.
144
+ def fold_predicate(receiver, method_name, truthiness)
145
+ emptiness = receiver_emptiness(receiver)
146
+ decision = predicate_decision(method_name, truthiness, emptiness)
147
+ return nil if decision.nil?
148
+
149
+ case decision
150
+ when :always_true then Type::Combinator.constant_of(true)
151
+ when :always_false then Type::Combinator.constant_of(false)
152
+ when :bool then bool_union
153
+ end
154
+ end
155
+
156
+ # @return [:always_true, :always_false, :bool, nil]
157
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
158
+ def predicate_decision(method_name, truthiness, emptiness)
159
+ case method_name
160
+ when :all?
161
+ return :always_true if truthiness == :truthy
162
+ return :always_true if emptiness == :empty
163
+ return :always_false if emptiness == :non_empty
164
+
165
+ :bool
166
+ when :any?
167
+ return :always_false if truthiness == :falsey
168
+ return :always_true if emptiness == :non_empty
169
+ return :always_false if emptiness == :empty
170
+
171
+ :bool
172
+ when :none?
173
+ return :always_true if truthiness == :falsey
174
+ return :always_false if emptiness == :non_empty
175
+ return :always_true if emptiness == :empty
176
+
177
+ :bool
178
+ end
179
+ end
180
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
181
+
182
+ def bool_union
183
+ Type::Combinator.union(
184
+ Type::Combinator.constant_of(true),
185
+ Type::Combinator.constant_of(false)
186
+ )
187
+ end
188
+
189
+ # @return [:empty, :non_empty, :unknown]
190
+ def receiver_emptiness(receiver)
191
+ case receiver
192
+ when Type::Tuple
193
+ receiver.elements.empty? ? :empty : :non_empty
194
+ when Type::HashShape
195
+ receiver.pairs.empty? ? :empty : :non_empty
196
+ when Type::Constant
197
+ constant_emptiness(receiver.value)
198
+ when Type::Difference
199
+ difference_emptiness(receiver)
200
+ else
201
+ :unknown
202
+ end
203
+ end
204
+
205
+ def constant_emptiness(value)
206
+ # Only `Range` constants reach these folds: `Type::Constant`
207
+ # rejects Array / Hash literals (they become `Tuple` /
208
+ # `HashShape` carriers), and the remaining scalar
209
+ # constants (Integer / Float / Symbol / String / …)
210
+ # are not Enumerable receivers for the filter or
211
+ # predicate methods folded here.
212
+ return range_emptiness(value) if value.is_a?(Range)
213
+
214
+ :unknown
215
+ end
216
+
217
+ def range_emptiness(range)
218
+ beg = range.begin
219
+ en = range.end
220
+ return :unknown unless beg.is_a?(Numeric) && en.is_a?(Numeric)
221
+
222
+ if range.exclude_end?
223
+ beg < en ? :non_empty : :empty
224
+ else
225
+ beg <= en ? :non_empty : :empty
226
+ end
227
+ end
228
+
229
+ # `non-empty-array[T]` is encoded as
230
+ # `Difference[Array[T], Tuple[]]` — the imported built-in
231
+ # carrier for non-emptiness. Recognising it here lets
232
+ # `arr.any? { true }` fold to `Constant[true]` for
233
+ # callers who threaded the non-emptiness through their
234
+ # type signature.
235
+ def difference_emptiness(diff)
236
+ base = diff.base
237
+ removed = diff.removed
238
+ return :unknown unless removed.is_a?(Type::Tuple) && removed.elements.empty?
239
+ return :non_empty if array_or_hash_nominal?(base)
240
+
241
+ :unknown
242
+ end
243
+
244
+ def array_or_hash_nominal?(type)
245
+ type.is_a?(Type::Nominal) && %w[Array Hash Set].include?(type.class_name)
246
+ end
247
+
248
+ # Filter folds need at least a recognised collection
249
+ # carrier; `Top` / `Dynamic` / arbitrary nominals decline
250
+ # so the RBS tier answers (its `Array#select { … } -> Array[T]`
251
+ # projection is correct, just less precise on the empty
252
+ # endpoint).
253
+ def filter_receiver_known?(receiver)
254
+ case receiver
255
+ when Type::Tuple, Type::HashShape, Type::Constant, Type::Difference then true
256
+ when Type::Nominal then %w[Array Hash Set Range].include?(receiver.class_name)
257
+ else false
258
+ end
259
+ end
260
+
261
+ # `find` / `detect` / `find_index` / `index` (block form)
262
+ # short-circuit to nil when the block is provably falsey.
263
+ # `index` and `find_index` also accept a non-block argument
264
+ # form (`arr.index(value)`); we decline whenever the call
265
+ # carries a positional argument so the RBS tier still
266
+ # answers the value-search variant correctly.
267
+ def fold_falsey_nil_short_circuit(_method_name, truthiness, args)
268
+ return nil unless args.empty?
269
+ return nil unless truthiness == :falsey
270
+
271
+ Type::Combinator.constant_of(nil)
272
+ end
273
+
274
+ # `count` with a block returns the count of elements
275
+ # for which the block is truthy. The non-block forms
276
+ # (`count` / `count(value)`) carry positional arguments
277
+ # and are handled by the RBS tier; this fold only fires
278
+ # when the block is the sole source of selection.
279
+ def fold_count(receiver, truthiness, args)
280
+ return nil unless args.empty?
281
+ return Type::Combinator.constant_of(0) if truthiness == :falsey
282
+
283
+ fold_count_truthy(receiver)
284
+ end
285
+
286
+ def fold_count_truthy(receiver)
287
+ size = finite_size(receiver)
288
+ return nil if size.nil?
289
+
290
+ Type::Combinator.constant_of(size)
291
+ end
292
+
293
+ # Returns the receiver's known finite element count, or
294
+ # nil when the carrier does not pin a size. Tuple and
295
+ # HashShape are pinned by construction; `Constant<…>`
296
+ # exposes the literal's `.size`. Other shapes (Array[T],
297
+ # Range[T], Nominal) decline so the RBS tier widens.
298
+ def finite_size(receiver)
299
+ case receiver
300
+ when Type::Tuple then receiver.elements.size
301
+ when Type::HashShape then receiver.pairs.size
302
+ when Type::Constant then constant_size(receiver.value)
303
+ end
304
+ end
305
+
306
+ def constant_size(value)
307
+ # Mirrors `constant_emptiness` — only `Range` produces
308
+ # a meaningful finite size for the methods folded here.
309
+ range_size(value) if value.is_a?(Range)
310
+ end
311
+
312
+ def range_size(range)
313
+ beg = range.begin
314
+ en = range.end
315
+ return nil unless beg.is_a?(Integer) && en.is_a?(Integer)
316
+
317
+ range.exclude_end? ? [en - beg, 0].max : [en - beg + 1, 0].max
318
+ end
319
+ end
320
+ end
321
+ end
322
+ end