rigortype 0.1.3 → 0.1.5

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 (149) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +154 -33
  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 +26 -6
  12. data/lib/rigor/analysis/result.rb +11 -3
  13. data/lib/rigor/analysis/rule_catalog.rb +2 -2
  14. data/lib/rigor/analysis/run_stats.rb +193 -0
  15. data/lib/rigor/analysis/runner.rb +498 -12
  16. data/lib/rigor/analysis/worker_session.rb +327 -0
  17. data/lib/rigor/builtins/imported_refinements.rb +364 -55
  18. data/lib/rigor/builtins/regex_refinement.rb +17 -12
  19. data/lib/rigor/cache/descriptor.rb +1 -1
  20. data/lib/rigor/cache/rbs_descriptor.rb +3 -1
  21. data/lib/rigor/cache/store.rb +39 -6
  22. data/lib/rigor/cli/diff_command.rb +1 -1
  23. data/lib/rigor/cli/sig_gen_command.rb +173 -0
  24. data/lib/rigor/cli/type_of_command.rb +1 -1
  25. data/lib/rigor/cli/type_scan_renderer.rb +1 -1
  26. data/lib/rigor/cli/type_scan_report.rb +2 -2
  27. data/lib/rigor/cli.rb +61 -3
  28. data/lib/rigor/configuration/dependencies.rb +2 -2
  29. data/lib/rigor/configuration.rb +131 -6
  30. data/lib/rigor/environment/bundle_sig_discovery.rb +198 -0
  31. data/lib/rigor/environment/class_registry.rb +12 -3
  32. data/lib/rigor/environment/lockfile_resolver.rb +125 -0
  33. data/lib/rigor/environment/rbs_collection_discovery.rb +126 -0
  34. data/lib/rigor/environment/rbs_coverage_report.rb +112 -0
  35. data/lib/rigor/environment/rbs_loader.rb +194 -6
  36. data/lib/rigor/environment/reflection.rb +152 -0
  37. data/lib/rigor/environment.rb +109 -6
  38. data/lib/rigor/flow_contribution/conflict.rb +2 -2
  39. data/lib/rigor/flow_contribution/element.rb +1 -1
  40. data/lib/rigor/flow_contribution/fact.rb +1 -1
  41. data/lib/rigor/flow_contribution/merge_result.rb +1 -1
  42. data/lib/rigor/flow_contribution/merger.rb +3 -3
  43. data/lib/rigor/flow_contribution.rb +2 -2
  44. data/lib/rigor/inference/acceptance.rb +35 -1
  45. data/lib/rigor/inference/block_parameter_binder.rb +0 -2
  46. data/lib/rigor/inference/builtins/method_catalog.rb +12 -5
  47. data/lib/rigor/inference/builtins/numeric_catalog.rb +15 -4
  48. data/lib/rigor/inference/coverage_scanner.rb +1 -1
  49. data/lib/rigor/inference/expression_typer.rb +77 -11
  50. data/lib/rigor/inference/fallback.rb +1 -1
  51. data/lib/rigor/inference/macro_block_self_type.rb +96 -0
  52. data/lib/rigor/inference/method_dispatcher/block_folding.rb +3 -5
  53. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +29 -41
  54. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -3
  55. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +4 -4
  56. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +1 -1
  57. data/lib/rigor/inference/method_dispatcher/method_folding.rb +135 -0
  58. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +7 -12
  59. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +27 -11
  60. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +46 -44
  61. data/lib/rigor/inference/method_dispatcher.rb +274 -5
  62. data/lib/rigor/inference/method_parameter_binder.rb +22 -14
  63. data/lib/rigor/inference/narrowing.rb +129 -12
  64. data/lib/rigor/inference/rbs_type_translator.rb +0 -2
  65. data/lib/rigor/inference/scope_indexer.rb +14 -9
  66. data/lib/rigor/inference/statement_evaluator.rb +7 -7
  67. data/lib/rigor/inference/synthetic_method.rb +86 -0
  68. data/lib/rigor/inference/synthetic_method_index.rb +82 -0
  69. data/lib/rigor/inference/synthetic_method_scanner.rb +521 -0
  70. data/lib/rigor/plugin/blueprint.rb +60 -0
  71. data/lib/rigor/plugin/io_boundary.rb +0 -2
  72. data/lib/rigor/plugin/loader.rb +5 -3
  73. data/lib/rigor/plugin/macro/block_as_method.rb +131 -0
  74. data/lib/rigor/plugin/macro/external_file.rb +143 -0
  75. data/lib/rigor/plugin/macro/heredoc_template.rb +201 -0
  76. data/lib/rigor/plugin/macro/trait_registry.rb +198 -0
  77. data/lib/rigor/plugin/macro.rb +31 -0
  78. data/lib/rigor/plugin/manifest.rb +102 -10
  79. data/lib/rigor/plugin/registry.rb +43 -2
  80. data/lib/rigor/plugin/services.rb +1 -1
  81. data/lib/rigor/plugin/type_node_resolver.rb +52 -0
  82. data/lib/rigor/plugin.rb +2 -0
  83. data/lib/rigor/rbs_extended/reporter.rb +91 -0
  84. data/lib/rigor/rbs_extended.rb +131 -32
  85. data/lib/rigor/scope.rb +25 -8
  86. data/lib/rigor/sig_gen/classification.rb +36 -0
  87. data/lib/rigor/sig_gen/generator.rb +1048 -0
  88. data/lib/rigor/sig_gen/layout_index.rb +108 -0
  89. data/lib/rigor/sig_gen/method_candidate.rb +62 -0
  90. data/lib/rigor/sig_gen/observation_collector.rb +391 -0
  91. data/lib/rigor/sig_gen/observed_call.rb +62 -0
  92. data/lib/rigor/sig_gen/path_mapper.rb +116 -0
  93. data/lib/rigor/sig_gen/renderer.rb +157 -0
  94. data/lib/rigor/sig_gen/type_elaborator.rb +92 -0
  95. data/lib/rigor/sig_gen/write_result.rb +48 -0
  96. data/lib/rigor/sig_gen/writer.rb +530 -0
  97. data/lib/rigor/sig_gen.rb +25 -0
  98. data/lib/rigor/trinary.rb +15 -11
  99. data/lib/rigor/type/bot.rb +6 -3
  100. data/lib/rigor/type/bound_method.rb +79 -0
  101. data/lib/rigor/type/combinator.rb +207 -3
  102. data/lib/rigor/type/constant.rb +13 -0
  103. data/lib/rigor/type/hash_shape.rb +0 -2
  104. data/lib/rigor/type/integer_range.rb +7 -7
  105. data/lib/rigor/type/refined.rb +18 -12
  106. data/lib/rigor/type/top.rb +4 -3
  107. data/lib/rigor/type/union.rb +20 -1
  108. data/lib/rigor/type.rb +1 -0
  109. data/lib/rigor/type_node/generic.rb +68 -0
  110. data/lib/rigor/type_node/identifier.rb +38 -0
  111. data/lib/rigor/type_node/indexed_access.rb +41 -0
  112. data/lib/rigor/type_node/integer_literal.rb +29 -0
  113. data/lib/rigor/type_node/name_scope.rb +52 -0
  114. data/lib/rigor/type_node/resolver_chain.rb +56 -0
  115. data/lib/rigor/type_node/string_literal.rb +32 -0
  116. data/lib/rigor/type_node/symbol_literal.rb +28 -0
  117. data/lib/rigor/type_node/union.rb +42 -0
  118. data/lib/rigor/type_node.rb +29 -0
  119. data/lib/rigor/version.rb +1 -1
  120. data/lib/rigor.rb +2 -0
  121. data/sig/rigor/analysis/check_rules/always_truthy_condition_collector.rbs +10 -0
  122. data/sig/rigor/analysis/check_rules/dead_assignment_collector.rbs +10 -0
  123. data/sig/rigor/analysis/dependency_source_inference/gem_resolver.rbs +25 -0
  124. data/sig/rigor/analysis/dependency_source_inference/index.rbs +9 -0
  125. data/sig/rigor/cli/diff_command.rbs +4 -0
  126. data/sig/rigor/cli/explain_command.rbs +4 -0
  127. data/sig/rigor/cli/sig_gen_command.rbs +4 -0
  128. data/sig/rigor/cli/type_scan_command.rbs +3 -0
  129. data/sig/rigor/environment.rbs +8 -2
  130. data/sig/rigor/inference/builtins/method_catalog.rbs +4 -0
  131. data/sig/rigor/inference/builtins/numeric_catalog.rbs +3 -0
  132. data/sig/rigor/inference/builtins.rbs +2 -0
  133. data/sig/rigor/plugin/access_denied_error.rbs +3 -0
  134. data/sig/rigor/plugin/base.rbs +6 -0
  135. data/sig/rigor/plugin/blueprint.rbs +7 -0
  136. data/sig/rigor/plugin/fact_store.rbs +11 -0
  137. data/sig/rigor/plugin/io_boundary.rbs +4 -0
  138. data/sig/rigor/plugin/load_error.rbs +6 -0
  139. data/sig/rigor/plugin/loader.rbs +20 -0
  140. data/sig/rigor/plugin/manifest.rbs +9 -0
  141. data/sig/rigor/plugin/registry.rbs +16 -0
  142. data/sig/rigor/plugin/services.rbs +3 -0
  143. data/sig/rigor/plugin/trust_policy.rbs +4 -0
  144. data/sig/rigor/plugin/type_node_resolver.rbs +3 -0
  145. data/sig/rigor/plugin.rbs +8 -0
  146. data/sig/rigor/scope.rbs +4 -2
  147. data/sig/rigor/type.rbs +28 -6
  148. data/sig/rigor.rbs +35 -2
  149. metadata +90 -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
