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.
Files changed (116) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +125 -31
  3. data/lib/rigor/analysis/check_rules.rb +10 -18
  4. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +75 -0
  5. data/lib/rigor/analysis/dependency_source_inference/builder.rb +47 -21
  6. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  7. data/lib/rigor/analysis/dependency_source_inference/index.rb +32 -3
  8. data/lib/rigor/analysis/dependency_source_inference/walker.rb +1 -1
  9. data/lib/rigor/analysis/dependency_source_inference.rb +1 -0
  10. data/lib/rigor/analysis/diagnostic.rb +0 -2
  11. data/lib/rigor/analysis/fact_store.rb +11 -3
  12. data/lib/rigor/analysis/rule_catalog.rb +2 -2
  13. data/lib/rigor/analysis/runner.rb +114 -3
  14. data/lib/rigor/builtins/imported_refinements.rb +360 -55
  15. data/lib/rigor/cache/descriptor.rb +1 -1
  16. data/lib/rigor/cache/store.rb +1 -1
  17. data/lib/rigor/cli/diff_command.rb +1 -1
  18. data/lib/rigor/cli/sig_gen_command.rb +173 -0
  19. data/lib/rigor/cli/type_of_command.rb +1 -1
  20. data/lib/rigor/cli/type_scan_renderer.rb +1 -1
  21. data/lib/rigor/cli/type_scan_report.rb +2 -2
  22. data/lib/rigor/cli.rb +9 -1
  23. data/lib/rigor/configuration/dependencies.rb +2 -2
  24. data/lib/rigor/configuration.rb +2 -2
  25. data/lib/rigor/environment.rb +35 -4
  26. data/lib/rigor/flow_contribution/conflict.rb +2 -2
  27. data/lib/rigor/flow_contribution/element.rb +1 -1
  28. data/lib/rigor/flow_contribution/fact.rb +1 -1
  29. data/lib/rigor/flow_contribution/merge_result.rb +1 -1
  30. data/lib/rigor/flow_contribution/merger.rb +3 -3
  31. data/lib/rigor/flow_contribution.rb +2 -2
  32. data/lib/rigor/inference/block_parameter_binder.rb +0 -2
  33. data/lib/rigor/inference/coverage_scanner.rb +1 -1
  34. data/lib/rigor/inference/expression_typer.rb +67 -11
  35. data/lib/rigor/inference/fallback.rb +1 -1
  36. data/lib/rigor/inference/method_dispatcher/block_folding.rb +3 -5
  37. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +0 -12
  38. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -3
  39. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +1 -1
  40. data/lib/rigor/inference/method_dispatcher/method_folding.rb +118 -0
  41. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +6 -11
  42. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +27 -11
  43. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +0 -4
  44. data/lib/rigor/inference/method_dispatcher.rb +146 -2
  45. data/lib/rigor/inference/method_parameter_binder.rb +1 -3
  46. data/lib/rigor/inference/narrowing.rb +2 -4
  47. data/lib/rigor/inference/rbs_type_translator.rb +0 -2
  48. data/lib/rigor/inference/scope_indexer.rb +14 -9
  49. data/lib/rigor/inference/statement_evaluator.rb +7 -7
  50. data/lib/rigor/plugin/io_boundary.rb +0 -2
  51. data/lib/rigor/plugin/loader.rb +2 -2
  52. data/lib/rigor/plugin/manifest.rb +30 -9
  53. data/lib/rigor/plugin/registry.rb +11 -0
  54. data/lib/rigor/plugin/services.rb +1 -1
  55. data/lib/rigor/plugin/type_node_resolver.rb +52 -0
  56. data/lib/rigor/plugin.rb +1 -0
  57. data/lib/rigor/rbs_extended/reporter.rb +91 -0
  58. data/lib/rigor/rbs_extended.rb +131 -32
  59. data/lib/rigor/scope.rb +25 -8
  60. data/lib/rigor/sig_gen/classification.rb +36 -0
  61. data/lib/rigor/sig_gen/generator.rb +1048 -0
  62. data/lib/rigor/sig_gen/layout_index.rb +108 -0
  63. data/lib/rigor/sig_gen/method_candidate.rb +62 -0
  64. data/lib/rigor/sig_gen/observation_collector.rb +391 -0
  65. data/lib/rigor/sig_gen/observed_call.rb +62 -0
  66. data/lib/rigor/sig_gen/path_mapper.rb +116 -0
  67. data/lib/rigor/sig_gen/renderer.rb +157 -0
  68. data/lib/rigor/sig_gen/type_elaborator.rb +92 -0
  69. data/lib/rigor/sig_gen/write_result.rb +48 -0
  70. data/lib/rigor/sig_gen/writer.rb +530 -0
  71. data/lib/rigor/sig_gen.rb +25 -0
  72. data/lib/rigor/type/bound_method.rb +79 -0
  73. data/lib/rigor/type/combinator.rb +195 -2
  74. data/lib/rigor/type/constant.rb +13 -0
  75. data/lib/rigor/type/hash_shape.rb +0 -2
  76. data/lib/rigor/type/union.rb +20 -1
  77. data/lib/rigor/type.rb +1 -0
  78. data/lib/rigor/type_node/generic.rb +62 -0
  79. data/lib/rigor/type_node/identifier.rb +30 -0
  80. data/lib/rigor/type_node/indexed_access.rb +41 -0
  81. data/lib/rigor/type_node/integer_literal.rb +29 -0
  82. data/lib/rigor/type_node/name_scope.rb +52 -0
  83. data/lib/rigor/type_node/resolver_chain.rb +56 -0
  84. data/lib/rigor/type_node/string_literal.rb +29 -0
  85. data/lib/rigor/type_node/symbol_literal.rb +28 -0
  86. data/lib/rigor/type_node/union.rb +42 -0
  87. data/lib/rigor/type_node.rb +29 -0
  88. data/lib/rigor/version.rb +1 -1
  89. data/lib/rigor.rb +2 -0
  90. data/sig/rigor/analysis/check_rules/always_truthy_condition_collector.rbs +10 -0
  91. data/sig/rigor/analysis/check_rules/dead_assignment_collector.rbs +10 -0
  92. data/sig/rigor/analysis/dependency_source_inference/gem_resolver.rbs +25 -0
  93. data/sig/rigor/analysis/dependency_source_inference/index.rbs +9 -0
  94. data/sig/rigor/cli/diff_command.rbs +4 -0
  95. data/sig/rigor/cli/explain_command.rbs +4 -0
  96. data/sig/rigor/cli/sig_gen_command.rbs +4 -0
  97. data/sig/rigor/cli/type_scan_command.rbs +3 -0
  98. data/sig/rigor/environment.rbs +5 -2
  99. data/sig/rigor/inference/builtins/method_catalog.rbs +4 -0
  100. data/sig/rigor/inference/builtins/numeric_catalog.rbs +3 -0
  101. data/sig/rigor/inference/builtins.rbs +2 -0
  102. data/sig/rigor/plugin/access_denied_error.rbs +3 -0
  103. data/sig/rigor/plugin/base.rbs +6 -0
  104. data/sig/rigor/plugin/fact_store.rbs +11 -0
  105. data/sig/rigor/plugin/io_boundary.rbs +4 -0
  106. data/sig/rigor/plugin/load_error.rbs +6 -0
  107. data/sig/rigor/plugin/loader.rbs +20 -0
  108. data/sig/rigor/plugin/manifest.rbs +9 -0
  109. data/sig/rigor/plugin/registry.rbs +3 -0
  110. data/sig/rigor/plugin/services.rbs +3 -0
  111. data/sig/rigor/plugin/trust_policy.rbs +4 -0
  112. data/sig/rigor/plugin/type_node_resolver.rbs +3 -0
  113. data/sig/rigor/plugin.rbs +8 -0
  114. data/sig/rigor/scope.rbs +4 -2
  115. data/sig/rigor/type.rbs +28 -6
  116. 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
 
@@ -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
@@ -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).join(" | ")
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
@@ -23,5 +23,6 @@ require_relative "type/union"
23
23
  require_relative "type/difference"
24
24
  require_relative "type/refined"
25
25
  require_relative "type/intersection"
26
+ require_relative "type/bound_method"
26
27
  require_relative "type/accepts_result"
27
28
  require_relative "type/combinator"
@@ -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