rigortype 0.0.6 → 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.
@@ -30,18 +30,59 @@ module Rigor
30
30
  module KernelDispatch
31
31
  module_function
32
32
 
33
+ # `Kernel#Rational` / `Kernel#Complex` constructor folds.
34
+ # When every argument is a `Type::Constant` whose value is
35
+ # numeric, we can run the actual Ruby constructor and lift
36
+ # the result into a `Constant<Rational>` / `Constant<Complex>`.
37
+ # The factory accepts the same shapes as Ruby:
38
+ # `Rational(a)`, `Rational(a, b)`, `Complex(a)`, `Complex(a, b)`.
39
+ NUMERIC_CONSTRUCTORS = {
40
+ Rational: ->(*args) { Rational(*args) },
41
+ Complex: ->(*args) { Complex(*args) }
42
+ }.freeze
43
+ private_constant :NUMERIC_CONSTRUCTORS
44
+
33
45
  def try_dispatch(receiver:, method_name:, args:)
34
- return nil unless method_name == :Array
35
- return nil if args.length != 1
36
46
  return nil if receiver.nil?
47
+ return try_array(args) if method_name == :Array
48
+ return try_numeric_constructor(method_name, args) if NUMERIC_CONSTRUCTORS.key?(method_name)
49
+
50
+ nil
51
+ end
52
+
53
+ def try_array(args)
54
+ return nil if args.length != 1
37
55
 
38
- arg = args.first
39
- element = element_type_of(arg)
56
+ element = element_type_of(args.first)
40
57
  return nil if element.nil?
41
58
 
42
59
  Type::Combinator.nominal_of("Array", type_args: [element])
43
60
  end
44
61
 
62
+ # `Rational(int)` / `Rational(num, den)` and `Complex(re)`
63
+ # / `Complex(re, im)` fold when every arg is a numeric
64
+ # Constant. The actual Ruby constructor runs at fold time
65
+ # (host-side), so the result respects Ruby's normalisation
66
+ # (`Rational(2, 4)` → `Rational(1, 2)`).
67
+ def try_numeric_constructor(method_name, args)
68
+ return nil unless [1, 2].include?(args.size)
69
+ return nil unless args.all? { |arg| numeric_constant?(arg) }
70
+
71
+ values = args.map(&:value)
72
+ result = NUMERIC_CONSTRUCTORS[method_name].call(*values)
73
+ Type::Combinator.constant_of(result)
74
+ rescue StandardError
75
+ nil
76
+ end
77
+
78
+ def numeric_constant?(type)
79
+ type.is_a?(Type::Constant) &&
80
+ (type.value.is_a?(Integer) ||
81
+ type.value.is_a?(Float) ||
82
+ type.value.is_a?(Rational) ||
83
+ type.value.is_a?(Complex))
84
+ end
85
+
45
86
  # Computes the element type the argument contributes to the
46
87
  # `Array(arg)` result, mirroring Ruby's coercion contract:
47
88
  #
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../../reflection"
3
4
  require_relative "../../type"
4
5
  require_relative "../../rbs_extended"
5
6
  require_relative "../rbs_type_translator"
@@ -218,15 +219,9 @@ module Rigor
218
219
  def lookup_method(environment, class_name, kind, method_name)
219
220
  case kind
220
221
  when :instance
221
- environment.rbs_loader.instance_method(
222
- class_name: class_name,
223
- method_name: method_name
224
- )
222
+ Rigor::Reflection.instance_method_definition(class_name, method_name, environment: environment)
225
223
  when :singleton
226
- environment.rbs_loader.singleton_method(
227
- class_name: class_name,
228
- method_name: method_name
229
- )
224
+ Rigor::Reflection.singleton_method_definition(class_name, method_name, environment: environment)
230
225
  end
231
226
  end
232
227
 
@@ -239,7 +234,7 @@ module Rigor
239
234
  def build_type_vars(environment, class_name, receiver_args)
240
235
  return {} if receiver_args.empty?
