rigortype 0.0.5 → 0.0.7

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.
@@ -52,6 +52,12 @@ module Rigor
52
52
  # Literals
53
53
  Prism::IntegerNode => :type_of_literal_value,
54
54
  Prism::FloatNode => :type_of_literal_value,
55
+ # `1i` / `2.5ri` lift via `node.value` which is already a
56
+ # `Complex` Ruby value; same for `1r` / `1.5r` whose
57
+ # value is a `Rational`. `Type::Constant` accepts both
58
+ # via `SCALAR_CLASSES`.
59
+ Prism::ImaginaryNode => :type_of_literal_value,
60
+ Prism::RationalNode => :type_of_literal_value,
55
61
  Prism::SymbolNode => :symbol_type_for,
56
62
  Prism::StringNode => :string_type_for,
57
63
  Prism::TrueNode => :type_of_true,
@@ -401,7 +407,13 @@ module Rigor
401
407
  # so callers stay backward compatible.
402
408
  def type_of_hash(node)
403
409
  elements = node.respond_to?(:elements) ? node.elements : []
404
- return Type::Combinator.nominal_of(Hash) if elements.empty?
410
+ # v0.0.7 `{}` resolves to the empty `HashShape{}` carrier
411
+ # rather than `Nominal[Hash]`, mirroring the v0.0.6 empty-
412
+ # array literal change. Both forms erase to plain `Hash`,
413
+ # but `HashShape{}` pins the literal's known size (zero)
414
+ # so HashShape projections (`empty?`, `first`, `count`,
415
+ # …) fold against it.
416
+ return Type::Combinator.hash_shape_of({}) if elements.empty?
405
417
 
406
418
  shape = static_hash_shape_for(elements)
407
419
  return shape if shape
@@ -500,39 +512,99 @@ module Rigor
500
512
  # rebinding is the StatementEvaluator's job (Slice 3 phase 2).
501
513
  # Without an else clause the branch's implicit value is nil, which
502
514
  # is included in the union.
515
+ #
516
+ # v0.0.6 — when the predicate folds to a `Type::Constant` whose
517
+ # value is Ruby-truthy (resp. Ruby-falsey), the unreachable
518
+ # branch is elided so the if-expression's type is the live
519
+ # branch alone. Statement-level branch elision lives in
520
+ # `StatementEvaluator#eval_if`; this handler covers the
521
+ # expression-position ternary form (`a ? b : c`) and any
522
+ # `if`/`unless` reached through `type_of`.
503
523
  def type_of_if(node)
504
524
  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)
525
+ else_type = if_else_type(node.subsequent)
526
+ elide_or_union(node.predicate, then_type, else_type)
512
527
  end
513
528
 
514
529
  # `unless c; t; else; e; end`. Prism uses `else_clause` here (no
515
- # `elsif` chain).
530
+ # `elsif` chain). Branch-elision logic mirrors `type_of_if`,
531
+ # inverted: a truthy predicate selects the else branch.
516
532
  def type_of_unless(node)
517
533
  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)
534
+ else_type = if_else_type(node.else_clause)
535
+ elide_or_union(node.predicate, else_type, then_type)
536
+ end
537
+
538
+ def if_else_type(subsequent)
539
+ return Type::Combinator.constant_of(nil) if subsequent.nil?
540
+
541
+ type_of(subsequent)
542
+ end
543
+
544
+ # Routes the predicate's typed value through branch elision.
545
+ # `live_when_truthy` and `live_when_falsey` are the branch
546
+ # types selected by the predicate's polarity; the names
547
+ # match `IfNode` semantics directly and invert at the
548
+ # `type_of_unless` call site.
549
+ def elide_or_union(predicate, live_when_truthy, live_when_falsey)
550
+ case constant_predicate_polarity(predicate)
551
+ when :truthy then live_when_truthy
552
+ when :falsey then live_when_falsey
553
+ else Type::Combinator.union(live_when_truthy, live_when_falsey)
554
+ end
555
+ end
556
+
557
+ # Returns `:truthy`, `:falsey`, or `nil` for an arbitrary
558
+ # predicate expression. Only `Type::Constant` answers
559
+ # decisively — `Union[true, false]`, `Nominal[bool]`, and
560
+ # `Dynamic[T]` keep both branches live.
561
+ def constant_predicate_polarity(predicate)
562
+ return nil if predicate.nil?
563
+
564
+ type = type_of(predicate)
565
+ return nil unless type.is_a?(Type::Constant)
566
+
567
+ type.value ? :truthy : :falsey
525
568
  end
