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.
- checksums.yaml +4 -4
- data/data/builtins/ruby_core/pathname.yml +1067 -0
- data/lib/rigor/analysis/check_rules.rb +38 -41
- data/lib/rigor/builtins/imported_refinements.rb +93 -3
- data/lib/rigor/inference/builtins/pathname_catalog.rb +35 -0
- data/lib/rigor/inference/expression_typer.rb +310 -25
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +322 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +325 -8
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +45 -4
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +4 -9
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +409 -0
- data/lib/rigor/inference/method_dispatcher.rb +88 -18
- data/lib/rigor/inference/method_parameter_binder.rb +3 -5
- data/lib/rigor/inference/narrowing.rb +38 -6
- data/lib/rigor/inference/statement_evaluator.rb +5 -7
- data/lib/rigor/reflection.rb +203 -0
- data/lib/rigor/type/combinator.rb +244 -1
- data/lib/rigor/type/constant.rb +2 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +1 -0
- data/sig/rigor/reflection.rbs +17 -0
- data/sig/rigor/type.rbs +5 -0
- metadata +6 -1
|
@@ -72,6 +72,19 @@ module Rigor
|
|
|
72
72
|
size: :tuple_size,
|
|
73
73
|
length: :tuple_size,
|
|
74
74
|
count: :tuple_size,
|
|
75
|
+
empty?: :tuple_empty?,
|
|
76
|
+
any?: :tuple_any?,
|
|
77
|
+
all?: :tuple_all?,
|
|
78
|
+
none?: :tuple_none?,
|
|
79
|
+
include?: :tuple_include?,
|
|
80
|
+
sum: :tuple_sum,
|
|
81
|
+
min: :tuple_min,
|
|
82
|
+
max: :tuple_max,
|
|
83
|
+
sort: :tuple_sort,
|
|
84
|
+
reverse: :tuple_reverse,
|
|
85
|
+
to_a: :tuple_to_a,
|
|
86
|
+
to_h: :tuple_to_h,
|
|
87
|
+
zip: :tuple_zip,
|
|
75
88
|
:[] => :tuple_index,
|
|
76
89
|
fetch: :tuple_index,
|
|
77
90
|
dig: :tuple_dig
|
|
@@ -80,6 +93,18 @@ module Rigor
|
|
|
80
93
|
HASH_SHAPE_HANDLERS = {
|
|
81
94
|
size: :hash_size,
|
|
82
95
|
length: :hash_size,
|
|
96
|
+
count: :hash_size,
|
|
97
|
+
empty?: :hash_empty?,
|
|
98
|
+
any?: :hash_any?,
|
|
99
|
+
keys: :hash_keys,
|
|
100
|
+
values: :hash_values,
|
|
101
|
+
first: :hash_first,
|
|
102
|
+
flatten: :hash_flatten,
|
|
103
|
+
compact: :hash_compact,
|
|
104
|
+
to_a: :hash_to_a,
|
|
105
|
+
to_h: :hash_to_h,
|
|
106
|
+
invert: :hash_invert,
|
|
107
|
+
merge: :hash_merge,
|
|
83
108
|
:[] => :hash_lookup,
|
|
84
109
|
fetch: :hash_lookup,
|
|
85
110
|
dig: :hash_dig,
|
|
@@ -369,6 +394,239 @@ module Rigor
|
|
|
369
394
|
Type::Combinator.constant_of(tuple.elements.size)
|
|
370
395
|
end
|
|
371
396
|
|
|
397
|
+
# `tuple.empty?` — folds to a precise bool from the
|
|
398
|
+
# tuple's known arity.
|
|
399
|
+
# rubocop:disable Style/ReturnNilInPredicateMethodDefinition
|
|
400
|
+
def tuple_empty?(tuple, _method_name, args)
|
|
401
|
+
return nil unless args.empty?
|
|
402
|
+
|
|
403
|
+
Type::Combinator.constant_of(tuple.elements.empty?)
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
# `tuple.any?` (no-arg, no-block) — empty tuple → false,
|
|
407
|
+
# non-empty → true. The block / arg forms flow through
|
|
408
|
+
# `BlockFolding` and the RBS tier.
|
|
409
|
+
def tuple_any?(tuple, _method_name, args)
|
|
410
|
+
return nil unless args.empty?
|
|
411
|
+
|
|
412
|
+
Type::Combinator.constant_of(!tuple.elements.empty?)
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
# `tuple.all?` (no-arg, no-block) — true for empty
|
|
416
|
+
# tuple (vacuous truth) AND for non-empty tuples whose
|
|
417
|
+
# every element is provably truthy. Mixed / unknown
|
|
418
|
+
# element truthiness declines so the RBS / BlockFolding
|
|
419
|
+
# tiers can still answer.
|
|
420
|
+
def tuple_all?(tuple, _method_name, args)
|
|
421
|
+
return nil unless args.empty?
|
|
422
|
+
return Type::Combinator.constant_of(true) if tuple.elements.empty?
|
|
423
|
+
|
|
424
|
+
decision = tuple_predicate_truthiness(tuple, all: true)
|
|
425
|
+
return nil if decision.nil?
|
|
426
|
+
|
|
427
|
+
Type::Combinator.constant_of(decision)
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
# `tuple.none?` (no-arg, no-block) — true when every
|
|
431
|
+
# element is provably falsey, false when any element is
|
|
432
|
+
# provably truthy. Empty tuple folds to true (vacuous).
|
|
433
|
+
def tuple_none?(tuple, _method_name, args)
|
|
434
|
+
return nil unless args.empty?
|
|
435
|
+
return Type::Combinator.constant_of(true) if tuple.elements.empty?
|
|
436
|
+
|
|
437
|
+
decision = tuple_predicate_truthiness(tuple, all: false)
|
|
438
|
+
return nil if decision.nil?
|
|
439
|
+
|
|
440
|
+
Type::Combinator.constant_of(decision)
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
# `tuple.include?(needle)` — folds to a precise bool when
|
|
444
|
+
# the needle is a `Constant` and the tuple's elements
|
|
445
|
+
# are all `Constant` (so disjointness is checkable).
|
|
446
|
+
# If any element matches the needle's value the answer
|
|
447
|
+
# is `Constant[true]`; if every element is a Constant
|
|
448
|
+
# whose value is structurally distinct from the needle
|
|
449
|
+
# the answer is `Constant[false]`.
|
|
450
|
+
def tuple_include?(tuple, _method_name, args)
|
|
451
|
+
return nil unless args.size == 1
|
|
452
|
+
|
|
453
|
+
needle = args.first
|
|
454
|
+
return nil unless needle.is_a?(Type::Constant)
|
|
455
|
+
return Type::Combinator.constant_of(false) if tuple.elements.empty?
|
|
456
|
+
|
|
457
|
+
return Type::Combinator.constant_of(true) if any_element_matches?(tuple.elements, needle.value)
|
|
458
|
+
return Type::Combinator.constant_of(false) if tuple.elements.all?(Type::Constant)
|
|
459
|
+
|
|
460
|
+
nil
|
|
461
|
+
end
|
|
462
|
+
# rubocop:enable Style/ReturnNilInPredicateMethodDefinition
|
|
463
|
+
|
|
464
|
+
# `tuple.sum` — when every element is a numeric Constant,
|
|
465
|
+
# fold to `Constant[sum]`. Mixed / non-numeric elements
|
|
466
|
+
# decline so RBS widens.
|
|
467
|
+
def tuple_sum(tuple, _method_name, args)
|
|
468
|
+
return nil unless args.empty?
|
|
469
|
+
return Type::Combinator.constant_of(0) if tuple.elements.empty?
|
|
470
|
+
|
|
471
|
+
values = constant_numeric_values(tuple.elements)
|
|
472
|
+
return nil if values.nil?
|
|
473
|
+
|
|
474
|
+
Type::Combinator.constant_of(values.sum)
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
# `tuple.min` / `tuple.max` — fold when every element is
|
|
478
|
+
# a `Constant` whose values share a Ruby-comparable
|
|
479
|
+
# domain. Empty tuples fold to `Constant[nil]`.
|
|
480
|
+
def tuple_min(tuple, _method_name, args)
|
|
481
|
+
tuple_minmax(tuple, args, :min)
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
def tuple_max(tuple, _method_name, args)
|
|
485
|
+
tuple_minmax(tuple, args, :max)
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
def tuple_minmax(tuple, args, edge)
|
|
489
|
+
return nil unless args.empty?
|
|
490
|
+
return Type::Combinator.constant_of(nil) if tuple.elements.empty?
|
|
491
|
+
|
|
492
|
+
values = constant_values(tuple.elements)
|
|
493
|
+
return nil if values.nil?
|
|
494
|
+
|
|
495
|
+
result = values.public_send(edge)
|
|
496
|
+
Type::Combinator.constant_of(result)
|
|
497
|
+
rescue StandardError
|
|
498
|
+
nil
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
# `tuple.sort` — every element must be a `Constant` and
|
|
502
|
+
# the values must Ruby-compare. The result is a Tuple
|
|
503
|
+
# with the same elements in sorted order. Comparison
|
|
504
|
+
# failures (mixed-class incomparable values) decline.
|
|
505
|
+
def tuple_sort(tuple, _method_name, args)
|
|
506
|
+
return nil unless args.empty?
|
|
507
|
+
return tuple if tuple.elements.size <= 1
|
|
508
|
+
|
|
509
|
+
values = constant_values(tuple.elements)
|
|
510
|
+
return nil if values.nil?
|
|
511
|
+
|
|
512
|
+
sorted = values.sort
|
|
513
|
+
Type::Combinator.tuple_of(*sorted.map { |v| Type::Combinator.constant_of(v) })
|
|
514
|
+
rescue StandardError
|
|
515
|
+
nil
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
# `tuple.reverse` — independent of element shape; a
|
|
519
|
+
# tuple-precise reversed Tuple.
|
|
520
|
+
def tuple_reverse(tuple, _method_name, args)
|
|
521
|
+
return nil unless args.empty?
|
|
522
|
+
|
|
523
|
+
Type::Combinator.tuple_of(*tuple.elements.reverse)
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
# `tuple.to_a` — Tuple is structurally identical to its
|
|
527
|
+
# to_a (Ruby returns the receiver itself for an Array).
|
|
528
|
+
def tuple_to_a(tuple, _method_name, args)
|
|
529
|
+
return nil unless args.empty?
|
|
530
|
+
|
|
531
|
+
tuple
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
# `tuple.zip(other_1, other_2, …)` — pairs the receiver's
|
|
535
|
+
# per-position elements with the per-position elements of
|
|
536
|
+
# each other Tuple-shaped argument. The result is a Tuple
|
|
537
|
+
# of Tuples whose arity matches the receiver: position
|
|
538
|
+
# `i` is `Tuple[receiver[i], other_1[i], other_2[i], …]`.
|
|
539
|
+
# Out-of-range positions in any `other_k` contribute
|
|
540
|
+
# `Constant[nil]` (matching Ruby's runtime semantics).
|
|
541
|
+
# Declines when any `other_k` is not a Tuple, since the
|
|
542
|
+
# arity is then unknown and the result would be
|
|
543
|
+
# `Array[Array[…]]` — RBS already gives that answer.
|
|
544
|
+
def tuple_zip(tuple, _method_name, args)
|
|
545
|
+
return nil if args.empty? || args.size > MAX_ZIP_ARITY
|
|
546
|
+
return nil unless args.all?(Type::Tuple)
|
|
547
|
+
|
|
548
|
+
zipped = tuple.elements.each_with_index.map do |elem, i|
|
|
549
|
+
positions = [elem] + args.map { |other| other.elements[i] || Type::Combinator.constant_of(nil) }
|
|
550
|
+
Type::Combinator.tuple_of(*positions)
|
|
551
|
+
end
|
|
552
|
+
Type::Combinator.tuple_of(*zipped)
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
MAX_ZIP_ARITY = 8
|
|
556
|
+
private_constant :MAX_ZIP_ARITY
|
|
557
|
+
|
|
558
|
+
# `tuple.to_h` — folds when every Tuple element is itself
|
|
559
|
+
# a 2-element Tuple whose first element is a `Constant`
|
|
560
|
+
# (so it can serve as a Hash key). Produces a closed
|
|
561
|
+
# `HashShape` whose entries mirror the per-position
|
|
562
|
+
# pairs. Empty Tuples fold to the empty HashShape.
|
|
563
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
|
564
|
+
def tuple_to_h(tuple, _method_name, args)
|
|
565
|
+
return nil unless args.empty?
|
|
566
|
+
return Type::Combinator.hash_shape_of({}) if tuple.elements.empty?
|
|
567
|
+
|
|
568
|
+
pairs = tuple.elements.map { |e| tuple_to_h_pair(e) }
|
|
569
|
+
return nil if pairs.any?(&:nil?)
|
|
570
|
+
return nil unless pairs.map(&:first).uniq.size == pairs.size
|
|
571
|
+
|
|
572
|
+
Type::Combinator.hash_shape_of(pairs.to_h)
|
|
573
|
+
end
|
|
574
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
|
575
|
+
|
|
576
|
+
def tuple_to_h_pair(element)
|
|
577
|
+
return nil unless element.is_a?(Type::Tuple)
|
|
578
|
+
return nil unless element.elements.size == 2
|
|
579
|
+
|
|
580
|
+
key = element.elements[0]
|
|
581
|
+
value = element.elements[1]
|
|
582
|
+
return nil unless key.is_a?(Type::Constant)
|
|
583
|
+
|
|
584
|
+
[key.value, value]
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
# Returns `true` / `false` if every element's truthiness
|
|
588
|
+
# agrees, nil for mixed-or-unknown shapes. `all: true`
|
|
589
|
+
# checks every element is truthy; `all: false` checks
|
|
590
|
+
# every element is falsey.
|
|
591
|
+
def tuple_predicate_truthiness(tuple, all:)
|
|
592
|
+
samples = tuple.elements.map { |e| element_truthiness(e) }
|
|
593
|
+
return nil if samples.any?(:unknown)
|
|
594
|
+
|
|
595
|
+
if all
|
|
596
|
+
samples.all?(:truthy)
|
|
597
|
+
else
|
|
598
|
+
samples.all?(:falsey)
|
|
599
|
+
end
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
def element_truthiness(type)
|
|
603
|
+
return :unknown unless type.is_a?(Type::Constant)
|
|
604
|
+
|
|
605
|
+
falsey = type.value.nil? || type.value == false
|
|
606
|
+
falsey ? :falsey : :truthy
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
def any_element_matches?(elements, value)
|
|
610
|
+
elements.any? { |e| e.is_a?(Type::Constant) && e.value == value }
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
# Per-element Constant value extraction. Returns nil
|
|
614
|
+
# when any element is non-Constant, so the caller can
|
|
615
|
+
# decline.
|
|
616
|
+
def constant_values(elements)
|
|
617
|
+
return nil unless elements.all?(Type::Constant)
|
|
618
|
+
|
|
619
|
+
elements.map(&:value)
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
def constant_numeric_values(elements)
|
|
623
|
+
values = constant_values(elements)
|
|
624
|
+
return nil if values.nil?
|
|
625
|
+
return nil unless values.all?(Numeric)
|
|
626
|
+
|
|
627
|
+
values
|
|
628
|
+
end
|
|
629
|
+
|
|
372
630
|
# `tuple[i]`, `tuple[range]`, `tuple[start, length]`, and
|
|
373
631
|
# `tuple.fetch(i)` for static arguments. Out-of-range single
|
|
374
632
|
# indices still fall through because the same handler serves
|
|
@@ -463,6 +721,157 @@ module Rigor
|
|
|
463
721
|
Type::Combinator.constant_of(shape.pairs.size)
|
|
464
722
|
end
|
|
465
723
|
|
|
724
|
+
# `shape.empty?` — folds to a precise bool when the
|
|
725
|
+
# shape's emptiness is statically known. Closed shapes
|
|
726
|
+
# with no optional keys have a fixed size, so empty?
|
|
727
|
+
# is `Constant[shape.pairs.empty?]`. The handler returns
|
|
728
|
+
# `Type::t | nil` (nil signals "no rule, defer to next
|
|
729
|
+
# tier") so the standard predicate-return rubocop rule
|
|
730
|
+
# does not apply.
|
|
731
|
+
# rubocop:disable Style/ReturnNilInPredicateMethodDefinition
|
|
732
|
+
def hash_empty?(shape, _method_name, args)
|
|
733
|
+
return nil unless args.empty?
|
|
734
|
+
return nil unless shape.closed?
|
|
735
|
+
return nil unless shape.optional_keys.empty?
|
|
736
|
+
|
|
737
|
+
Type::Combinator.constant_of(shape.pairs.empty?)
|
|
738
|
+
end
|
|
739
|
+
|
|
740
|
+
# `shape.any?` (no block, no arg) — opposite of
|
|
741
|
+
# `empty?`. The block / arg forms are answered by the
|
|
742
|
+
# RBS / BlockFolding tier.
|
|
743
|
+
def hash_any?(shape, _method_name, args)
|
|
744
|
+
return nil unless args.empty?
|
|
745
|
+
return nil unless shape.closed?
|
|
746
|
+
return nil unless shape.optional_keys.empty?
|
|
747
|
+
|
|
748
|
+
Type::Combinator.constant_of(!shape.pairs.empty?)
|
|
749
|
+
end
|
|
750
|
+
# rubocop:enable Style/ReturnNilInPredicateMethodDefinition
|
|
751
|
+
|
|
752
|
+
# `shape.keys` — returns a `Tuple[Constant<k>…]` for a
|
|
753
|
+
# closed shape with no optional keys; the Tuple's
|
|
754
|
+
# arity matches the shape's per-key declaration order
|
|
755
|
+
# so downstream `tuple[i]` projections stay precise.
|
|
756
|
+
def hash_keys(shape, _method_name, args)
|
|
757
|
+
return nil unless args.empty?
|
|
758
|
+
return nil unless shape.closed?
|
|
759
|
+
return nil unless shape.optional_keys.empty?
|
|
760
|
+
|
|
761
|
+
Type::Combinator.tuple_of(*shape.pairs.keys.map { |k| Type::Combinator.constant_of(k) })
|
|
762
|
+
end
|
|
763
|
+
|
|
764
|
+
# `shape.values` — returns a `Tuple[V_1, …, V_n]` for a
|
|
765
|
+
# closed shape with no optional keys, the Tuple's arity
|
|
766
|
+
# matching the shape's per-key value order.
|
|
767
|
+
def hash_values(shape, _method_name, args)
|
|
768
|
+
return nil unless args.empty?
|
|
769
|
+
return nil unless shape.closed?
|
|
770
|
+
return nil unless shape.optional_keys.empty?
|
|
771
|
+
|
|
772
|
+
Type::Combinator.tuple_of(*shape.pairs.values)
|
|
773
|
+
end
|
|
774
|
+
|
|
775
|
+
# `shape.to_a` — returns a per-entry `Tuple[Tuple[K, V], …]`
|
|
776
|
+
# for a closed shape with no optional keys.
|
|
777
|
+
def hash_to_a(shape, _method_name, args)
|
|
778
|
+
return nil unless args.empty?
|
|
779
|
+
return nil unless shape.closed?
|
|
780
|
+
return nil unless shape.optional_keys.empty?
|
|
781
|
+
|
|
782
|
+
entries = shape.pairs.map do |k, v|
|
|
783
|
+
Type::Combinator.tuple_of(Type::Combinator.constant_of(k), v)
|
|
784
|
+
end
|
|
785
|
+
Type::Combinator.tuple_of(*entries)
|
|
786
|
+
end
|
|
787
|
+
|
|
788
|
+
# `shape.to_h` — Hash is structurally identical to its
|
|
789
|
+
# to_h (Ruby returns the receiver itself for a Hash).
|
|
790
|
+
def hash_to_h(shape, _method_name, args)
|
|
791
|
+
return nil unless args.empty?
|
|
792
|
+
|
|
793
|
+
shape
|
|
794
|
+
end
|
|
795
|
+
|
|
796
|
+
# `shape.invert` — swaps keys and values. Folds when
|
|
797
|
+
# every value is a `Constant` whose value is a Symbol
|
|
798
|
+
# or String (the only hashable types that
|
|
799
|
+
# `HashShape` accepts as keys). Duplicate values would
|
|
800
|
+
# alias under inversion, so Rigor declines on
|
|
801
|
+
# collisions rather than silently dropping entries.
|
|
802
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
803
|
+
def hash_invert(shape, _method_name, args)
|
|
804
|
+
return nil unless args.empty?
|
|
805
|
+
return nil unless shape.closed?
|
|
806
|
+
return nil unless shape.optional_keys.empty?
|
|
807
|
+
return nil unless shape.pairs.values.all?(Type::Constant)
|
|
808
|
+
return nil unless shape.pairs.values.all? { |v| v.value.is_a?(Symbol) || v.value.is_a?(String) }
|
|
809
|
+
|
|
810
|
+
inverted = shape.pairs.each_with_object({}) do |(k, v), acc|
|
|
811
|
+
return nil if acc.key?(v.value)
|
|
812
|
+
|
|
813
|
+
acc[v.value] = Type::Combinator.constant_of(k)
|
|
814
|
+
end
|
|
815
|
+
Type::Combinator.hash_shape_of(inverted)
|
|
816
|
+
end
|
|
817
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
818
|
+
|
|
819
|
+
# `shape.first` — returns the first `[k, v]` pair as a
|
|
820
|
+
# 2-Tuple, or `Constant[nil]` when the shape is empty.
|
|
821
|
+
# Folds only on closed shapes with no optional keys
|
|
822
|
+
# (open shapes might contribute extra keys at runtime).
|
|
823
|
+
def hash_first(shape, _method_name, args)
|
|
824
|
+
return nil unless args.empty?
|
|
825
|
+
return nil unless shape.closed?
|
|
826
|
+
return nil unless shape.optional_keys.empty?
|
|
827
|
+
return Type::Combinator.constant_of(nil) if shape.pairs.empty?
|
|
828
|
+
|
|
829
|
+
key, value = shape.pairs.first
|
|
830
|
+
Type::Combinator.tuple_of(Type::Combinator.constant_of(key), value)
|
|
831
|
+
end
|
|
832
|
+
|
|
833
|
+
# `shape.flatten` — flattens to `[k_1, v_1, k_2, v_2, …]`
|
|
834
|
+
# at depth 1. Closed shapes only; element order matches
|
|
835
|
+
# the per-key declaration order.
|
|
836
|
+
def hash_flatten(shape, _method_name, args)
|
|
837
|
+
return nil unless args.empty?
|
|
838
|
+
return nil unless shape.closed?
|
|
839
|
+
return nil unless shape.optional_keys.empty?
|
|
840
|
+
|
|
841
|
+
elements = shape.pairs.flat_map { |k, v| [Type::Combinator.constant_of(k), v] }
|
|
842
|
+
Type::Combinator.tuple_of(*elements)
|
|
843
|
+
end
|
|
844
|
+
|
|
845
|
+
# `shape.compact` — drops every entry whose value is
|
|
846
|
+
# `Constant[nil]`. Folds only when every value is a
|
|
847
|
+
# `Constant` (so the drop set is decidable). Mixed-shape
|
|
848
|
+
# values decline so the RBS tier widens.
|
|
849
|
+
def hash_compact(shape, _method_name, args)
|
|
850
|
+
return nil unless args.empty?
|
|
851
|
+
return nil unless shape.closed?
|
|
852
|
+
return nil unless shape.optional_keys.empty?
|
|
853
|
+
return nil unless shape.pairs.values.all?(Type::Constant)
|
|
854
|
+
|
|
855
|
+
kept = shape.pairs.reject { |_k, v| v.value.nil? }
|
|
856
|
+
Type::Combinator.hash_shape_of(kept)
|
|
857
|
+
end
|
|
858
|
+
|
|
859
|
+
# `shape.merge(other)` — when both sides are closed
|
|
860
|
+
# HashShape with no optional keys, fold to the merged
|
|
861
|
+
# HashShape. Right-hand entries override left-hand
|
|
862
|
+
# entries on key collision (matching Ruby's runtime
|
|
863
|
+
# `Hash#merge`).
|
|
864
|
+
def hash_merge(shape, _method_name, args)
|
|
865
|
+
return nil unless args.size == 1
|
|
866
|
+
return nil unless shape.closed? && shape.optional_keys.empty?
|
|
867
|
+
|
|
868
|
+
other = args.first
|
|
869
|
+
return nil unless other.is_a?(Type::HashShape)
|
|
870
|
+
return nil unless other.closed? && other.optional_keys.empty?
|
|
871
|
+
|
|
872
|
+
Type::Combinator.hash_shape_of(shape.pairs.merge(other.pairs))
|
|
873
|
+
end
|
|
874
|
+
|
|
466
875
|
# `shape[k]` and `shape.fetch(k)` for a static symbol/string
|
|
467
876
|
# key. Missing-key resolution depends on the method:
|
|
468
877
|
#
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../reflection"
|
|
3
4
|
require_relative "../type"
|
|
4
5
|
require_relative "method_dispatcher/constant_folding"
|
|
5
6
|
require_relative "method_dispatcher/shape_dispatch"
|
|
6
7
|
require_relative "method_dispatcher/rbs_dispatch"
|
|
7
8
|
require_relative "method_dispatcher/iterator_dispatch"
|
|
9
|
+
require_relative "method_dispatcher/block_folding"
|
|
8
10
|
require_relative "method_dispatcher/file_folding"
|
|
9
11
|
require_relative "method_dispatcher/kernel_dispatch"
|
|
10
12
|
|
|
@@ -40,7 +42,7 @@ module Rigor
|
|
|
40
42
|
# The dispatcher's public signature reserves space for `block_type:`
|
|
41
43
|
# and ADR-2 plugin extensions (later slices), so call sites added
|
|
42
44
|
# now do not have to be rewritten when those tiers arrive.
|
|
43
|
-
module MethodDispatcher
|
|
45
|
+
module MethodDispatcher # rubocop:disable Metrics/ModuleLength
|
|
44
46
|
module_function
|
|
45
47
|
|
|
46
48
|
# @param receiver_type [Rigor::Type, nil] type of the receiver expression, or
|
|
@@ -59,7 +61,7 @@ module Rigor
|
|
|
59
61
|
def dispatch(receiver_type:, method_name:, arg_types:, block_type: nil, environment: nil)
|
|
60
62
|
return nil if receiver_type.nil?
|
|
61
63
|
|
|
62
|
-
precise = dispatch_precise_tiers(receiver_type, method_name, arg_types)
|
|
64
|
+
precise = dispatch_precise_tiers(receiver_type, method_name, arg_types, block_type)
|
|
63
65
|
return precise if precise
|
|
64
66
|
|
|
65
67
|
rbs_result = RbsDispatch.try_dispatch(
|
|
@@ -83,19 +85,28 @@ module Rigor
|
|
|
83
85
|
end
|
|
84
86
|
|
|
85
87
|
# Runs the precision tiers (constant fold, shape dispatch,
|
|
86
|
-
# file-path fold) in order and returns the first
|
|
87
|
-
# answer. Each tier owns its own receiver/argument
|
|
88
|
-
# checks; a tier that does not recognise the receiver
|
|
89
|
-
# nil so the next tier can try. The RBS tier sits
|
|
90
|
-
# chain and is invoked by the outer `dispatch`
|
|
91
|
-
|
|
92
|
-
|
|
88
|
+
# file-path fold, block fold) in order and returns the first
|
|
89
|
+
# non-nil answer. Each tier owns its own receiver/argument
|
|
90
|
+
# shape checks; a tier that does not recognise the receiver
|
|
91
|
+
# returns nil so the next tier can try. The RBS tier sits
|
|
92
|
+
# below this chain and is invoked by the outer `dispatch`
|
|
93
|
+
# method.
|
|
94
|
+
#
|
|
95
|
+
# `BlockFolding` runs last among the precision tiers because
|
|
96
|
+
# its rules apply only to block-taking calls, so the cheaper
|
|
97
|
+
# arity-based fold tiers above it filter out the common
|
|
98
|
+
# cases first. When `block_type` is nil the tier is a no-op.
|
|
99
|
+
def dispatch_precise_tiers(receiver_type, method_name, arg_types, block_type = nil)
|
|
100
|
+
meta_result = try_meta_introspection(receiver_type, method_name, arg_types)
|
|
93
101
|
return meta_result if meta_result
|
|
94
102
|
|
|
95
103
|
ConstantFolding.try_fold(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
96
104
|
ShapeDispatch.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
97
105
|
FileFolding.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
98
|
-
KernelDispatch.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types)
|
|
106
|
+
KernelDispatch.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
107
|
+
BlockFolding.try_fold(
|
|
108
|
+
receiver: receiver_type, method_name: method_name, args: arg_types, block_type: block_type
|
|
109
|
+
)
|
|
99
110
|
end
|
|
100
111
|
|
|
101
112
|
def try_user_class_fallback(receiver_type, method_name, arg_types, environment, block_type)
|
|
@@ -114,16 +125,13 @@ module Rigor
|
|
|
114
125
|
end
|
|
115
126
|
|
|
116
127
|
def user_class_fallback_receiver(receiver_type, environment)
|
|
117
|
-
loader = environment.rbs_loader
|
|
118
|
-
return nil if loader.nil?
|
|
119
|
-
|
|
120
128
|
case receiver_type
|
|
121
129
|
when Type::Nominal
|
|
122
|
-
return nil if
|
|
130
|
+
return nil if Rigor::Reflection.rbs_class_known?(receiver_type.class_name, environment: environment)
|
|
123
131
|
|
|
124
132
|
environment.nominal_for_name("Object")
|
|
125
133
|
when Type::Singleton
|
|
126
|
-
return nil if
|
|
134
|
+
return nil if Rigor::Reflection.rbs_class_known?(receiver_type.class_name, environment: environment)
|
|
127
135
|
|
|
128
136
|
environment.singleton_for_name("Class")
|
|
129
137
|
end
|
|
@@ -141,10 +149,10 @@ module Rigor
|
|
|
141
149
|
# adjacent calls and the trivial `instance_of?(self)`
|
|
142
150
|
# later as the rule catalogue grows; for now only `class`
|
|
143
151
|
# is handled.
|
|
144
|
-
def try_meta_introspection(receiver_type, method_name)
|
|
152
|
+
def try_meta_introspection(receiver_type, method_name, arg_types = [])
|
|
145
153
|
case method_name
|
|
146
154
|
when :class then meta_class(receiver_type)
|
|
147
|
-
when :new then meta_new(receiver_type)
|
|
155
|
+
when :new then meta_new(receiver_type, arg_types)
|
|
148
156
|
end
|
|
149
157
|
end
|
|
150
158
|
|
|
@@ -161,12 +169,74 @@ module Rigor
|
|
|
161
169
|
# plumbing for user classes, so a discovered-class
|
|
162
170
|
# `ScanAccumulator.new` types as `Nominal[ScanAccumulator]`
|
|
163
171
|
# rather than `Class`.
|
|
164
|
-
|
|
172
|
+
#
|
|
173
|
+
# v0.0.7 — for the curated set of immutable scalar-shaped
|
|
174
|
+
# classes that `Type::Constant::SCALAR_CLASSES` accepts
|
|
175
|
+
# (today: `Pathname`), `.new(Constant<…>)` lifts to a
|
|
176
|
+
# `Constant<…>` carrier so downstream method calls fold
|
|
177
|
+
# through the standard catalog tier.
|
|
178
|
+
def meta_new(receiver_type, arg_types = [])
|
|
165
179
|
return nil unless receiver_type.is_a?(Type::Singleton)
|
|
166
180
|
|
|
181
|
+
constant_lift = constant_constructor_lift(receiver_type.class_name, arg_types)
|
|
182
|
+
return constant_lift if constant_lift
|
|
183
|
+
|
|
184
|
+
array_lift = array_new_lift(receiver_type.class_name, arg_types)
|
|
185
|
+
return array_lift if array_lift
|
|
186
|
+
|
|
167
187
|
Type::Combinator.nominal_of(receiver_type.class_name)
|
|
168
188
|
end
|
|
169
189
|
|
|
190
|
+
CONSTANT_CONSTRUCTORS = {
|
|
191
|
+
"Pathname" => ->(arg) { Pathname.new(arg) }
|
|
192
|
+
}.freeze
|
|
193
|
+
private_constant :CONSTANT_CONSTRUCTORS
|
|
194
|
+
|
|
195
|
+
def constant_constructor_lift(class_name, arg_types)
|
|
196
|
+
builder = CONSTANT_CONSTRUCTORS[class_name]
|
|
197
|
+
return nil if builder.nil?
|
|
198
|
+
return nil unless arg_types.size == 1
|
|
199
|
+
|
|
200
|
+
arg = arg_types.first
|
|
201
|
+
return nil unless arg.is_a?(Type::Constant) && arg.value.is_a?(String)
|
|
202
|
+
|
|
203
|
+
result = builder.call(arg.value)
|
|
204
|
+
Type::Combinator.constant_of(result)
|
|
205
|
+
rescue StandardError
|
|
206
|
+
nil
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# `Array.new(n, value)` and `Array.new(n)` (no value, default
|
|
210
|
+
# `nil`) lift to a per-position `Tuple[…]` when `n` is a
|
|
211
|
+
# small `Constant<Integer>`. Cap at `ARRAY_NEW_TUPLE_LIMIT`
|
|
212
|
+
# (16) so a `Array.new(1_000_000)` does not balloon the
|
|
213
|
+
# carrier; oversize calls fall back to `Nominal[Array]`.
|
|
214
|
+
ARRAY_NEW_TUPLE_LIMIT = 16
|
|
215
|
+
private_constant :ARRAY_NEW_TUPLE_LIMIT
|
|
216
|
+
|
|
217
|
+
def array_new_lift(class_name, arg_types)
|
|
218
|
+
return nil unless class_name == "Array"
|
|
219
|
+
return nil if arg_types.empty? || arg_types.size > 2
|
|
220
|
+
|
|
221
|
+
size = array_new_size(arg_types.first)
|
|
222
|
+
return nil if size.nil? || size.negative? || size > ARRAY_NEW_TUPLE_LIMIT
|
|
223
|
+
|
|
224
|
+
fill = array_new_fill(arg_types[1])
|
|
225
|
+
Type::Combinator.tuple_of(*Array.new(size, fill))
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def array_new_size(type)
|
|
229
|
+
return nil unless type.is_a?(Type::Constant) && type.value.is_a?(Integer)
|
|
230
|
+
|
|
231
|
+
type.value
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def array_new_fill(type)
|
|
235
|
+
return Type::Combinator.constant_of(nil) if type.nil?
|
|
236
|
+
|
|
237
|
+
type
|
|
238
|
+
end
|
|
239
|
+
|
|
170
240
|
CONSTANT_METACLASSES = {
|
|
171
241
|
Integer => "Integer", Float => "Float", String => "String",
|
|
172
242
|
Symbol => "Symbol", Range => "Range",
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
|
+
require_relative "../reflection"
|
|
5
6
|
require_relative "../type"
|
|
6
7
|
require_relative "../rbs_extended"
|
|
7
8
|
require_relative "rbs_type_translator"
|
|
@@ -136,18 +137,15 @@ module Rigor
|
|
|
136
137
|
def lookup_rbs_method(def_node)
|
|
137
138
|
return nil if @class_path.nil?
|
|
138
139
|
|
|
139
|
-
loader = @environment.rbs_loader
|
|
140
|
-
return nil if loader.nil?
|
|
141
|
-
|
|
142
140
|
method_name = def_node.name
|
|
143
141
|
# `def self.foo` always means a singleton method on the
|
|
144
142
|
# immediate enclosing class. `def foo` inside `class << self`
|
|
145
143
|
# is also a singleton method (the StatementEvaluator threads
|
|
146
144
|
# the `singleton:` flag through this case).
|
|
147
145
|
if def_node.receiver.is_a?(Prism::SelfNode) || @singleton
|
|
148
|
-
|
|
146
|
+
Rigor::Reflection.singleton_method_definition(@class_path, method_name, environment: @environment)
|
|
149
147
|
else
|
|
150
|
-
|
|
148
|
+
Rigor::Reflection.instance_method_definition(@class_path, method_name, environment: @environment)
|
|
151
149
|
end
|
|
152
150
|
end
|
|
153
151
|
|