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.
@@ -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 non-nil
87
- # answer. Each tier owns its own receiver/argument shape
88
- # checks; a tier that does not recognise the receiver returns
89
- # nil so the next tier can try. The RBS tier sits below this
90
- # chain and is invoked by the outer `dispatch` method.
91
- def dispatch_precise_tiers(receiver_type, method_name, arg_types)
92
- meta_result = try_meta_introspection(receiver_type, method_name)
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 loader.class_known?(receiver_type.class_name)
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 loader.class_known?(receiver_type.class_name)
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
- 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 = [])
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
- 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