526
569
 
527
570
  def type_of_else(node)
528
571
  statements_or_nil(node.statements)
529
572
  end
530
573
 
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.
574
+ # `a && b` and `a || b` short-circuit at the value level:
575
+ # `a && b` returns `a` when `a` is falsey, else `b`.
576
+ # `a || b` returns `a` when `a` is truthy, else `b`.
577
+ #
578
+ # v0.0.6 — when the left operand folds to a `Type::Constant`,
579
+ # we know which side actually flows through, so the result
580
+ # is one operand's type instead of a union. Otherwise the
581
+ # union-of-both-operands fallback is preserved.
534
582
  def type_of_and_or(node)
535
- Type::Combinator.union(type_of(node.left), type_of(node.right))
583
+ left_type = type_of(node.left)
584
+ polarity = constant_value_polarity(left_type)
585
+ return short_circuit_for(node, left_type, polarity) if polarity
586
+
587
+ Type::Combinator.union(left_type, type_of(node.right))
588
+ end
589
+
590
+ def short_circuit_for(node, left_type, polarity)
591
+ and_node = node.is_a?(Prism::AndNode)
592
+ if polarity == :truthy
593
+ and_node ? type_of(node.right) : left_type
594
+ else
595
+ and_node ? left_type : type_of(node.right)
596
+ end
597
+ end
598
+
599
+ # Returns `:truthy` / `:falsey` for a `Type::Constant`,
600
+ # nil otherwise. Mirrors `constant_predicate_polarity` but
601
+ # operates on a typed value (already-type-of'd) rather
602
+ # than a Prism node, so the same predicate analysis can
603
+ # be reused in both contexts.
604
+ def constant_value_polarity(type)
605
+ return nil unless type.is_a?(Type::Constant)
606
+
607
+ type.value ? :truthy : :falsey
536
608
  end
537
609
 
538
610
  def type_of_case(node)
@@ -631,7 +703,18 @@ module Rigor
631
703
  Type::Combinator.constant_of(Range.new(left, right, node.exclude_end?))
632
704
  end
633
705
 
634
- def type_of_regexp(_node)
706
+ # v0.0.7 — non-interpolated regex literals lift to
707
+ # `Constant<Regexp>` so `Constant<String>#scan(/regex/)`
708
+ # / `#match(/regex/)` etc. can fold through the catalog
709
+ # tier. Interpolated regexes (`/foo#{x}/`) reach the
710
+ # second `Prism::InterpolatedRegularExpressionNode` arm
711
+ # which keeps the conservative `Nominal[Regexp]` answer.
712
+ def type_of_regexp(node)
713
+ return Type::Combinator.nominal_of(Regexp) unless node.is_a?(Prism::RegularExpressionNode)
714
+
715
+ regex = Regexp.new(node.unescaped, node.options)
716
+ Type::Combinator.constant_of(regex)
717
+ rescue StandardError
635
718
  Type::Combinator.nominal_of(Regexp)
636
719
  end
637
720
 
@@ -701,12 +784,17 @@ module Rigor
701
784
  # when every element is a non-splat value. Splatted entries
702
785
  # (`[*xs, 1]`) preserve the Slice 4 phase 2d behavior: we union
703
786
  # 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).