@@ -35,8 +36,14 @@ module Rigor
35
36
  Bot.instance
36
37
  end
37
38
 
39
+ # ADR-15 Phase 4b.x — read the eagerly-allocated
40
+ # `@untyped` ivar instead of `||=`. The singleton-class
41
+ # `@untyped = Dynamic.new(top)` initializer runs at module
42
+ # body (below) on the main Ractor at load time. Workers
43
+ # READ the populated ivar without performing the lazy
44
+ # write that non-main Ractors are forbidden from doing.
38
45
  def untyped
39
- @untyped ||= Dynamic.new(top)
46
+ @untyped
40
47
  end
41
48
 
42
49
  # Wraps the static facet in a Dynamic[T] carrier. Idempotent on the
@@ -69,6 +76,14 @@ module Rigor
69
76
  Constant.new(value)
70
77
  end
71
78
 
79
+ # `Object#method(:name)` carrier. Stores the bound
80
+ # `(receiver, method_name)` pair so the dispatcher can
81
+ # substitute the original dispatch at `.call` / `.()` /
82
+ # `[]` time. See {Type::BoundMethod}.
83
+ def bound_method_of(receiver_type, method_name)
84
+ BoundMethod.new(receiver_type: receiver_type, method_name: method_name)
85
+ end
86
+
72
87
  # Bounded-integer carrier. Each bound is either an `Integer` or
