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.
- checksums.yaml +4 -4
- data/lib/rigor/analysis/check_rules.rb +38 -41
- data/lib/rigor/builtins/imported_refinements.rb +93 -3
- data/lib/rigor/inference/expression_typer.rb +25 -2
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +247 -1
- 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 +70 -10
- 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 +3 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|