241
236
 
242
- param_names = environment.rbs_loader.class_type_param_names(class_name)
237
+ param_names = Rigor::Reflection.class_type_param_names(class_name, environment: environment)
243
238
  return {} if param_names.empty?
244
239
  return {} if param_names.size != receiver_args.size
245
240
 
@@ -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,5 +1,6 @@
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"
@@ -41,7 +42,7 @@ module Rigor
41
42
  # The dispatcher's public signature reserves space for `block_type:`
42
43
  # and ADR-2 plugin extensions (later slices), so call sites added
43
44
  # now do not have to be rewritten when those tiers arrive.
44
- module MethodDispatcher
45
+ module MethodDispatcher # rubocop:disable Metrics/ModuleLength
45
46
  module_function
46
47
 
47
48
  # @param receiver_type [Rigor::Type, nil] type of the receiver expression, or
@@ -96,7 +97,7 @@ module Rigor
96
97
  # arity-based fold tiers above it filter out the common
97
98
  # cases first. When `block_type` is nil the tier is a no-op.
98
99
  def dispatch_precise_tiers(receiver_type, method_name, arg_types, block_type = nil)
99
- meta_result = try_meta_introspection(receiver_type, method_name)
100
+ meta_result = try_meta_introspection(receiver_type, method_name, arg_types)
100
101
  return meta_result if meta_result
101
102
 
102
103
  ConstantFolding.try_fold(receiver: receiver_type, method_name: method_name, args: arg_types) ||
@@ -124,16 +125,13 @@ module Rigor
124
125
  end
125
126
 
126
127
  def user_class_fallback_receiver(receiver_type, environment)
127
- loader = environment.rbs_loader
128
- return nil if loader.nil?
129
-
130
128
  case receiver_type
131
129
  when Type::Nominal
132
- return nil if loader.class_known?(receiver_type.class_name)
130
+ return nil if Rigor::Reflection.rbs_class_known?(receiver_type.class_name, environment: environment)
133
131
 
134
132
  environment.nominal_for_name("Object")
135
133
  when Type::Singleton
136
- return nil if loader.class_known?(receiver_type.class_name)
134
+ return nil if Rigor::Reflection.rbs_class_known?(receiver_type.class_name, environment: environment)
137
135
 
138
136
  environment.singleton_for_name("Class")
139
137
  end
@@ -151,10 +149,10 @@ module Rigor
151
149
  # adjacent calls and the trivial `instance_of?(self)`
152
150
  # later as the rule catalogue grows; for now only `class`
153
151
  # is handled.
154
- def try_meta_introspection(receiver_type, method_name)
152
+ def try_meta_introspection(receiver_type, method_name, arg_types = [])
155
153
  case method_name
156
154
  when :class then meta_class(receiver_type)
157
- when :new then meta_new(receiver_type)
155
+ when :new then meta_new(receiver_type, arg_types)
158
156
  end
159
157
  end
160
158
 
@@ -171,12 +169,74 @@ module Rigor
171
169
  # plumbing for user classes, so a discovered-class
172
170
  # `ScanAccumulator.new` types as `Nominal[ScanAccumulator]`
173
171
  # rather than `Class`.
174
- def meta_new(receiver_type)
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 = [])
175
179
  return nil unless receiver_type.is_a?(Type::Singleton)
176
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
+
177
187
  Type::Combinator.nominal_of(receiver_type.class_name)
178
188
  end
179
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
+
180
240
  CONSTANT_METACLASSES = {
181
241
  Integer => "Integer", Float => "Float", String => "String",
182
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
- loader.singleton_method(class_name: @class_path, method_name: method_name)
146
+ Rigor::Reflection.singleton_method_definition(@class_path, method_name, environment: @environment)
149
147
  else
150
- loader.instance_method(class_name: @class_path, method_name: method_name)
148
+ Rigor::Reflection.instance_method_definition(@class_path, method_name, environment: @environment)
151
149
  end
152
150
  end
153
151