73
88
  # one of `:neg_infinity` / `:pos_infinity` (sentinels exposed as
74
89
  # `IntegerRange::NEG_INFINITY` / `POS_INFINITY`).
@@ -347,7 +362,6 @@ module Rigor
347
362
  INT_MASK_UNION_LIMIT = 16
348
363
  private_constant :INT_MASK_FLAG_LIMIT, :INT_MASK_UNION_LIMIT
349
364
 
350
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
351
365
  def int_mask(flags)
352
366
  return nil unless flags.is_a?(Array) && flags.all?(Integer)
353
367
  return nil if flags.any?(&:negative?)
@@ -362,7 +376,6 @@ module Rigor
362
376
  integer_range(values.min, values.max)
363
377
  end
364
378
  end
365
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
366
379
 
367
380
  # `int_mask_of[T]` — derives the int_mask closure from
368
381
  # a finite integer-literal type:
@@ -401,6 +414,99 @@ module Rigor
401
414
  end
402
415
  end
403
416
 
417
+ # `pick_of[T, K]` shape-projection — keeps only the entries
418
+ # of `T` whose key is in the literal-key set extracted from
419
+ # `K`. ADR-13 § "Shape projection / Restriction and removal".
420
+ #
421
+ # Phase A handles `Type::HashShape` (literal-key K).
422
+ # Phase B (slice 5) extends to `Type::Tuple` (integer-index
423
+ # K) — `pick_of[Tuple[A, B, C], 0 | 2]` evaluates to
424
+ # `Tuple[A, C]`. Non-shape inputs (`Type::Nominal`, etc.)
425
+ # return `type` unchanged ("lossy degradation"; the
426
+ # `dynamic.shape.lossy-projection` diagnostic that flags
427
+ # the boundary lands when caller-side diagnostic threading
428
+ # arrives).
429
+ def pick_of(type, keys)
430
+ case type
431
+ when HashShape then hash_shape_pick(type, keys)
432
+ when Tuple then tuple_pick(type, keys)
433
+ else type
434
+ end
435
+ end
436
+
437
+ # `omit_of[T, K]` shape-projection — dual of {pick_of}.
438
+ # Drops the entries / positions whose key (or index, for a
439
+ # `Tuple`) is in the literal-key set extracted from `K`.
440
+ def omit_of(type, keys)
441
+ case type
442
+ when HashShape then hash_shape_omit(type, keys)
443
+ when Tuple then tuple_omit(type, keys)
444
+ else type
445
+ end
446
+ end
447
+
448
+ # `partial_of[T]` shape-projection — flips every required
449
+ # entry of `T` to optional. ADR-13 § "Required-ness flips".
450
+ # Does NOT add `nil` to value types — Rigor's HashShape
451
+ # distinguishes "key absent" from "key present with nil
452
+ # value", so flipping required-ness is sufficient.
453
+ def partial_of(type)
454
+ return type unless type.is_a?(HashShape)
455
+
456
+ HashShape.new(
457
+ type.pairs,
458
+ required_keys: [],
459
+ optional_keys: type.pairs.keys,
460
+ read_only_keys: type.read_only_keys,
461
+ extra_keys: type.extra_keys
462
+ )
463
+ end
464
+
465
+ # `required_of[T]` shape-projection — inverse of
466
+ # {partial_of}; flips every optional entry to required.
467
+ def required_of(type)
468
+ return type unless type.is_a?(HashShape)
469
+
470
+ HashShape.new(
471
+ type.pairs,
472
+ required_keys: type.pairs.keys,
473
+ optional_keys: [],
474
+ read_only_keys: type.read_only_keys,
475
+ extra_keys: type.extra_keys
476
+ )
477
+ end
478
+
479
+ # `readonly_of[T]` shape-projection — marks every entry of
480
+ # `T` as read-only in the current view. View-level only —
481
+ # does NOT prove the underlying Ruby Hash is frozen.
482
+ def readonly_of(type)
483
+ return type unless type.is_a?(HashShape)
484
+
485
+ HashShape.new(
486
+ type.pairs,
487
+ required_keys: type.required_keys,
488
+ optional_keys: type.optional_keys,
489
+ read_only_keys: type.pairs.keys,
490
+ extra_keys: type.extra_keys
491
+ )
492
+ end
493
+
494
+ # Predicate that a shape-projection (`pick_of`, `omit_of`,
495
+ # `partial_of`, `required_of`, `readonly_of`) would degrade
496
+ # to "input unchanged" on this carrier. Callers consult
497
+ # this BEFORE invoking the projection so they can emit a
498
+ # `dynamic.shape.lossy-projection` diagnostic at the site
499
+ # where the projection was authored.
500
+ #
501
+ # `HashShape` and `Tuple` carry shape-level information
502
+ # the projections honour; every other carrier is lossy.
503
+ # Slice 5b wires diagnostic emission through `RbsExtended`
504
+ # / parser callers; this predicate stands alone in slice 5
505
+ # for unit-test coverage and future composition.
506
+ def shape_projection_lossy?(type)
507
+ !type.is_a?(HashShape) && !type.is_a?(Tuple)
508
+ end
509
+
404
510
  class << self # rubocop:disable Metrics/ClassLength
