rigortype 0.1.3 → 0.1.4
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/README.md +125 -31
- data/lib/rigor/analysis/check_rules.rb +10 -18
- data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +75 -0
- data/lib/rigor/analysis/dependency_source_inference/builder.rb +47 -21
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
- data/lib/rigor/analysis/dependency_source_inference/index.rb +32 -3
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +1 -1
- data/lib/rigor/analysis/dependency_source_inference.rb +1 -0
- data/lib/rigor/analysis/diagnostic.rb +0 -2
- data/lib/rigor/analysis/fact_store.rb +11 -3
- data/lib/rigor/analysis/rule_catalog.rb +2 -2
- data/lib/rigor/analysis/runner.rb +114 -3
- data/lib/rigor/builtins/imported_refinements.rb +360 -55
- data/lib/rigor/cache/descriptor.rb +1 -1
- data/lib/rigor/cache/store.rb +1 -1
- data/lib/rigor/cli/diff_command.rb +1 -1
- data/lib/rigor/cli/sig_gen_command.rb +173 -0
- data/lib/rigor/cli/type_of_command.rb +1 -1
- data/lib/rigor/cli/type_scan_renderer.rb +1 -1
- data/lib/rigor/cli/type_scan_report.rb +2 -2
- data/lib/rigor/cli.rb +9 -1
- data/lib/rigor/configuration/dependencies.rb +2 -2
- data/lib/rigor/configuration.rb +2 -2
- data/lib/rigor/environment.rb +35 -4
- data/lib/rigor/flow_contribution/conflict.rb +2 -2
- data/lib/rigor/flow_contribution/element.rb +1 -1
- data/lib/rigor/flow_contribution/fact.rb +1 -1
- data/lib/rigor/flow_contribution/merge_result.rb +1 -1
- data/lib/rigor/flow_contribution/merger.rb +3 -3
- data/lib/rigor/flow_contribution.rb +2 -2
- data/lib/rigor/inference/block_parameter_binder.rb +0 -2
- data/lib/rigor/inference/coverage_scanner.rb +1 -1
- data/lib/rigor/inference/expression_typer.rb +67 -11
- data/lib/rigor/inference/fallback.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +3 -5
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +0 -12
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -3
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +118 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +6 -11
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +27 -11
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +0 -4
- data/lib/rigor/inference/method_dispatcher.rb +146 -2
- data/lib/rigor/inference/method_parameter_binder.rb +1 -3
- data/lib/rigor/inference/narrowing.rb +2 -4
- data/lib/rigor/inference/rbs_type_translator.rb +0 -2
- data/lib/rigor/inference/scope_indexer.rb +14 -9
- data/lib/rigor/inference/statement_evaluator.rb +7 -7
- data/lib/rigor/plugin/io_boundary.rb +0 -2
- data/lib/rigor/plugin/loader.rb +2 -2
- data/lib/rigor/plugin/manifest.rb +30 -9
- data/lib/rigor/plugin/registry.rb +11 -0
- data/lib/rigor/plugin/services.rb +1 -1
- data/lib/rigor/plugin/type_node_resolver.rb +52 -0
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/rbs_extended/reporter.rb +91 -0
- data/lib/rigor/rbs_extended.rb +131 -32
- data/lib/rigor/scope.rb +25 -8
- data/lib/rigor/sig_gen/classification.rb +36 -0
- data/lib/rigor/sig_gen/generator.rb +1048 -0
- data/lib/rigor/sig_gen/layout_index.rb +108 -0
- data/lib/rigor/sig_gen/method_candidate.rb +62 -0
- data/lib/rigor/sig_gen/observation_collector.rb +391 -0
- data/lib/rigor/sig_gen/observed_call.rb +62 -0
- data/lib/rigor/sig_gen/path_mapper.rb +116 -0
- data/lib/rigor/sig_gen/renderer.rb +157 -0
- data/lib/rigor/sig_gen/type_elaborator.rb +92 -0
- data/lib/rigor/sig_gen/write_result.rb +48 -0
- data/lib/rigor/sig_gen/writer.rb +530 -0
- data/lib/rigor/sig_gen.rb +25 -0
- data/lib/rigor/type/bound_method.rb +79 -0
- data/lib/rigor/type/combinator.rb +195 -2
- data/lib/rigor/type/constant.rb +13 -0
- data/lib/rigor/type/hash_shape.rb +0 -2
- data/lib/rigor/type/union.rb +20 -1
- data/lib/rigor/type.rb +1 -0
- data/lib/rigor/type_node/generic.rb +62 -0
- data/lib/rigor/type_node/identifier.rb +30 -0
- data/lib/rigor/type_node/indexed_access.rb +41 -0
- data/lib/rigor/type_node/integer_literal.rb +29 -0
- data/lib/rigor/type_node/name_scope.rb +52 -0
- data/lib/rigor/type_node/resolver_chain.rb +56 -0
- data/lib/rigor/type_node/string_literal.rb +29 -0
- data/lib/rigor/type_node/symbol_literal.rb +28 -0
- data/lib/rigor/type_node/union.rb +42 -0
- data/lib/rigor/type_node.rb +29 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +2 -0
- data/sig/rigor/analysis/check_rules/always_truthy_condition_collector.rbs +10 -0
- data/sig/rigor/analysis/check_rules/dead_assignment_collector.rbs +10 -0
- data/sig/rigor/analysis/dependency_source_inference/gem_resolver.rbs +25 -0
- data/sig/rigor/analysis/dependency_source_inference/index.rbs +9 -0
- data/sig/rigor/cli/diff_command.rbs +4 -0
- data/sig/rigor/cli/explain_command.rbs +4 -0
- data/sig/rigor/cli/sig_gen_command.rbs +4 -0
- data/sig/rigor/cli/type_scan_command.rbs +3 -0
- data/sig/rigor/environment.rbs +5 -2
- data/sig/rigor/inference/builtins/method_catalog.rbs +4 -0
- data/sig/rigor/inference/builtins/numeric_catalog.rbs +3 -0
- data/sig/rigor/inference/builtins.rbs +2 -0
- data/sig/rigor/plugin/access_denied_error.rbs +3 -0
- data/sig/rigor/plugin/base.rbs +6 -0
- data/sig/rigor/plugin/fact_store.rbs +11 -0
- data/sig/rigor/plugin/io_boundary.rbs +4 -0
- data/sig/rigor/plugin/load_error.rbs +6 -0
- data/sig/rigor/plugin/loader.rbs +20 -0
- data/sig/rigor/plugin/manifest.rbs +9 -0
- data/sig/rigor/plugin/registry.rbs +3 -0
- data/sig/rigor/plugin/services.rbs +3 -0
- data/sig/rigor/plugin/trust_policy.rbs +4 -0
- data/sig/rigor/plugin/type_node_resolver.rbs +3 -0
- data/sig/rigor/plugin.rbs +8 -0
- data/sig/rigor/scope.rbs +4 -2
- data/sig/rigor/type.rbs +28 -6
- metadata +52 -1
|
@@ -13,6 +13,7 @@ require_relative "union"
|
|
|
13
13
|
require_relative "difference"
|
|
14
14
|
require_relative "refined"
|
|
15
15
|
require_relative "intersection"
|
|
16
|
+
require_relative "bound_method"
|
|
16
17
|
|
|
17
18
|
module Rigor
|
|
18
19
|
module Type
|
|
@@ -69,6 +70,14 @@ module Rigor
|
|
|
69
70
|
Constant.new(value)
|
|
70
71
|
end
|
|
71
72
|
|
|
73
|
+
# `Object#method(:name)` carrier. Stores the bound
|
|
74
|
+
# `(receiver, method_name)` pair so the dispatcher can
|
|
75
|
+
# substitute the original dispatch at `.call` / `.()` /
|
|
76
|
+
# `[]` time. See {Type::BoundMethod}.
|
|
77
|
+
def bound_method_of(receiver_type, method_name)
|
|
78
|
+
BoundMethod.new(receiver_type: receiver_type, method_name: method_name)
|
|
79
|
+
end
|
|
80
|
+
|
|
72
81
|
# Bounded-integer carrier. Each bound is either an `Integer` or
|
|
73
82
|
# one of `:neg_infinity` / `:pos_infinity` (sentinels exposed as
|
|
74
83
|
# `IntegerRange::NEG_INFINITY` / `POS_INFINITY`).
|
|
@@ -347,7 +356,6 @@ module Rigor
|
|
|
347
356
|
INT_MASK_UNION_LIMIT = 16
|
|
348
357
|
private_constant :INT_MASK_FLAG_LIMIT, :INT_MASK_UNION_LIMIT
|
|
349
358
|
|
|
350
|
-
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
351
359
|
def int_mask(flags)
|
|
352
360
|
return nil unless flags.is_a?(Array) && flags.all?(Integer)
|
|
353
361
|
return nil if flags.any?(&:negative?)
|
|
@@ -362,7 +370,6 @@ module Rigor
|
|
|
362
370
|
integer_range(values.min, values.max)
|
|
363
371
|
end
|
|
364
372
|
end
|
|
365
|
-
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
366
373
|
|
|
367
374
|
# `int_mask_of[T]` — derives the int_mask closure from
|
|
368
375
|
# a finite integer-literal type:
|
|
@@ -401,6 +408,99 @@ module Rigor
|
|
|
401
408
|
end
|
|
402
409
|
end
|
|
403
410
|
|
|
411
|
+
# `pick_of[T, K]` shape-projection — keeps only the entries
|
|
412
|
+
# of `T` whose key is in the literal-key set extracted from
|
|
413
|
+
# `K`. ADR-13 § "Shape projection / Restriction and removal".
|
|
414
|
+
#
|
|
415
|
+
# Phase A handles `Type::HashShape` (literal-key K).
|
|
416
|
+
# Phase B (slice 5) extends to `Type::Tuple` (integer-index
|
|
417
|
+
# K) — `pick_of[Tuple[A, B, C], 0 | 2]` evaluates to
|
|
418
|
+
# `Tuple[A, C]`. Non-shape inputs (`Type::Nominal`, etc.)
|
|
419
|
+
# return `type` unchanged ("lossy degradation"; the
|
|
420
|
+
# `dynamic.shape.lossy-projection` diagnostic that flags
|
|
421
|
+
# the boundary lands when caller-side diagnostic threading
|
|
422
|
+
# arrives).
|
|
423
|
+
def pick_of(type, keys)
|
|
424
|
+
case type
|
|
425
|
+
when HashShape then hash_shape_pick(type, keys)
|
|
426
|
+
when Tuple then tuple_pick(type, keys)
|
|
427
|
+
else type
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
# `omit_of[T, K]` shape-projection — dual of {pick_of}.
|
|
432
|
+
# Drops the entries / positions whose key (or index, for a
|
|
433
|
+
# `Tuple`) is in the literal-key set extracted from `K`.
|
|
434
|
+
def omit_of(type, keys)
|
|
435
|
+
case type
|
|
436
|
+
when HashShape then hash_shape_omit(type, keys)
|
|
437
|
+
when Tuple then tuple_omit(type, keys)
|
|
438
|
+
else type
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
# `partial_of[T]` shape-projection — flips every required
|
|
443
|
+
# entry of `T` to optional. ADR-13 § "Required-ness flips".
|
|
444
|
+
# Does NOT add `nil` to value types — Rigor's HashShape
|
|
445
|
+
# distinguishes "key absent" from "key present with nil
|
|
446
|
+
# value", so flipping required-ness is sufficient.
|
|
447
|
+
def partial_of(type)
|
|
448
|
+
return type unless type.is_a?(HashShape)
|
|
449
|
+
|
|
450
|
+
HashShape.new(
|
|
451
|
+
type.pairs,
|
|
452
|
+
required_keys: [],
|
|
453
|
+
optional_keys: type.pairs.keys,
|
|
454
|
+
read_only_keys: type.read_only_keys,
|
|
455
|
+
extra_keys: type.extra_keys
|
|
456
|
+
)
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
# `required_of[T]` shape-projection — inverse of
|
|
460
|
+
# {partial_of}; flips every optional entry to required.
|
|
461
|
+
def required_of(type)
|
|
462
|
+
return type unless type.is_a?(HashShape)
|
|
463
|
+
|
|
464
|
+
HashShape.new(
|
|
465
|
+
type.pairs,
|
|
466
|
+
required_keys: type.pairs.keys,
|
|
467
|
+
optional_keys: [],
|
|
468
|
+
read_only_keys: type.read_only_keys,
|
|
469
|
+
extra_keys: type.extra_keys
|
|
470
|
+
)
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
# `readonly_of[T]` shape-projection — marks every entry of
|
|
474
|
+
# `T` as read-only in the current view. View-level only —
|
|
475
|
+
# does NOT prove the underlying Ruby Hash is frozen.
|
|
476
|
+
def readonly_of(type)
|
|
477
|
+
return type unless type.is_a?(HashShape)
|
|
478
|
+
|
|
479
|
+
HashShape.new(
|
|
480
|
+
type.pairs,
|
|
481
|
+
required_keys: type.required_keys,
|
|
482
|
+
optional_keys: type.optional_keys,
|
|
483
|
+
read_only_keys: type.pairs.keys,
|
|
484
|
+
extra_keys: type.extra_keys
|
|
485
|
+
)
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
# Predicate that a shape-projection (`pick_of`, `omit_of`,
|
|
489
|
+
# `partial_of`, `required_of`, `readonly_of`) would degrade
|
|
490
|
+
# to "input unchanged" on this carrier. Callers consult
|
|
491
|
+
# this BEFORE invoking the projection so they can emit a
|
|
492
|
+
# `dynamic.shape.lossy-projection` diagnostic at the site
|
|
493
|
+
# where the projection was authored.
|
|
494
|
+
#
|
|
495
|
+
# `HashShape` and `Tuple` carry shape-level information
|
|
496
|
+
# the projections honour; every other carrier is lossy.
|
|
497
|
+
# Slice 5b wires diagnostic emission through `RbsExtended`
|
|
498
|
+
# / parser callers; this predicate stands alone in slice 5
|
|
499
|
+
# for unit-test coverage and future composition.
|
|
500
|
+
def shape_projection_lossy?(type)
|
|
501
|
+
!type.is_a?(HashShape) && !type.is_a?(Tuple)
|
|
502
|
+
end
|
|
503
|
+
|
|
404
504
|
class << self # rubocop:disable Metrics/ClassLength
|
|
405
505
|
private
|
|
406
506
|
|
|
@@ -480,6 +580,99 @@ module Rigor
|
|
|
480
580
|
end
|
|
481
581
|
end
|
|
482
582
|
|
|
583
|
+
# Literal-key set extraction for {pick_of} / {omit_of}.
|
|
584
|
+
# Accepts `Constant<Symbol|String>` or `Union[Constant…]`
|
|
585
|
+
# where every member is such a Constant. Returns `nil`
|
|
586
|
+
# when the shape can't be reduced to a finite key set
|
|
587
|
+
# (untyped, Top, Difference, Refined, mixed-kind union,
|
|
588
|
+
# etc.) — callers degrade to "input unchanged" per
|
|
589
|
+
# ADR-13's lossy-projection rule.
|
|
590
|
+
def extract_constant_key_set(type)
|
|
591
|
+
case type
|
|
592
|
+
when Constant then constant_key_set(type)
|
|
593
|
+
when Union then union_key_set(type)
|
|
594
|
+
end
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
def constant_key_set(type)
|
|
598
|
+
literal_key?(type.value) ? [type.value] : nil
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
def union_key_set(type)
|
|
602
|
+
return nil unless type.members.all?(Constant)
|
|
603
|
+
|
|
604
|
+
values = type.members.map(&:value)
|
|
605
|
+
values.all? { |v| literal_key?(v) } ? values : nil
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
def literal_key?(value)
|
|
609
|
+
value.is_a?(Symbol) || value.is_a?(String)
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
# Rebuild a {HashShape} from the subset of `keys` the
|
|
613
|
+
# caller decided to keep. Preserves required / optional /
|
|
614
|
+
# read-only classification AND the extra-keys policy of
|
|
615
|
+
# the source shape; entries dropped from `pairs` also
|
|
616
|
+
# drop from each policy list. Used by both {pick_of}
|
|
617
|
+
# (intersection with K) and {omit_of} (set difference).
|
|
618
|
+
def rebuild_hash_shape_with_keys(shape, kept_keys)
|
|
619
|
+
HashShape.new(
|
|
620
|
+
shape.pairs.slice(*kept_keys),
|
|
621
|
+
required_keys: shape.required_keys.select { |k| kept_keys.include?(k) },
|
|
622
|
+
optional_keys: shape.optional_keys.select { |k| kept_keys.include?(k) },
|
|
623
|
+
read_only_keys: shape.read_only_keys.select { |k| kept_keys.include?(k) },
|
|
624
|
+
extra_keys: shape.extra_keys
|
|
625
|
+
)
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
def hash_shape_pick(type, keys)
|
|
629
|
+
key_set = extract_constant_key_set(keys)
|
|
630
|
+
return type if key_set.nil?
|
|
631
|
+
|
|
632
|
+
rebuild_hash_shape_with_keys(type, type.pairs.keys & key_set)
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
def hash_shape_omit(type, keys)
|
|
636
|
+
key_set = extract_constant_key_set(keys)
|
|
637
|
+
return type if key_set.nil?
|
|
638
|
+
|
|
639
|
+
rebuild_hash_shape_with_keys(type, type.pairs.keys - key_set)
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
# ADR-13 slice 5 — Tuple support. `K` MUST be a
|
|
643
|
+
# `Constant<Integer>` or `Union[Constant<Integer>, …]`;
|
|
644
|
+
# other K shapes (or non-integer Constants in a Union)
|
|
645
|
+
# return the input unchanged. Negative or out-of-range
|
|
646
|
+
# indices are dropped silently per slice 5's permissive
|
|
647
|
+
# take — surface diagnostics are slice 5b material.
|
|
648
|
+
def tuple_pick(type, keys)
|
|
649
|
+
index_set = extract_tuple_index_set(keys, type.elements.size)
|
|
650
|
+
return type if index_set.nil?
|
|
651
|
+
|
|
652
|
+
Tuple.new(index_set.map { |i| type.elements[i] })
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
def tuple_omit(type, keys)
|
|
656
|
+
index_set = extract_tuple_index_set(keys, type.elements.size)
|
|
657
|
+
return type if index_set.nil?
|
|
658
|
+
|
|
659
|
+
dropped = index_set.to_a
|
|
660
|
+
Tuple.new(type.elements.each_with_index.reject { |_, i| dropped.include?(i) }.map(&:first))
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
# Extracts a sorted, deduplicated set of in-range integer
|
|
664
|
+
# indices from a `Constant<Integer>` / `Union[Constant<Integer>, …]`
|
|
665
|
+
# carrier. Out-of-range indices are dropped silently; the
|
|
666
|
+
# caller decides whether an empty result still means
|
|
667
|
+
# "lossy projection" (current pick / omit just produce an
|
|
668
|
+
# empty Tuple).
|
|
669
|
+
def extract_tuple_index_set(type, size)
|
|
670
|
+
flags = extract_constant_int_set(type)
|
|
671
|
+
return nil if flags.nil?
|
|
672
|
+
|
|
673
|
+
flags.uniq.select { |i| i >= 0 && i < size }.sort
|
|
674
|
+
end
|
|
675
|
+
|
|
483
676
|
def tuple_indexed_access(tuple, key)
|
|
484
677
|
return top unless key.is_a?(Constant) && key.value.is_a?(Integer)
|
|
485
678
|
|
data/lib/rigor/type/constant.rb
CHANGED
|
@@ -50,11 +50,24 @@ module Rigor
|
|
|
50
50
|
value.inspect
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
+
# RBS supports `Literal` types for booleans, nil, integer
|
|
54
|
+
# literals (positive and negative), symbol literals, and
|
|
55
|
+
# string literals. Erasing to these preserves the
|
|
56
|
+
# carrier's precision at the RBS boundary — `Constant<64>`
|
|
57
|
+
# round-trips as `64`, not as `Integer` — and
|
|
58
|
+
# `RbsTypeTranslator#translate_literal` already maps the
|
|
59
|
+
# parsed RBS Literal back to `Constant`. Scalar carriers
|
|
60
|
+
# without RBS Literal support (Float, Range, Rational,
|
|
61
|
+
# Complex, Regexp, Pathname) keep their pre-existing
|
|
62
|
+
# widen-to-class-name behaviour because RBS rejects their
|
|
63
|
+
# literal spellings as syntax errors.
|
|
53
64
|
def erase_to_rbs
|
|
54
65
|
case value
|
|
55
66
|
when true then "true"
|
|
56
67
|
when false then "false"
|
|
57
68
|
when nil then "nil"
|
|
69
|
+
when Integer then value.to_s
|
|
70
|
+
when Symbol, String then value.inspect
|
|
58
71
|
else value.class.name
|
|
59
72
|
end
|
|
60
73
|
end
|
|
@@ -22,7 +22,6 @@ module Rigor
|
|
|
22
22
|
#
|
|
23
23
|
# See docs/type-specification/rbs-compatible-types.md (records) and
|
|
24
24
|
# docs/type-specification/rigor-extensions.md (hash shape).
|
|
25
|
-
# rubocop:disable Metrics/ClassLength
|
|
26
25
|
class HashShape
|
|
27
26
|
ALLOWED_KEY_CLASSES = [Symbol, String].freeze
|
|
28
27
|
EXTRA_KEY_POLICIES = %i[open closed].freeze
|
|
@@ -241,6 +240,5 @@ module Rigor
|
|
|
241
240
|
Type::Combinator.union(*key_types)
|
|
242
241
|
end
|
|
243
242
|
end
|
|
244
|
-
# rubocop:enable Metrics/ClassLength
|
|
245
243
|
end
|
|
246
244
|
end
|
data/lib/rigor/type/union.rb
CHANGED
|
@@ -28,8 +28,27 @@ module Rigor
|
|
|
28
28
|
members.map { |m| m.describe(verbosity) }.join(" | ")
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
# ADR-1 § "RBS round-trip is lossless" + the value-lattice
|
|
32
|
+
# rule `untyped | T = untyped` (every `T` is gradually
|
|
33
|
+
# consistent with `untyped`). When any union member erases
|
|
34
|
+
# to `"untyped"`, the whole union erases to `"untyped"` —
|
|
35
|
+
# the RBS surface has no carrier for "Dynamic-origin
|
|
36
|
+
# alongside a static facet", and the gradual-consistency
|
|
37
|
+
# contract guarantees the substitution is sound at every
|
|
38
|
+
# call site.
|
|
39
|
+
#
|
|
40
|
+
# Post-erasure dedupe removes `String | String` artefacts
|
|
41
|
+
# that arise when two structurally-distinct `Constant`
|
|
42
|
+
# carriers (e.g. `Constant<"Alice">` / `Constant<"Bob">`)
|
|
43
|
+
# share an RBS-erased envelope. The members themselves
|
|
44
|
+
# are already structurally deduped at construction by
|
|
45
|
+
# `Type::Combinator.union`, but the post-erase strings
|
|
46
|
+
# can collide.
|
|
31
47
|
def erase_to_rbs
|
|
32
|
-
members.map(&:erase_to_rbs)
|
|
48
|
+
erased = members.map(&:erase_to_rbs)
|
|
49
|
+
return "untyped" if erased.include?("untyped")
|
|
50
|
+
|
|
51
|
+
erased.uniq.join(" | ")
|
|
33
52
|
end
|
|
34
53
|
|
|
35
54
|
def top
|
data/lib/rigor/type.rb
CHANGED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module TypeNode
|
|
5
|
+
# A parameterised named-type reference (`Pick<T, K>`,
|
|
6
|
+
# `non-empty-array[Integer]`, `pick_of[T, "name" | "email"]`,
|
|
7
|
+
# …) in an RBS::Extended payload. The `head` is the parser-
|
|
8
|
+
# observed name (no bracket type); `args` is the ordered
|
|
9
|
+
# sequence of type-argument nodes already produced by the
|
|
10
|
+
# parser at one level of depth.
|
|
11
|
+
#
|
|
12
|
+
# Args are themselves {TypeNode::Identifier} or
|
|
13
|
+
# {TypeNode::Generic}. Nested generics ride the same shape:
|
|
14
|
+
# `Pick<Address, "name" | "surname">` reaches the resolver as
|
|
15
|
+
# `Generic("Pick", [Identifier("Address"), Generic("Union", [...])])`
|
|
16
|
+
# — actually the union spelling depends on the parser's
|
|
17
|
+
# eventual convention (slice 3 pins it); for now the field
|
|
18
|
+
# set is the only public commitment.
|
|
19
|
+
#
|
|
20
|
+
# The carrier is intentionally permissive about `args.size`.
|
|
21
|
+
# The grammar-level rule "no brackets ⇒ Identifier; brackets ⇒
|
|
22
|
+
# Generic" lives on the parser side; nothing here forbids a
|
|
23
|
+
# zero-arg Generic so plugins can synthesise nodes for
|
|
24
|
+
# diagnostic or testing purposes without the parser fighting
|
|
25
|
+
# back.
|
|
26
|
+
class Generic < Data.define(:head, :args)
|
|
27
|
+
def initialize(head:, args:)
|
|
28
|
+
unless head.is_a?(String) && !head.empty?
|
|
29
|
+
raise ArgumentError,
|
|
30
|
+
"TypeNode::Generic head must be a non-empty String, " \
|
|
31
|
+
"got #{head.inspect}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
unless args.is_a?(Array) && args.all? { |a| valid_arg?(a) }
|
|
35
|
+
raise ArgumentError,
|
|
36
|
+
"TypeNode::Generic args must be an Array of " \
|
|
37
|
+
"TypeNode::Identifier / TypeNode::Generic / " \
|
|
38
|
+
"TypeNode::IntegerLiteral, got #{args.inspect}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
super(head: head, args: args.freeze)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
# ADR-13 slice 3 expanded the accepted set to include
|
|
47
|
+
# {IntegerLiteral} so the parser can emit a uniform AST for
|
|
48
|
+
# `int<5, 10>` (angle bounds) and `int_mask[1, 2, 4]`
|
|
49
|
+
# (square-bracketed bitflag union). The follow-up further
|
|
50
|
+
# admits {SymbolLiteral} / {StringLiteral} / {IndexedAccess}
|
|
51
|
+
# / {Union} so `Pick[T, :a | "b"]` carries through to the
|
|
52
|
+
# resolver as a uniform AST. Slice 1 originally accepted
|
|
53
|
+
# only `Identifier` / `Generic`; every later addition stays
|
|
54
|
+
# additive — every slice-1-shape Generic remains valid.
|
|
55
|
+
def valid_arg?(arg)
|
|
56
|
+
arg.is_a?(Identifier) || arg.is_a?(Generic) || arg.is_a?(IntegerLiteral) ||
|
|
57
|
+
arg.is_a?(SymbolLiteral) || arg.is_a?(StringLiteral) ||
|
|
58
|
+
arg.is_a?(IndexedAccess) || arg.is_a?(Union)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module TypeNode
|
|
5
|
+
# A bare named-type reference in an RBS::Extended payload. The
|
|
6
|
+
# `name` is the head as the parser saw it — kebab-case for
|
|
7
|
+
# built-in refinement names (`"non-empty-string"`),
|
|
8
|
+
# PascalCase for class-like names (`"String"`, `"Pick"`),
|
|
9
|
+
# `lower_snake` for type-function-shaped names without
|
|
10
|
+
# arguments (rare).
|
|
11
|
+
#
|
|
12
|
+
# The resolver dispatch path treats an `Identifier` as the
|
|
13
|
+
# no-arg form: if a plugin recognises `Pick` as a TS-utility
|
|
14
|
+
# name, it MAY still return `Dynamic[top]` for the bare
|
|
15
|
+
# `Identifier("Pick")` since TypeScript's `Pick` is only
|
|
16
|
+
# meaningful with two type arguments. The `Generic` carrier
|
|
17
|
+
# is what plugin resolvers normally key on.
|
|
18
|
+
class Identifier < Data.define(:name)
|
|
19
|
+
def initialize(name:)
|
|
20
|
+
unless name.is_a?(String) && !name.empty?
|
|
21
|
+
raise ArgumentError,
|
|
22
|
+
"TypeNode::Identifier name must be a non-empty String, " \
|
|
23
|
+
"got #{name.inspect}"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
super
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module TypeNode
|
|
5
|
+
# AST wrapper for the trailing `T[K]` indexed-access projection
|
|
6
|
+
# chain. The parser emits a left-associative chain by wrapping
|
|
7
|
+
# the receiver AST in successive `IndexedAccess` nodes
|
|
8
|
+
# (`Tuple[A, B][1][0]` parses to
|
|
9
|
+
# `IndexedAccess(IndexedAccess(Generic(Tuple, [A, B]), 1), 0)`).
|
|
10
|
+
#
|
|
11
|
+
# `receiver` and `key` are themselves any AST node — the
|
|
12
|
+
# indexed-access chain is applied at resolution time, after the
|
|
13
|
+
# receiver has been resolved to a {Rigor::Type} carrier and the
|
|
14
|
+
# key has been resolved (typically to a `Constant<Integer>` or
|
|
15
|
+
# a constant String/Symbol singleton).
|
|
16
|
+
class IndexedAccess < Data.define(:receiver, :key)
|
|
17
|
+
def initialize(receiver:, key:)
|
|
18
|
+
unless valid_node?(receiver)
|
|
19
|
+
raise ArgumentError,
|
|
20
|
+
"TypeNode::IndexedAccess receiver must be a TypeNode " \
|
|
21
|
+
"node, got #{receiver.inspect}"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
unless valid_node?(key)
|
|
25
|
+
raise ArgumentError,
|
|
26
|
+
"TypeNode::IndexedAccess key must be a TypeNode " \
|
|
27
|
+
"node, got #{key.inspect}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
super
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def valid_node?(node)
|
|
36
|
+
node.is_a?(Identifier) || node.is_a?(Generic) ||
|
|
37
|
+
node.is_a?(IntegerLiteral) || node.is_a?(IndexedAccess)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module TypeNode
|
|
5
|
+
# Integer-literal AST node. Used as a {Generic#args} entry for
|
|
6
|
+
# parametric forms whose arguments are bare integers — namely
|
|
7
|
+
# `int<5, 10>` (angle-bracketed integer bounds for
|
|
8
|
+
# {Type::IntegerRange}) and `int_mask[1, 2, 4]` (square-
|
|
9
|
+
# bracketed bitflag union for {Type::Combinator.int_mask}).
|
|
10
|
+
#
|
|
11
|
+
# ADR-13 slice 3 introduces this node so the parser can emit a
|
|
12
|
+
# uniform AST regardless of bracket flavour: the resolver pass
|
|
13
|
+
# then dispatches to the appropriate built-in builder by head
|
|
14
|
+
# name. Plugin resolvers receive the same shape and MAY treat
|
|
15
|
+
# integer literals as input to custom carriers (e.g. an
|
|
16
|
+
# opinionated `port_number<8000>` plugin).
|
|
17
|
+
class IntegerLiteral < Data.define(:value)
|
|
18
|
+
def initialize(value:)
|
|
19
|
+
unless value.is_a?(Integer)
|
|
20
|
+
raise ArgumentError,
|
|
21
|
+
"TypeNode::IntegerLiteral value must be an Integer, " \
|
|
22
|
+
"got #{value.inspect}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
super
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module TypeNode
|
|
5
|
+
# Companion context handed to every {Rigor::Plugin::TypeNodeResolver}
|
|
6
|
+
# invocation. ADR-13 § "`Plugin::TypeNodeResolver` shape".
|
|
7
|
+
#
|
|
8
|
+
# Three slots:
|
|
9
|
+
#
|
|
10
|
+
# - `resolver`: re-entry point so a plugin can recursively
|
|
11
|
+
# resolve its own arguments. Any object responding to
|
|
12
|
+
# `#resolve(node, scope)`. Slice 3 uses {ResolverChain} as
|
|
13
|
+
# the concrete implementation; tests may pass a stub that
|
|
14
|
+
# answers `resolve` directly.
|
|
15
|
+
# - `class_context`: the surrounding class / module name, if
|
|
16
|
+
# any (`String` or `nil`). Plugins use this to resolve
|
|
17
|
+
# `self`-relative type references or to scope nominal-name
|
|
18
|
+
# lookups.
|
|
19
|
+
# - `type_alias_table`: a frozen read-only view of the
|
|
20
|
+
# project's RBS type aliases for forward references. Slice
|
|
21
|
+
# 3 lands the slot with a default empty Hash; the
|
|
22
|
+
# populated table is wired from {Rigor::Environment} in a
|
|
23
|
+
# later slice once plugin authors ask for it.
|
|
24
|
+
class NameScope < Data.define(:resolver, :class_context, :type_alias_table)
|
|
25
|
+
def initialize(resolver:, class_context: nil, type_alias_table: {})
|
|
26
|
+
unless resolver.respond_to?(:resolve)
|
|
27
|
+
raise ArgumentError,
|
|
28
|
+
"TypeNode::NameScope resolver must respond to #resolve(node, scope), " \
|
|
29
|
+
"got #{resolver.inspect}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
unless class_context.nil? || class_context.is_a?(String)
|
|
33
|
+
raise ArgumentError,
|
|
34
|
+
"TypeNode::NameScope class_context must be nil or a String, " \
|
|
35
|
+
"got #{class_context.inspect}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
unless type_alias_table.is_a?(Hash)
|
|
39
|
+
raise ArgumentError,
|
|
40
|
+
"TypeNode::NameScope type_alias_table must be a Hash, " \
|
|
41
|
+
"got #{type_alias_table.inspect}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
super(
|
|
45
|
+
resolver: resolver,
|
|
46
|
+
class_context: class_context.nil? ? nil : class_context.dup.freeze,
|
|
47
|
+
type_alias_table: type_alias_table.dup.freeze
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module TypeNode
|
|
5
|
+
# Walks an ordered list of {Rigor::Plugin::TypeNodeResolver}
|
|
6
|
+
# instances, returning the first non-nil `resolve(node, scope)`
|
|
7
|
+
# answer. ADR-13 § "`Plugin::TypeNodeResolver` shape" — first
|
|
8
|
+
# non-nil wins; registration order is the user's lever for
|
|
9
|
+
# shadowing per WD3 / WD5.
|
|
10
|
+
#
|
|
11
|
+
# The chain is itself a `TypeNodeResolver`-shaped object
|
|
12
|
+
# (`#resolve(node, scope)`) so it slots into a {NameScope} as
|
|
13
|
+
# the `resolver:` field without further indirection: a plugin
|
|
14
|
+
# resolver that wants to recursively resolve a nested argument
|
|
15
|
+
# calls `scope.resolver.resolve(arg, scope)` and reaches every
|
|
16
|
+
# resolver in the chain plus the built-in registry through the
|
|
17
|
+
# same entry point.
|
|
18
|
+
#
|
|
19
|
+
# Constructed once per `Analysis::Runner.run` from
|
|
20
|
+
# `Plugin::Registry#type_node_resolvers`. The chain is
|
|
21
|
+
# immutable and re-entrant; the parser may consult it many
|
|
22
|
+
# times for the same node.
|
|
23
|
+
class ResolverChain
|
|
24
|
+
def initialize(resolvers)
|
|
25
|
+
unless resolvers.is_a?(Array) && resolvers.all? { |r| r.respond_to?(:resolve) }
|
|
26
|
+
raise ArgumentError,
|
|
27
|
+
"TypeNode::ResolverChain expects an Array of resolvers " \
|
|
28
|
+
"responding to #resolve(node, scope), got #{resolvers.inspect}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
@resolvers = resolvers.dup.freeze
|
|
32
|
+
freeze
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @return [Array<Rigor::Plugin::TypeNodeResolver>] ordered
|
|
36
|
+
# resolver instances, in plugin-registration order.
|
|
37
|
+
attr_reader :resolvers
|
|
38
|
+
|
|
39
|
+
# First non-nil `resolve(node, scope)` answer from the chain;
|
|
40
|
+
# `nil` when every resolver declined.
|
|
41
|
+
def resolve(node, scope)
|
|
42
|
+
@resolvers.each do |resolver|
|
|
43
|
+
result = resolver.resolve(node, scope)
|
|
44
|
+
return result unless result.nil?
|
|
45
|
+
end
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Shared empty chain — a `NameScope` constructed without any
|
|
50
|
+
# plugin-supplied resolvers can use this to satisfy the
|
|
51
|
+
# `responds_to?(:resolve)` contract without a per-call
|
|
52
|
+
# allocation.
|
|
53
|
+
EMPTY = new([]).freeze
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module TypeNode
|
|
5
|
+
# String-literal AST node. Used as a {Generic#args} entry for
|
|
6
|
+
# parametric forms whose argument is a String literal — namely
|
|
7
|
+
# `Pick[T, "name"]`, `pick_of[Shape, "a" | "b"]`, and downstream
|
|
8
|
+
# plugin resolvers that accept literal key selectors.
|
|
9
|
+
#
|
|
10
|
+
# ADR-13 follow-up (`docs/CURRENT_WORK.md` engineering item
|
|
11
|
+
# #2): the RBS::Extended grammar previously could not tokenise
|
|
12
|
+
# `"name"` inside a type-arg position. The resolver translates
|
|
13
|
+
# this node to a `Type::Constant` carrying the string value.
|
|
14
|
+
# Slice 1 supports double-quoted strings without escape
|
|
15
|
+
# sequences (the most common shape — TS-style key unions
|
|
16
|
+
# are bare identifier-ish names).
|
|
17
|
+
class StringLiteral < Data.define(:value)
|
|
18
|
+
def initialize(value:)
|
|
19
|
+
unless value.is_a?(String)
|
|
20
|
+
raise ArgumentError,
|
|
21
|
+
"TypeNode::StringLiteral value must be a String, " \
|
|
22
|
+
"got #{value.inspect}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
super
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module TypeNode
|
|
5
|
+
# Symbol-literal AST node. Used as a {Generic#args} entry for
|
|
6
|
+
# parametric forms whose argument is a Symbol literal — namely
|
|
7
|
+
# `Pick[T, :name]`, `pick_of[Shape, :a | :b]`, and downstream
|
|
8
|
+
# plugin resolvers that accept literal key selectors.
|
|
9
|
+
#
|
|
10
|
+
# ADR-13 follow-up (`docs/CURRENT_WORK.md` engineering item
|
|
11
|
+
# #2): the RBS::Extended grammar previously could not tokenise
|
|
12
|
+
# `:name` inside a type-arg position; this addition closes the
|
|
13
|
+
# gap so `ImportedRefinements.parse` produces a uniform AST.
|
|
14
|
+
# The resolver translates this node to a `Type::Constant`
|
|
15
|
+
# carrying the symbol value.
|
|
16
|
+
class SymbolLiteral < Data.define(:value)
|
|
17
|
+
def initialize(value:)
|
|
18
|
+
unless value.is_a?(Symbol)
|
|
19
|
+
raise ArgumentError,
|
|
20
|
+
"TypeNode::SymbolLiteral value must be a Symbol, " \
|
|
21
|
+
"got #{value.inspect}"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
super
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|