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.
- checksums.yaml +4 -4
- data/README.md +215 -134
- data/data/builtins/ruby_core/comparable.yml +87 -0
- data/data/builtins/ruby_core/complex.yml +505 -0
- data/data/builtins/ruby_core/date.yml +1737 -0
- data/data/builtins/ruby_core/enumerable.yml +557 -0
- data/data/builtins/ruby_core/file.yml +9 -0
- data/data/builtins/ruby_core/pathname.yml +1067 -0
- data/data/builtins/ruby_core/rational.yml +365 -0
- data/data/builtins/ruby_core/string.yml +9 -0
- data/data/builtins/ruby_core/time.yml +6 -4
- data/lib/rigor/cli.rb +1 -1
- data/lib/rigor/inference/builtins/comparable_catalog.rb +27 -0
- data/lib/rigor/inference/builtins/complex_catalog.rb +41 -0
- data/lib/rigor/inference/builtins/date_catalog.rb +98 -0
- data/lib/rigor/inference/builtins/enumerable_catalog.rb +27 -0
- data/lib/rigor/inference/builtins/pathname_catalog.rb +35 -0
- data/lib/rigor/inference/builtins/rational_catalog.rb +38 -0
- data/lib/rigor/inference/expression_typer.rb +285 -23
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +322 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +197 -12
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +99 -0
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +95 -0
- data/lib/rigor/inference/method_dispatcher.rb +20 -8
- data/lib/rigor/inference/narrowing.rb +210 -1
- data/lib/rigor/inference/scope_indexer.rb +87 -11
- data/lib/rigor/inference/statement_evaluator.rb +5 -1
- data/lib/rigor/rbs_extended.rb +11 -6
- data/lib/rigor/type/integer_range.rb +4 -2
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/environment.rbs +4 -6
- data/sig/rigor/inference.rbs +2 -1
- data/sig/rigor/type.rbs +41 -41
- 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
|
-
|
|
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
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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
|
|
532
|
-
#
|
|
533
|
-
#
|
|
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
|
-
|
|
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]]`.
|
|
705
|
-
#
|
|
706
|
-
#
|
|
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.
|
|
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
|