405
511
  private
406
512
 
@@ -480,6 +586,99 @@ module Rigor
480
586
  end
481
587
  end
482
588
 
589
+ # Literal-key set extraction for {pick_of} / {omit_of}.
590
+ # Accepts `Constant<Symbol|String>` or `Union[Constant…]`
591
+ # where every member is such a Constant. Returns `nil`
592
+ # when the shape can't be reduced to a finite key set
593
+ # (untyped, Top, Difference, Refined, mixed-kind union,
594
+ # etc.) — callers degrade to "input unchanged" per
595
+ # ADR-13's lossy-projection rule.
596
+ def extract_constant_key_set(type)
597
+ case type
598
+ when Constant then constant_key_set(type)
599
+ when Union then union_key_set(type)
600
+ end
601
+ end
602
+
603
+ def constant_key_set(type)
604
+ literal_key?(type.value) ? [type.value] : nil
605
+ end
606
+
607
+ def union_key_set(type)
608
+ return nil unless type.members.all?(Constant)
609
+
610
+ values = type.members.map(&:value)
611
+ values.all? { |v| literal_key?(v) } ? values : nil
612
+ end
613
+
614
+ def literal_key?(value)
615
+ value.is_a?(Symbol) || value.is_a?(String)
616
+ end
617
+
618
+ # Rebuild a {HashShape} from the subset of `keys` the
619
+ # caller decided to keep. Preserves required / optional /
620
+ # read-only classification AND the extra-keys policy of
621
+ # the source shape; entries dropped from `pairs` also
622
+ # drop from each policy list. Used by both {pick_of}
623
+ # (intersection with K) and {omit_of} (set difference).
624
+ def rebuild_hash_shape_with_keys(shape, kept_keys)
625
+ HashShape.new(
626
+ shape.pairs.slice(*kept_keys),
627
+ required_keys: shape.required_keys.select { |k| kept_keys.include?(k) },
628
+ optional_keys: shape.optional_keys.select { |k| kept_keys.include?(k) },
629
+ read_only_keys: shape.read_only_keys.select { |k| kept_keys.include?(k) },
630
+ extra_keys: shape.extra_keys
631
+ )
632
+ end
633
+
634
+ def hash_shape_pick(type, keys)
635
+ key_set = extract_constant_key_set(keys)
636
+ return type if key_set.nil?
637
+
638
+ rebuild_hash_shape_with_keys(type, type.pairs.keys & key_set)
639
+ end
640
+
641
+ def hash_shape_omit(type, keys)
642
+ key_set = extract_constant_key_set(keys)
643
+ return type if key_set.nil?
644
+
645
+ rebuild_hash_shape_with_keys(type, type.pairs.keys - key_set)
646
+ end
647
+
648
+ # ADR-13 slice 5 — Tuple support. `K` MUST be a
649
+ # `Constant<Integer>` or `Union[Constant<Integer>, …]`;
650
+ # other K shapes (or non-integer Constants in a Union)
651
+ # return the input unchanged. Negative or out-of-range
652
+ # indices are dropped silently per slice 5's permissive
653
+ # take — surface diagnostics are slice 5b material.
654
+ def tuple_pick(type, keys)
655
+ index_set = extract_tuple_index_set(keys, type.elements.size)
656
+ return type if index_set.nil?
657
+
658
+ Tuple.new(index_set.map { |i| type.elements[i] })
659
+ end
660
+
661
+ def tuple_omit(type, keys)
662
+ index_set = extract_tuple_index_set(keys, type.elements.size)
663
+ return type if index_set.nil?
664
+
665
+ dropped = index_set.to_a
666
+ Tuple.new(type.elements.each_with_index.reject { |_, i| dropped.include?(i) }.map(&:first))
667
+ end
668
+
669
+ # Extracts a sorted, deduplicated set of in-range integer
670
+ # indices from a `Constant<Integer>` / `Union[Constant<Integer>, …]`
671
+ # carrier. Out-of-range indices are dropped silently; the
672
+ # caller decides whether an empty result still means
673
+ # "lossy projection" (current pick / omit just produce an
674
+ # empty Tuple).
675
+ def extract_tuple_index_set(type, size)
676
+ flags = extract_constant_int_set(type)
677
+ return nil if flags.nil?
678
+
679
+ flags.uniq.select { |i| i >= 0 && i < size }.sort
680
+ end
681
+
483
682
  def tuple_indexed_access(tuple, key)
