rigortype 0.0.1 → 0.0.3
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/array.yml +1470 -0
- data/data/builtins/ruby_core/file.yml +501 -0
- data/data/builtins/ruby_core/io.yml +1594 -0
- data/data/builtins/ruby_core/numeric.yml +1809 -0
- data/data/builtins/ruby_core/string.yml +1850 -0
- data/lib/rigor/analysis/check_rules.rb +297 -5
- data/lib/rigor/analysis/diagnostic.rb +13 -2
- data/lib/rigor/analysis/runner.rb +52 -5
- data/lib/rigor/builtins/imported_refinements.rb +69 -0
- data/lib/rigor/cli/type_of_command.rb +11 -5
- data/lib/rigor/cli/type_scan_command.rb +13 -8
- data/lib/rigor/cli.rb +26 -6
- data/lib/rigor/configuration.rb +18 -2
- data/lib/rigor/environment.rb +3 -1
- data/lib/rigor/inference/acceptance.rb +180 -0
- data/lib/rigor/inference/builtins/array_catalog.rb +46 -0
- data/lib/rigor/inference/builtins/method_catalog.rb +90 -0
- data/lib/rigor/inference/builtins/numeric_catalog.rb +93 -0
- data/lib/rigor/inference/builtins/string_catalog.rb +39 -0
- data/lib/rigor/inference/expression_typer.rb +151 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +650 -16
- data/lib/rigor/inference/method_dispatcher/file_folding.rb +144 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +113 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +4 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +107 -0
- data/lib/rigor/inference/method_dispatcher.rb +28 -21
- data/lib/rigor/inference/narrowing.rb +471 -10
- data/lib/rigor/inference/scope_indexer.rb +66 -0
- data/lib/rigor/inference/statement_evaluator.rb +305 -2
- data/lib/rigor/rbs_extended.rb +174 -14
- data/lib/rigor/scope.rb +44 -5
- data/lib/rigor/type/combinator.rb +69 -1
- data/lib/rigor/type/difference.rb +155 -0
- data/lib/rigor/type/integer_range.rb +137 -0
- data/lib/rigor/type.rb +2 -0
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/inference.rbs +5 -2
- data/sig/rigor/rbs_extended.rbs +25 -1
- data/sig/rigor/scope.rbs +4 -0
- data/sig/rigor/type.rbs +51 -1
- metadata +15 -1
|
@@ -155,6 +155,61 @@ module Rigor
|
|
|
155
155
|
end
|
|
156
156
|
end
|
|
157
157
|
|
|
158
|
+
# Integer-comparison fragment of `type` against an Integer
|
|
159
|
+
# literal `bound`. Narrows the receiver of `x < n`, `x <= n`,
|
|
160
|
+
# `x > n`, `x >= n` (and the reversed forms) to the subset of
|
|
161
|
+
# the existing domain that satisfies the comparison. Hooks in:
|
|
162
|
+
# - `Constant<Integer>` is preserved when it satisfies the
|
|
163
|
+
# comparison, otherwise collapsed to `Bot`.
|
|
164
|
+
# - `IntegerRange[a..b]` becomes the intersection with the
|
|
165
|
+
# half-line implied by the comparison; an empty intersection
|
|
166
|
+
# collapses to `Bot`, a single-point intersection collapses
|
|
167
|
+
# to `Constant<Integer>`.
|
|
168
|
+
# - `Nominal[Integer]` becomes the half-line itself (e.g.
|
|
169
|
+
# `x > 0` on `Nominal[Integer]` is `positive_int`).
|
|
170
|
+
# - `Union` narrows each member independently.
|
|
171
|
+
# - Other carriers (Float, String, Top, Dynamic) flow through
|
|
172
|
+
# unchanged: the analyzer does not have a Float-range carrier
|
|
173
|
+
# today, and no other carrier participates in numeric ordering.
|
|
174
|
+
def narrow_integer_comparison(type, comparator, bound)
|
|
175
|
+
return type unless bound.is_a?(Integer) && %i[< <= > >=].include?(comparator)
|
|
176
|
+
|
|
177
|
+
narrow_integer_comparison_dispatch(type, comparator, bound)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Equality fragment of `type` against an Integer `value`.
|
|
181
|
+
# `Constant<Integer>` is preserved when it equals `value`,
|
|
182
|
+
# otherwise collapses to `Bot`. `IntegerRange` covers? `value`
|
|
183
|
+
# narrows to `Constant[value]`; an out-of-range comparison
|
|
184
|
+
# collapses to `Bot`. `Nominal[Integer]` narrows to
|
|
185
|
+
# `Constant[value]`. `Union` narrows each member.
|
|
186
|
+
def narrow_integer_equal(type, value)
|
|
187
|
+
return type unless value.is_a?(Integer)
|
|
188
|
+
|
|
189
|
+
narrow_integer_equal_dispatch(type, value)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Complement of {.narrow_integer_equal}. Removes a single
|
|
193
|
+
# integer value from the domain when one endpoint of an
|
|
194
|
+
# `IntegerRange` is exactly that value (so the result stays a
|
|
195
|
+
# contiguous range). Domains where the value sits strictly
|
|
196
|
+
# between the endpoints stay unchanged: punching a hole would
|
|
197
|
+
# require a two-piece carrier the lattice does not yet model.
|
|
198
|
+
def narrow_integer_not_equal(type, value)
|
|
199
|
+
return type unless value.is_a?(Integer)
|
|
200
|
+
|
|
201
|
+
case type
|
|
202
|
+
when Type::Constant
|
|
203
|
+
type.value == value ? Type::Combinator.bot : type
|
|
204
|
+
when Type::IntegerRange
|
|
205
|
+
narrow_integer_range_not_equal(type, value)
|
|
206
|
+
when Type::Union
|
|
207
|
+
Type::Combinator.union(*type.members.map { |m| narrow_integer_not_equal(m, value) })
|
|
208
|
+
else
|
|
209
|
+
type
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
158
213
|
# Class-membership fragment of `type`: the subset whose
|
|
159
214
|
# inhabitants are instances of `class_name` (or its subclasses
|
|
160
215
|
# when `exact: false`). `class_name` is the qualified name of
|
|
@@ -424,10 +479,49 @@ module Rigor
|
|
|
424
479
|
# `rigor:v1:predicate-if-true` / `predicate-if-false`
|
|
425
480
|
# annotations, apply them to narrow the corresponding
|
|
426
481
|
# local-variable arguments on each edge.
|
|
427
|
-
analyse_rbs_extended_predicate(node, scope)
|
|
482
|
+
predicate_result = analyse_rbs_extended_predicate(node, scope)
|
|
483
|
+
assert_result = analyse_rbs_extended_assert_if(node, scope)
|
|
484
|
+
merge_extended_results(predicate_result, assert_result, scope)
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
# Combines two `[truthy_scope, falsey_scope]` pair
|
|
488
|
+
# results from sibling RBS::Extended analysers
|
|
489
|
+
# (`predicate-if-*` and `assert-if-*`). When only one
|
|
490
|
+
# side fires, return it directly; when both fire the
|
|
491
|
+
# right side's per-local deltas are applied on top of
|
|
492
|
+
# the left side's edges so the rules compose.
|
|
493
|
+
def merge_extended_results(left, right, base_scope)
|
|
494
|
+
return left if right.nil?
|
|
495
|
+
return right if left.nil?
|
|
496
|
+
|
|
497
|
+
[
|
|
498
|
+
merge_scope_pair(left[0], right[0], base_scope),
|
|
499
|
+
merge_scope_pair(left[1], right[1], base_scope)
|
|
500
|
+
]
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
def merge_scope_pair(left_scope, right_scope, base_scope)
|
|
504
|
+
right_scope.locals.reduce(left_scope) do |acc, (name, type)|
|
|
505
|
+
base_type = base_scope.local(name)
|
|
506
|
+
type.equal?(base_type) ? acc : acc.with_local(name, type)
|
|
507
|
+
end
|
|
428
508
|
end
|
|
429
509
|
|
|
510
|
+
ZERO_CLASS_PREDICATES = %i[positive? negative? zero? nonzero?].freeze
|
|
511
|
+
COMPARISON_OPERATORS = %i[< <= > >=].freeze
|
|
512
|
+
private_constant :ZERO_CLASS_PREDICATES, :COMPARISON_OPERATORS
|
|
513
|
+
|
|
430
514
|
def dispatch_call(node, scope, name)
|
|
515
|
+
return dispatch_call_simple(node, scope, name) if simple_dispatch_name?(name)
|
|
516
|
+
|
|
517
|
+
dispatch_call_numeric(node, scope, name)
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
def simple_dispatch_name?(name)
|
|
521
|
+
%i[nil? ! is_a? kind_of? instance_of? == != ===].include?(name)
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
def dispatch_call_simple(node, scope, name)
|
|
431
525
|
case name
|
|
432
526
|
when :nil?, :! then dispatch_unary_predicate(node, scope, name)
|
|
433
527
|
when :is_a?, :kind_of? then analyse_class_predicate(node, scope, exact: false)
|
|
@@ -437,6 +531,92 @@ module Rigor
|
|
|
437
531
|
end
|
|
438
532
|
end
|
|
439
533
|
|
|
534
|
+
def dispatch_call_numeric(node, scope, name)
|
|
535
|
+
if COMPARISON_OPERATORS.include?(name)
|
|
536
|
+
analyse_comparison_predicate(node, scope, comparator: name)
|
|
537
|
+
elsif ZERO_CLASS_PREDICATES.include?(name)
|
|
538
|
+
analyse_zero_class_predicate(node, scope, predicate: name)
|
|
539
|
+
elsif name == :between?
|
|
540
|
+
analyse_between_predicate(node, scope)
|
|
541
|
+
end
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
# `:positive?` / `:negative?` / `:zero?` / `:nonzero?` are
|
|
545
|
+
# zero-arg predicates on `Numeric`. We model them as
|
|
546
|
+
# comparisons against the literal 0 so the existing range
|
|
547
|
+
# narrowing handles them uniformly.
|
|
548
|
+
ZERO_CLASS_PREDICATE_RULES = {
|
|
549
|
+
positive?: { truthy: [:>, 0], falsey: [:<=, 0] },
|
|
550
|
+
negative?: { truthy: [:<, 0], falsey: [:>=, 0] },
|
|
551
|
+
zero?: { truthy: [:eq, 0], falsey: [:ne, 0] },
|
|
552
|
+
nonzero?: { truthy: [:ne, 0], falsey: [:eq, 0] }
|
|
553
|
+
}.freeze
|
|
554
|
+
private_constant :ZERO_CLASS_PREDICATE_RULES
|
|
555
|
+
|
|
556
|
+
def analyse_zero_class_predicate(node, scope, predicate:)
|
|
557
|
+
return nil unless argument_free?(node)
|
|
558
|
+
return nil unless node.receiver.is_a?(Prism::LocalVariableReadNode)
|
|
559
|
+
|
|
560
|
+
local_name = node.receiver.name
|
|
561
|
+
current = scope.local(local_name)
|
|
562
|
+
return nil if current.nil?
|
|
563
|
+
|
|
564
|
+
rules = ZERO_CLASS_PREDICATE_RULES[predicate]
|
|
565
|
+
truthy = apply_zero_rule(current, rules[:truthy])
|
|
566
|
+
falsey = apply_zero_rule(current, rules[:falsey])
|
|
567
|
+
[scope.with_local(local_name, truthy), scope.with_local(local_name, falsey)]
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
def apply_zero_rule(type, rule)
|
|
571
|
+
case rule[0]
|
|
572
|
+
when :eq then narrow_integer_equal(type, rule[1])
|
|
573
|
+
when :ne then narrow_integer_not_equal(type, rule[1])
|
|
574
|
+
else
|
|
575
|
+
narrow_integer_comparison(type, rule[0], rule[1])
|
|
576
|
+
end
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
# `x.between?(a, b)` truthy edge narrows to
|
|
580
|
+
# `narrow_integer_comparison(>=, a)` ∩
|
|
581
|
+
# `narrow_integer_comparison(<=, b)`. The falsey edge is left
|
|
582
|
+
# unchanged because the complement is a two-piece domain
|
|
583
|
+
# (`x < a || x > b`) that the lattice cannot express
|
|
584
|
+
# precisely. `a` and `b` MUST both be integer literals.
|
|
585
|
+
def analyse_between_predicate(node, scope)
|
|
586
|
+
return nil unless node.receiver.is_a?(Prism::LocalVariableReadNode)
|
|
587
|
+
return nil if node.arguments.nil?
|
|
588
|
+
return nil unless node.arguments.arguments.size == 2
|
|
589
|
+
|
|
590
|
+
low, high = node.arguments.arguments
|
|
591
|
+
return nil unless low.is_a?(Prism::IntegerNode) && high.is_a?(Prism::IntegerNode)
|
|
592
|
+
|
|
593
|
+
local_name = node.receiver.name
|
|
594
|
+
current = scope.local(local_name)
|
|
595
|
+
return nil if current.nil?
|
|
596
|
+
|
|
597
|
+
truthy = narrow_integer_comparison(
|
|
598
|
+
narrow_integer_comparison(current, :>=, low.value),
|
|
599
|
+
:<=, high.value
|
|
600
|
+
)
|
|
601
|
+
[scope.with_local(local_name, truthy), scope]
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
# Helper for {.narrow_integer_not_equal}. Only adjusts when the
|
|
605
|
+
# value sits exactly on one endpoint, so the result stays
|
|
606
|
+
# contiguous; otherwise the input range is preserved.
|
|
607
|
+
def narrow_integer_range_not_equal(range, value)
|
|
608
|
+
return range if range.lower > value || range.upper < value
|
|
609
|
+
return Type::Combinator.bot if single_point_range_equal?(range, value)
|
|
610
|
+
return build_narrowing_integer_range(value + 1, range.upper) if range.lower == value
|
|
611
|
+
return build_narrowing_integer_range(range.lower, value - 1) if range.upper == value
|
|
612
|
+
|
|
613
|
+
range
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
def single_point_range_equal?(range, value)
|
|
617
|
+
range.finite? && range.min == value && range.max == value
|
|
618
|
+
end
|
|
619
|
+
|
|
440
620
|
def dispatch_unary_predicate(node, scope, name)
|
|
441
621
|
return nil unless argument_free?(node)
|
|
442
622
|
|
|
@@ -466,6 +646,126 @@ module Rigor
|
|
|
466
646
|
equality == :== ? [positive, negative] : [negative, positive]
|
|
467
647
|
end
|
|
468
648
|
|
|
649
|
+
# Comparison predicate analyser. Recognised shapes:
|
|
650
|
+
# x < Int x <= Int x > Int x >= Int
|
|
651
|
+
# Int < x Int <= x Int > x Int >= x
|
|
652
|
+
# The reversed (literal-on-left) form is normalised by
|
|
653
|
+
# transposing the operator so the receiver-local always
|
|
654
|
+
# appears on the left of the rule.
|
|
655
|
+
INVERT_COMPARISON_OP = { :< => :>=, :<= => :>, :> => :<=, :>= => :< }.freeze
|
|
656
|
+
REVERSE_COMPARISON_OP = { :< => :>, :<= => :>=, :> => :<, :>= => :<= }.freeze
|
|
657
|
+
private_constant :INVERT_COMPARISON_OP, :REVERSE_COMPARISON_OP
|
|
658
|
+
|
|
659
|
+
def analyse_comparison_predicate(node, scope, comparator:)
|
|
660
|
+
return nil if node.arguments.nil?
|
|
661
|
+
return nil unless node.arguments.arguments.size == 1
|
|
662
|
+
|
|
663
|
+
match = comparison_local_literal(node.receiver, node.arguments.arguments.first, comparator)
|
|
664
|
+
return nil if match.nil?
|
|
665
|
+
|
|
666
|
+
local_name, normalised_op, bound = match
|
|
667
|
+
current = scope.local(local_name)
|
|
668
|
+
return nil if current.nil?
|
|
669
|
+
|
|
670
|
+
truthy = narrow_integer_comparison(current, normalised_op, bound)
|
|
671
|
+
falsey = narrow_integer_comparison(current, INVERT_COMPARISON_OP[normalised_op], bound)
|
|
672
|
+
[scope.with_local(local_name, truthy), scope.with_local(local_name, falsey)]
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
def comparison_local_literal(left, right, comparator)
|
|
676
|
+
if left.is_a?(Prism::LocalVariableReadNode) && right.is_a?(Prism::IntegerNode)
|
|
677
|
+
return [left.name, comparator, right.value]
|
|
678
|
+
end
|
|
679
|
+
return nil unless right.is_a?(Prism::LocalVariableReadNode) && left.is_a?(Prism::IntegerNode)
|
|
680
|
+
|
|
681
|
+
[right.name, REVERSE_COMPARISON_OP[comparator], left.value]
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
def narrow_integer_equal_dispatch(type, value)
|
|
685
|
+
case type
|
|
686
|
+
when Type::Constant then narrow_integer_equal_constant(type, value)
|
|
687
|
+
when Type::IntegerRange then narrow_integer_equal_range(type, value)
|
|
688
|
+
when Type::Nominal then narrow_integer_equal_nominal(type, value)
|
|
689
|
+
when Type::Union
|
|
690
|
+
Type::Combinator.union(*type.members.map { |m| narrow_integer_equal(m, value) })
|
|
691
|
+
else type
|
|
692
|
+
end
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
def narrow_integer_equal_constant(constant, value)
|
|
696
|
+
constant.value == value ? constant : Type::Combinator.bot
|
|
697
|
+
end
|
|
698
|
+
|
|
699
|
+
def narrow_integer_equal_range(range, value)
|
|
700
|
+
range.covers?(value) ? Type::Combinator.constant_of(value) : Type::Combinator.bot
|
|
701
|
+
end
|
|
702
|
+
|
|
703
|
+
def narrow_integer_equal_nominal(nominal, value)
|
|
704
|
+
return nominal unless nominal.class_name == "Integer" && nominal.type_args.empty?
|
|
705
|
+
|
|
706
|
+
Type::Combinator.constant_of(value)
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
def narrow_integer_comparison_dispatch(type, comparator, bound)
|
|
710
|
+
case type
|
|
711
|
+
when Type::Constant
|
|
712
|
+
integer_constant_satisfies?(type.value, comparator, bound) ? type : Type::Combinator.bot
|
|
713
|
+
when Type::IntegerRange
|
|
714
|
+
intersect_integer_range(type, comparator, bound)
|
|
715
|
+
when Type::Nominal
|
|
716
|
+
narrow_integer_comparison_nominal(type, comparator, bound)
|
|
717
|
+
when Type::Union
|
|
718
|
+
Type::Combinator.union(
|
|
719
|
+
*type.members.map { |m| narrow_integer_comparison(m, comparator, bound) }
|
|
720
|
+
)
|
|
721
|
+
else
|
|
722
|
+
type
|
|
723
|
+
end
|
|
724
|
+
end
|
|
725
|
+
|
|
726
|
+
def narrow_integer_comparison_nominal(nominal, comparator, bound)
|
|
727
|
+
return nominal unless nominal.class_name == "Integer" && nominal.type_args.empty?
|
|
728
|
+
|
|
729
|
+
intersect_integer_range(Type::Combinator.universal_int, comparator, bound)
|
|
730
|
+
end
|
|
731
|
+
|
|
732
|
+
def integer_constant_satisfies?(value, comparator, bound)
|
|
733
|
+
return false unless value.is_a?(Integer)
|
|
734
|
+
|
|
735
|
+
case comparator
|
|
736
|
+
when :< then value < bound
|
|
737
|
+
when :<= then value <= bound
|
|
738
|
+
when :> then value > bound
|
|
739
|
+
when :>= then value >= bound
|
|
740
|
+
end
|
|
741
|
+
end
|
|
742
|
+
|
|
743
|
+
def intersect_integer_range(range, comparator, bound)
|
|
744
|
+
new_lower, new_upper = comparison_endpoints(range, comparator, bound)
|
|
745
|
+
return Type::Combinator.bot if new_lower > new_upper
|
|
746
|
+
|
|
747
|
+
build_narrowing_integer_range(new_lower, new_upper)
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
def comparison_endpoints(range, comparator, bound)
|
|
751
|
+
case comparator
|
|
752
|
+
when :< then [range.lower, [range.upper, bound - 1].min]
|
|
753
|
+
when :<= then [range.lower, [range.upper, bound].min]
|
|
754
|
+
when :> then [[range.lower, bound + 1].max, range.upper]
|
|
755
|
+
when :>= then [[range.lower, bound].max, range.upper]
|
|
756
|
+
end
|
|
757
|
+
end
|
|
758
|
+
|
|
759
|
+
def build_narrowing_integer_range(lower, upper)
|
|
760
|
+
min = lower == -Float::INFINITY ? Type::IntegerRange::NEG_INFINITY : Integer(lower)
|
|
761
|
+
max = upper == Float::INFINITY ? Type::IntegerRange::POS_INFINITY : Integer(upper)
|
|
762
|
+
if min.is_a?(Integer) && max.is_a?(Integer) && min == max
|
|
763
|
+
Type::Combinator.constant_of(min)
|
|
764
|
+
else
|
|
765
|
+
Type::Combinator.integer_range(min, max)
|
|
766
|
+
end
|
|
767
|
+
end
|
|
768
|
+
|
|
469
769
|
def equality_local_literal(left, right, scope)
|
|
470
770
|
if left.is_a?(Prism::LocalVariableReadNode)
|
|
471
771
|
literal = static_literal_type(right, scope)
|
|
@@ -669,6 +969,73 @@ module Rigor
|
|
|
669
969
|
[truthy_scope, falsey_scope]
|
|
670
970
|
end
|
|
671
971
|
|
|
972
|
+
# v0.0.2 — `assert-if-true` / `assert-if-false`. Reads
|
|
973
|
+
# the conditional assertion effects off the called
|
|
974
|
+
# method and narrows the matching argument on the
|
|
975
|
+
# corresponding edge. The unconditional `assert`
|
|
976
|
+
# variant is NOT applied here; `StatementEvaluator`
|
|
977
|
+
# applies it directly to the post-call scope.
|
|
978
|
+
def analyse_rbs_extended_assert_if(node, scope)
|
|
979
|
+
method_def = resolve_rbs_extended_method(node, scope)
|
|
980
|
+
return nil if method_def.nil?
|
|
981
|
+
|
|
982
|
+
effects = RbsExtended.read_assert_effects(method_def).reject(&:always?)
|
|
983
|
+
return nil if effects.empty?
|
|
984
|
+
|
|
985
|
+
truthy_scope = scope
|
|
986
|
+
falsey_scope = scope
|
|
987
|
+
effects.each do |effect|
|
|
988
|
+
truthy_scope, falsey_scope =
|
|
989
|
+
apply_assert_if_effect(effect, node, scope, truthy_scope, falsey_scope, method_def)
|
|
990
|
+
end
|
|
991
|
+
[truthy_scope, falsey_scope]
|
|
992
|
+
end
|
|
993
|
+
|
|
994
|
+
# rubocop:disable Metrics/ParameterLists
|
|
995
|
+
def apply_assert_if_effect(effect, call_node, entry_scope, truthy_scope, falsey_scope, method_def)
|
|
996
|
+
target_node = effect_target_node(effect, call_node, method_def)
|
|
997
|
+
return [truthy_scope, falsey_scope] unless target_node.is_a?(Prism::LocalVariableReadNode)
|
|
998
|
+
|
|
999
|
+
local_name = target_node.name
|
|
1000
|
+
current = entry_scope.local(local_name)
|
|
1001
|
+
return [truthy_scope, falsey_scope] if current.nil?
|
|
1002
|
+
|
|
1003
|
+
narrowed = narrow_for_effect(current, effect, entry_scope.environment)
|
|
1004
|
+
if effect.if_truthy_return?
|
|
1005
|
+
[truthy_scope.with_local(local_name, narrowed), falsey_scope]
|
|
1006
|
+
else
|
|
1007
|
+
[truthy_scope, falsey_scope.with_local(local_name, narrowed)]
|
|
1008
|
+
end
|
|
1009
|
+
end
|
|
1010
|
+
# rubocop:enable Metrics/ParameterLists
|
|
1011
|
+
|
|
1012
|
+
# v0.0.2 #3 — resolves an effect's target node. For
|
|
1013
|
+
# `target: <param>` we look up the matching positional
|
|
1014
|
+
# argument; for `target: self` we use the call's
|
|
1015
|
+
# receiver. In both cases the caller still requires a
|
|
1016
|
+
# `Prism::LocalVariableReadNode` for narrowing to
|
|
1017
|
+
# actually fire (the engine's narrowing surface only
|
|
1018
|
+
# rebinds locals).
|
|
1019
|
+
def effect_target_node(effect, call_node, method_def)
|
|
1020
|
+
if effect.target_kind == :self
|
|
1021
|
+
call_node.receiver
|
|
1022
|
+
else
|
|
1023
|
+
lookup_positional_arg(call_node, method_def, effect.target_name)
|
|
1024
|
+
end
|
|
1025
|
+
end
|
|
1026
|
+
|
|
1027
|
+
# v0.0.2 — selects `narrow_class` (positive) or
|
|
1028
|
+
# `narrow_not_class` (negative `~T` form) based on
|
|
1029
|
+
# the effect's `negative?` flag. Shared between
|
|
1030
|
+
# predicate-if-* and assert-if-* application paths.
|
|
1031
|
+
def narrow_for_effect(current, effect, environment)
|
|
1032
|
+
if effect.negative?
|
|
1033
|
+
narrow_not_class(current, effect.class_name, exact: false, environment: environment)
|
|
1034
|
+
else
|
|
1035
|
+
narrow_class(current, effect.class_name, exact: false, environment: environment)
|
|
1036
|
+
end
|
|
1037
|
+
end
|
|
1038
|
+
|
|
672
1039
|
def resolve_rbs_extended_method(node, scope)
|
|
673
1040
|
loader = scope.environment.rbs_loader
|
|
674
1041
|
return nil if loader.nil?
|
|
@@ -709,15 +1076,14 @@ module Rigor
|
|
|
709
1076
|
|
|
710
1077
|
# rubocop:disable Metrics/ParameterLists
|
|
711
1078
|
def apply_predicate_effect(effect, call_node, entry_scope, truthy_scope, falsey_scope, method_def)
|
|
712
|
-
|
|
713
|
-
return [truthy_scope, falsey_scope]
|
|
714
|
-
return [truthy_scope, falsey_scope] unless arg_node.is_a?(Prism::LocalVariableReadNode)
|
|
1079
|
+
target_node = effect_target_node(effect, call_node, method_def)
|
|
1080
|
+
return [truthy_scope, falsey_scope] unless target_node.is_a?(Prism::LocalVariableReadNode)
|
|
715
1081
|
|
|
716
|
-
local_name =
|
|
1082
|
+
local_name = target_node.name
|
|
717
1083
|
current = entry_scope.local(local_name)
|
|
718
1084
|
return [truthy_scope, falsey_scope] if current.nil?
|
|
719
1085
|
|
|
720
|
-
narrowed =
|
|
1086
|
+
narrowed = narrow_for_effect(current, effect, entry_scope.environment)
|
|
721
1087
|
if effect.truthy_only?
|
|
722
1088
|
[truthy_scope.with_local(local_name, narrowed), falsey_scope]
|
|
723
1089
|
else
|
|
@@ -759,10 +1125,11 @@ module Rigor
|
|
|
759
1125
|
fully_narrowable = true
|
|
760
1126
|
|
|
761
1127
|
conditions.each do |condition|
|
|
762
|
-
|
|
763
|
-
if
|
|
764
|
-
truthy_members <<
|
|
765
|
-
falsey_type =
|
|
1128
|
+
applied = apply_case_when_condition(scope, current, condition, falsey_type)
|
|
1129
|
+
if applied
|
|
1130
|
+
truthy_members << applied[:truthy]
|
|
1131
|
+
falsey_type = applied[:falsey]
|
|
1132
|
+
fully_narrowable &&= applied[:fully_narrowable]
|
|
766
1133
|
else
|
|
767
1134
|
fully_narrowable = false
|
|
768
1135
|
end
|
|
@@ -775,6 +1142,100 @@ module Rigor
|
|
|
775
1142
|
]
|
|
776
1143
|
end
|
|
777
1144
|
|
|
1145
|
+
# Per-condition rule. Returns `nil` when the condition shape
|
|
1146
|
+
# is not recognised (caller marks `fully_narrowable = false`),
|
|
1147
|
+
# or `{truthy:, falsey:, fully_narrowable:}` when it is.
|
|
1148
|
+
def apply_case_when_condition(scope, current, condition, falsey_acc)
|
|
1149
|
+
int_range = case_equality_integer_range(condition)
|
|
1150
|
+
return integer_range_when_result(current, int_range, falsey_acc) if int_range && integer_rooted_type?(current)
|
|
1151
|
+
|
|
1152
|
+
int_literal = case_equality_integer_literal(condition)
|
|
1153
|
+
if int_literal && integer_rooted_type?(current)
|
|
1154
|
+
return integer_literal_when_result(current, int_literal, falsey_acc)
|
|
1155
|
+
end
|
|
1156
|
+
|
|
1157
|
+
target = static_class_name(condition) || case_equality_target_class(condition)
|
|
1158
|
+
return class_when_result(scope, current, target, falsey_acc) if target
|
|
1159
|
+
|
|
1160
|
+
nil
|
|
1161
|
+
end
|
|
1162
|
+
|
|
1163
|
+
def case_equality_integer_literal(condition)
|
|
1164
|
+
condition = unwrap_parens(condition)
|
|
1165
|
+
condition.is_a?(Prism::IntegerNode) ? condition.value : nil
|
|
1166
|
+
end
|
|
1167
|
+
|
|
1168
|
+
def integer_literal_when_result(current, value, falsey_acc)
|
|
1169
|
+
# `case n when k` is `k === n` which for Integer is value
|
|
1170
|
+
# equality. The truthy edge collapses the local to
|
|
1171
|
+
# `Constant[k]`; the falsey edge tightens via
|
|
1172
|
+
# `narrow_integer_not_equal` (only effective when k sits
|
|
1173
|
+
# at one endpoint of the current range).
|
|
1174
|
+
{
|
|
1175
|
+
truthy: narrow_integer_equal(current, value),
|
|
1176
|
+
falsey: narrow_integer_not_equal(falsey_acc, value),
|
|
1177
|
+
fully_narrowable: true
|
|
1178
|
+
}
|
|
1179
|
+
end
|
|
1180
|
+
|
|
1181
|
+
def integer_range_when_result(current, range_pair, falsey_acc)
|
|
1182
|
+
low, high = range_pair
|
|
1183
|
+
truthy = narrow_integer_comparison(
|
|
1184
|
+
narrow_integer_comparison(current, :>=, low),
|
|
1185
|
+
:<=, high
|
|
1186
|
+
)
|
|
1187
|
+
# The falsey edge of `n in [a, b]` is two-piece; we cannot
|
|
1188
|
+
# express the complement precisely with a single carrier,
|
|
1189
|
+
# so keep the accumulator unchanged. `fully_narrowable: false`
|
|
1190
|
+
# forces the else-branch to see `current` (the unmodified
|
|
1191
|
+
# entry type), which mirrors `between?` falsey behaviour.
|
|
1192
|
+
{ truthy: truthy, falsey: falsey_acc, fully_narrowable: false }
|
|
1193
|
+
end
|
|
1194
|
+
|
|
1195
|
+
def class_when_result(scope, current, target, falsey_acc)
|
|
1196
|
+
{
|
|
1197
|
+
truthy: narrow_class(current, target, exact: false, environment: scope.environment),
|
|
1198
|
+
falsey: narrow_not_class(falsey_acc, target, exact: false, environment: scope.environment),
|
|
1199
|
+
fully_narrowable: true
|
|
1200
|
+
}
|
|
1201
|
+
end
|
|
1202
|
+
|
|
1203
|
+
# Returns `[low, high]` for a `Prism::RangeNode` whose
|
|
1204
|
+
# endpoints are both `Prism::IntegerNode` literals, with
|
|
1205
|
+
# `..`/`...` exclusivity respected. Open-ended ranges use
|
|
1206
|
+
# the symbolic infinities so the existing comparison
|
|
1207
|
+
# narrowing tier handles them. Returns `nil` for any other
|
|
1208
|
+
# shape (Float endpoints, String endpoints, dynamic
|
|
1209
|
+
# expressions).
|
|
1210
|
+
def case_equality_integer_range(condition)
|
|
1211
|
+
condition = unwrap_parens(condition)
|
|
1212
|
+
return nil unless condition.is_a?(Prism::RangeNode)
|
|
1213
|
+
|
|
1214
|
+
low = integer_range_endpoint(condition.left, default: Type::IntegerRange::NEG_INFINITY)
|
|
1215
|
+
high = integer_range_endpoint(condition.right, default: Type::IntegerRange::POS_INFINITY)
|
|
1216
|
+
return nil if low.nil? || high.nil?
|
|
1217
|
+
|
|
1218
|
+
high -= 1 if condition.exclude_end? && high.is_a?(Integer)
|
|
1219
|
+
[low, high]
|
|
1220
|
+
end
|
|
1221
|
+
|
|
1222
|
+
def integer_range_endpoint(node, default:)
|
|
1223
|
+
return default if node.nil?
|
|
1224
|
+
return node.value if node.is_a?(Prism::IntegerNode)
|
|
1225
|
+
|
|
1226
|
+
nil
|
|
1227
|
+
end
|
|
1228
|
+
|
|
1229
|
+
def integer_rooted_type?(type)
|
|
1230
|
+
case type
|
|
1231
|
+
when Type::Constant then type.value.is_a?(Integer)
|
|
1232
|
+
when Type::IntegerRange then true
|
|
1233
|
+
when Type::Nominal then type.class_name == "Integer" && type.type_args.empty?
|
|
1234
|
+
when Type::Union then type.members.all? { |m| integer_rooted_type?(m) }
|
|
1235
|
+
else false
|
|
1236
|
+
end
|
|
1237
|
+
end
|
|
1238
|
+
|
|
778
1239
|
def range_target_class(range_node)
|
|
779
1240
|
left = range_node.left
|
|
780
1241
|
right = range_node.right
|
|
@@ -98,6 +98,13 @@ module Rigor
|
|
|
98
98
|
discovered_methods = build_discovered_methods(root)
|
|
99
99
|
seeded_scope = seeded_scope.with_discovered_methods(discovered_methods)
|
|
100
100
|
|
|
101
|
+
# v0.0.2 #5 — also record the def node itself for
|
|
102
|
+
# instance methods so the engine can re-type the body
|
|
103
|
+
# when a call site dispatches against a user-defined
|
|
104
|
+
# method without an RBS sig.
|
|
105
|
+
discovered_def_nodes = build_discovered_def_nodes(root)
|
|
106
|
+
seeded_scope = seeded_scope.with_discovered_def_nodes(discovered_def_nodes)
|
|
107
|
+
|
|
101
108
|
table = {}.compare_by_identity
|
|
102
109
|
table.default = seeded_scope
|
|
103
110
|
|
|
@@ -369,6 +376,65 @@ module Rigor
|
|
|
369
376
|
accumulator[class_name][def_node.name] = kind
|
|
370
377
|
end
|
|
371
378
|
|
|
379
|
+
# v0.0.2 #5 — instance-side def-node recording. Walks
|
|
380
|
+
# class bodies the same way as `build_discovered_methods`
|
|
381
|
+
# but records the actual `Prism::DefNode` for each
|
|
382
|
+
# **instance** method so `ExpressionTyper` can re-type
|
|
383
|
+
# the body at the call site for inter-procedural return
|
|
384
|
+
# inference. Singleton methods and `define_method` calls
|
|
385
|
+
# are intentionally skipped: the inference path needs a
|
|
386
|
+
# statically introspectable body, and singleton dispatch
|
|
387
|
+
# has its own complications (Class / Module ancestry)
|
|
388
|
+
# the first-iteration rule does not yet model.
|
|
389
|
+
def build_discovered_def_nodes(root)
|
|
390
|
+
accumulator = {}
|
|
391
|
+
walk_def_nodes(root, [], false, accumulator)
|
|
392
|
+
accumulator.transform_values(&:freeze).freeze
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def walk_def_nodes(node, qualified_prefix, in_singleton_class, accumulator) # rubocop:disable Metrics/CyclomaticComplexity
|
|
396
|
+
return unless node.is_a?(Prism::Node)
|
|
397
|
+
|
|
398
|
+
case node
|
|
399
|
+
when Prism::ClassNode, Prism::ModuleNode
|
|
400
|
+
name = qualified_name_for(node.constant_path)
|
|
401
|
+
if name
|
|
402
|
+
child_prefix = qualified_prefix + [name]
|
|
403
|
+
walk_def_nodes(node.body, child_prefix, false, accumulator) if node.body
|
|
404
|
+
return
|
|
405
|
+
end
|
|
406
|
+
when Prism::SingletonClassNode
|
|
407
|
+
if node.expression.is_a?(Prism::SelfNode) && node.body
|
|
408
|
+
walk_def_nodes(node.body, qualified_prefix, true, accumulator)
|
|
409
|
+
return
|
|
410
|
+
end
|
|
411
|
+
when Prism::DefNode
|
|
412
|
+
record_def_node(node, qualified_prefix, in_singleton_class, accumulator)
|
|
413
|
+
return
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
node.compact_child_nodes.each do |child|
|
|
417
|
+
walk_def_nodes(child, qualified_prefix, in_singleton_class, accumulator)
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
# v0.0.3 A — sentinel key under which `record_def_node`
|
|
422
|
+
# files DefNodes that live outside any class / module
|
|
423
|
+
# body (top-level helpers, `def`s nested inside DSL
|
|
424
|
+
# blocks like `RSpec.describe ... do; def helper; end`).
|
|
425
|
+
# Looked up by `Scope#top_level_def_for` to give
|
|
426
|
+
# implicit-self calls priority over RBS dispatch when
|
|
427
|
+
# the file defines a same-named local method.
|
|
428
|
+
TOP_LEVEL_DEF_KEY = "<toplevel>"
|
|
429
|
+
|
|
430
|
+
def record_def_node(def_node, qualified_prefix, in_singleton_class, accumulator)
|
|
431
|
+
return if def_node.receiver.is_a?(Prism::SelfNode) || in_singleton_class
|
|
432
|
+
|
|
433
|
+
class_name = qualified_prefix.empty? ? TOP_LEVEL_DEF_KEY : qualified_prefix.join("::")
|
|
434
|
+
accumulator[class_name] ||= {}
|
|
435
|
+
accumulator[class_name][def_node.name] = def_node
|
|
436
|
+
end
|
|
437
|
+
|
|
372
438
|
def record_define_method(call_node, qualified_prefix, in_singleton_class, accumulator)
|
|
373
439
|
return if qualified_prefix.empty?
|
|
374
440
|
return if call_node.arguments.nil? || call_node.arguments.arguments.empty?
|