787
+ # `Nominal[Array, [union]]`.
788
+ #
789
+ # v0.0.6 — the empty literal `[]` resolves to the empty
790
+ # `Tuple[]` carrier rather than the raw `Nominal[Array]`.
791
+ # Both carriers erase to RBS `Array`, but `Tuple[]` pins
792
+ # the literal's known arity (zero), which lets the
793
+ # per-element block fold concatenate across all-empty
794
+ # positions like `[1, 2].flat_map { |_| [] }`.
707
795
  def array_type_for(node)
708
796
  elements = node.elements
709
- return Type::Combinator.nominal_of(Array) if elements.empty?
797
+ return Type::Combinator.tuple_of if elements.empty?
710
798
 
711
799
  if elements.any?(Prism::SplatNode)
712
800
  element_types = elements.map { |e| type_of(e) }
@@ -739,6 +827,7 @@ module Rigor
739
827
  # for the CallNode itself (the inner type_of calls already record
740
828
  # their own fallbacks for unrecognised receivers/args, so the tracer
741
829
  # captures both the immediate dispatch miss and the deeper cause).
830
+ # rubocop:disable Metrics/CyclomaticComplexity
742
831
  def call_type_for(node)
743
832
  receiver = call_receiver_type_for(node)
744
833
  arg_types = call_arg_types(node)
@@ -771,6 +860,18 @@ module Rigor
771
860
  return dynamic_top
772
861
  end
773
862
 