484
683
  return top unless key.is_a?(Constant) && key.value.is_a?(Integer)
485
684
 
@@ -616,6 +815,11 @@ module Rigor
616
815
  members.sort_by { |m| m.describe(:short) }
617
816
  end
618
817
  end
818
+
819
+ # ADR-15 Phase 4b.x — eager-allocate the singleton
820
+ # `Dynamic[Top]` carrier on the main Ractor at load time.
821
+ # The `untyped` reader above just returns this ivar.
822
+ @untyped = Dynamic.new(Top.instance)
619
823
  end
620
824
  end
621
825
  end
@@ -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
@@ -78,13 +78,13 @@ module Rigor
78
78
  Float::INFINITY
79
79
  end
80
80
 
81
- ALIAS_NAMES = {
82
- [NEG_INFINITY, POS_INFINITY] => "int",
83
- [1, POS_INFINITY] => "positive-int",
84
- [0, POS_INFINITY] => "non-negative-int",
85
- [NEG_INFINITY, -1] => "negative-int",
86
- [NEG_INFINITY, 0] => "non-positive-int"
87
- }.freeze
81
+ ALIAS_NAMES = Ractor.make_shareable({
82
+ [NEG_INFINITY, POS_INFINITY] => "int",
83
+ [1, POS_INFINITY] => "positive-int",
84
+ [0, POS_INFINITY] => "non-negative-int",
85
+ [NEG_INFINITY, -1] => "negative-int",
86
+ [NEG_INFINITY, 0] => "non-positive-int"
87
+ })
88
88
 
