rigortype 0.0.5 → 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/data/builtins/ruby_core/pathname.yml +1067 -0
- data/lib/rigor/inference/builtins/pathname_catalog.rb +35 -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 +78 -7
- data/lib/rigor/inference/method_dispatcher.rb +18 -8
- data/lib/rigor/version.rb +1 -1
- metadata +4 -1
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "method_catalog"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module Builtins
|
|
8
|
+
# `Pathname` catalog. Singleton — load once, consult during
|
|
9
|
+
# dispatch.
|
|
10
|
+
#
|
|
11
|
+
# TODO(blocklist curation): read
|
|
12
|
+
# `data/builtins/ruby_core/pathname.yml` and add per-method
|
|
13
|
+
# blocklist entries for any `:leaf` classifications that are
|
|
14
|
+
# actually mutators or otherwise unsafe to fold. Each entry
|
|
15
|
+
# SHOULD carry a one-line comment naming the indirect mutator
|
|
16
|
+
# helper that triggered the false positive (see
|
|
17
|
+
# `string_catalog.rb`, `array_catalog.rb`, `time_catalog.rb`
|
|
18
|
+
# for the canonical shape).
|
|
19
|
+
PATHNAME_CATALOG = MethodCatalog.new(
|
|
20
|
+
path: File.expand_path(
|
|
21
|
+
"../../../../data/builtins/ruby_core/pathname.yml",
|
|
22
|
+
__dir__
|
|
23
|
+
),
|
|
24
|
+
mutating_selectors: {
|
|
25
|
+
"Pathname" => Set[
|
|
26
|
+
# initialize_copy is blocklisted by convention so a
|
|
27
|
+
# hypothetical future `Constant<Pathname>` carrier
|
|
28
|
+
# cannot fold an aliasing copy through the catalog.
|
|
29
|
+
:initialize_copy
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -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
|