863
+ # v0.0.6 phase 2 — per-element block fold for Tuple
864
+ # receivers. When `[a, b, c].map { |x| f(x) }` and the
865
+ # receiver is a `Tuple` carrier with finite elements,
866
+ # type the block body once per position with the
867
+ # corresponding element bound to the block parameter
868
+ # and assemble the results into a `Tuple[U_1..U_n]`.
869
+ # This sits ahead of `MethodDispatcher.dispatch` so
870
+ # the RBS tier does not re-widen the answer back to
871
+ # `Array[union]`.
872
+ per_element = try_per_element_block_fold(node, receiver)
873
+ return per_element if per_element
874
+
774
875
  result = MethodDispatcher.dispatch(
775
876
  receiver_type: receiver,
776
877
  method_name: node.name,
@@ -797,6 +898,7 @@ module Rigor
797
898
 
798
899
  fallback_for(node, family: :prism)
799
900
  end
901
+ # rubocop:enable Metrics/CyclomaticComplexity
800
902
 
801
903
  # v0.0.2 #5 — re-types the body of a user-defined
802
904
  # instance method with the call site's argument types
@@ -976,6 +1078,189 @@ module Rigor
976
1078
 
977
1079
  block_scope.type_of(body)
978
1080
  end
1081
+
1082
+ # v0.0.6 phase 2 — per-element block fold for Tuple
1083
+ # receivers under `:map` / `:collect`. Walks every Tuple
1084
+ # position, binds the block parameter to that element's
1085
+ # type, and re-types the block body. The per-position
1086
+ # results are assembled into `Tuple[U_1..U_n]`, strictly
1087
+ # tighter than the RBS-projected `Array[union]`.
1088
+ #
1089
+ # Declines (returns nil) when the receiver is not a
1090
+ # `Tuple` with at least one element, when the call has
1091
+ # no `Prism::BlockNode`, when the method is outside the
1092
+ # supported set, when block typing raises mid-loop, or
1093
+ # when the block has no body. The decline path leaves
1094
+ # the dispatch chain untouched.
1095
+ PER_ELEMENT_TUPLE_METHODS = Set[
1096
+ :map, :collect, :filter_map, :flat_map,
1097
+ :find, :detect, :find_index, :index
1098
+ ].freeze
1099
+ private_constant :PER_ELEMENT_TUPLE_METHODS
1100
+
1101
+ # Cardinality cap for per-element block fold over
1102
+ # finite-bound `Constant<Range>` receivers. Walking
1103
+ # `(1..1_000_000).map { … }` element-wise would balloon
1104
+ # block-typing cost and explode the resulting Tuple, so
1105
+ # only short ranges expand into per-position folds.
1106
+ # Larger ranges decline so the RBS tier widens.
1107
+ PER_ELEMENT_RANGE_LIMIT = 8
1108
+ private_constant :PER_ELEMENT_RANGE_LIMIT
1109
+
1110
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
1111
+ def try_per_element_block_fold(call_node, receiver_type)
1112
+ return nil unless PER_ELEMENT_TUPLE_METHODS.include?(call_node.name)
1113
+ return nil if find_family_with_args?(call_node)
1114
+
1115
+ element_types = per_element_elements_of(receiver_type)
1116
+ return nil if element_types.nil? || element_types.empty?
1117
+
1118
+ block_node = call_node.block
1119
+ return nil unless block_node.is_a?(Prism::BlockNode)
1120
+
1121
+ per_position = element_types.map do |element_type|
1122
+ type_block_body_with_param(block_node, [element_type])
1123
+ end
1124
+ return nil if per_position.any?(&:nil?)
1125
+
1126
+ assemble_per_element_result(call_node.name, per_position, element_types)
1127
+ end
1128
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
1129
+
1130
+ # Returns the per-position element types for a finite,
1131
+ # statically-known receiver shape — or nil when the
1132
+ # receiver does not pin a finite element list.
1133
+ #
1134
+ # `Tuple[A, B, …]` → [A, B, …]
1135
+ # `Constant<a..b>` → [Constant[a], …, Constant[b]]
1136
+ # everything else → nil
1137
+ #
1138
+ # Note: `Type::IntegerRange` is the bounded-Integer
1139
+ # carrier (`int<a, b>` represents "an Integer between
1140
+ # a and b"), not a Range value. Calls like `.map` /
1141
+ # `.find` on an `IntegerRange` receiver would resolve
1142
+ # to `Integer#map` / `Integer#find` — neither exists —
1143
+ # so IntegerRange does NOT participate in this fold.
1144
+ def per_element_elements_of(receiver_type)
1145
+ case receiver_type
1146
+ when Type::Tuple then receiver_type.elements
1147
+ when Type::Constant then constant_range_elements(receiver_type.value)
1148
+ end
1149
+ end
1150
+
1151
+ # rubocop:disable Metrics/CyclomaticComplexity
1152
+ def constant_range_elements(value)
1153
+ return nil unless value.is_a?(Range)
1154
+ return nil unless value.begin.is_a?(Integer) && value.end.is_a?(Integer)
1155
+
1156
+ cardinality = value.exclude_end? ? value.end - value.begin : value.end - value.begin + 1
1157
+ return nil if cardinality <= 0 || cardinality > PER_ELEMENT_RANGE_LIMIT
1158
+
1159
+ value.to_a.map { |v| Type::Combinator.constant_of(v) }
1160
+ end
1161
+ # rubocop:enable Metrics/CyclomaticComplexity
1162
+
1163
+ # `index(value)` and `find_index(value)` carry a positional
1164
+ # argument and search by `==` rather than running the block.
1165
+ # Decline so the RBS tier owns those forms.
1166
+ def find_family_with_args?(call_node)
1167
+ return false unless %i[find_index index].include?(call_node.name)
1168
+
1169
+ args = call_node.arguments
1170
+ !args.nil? && !args.arguments.empty?
1171
+ end
1172
+
1173
+ def assemble_per_element_result(method_name, per_position, element_types)
1174
+ case method_name
1175
+ when :map, :collect then Type::Combinator.tuple_of(*per_position)
1176
+ when :filter_map then assemble_filter_map_result(per_position)
1177
+ when :flat_map then assemble_flat_map_result(per_position)
1178
+ when :find, :detect then assemble_find_result(per_position, element_types)
1179
+ when :find_index, :index then assemble_find_index_result(per_position)
1180
+ end
1181
+ end
1182
+
1183
+ # `filter_map` folds tightly only when every per-position
1184
+ # result is a `Constant`: positions whose value is `nil`
1185
+ # or `false` drop, the rest survive in declaration order.
1186
+ # When any position is non-Constant the dispatcher
1187
+ # declines (returns nil) so the RBS tier widens to
1188
+ # `Array[U]`.
1189
+ def assemble_filter_map_result(per_position)
1190
+ return nil unless per_position.all?(Type::Constant)
1191
+
1192
+ kept = per_position.reject { |type| type.value.nil? || type.value == false }
1193
+ Type::Combinator.tuple_of(*kept)
1194
+ end
1195
+
1196
+ # `flat_map` flattens a single level: if the per-position
1197
+ # result is a `Tuple`, its elements are concatenated; if
1198
+ # it's a non-Array scalar carrier (`Constant<…>` over a
1199
+ # non-Array literal) it contributes one element. We fold
1200
+ # tightly only when every per-position result is one of
1201
+ # those two recognisable shapes — `Nominal[Array[T]]`,
1202
+ # `Union[…]`, and other opaque carriers decline so the
1203
+ # RBS tier widens to `Array[U]`.
1204
+ #
1205
+ # `Type::Constant` only ever holds non-Array scalars (the
1206
+ # carrier rejects Array literals), so a single `Constant`
1207
+ # safely contributes itself as a single Tuple element.
1208
+ def assemble_flat_map_result(per_position)
1209
+ flattened = per_position.flat_map { |type| flat_map_contribution(type) }
1210
+ return nil if flattened.nil? || flattened.any?(&:nil?)
1211
+
1212
+ Type::Combinator.tuple_of(*flattened)
1213
+ end
1214
+
1215
+ def flat_map_contribution(type)
1216
+ case type
1217
+ when Type::Tuple then type.elements
1218
+ when Type::Constant then [type]
1219
+ else [nil]
1220
+ end
1221
+ end
1222
+
1223
+ # `find` / `detect`: returns the first receiver element
1224
+ # whose block result is Ruby-truthy, or `nil` when no
1225
+ # position folds to truthy.
1226
+ #
1227
+ # Folds tightly only when every per-position block result
1228
+ # is a `Type::Constant` — otherwise we cannot decide which
1229
+ # position (if any) is "the first matching one". When the
1230
+ # first decisive truthy position is found, the answer is
1231
+ # the corresponding receiver element. When every position
1232
+ # folds to falsey, the answer is `Constant[nil]`.
1233
+ def assemble_find_result(per_position, element_types)
1234
+ return nil unless per_position.all?(Type::Constant)
1235
+
1236
+ first_truthy_index = per_position.index { |type| truthy_constant?(type) }
1237
+ return Type::Combinator.constant_of(nil) if first_truthy_index.nil?
1238
+
1239
+ element_types[first_truthy_index]
1240
+ end
1241
+
1242
+ # `find_index` / `index`: returns the index of the first
1243
+ # truthy position, or `Constant[nil]` when nothing matches.
1244
+ def assemble_find_index_result(per_position)
1245
+ return nil unless per_position.all?(Type::Constant)
1246
+
1247
+ first_truthy_index = per_position.index { |type| truthy_constant?(type) }
1248
+ return Type::Combinator.constant_of(nil) if first_truthy_index.nil?
1249
+
1250
+ Type::Combinator.constant_of(first_truthy_index)
1251
+ end
1252
+
1253
+ def truthy_constant?(type)
1254
+ type.is_a?(Type::Constant) && type.value && type.value != false
1255
+ end
1256
+
1257
+ def type_block_body_with_param(block_node, expected_param_types)
1258
+ bindings = BlockParameterBinder.new(expected_param_types: expected_param_types).bind(block_node)
1259
+ block_scope = bindings.reduce(scope) { |acc, (name, type)| acc.with_local(name, type) }
1260
+ type_block_body(block_node, block_scope)
1261
+ rescue StandardError
1262
+ nil
1263
+ end
979
1264
  end
980
1265
  # rubocop:enable Metrics/ClassLength
981
1266
  end