89
89
  def describe(_verbosity = :short)
90
90
  ALIAS_NAMES[[min, max]] || generic_description
@@ -165,18 +165,24 @@ module Rigor
165
165
  # kebab-case canonical name. Registered shapes print
166
166
  # through `describe`; unregistered combinations fall back
167
167
  # to the operator form.
168
- CANONICAL_NAMES = {
169
- ["String", :lowercase] => "lowercase-string",
170
- ["String", :not_lowercase] => "non-lowercase-string",
171
- ["String", :uppercase] => "uppercase-string",
172
- ["String", :not_uppercase] => "non-uppercase-string",
173
- ["String", :numeric] => "numeric-string",
174
- ["String", :not_numeric] => "non-numeric-string",
175
- ["String", :decimal_int] => "decimal-int-string",
176
- ["String", :octal_int] => "octal-int-string",
177
- ["String", :hex_int] => "hex-int-string",
178
- ["String", :literal_string] => "literal-string"
179
- }.freeze
168
+ #
169
+ # ADR-15 Phase 4b.x — `Ractor.make_shareable` (not `.freeze`)
170
+ # because the keys are nested two-element Arrays. Plain
171
+ # `.freeze` would leave the inner arrays mutable, so a
172
+ # worker Ractor reading `CANONICAL_NAMES[[base, predicate]]`
173
+ # would trip `Ractor::IsolationError`.
174
+ CANONICAL_NAMES = Ractor.make_shareable({
175
+ ["String", :lowercase] => "lowercase-string",
176
+ ["String", :not_lowercase] => "non-lowercase-string",
177
+ ["String", :uppercase] => "uppercase-string",
178
+ ["String", :not_uppercase] => "non-uppercase-string",
179
+ ["String", :numeric] => "numeric-string",
180
+ ["String", :not_numeric] => "non-numeric-string",
181
+ ["String", :decimal_int] => "decimal-int-string",
182
+ ["String", :octal_int] => "octal-int-string",
183
+ ["String", :hex_int] => "hex-int-string",
184
+ ["String", :literal_string] => "literal-string"
185
+ })
180
186
  private_constant :CANONICAL_NAMES
181
187
 
182
188
  # Bidirectional `predicate_id ↔ complement_predicate_id`
@@ -7,10 +7,11 @@ module Rigor
7
7
  # The top of the value lattice: contains every value, including untyped
8
8
  # boundaries. See docs/type-specification/special-types.md.
9
9
  class Top
10
+ # ADR-15 Phase 4b.x — eager singleton (see Bot.rb).
11
+ @instance = new.freeze
12
+
10
13
  class << self
11
- def instance
12
- @instance ||= new.freeze
13
- end
14
+ attr_reader :instance
14
15
 
15
16
  private :new
16
17
  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,68 @@
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
+ # Freeze the String head + Array args so the Data
42
+ # object is `Ractor.shareable?`. Each `a` is already a
43
+ # shareable TypeNode value object (checked above), so
44
+ # freezing the wrapping Array is sufficient.
45
+ frozen_head = head.frozen? ? head : head.dup.freeze
46
+ frozen_args = args.frozen? ? args : args.dup.freeze
47
+ super(head: frozen_head, args: frozen_args)
48
+ end
49
+
50
+ private
51
+
52
+ # ADR-13 slice 3 expanded the accepted set to include
53
+ # {IntegerLiteral} so the parser can emit a uniform AST for
54
+ # `int<5, 10>` (angle bounds) and `int_mask[1, 2, 4]`
55
+ # (square-bracketed bitflag union). The follow-up further
56
+ # admits {SymbolLiteral} / {StringLiteral} / {IndexedAccess}
57
+ # / {Union} so `Pick[T, :a | "b"]` carries through to the
58
+ # resolver as a uniform AST. Slice 1 originally accepted
59
+ # only `Identifier` / `Generic`; every later addition stays
60
+ # additive — every slice-1-shape Generic remains valid.
61
+ def valid_arg?(arg)
62
+ arg.is_a?(Identifier) || arg.is_a?(Generic) || arg.is_a?(IntegerLiteral) ||
63
+ arg.is_a?(SymbolLiteral) || arg.is_a?(StringLiteral) ||
64
+ arg.is_a?(IndexedAccess) || arg.is_a?(Union)
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,38 @@
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
+ # Freeze the String field so the resulting Data object
27
+ # is `Ractor.shareable?` regardless of whether the
28
+ # caller passed a `# frozen_string_literal: true`
29
+ # constant or a dynamically built String. The same
30
+ # discipline applies to every other TypeNode value
31
+ # object — they live in the parser's hot path and are
32
+ # the natural carriers to flow through future Ractor
33
+ # boundaries (see CURRENT_WORK Open Items #8).
34
+ super(name: name.frozen? ? name : name.dup.freeze)
35
+ end
36
+ end
37
+ end
38
+ 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