rigortype 0.2.1 → 0.2.3

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 (109) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +41 -14
  3. data/docs/handbook/01-getting-started.md +311 -0
  4. data/docs/handbook/02-everyday-types.md +337 -0
  5. data/docs/handbook/03-narrowing.md +359 -0
  6. data/docs/handbook/04-tuples-and-shapes.md +321 -0
  7. data/docs/handbook/05-methods-and-blocks.md +339 -0
  8. data/docs/handbook/06-classes.md +305 -0
  9. data/docs/handbook/07-rbs-and-extended.md +427 -0
  10. data/docs/handbook/08-understanding-errors.md +373 -0
  11. data/docs/handbook/09-plugins.md +241 -0
  12. data/docs/handbook/10-sorbet.md +347 -0
  13. data/docs/handbook/11-sig-gen.md +312 -0
  14. data/docs/handbook/12-lightweight-hkt.md +333 -0
  15. data/docs/handbook/README.md +275 -0
  16. data/docs/handbook/appendix-elixir.md +370 -0
  17. data/docs/handbook/appendix-go.md +399 -0
  18. data/docs/handbook/appendix-java-csharp.md +470 -0
  19. data/docs/handbook/appendix-liskov.md +580 -0
  20. data/docs/handbook/appendix-mypy.md +370 -0
  21. data/docs/handbook/appendix-phpstan.md +338 -0
  22. data/docs/handbook/appendix-protocols-and-structural-typing.md +292 -0
  23. data/docs/handbook/appendix-rust.md +446 -0
  24. data/docs/handbook/appendix-steep.md +336 -0
  25. data/docs/handbook/appendix-type-theory.md +1662 -0
  26. data/docs/handbook/appendix-typeprof.md +416 -0
  27. data/docs/handbook/appendix-typescript.md +332 -0
  28. data/docs/install.md +189 -0
  29. data/docs/llms.txt +72 -0
  30. data/docs/manual/01-installation.md +342 -0
  31. data/docs/manual/02-cli-reference.md +569 -0
  32. data/docs/manual/03-configuration.md +152 -0
  33. data/docs/manual/04-diagnostics.md +206 -0
  34. data/docs/manual/05-inspecting-types.md +109 -0
  35. data/docs/manual/06-baseline.md +104 -0
  36. data/docs/manual/07-plugins.md +92 -0
  37. data/docs/manual/08-skills.md +143 -0
  38. data/docs/manual/09-editor-integration.md +245 -0
  39. data/docs/manual/10-mcp-server.md +539 -0
  40. data/docs/manual/11-ci.md +274 -0
  41. data/docs/manual/12-caching.md +116 -0
  42. data/docs/manual/13-troubleshooting.md +120 -0
  43. data/docs/manual/14-rails-quickstart.md +332 -0
  44. data/docs/manual/15-type-protection-coverage.md +204 -0
  45. data/docs/manual/16-rbs-extended-annotations.md +190 -0
  46. data/docs/manual/17-driving-improvement.md +160 -0
  47. data/docs/manual/README.md +87 -0
  48. data/docs/manual/ci-templates/README.md +58 -0
  49. data/docs/manual/plugins/README.md +86 -0
  50. data/docs/manual/plugins/rigor-actioncable.md +78 -0
  51. data/docs/manual/plugins/rigor-actionmailer.md +74 -0
  52. data/docs/manual/plugins/rigor-actionpack.md +80 -0
  53. data/docs/manual/plugins/rigor-activejob.md +58 -0
  54. data/docs/manual/plugins/rigor-activerecord.md +102 -0
  55. data/docs/manual/plugins/rigor-activestorage.md +74 -0
  56. data/docs/manual/plugins/rigor-activesupport-core-ext.md +86 -0
  57. data/docs/manual/plugins/rigor-devise.md +70 -0
  58. data/docs/manual/plugins/rigor-dry-schema.md +56 -0
  59. data/docs/manual/plugins/rigor-dry-struct.md +60 -0
  60. data/docs/manual/plugins/rigor-dry-types.md +59 -0
  61. data/docs/manual/plugins/rigor-dry-validation.md +62 -0
  62. data/docs/manual/plugins/rigor-factorybot.md +76 -0
  63. data/docs/manual/plugins/rigor-graphql.md +89 -0
  64. data/docs/manual/plugins/rigor-hanami.md +83 -0
  65. data/docs/manual/plugins/rigor-mangrove.md +73 -0
  66. data/docs/manual/plugins/rigor-minitest.md +86 -0
  67. data/docs/manual/plugins/rigor-pundit.md +72 -0
  68. data/docs/manual/plugins/rigor-rails-i18n.md +92 -0
  69. data/docs/manual/plugins/rigor-rails-routes.md +94 -0
  70. data/docs/manual/plugins/rigor-rails.md +44 -0
  71. data/docs/manual/plugins/rigor-rbs-inline.md +83 -0
  72. data/docs/manual/plugins/rigor-rspec-rails.md +72 -0
  73. data/docs/manual/plugins/rigor-rspec.md +86 -0
  74. data/docs/manual/plugins/rigor-shoulda-matchers.md +78 -0
  75. data/docs/manual/plugins/rigor-sidekiq.md +78 -0
  76. data/docs/manual/plugins/rigor-sinatra.md +61 -0
  77. data/docs/manual/plugins/rigor-sorbet.md +63 -0
  78. data/docs/manual/plugins/rigor-statesman.md +75 -0
  79. data/docs/manual/plugins/rigor-typescript-utility-types.md +71 -0
  80. data/exe/rigor +1 -1
  81. data/lib/rigor/analysis/incremental_session.rb +4 -2
  82. data/lib/rigor/analysis/run_stats.rb +13 -1
  83. data/lib/rigor/analysis/runner.rb +54 -12
  84. data/lib/rigor/cli/check_command.rb +1 -1
  85. data/lib/rigor/cli/docs_command.rb +248 -0
  86. data/lib/rigor/cli/skill_command.rb +103 -41
  87. data/lib/rigor/cli/skill_describe.rb +346 -0
  88. data/lib/rigor/cli/triage_command.rb +8 -2
  89. data/lib/rigor/cli/triage_renderer.rb +4 -0
  90. data/lib/rigor/cli.rb +25 -3
  91. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +124 -32
  92. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +37 -6
  93. data/lib/rigor/inference/scope_indexer.rb +87 -89
  94. data/lib/rigor/plugin/isolation.rb +5 -5
  95. data/lib/rigor/plugin/loader.rb +4 -2
  96. data/lib/rigor/triage/catalogue.rb +16 -1
  97. data/lib/rigor/triage.rb +30 -7
  98. data/lib/rigor/version.rb +1 -1
  99. data/skills/rigor-ask/SKILL.md +172 -0
  100. data/skills/rigor-doctor/SKILL.md +87 -0
  101. data/skills/rigor-editor-setup/SKILL.md +114 -0
  102. data/skills/rigor-mcp-setup/SKILL.md +117 -0
  103. data/skills/rigor-monkeypatch-resolve/SKILL.md +79 -0
  104. data/skills/rigor-next-steps/SKILL.md +113 -0
  105. data/skills/rigor-plugin-tune/SKILL.md +79 -0
  106. data/skills/rigor-protection-uplift/SKILL.md +133 -0
  107. data/skills/rigor-rbs-setup/SKILL.md +128 -0
  108. data/skills/rigor-upgrade/SKILL.md +79 -0
  109. metadata +90 -1
@@ -51,7 +51,15 @@ module Rigor
51
51
  NUMERIC_BINARY = Set[
52
52
  :+, :-, :*, :/, :%, :**, :&, :|, :^, :<<, :>>,
53
53
  :<, :<=, :>, :>=, :==, :!=, :<=>,
54
- :gcd, :lcm, :fdiv, :quo, :ceildiv, :[]
54
+ :gcd, :lcm, :fdiv, :quo, :ceildiv, :[],
55
+ # Integer bit-test predicates (`(self & mask) <=> mask|0`). The
56
+ # catalog marks them `:dispatch` only because a non-Integer mask
57
+ # would route through `to_int`; a concrete Integer literal never
58
+ # does, so the fold is pure here — the sibling of the already-folded
59
+ # bit-reference `:[]`. Integer-only, but Float-safe to list: a Float
60
+ # receiver has no such method, so `invoke_binary` rescues the
61
+ # `NoMethodError` to nil and the RBS tier answers.
62
+ :allbits?, :anybits?, :nobits?
55
63
  ].freeze
56
64
  STRING_BINARY = Set[
57
65
  :+, :*, :==, :!=, :<, :<=, :>, :>=, :<=>,
@@ -86,6 +94,17 @@ module Rigor
86
94
  # delegates to operand `==`); ordering is undefined for Complex.
87
95
  COMPLEX_BINARY = Set[:+, :-, :*, :/, :**].freeze
88
96
 
97
+ # `Set#&` and its alias `Set#intersection` are leaf-pure for a
98
+ # concrete Set operand exactly like their siblings `|` / `-` / `^`
99
+ # (all `:leaf` in the catalog), but the catalog flags
100
+ # `set_i_intersection`'s C body `block_dependent` — it drives Set's
101
+ # own internal iterator — so the catalog tier declines and the
102
+ # intersection alone fails to fold. A concrete Set argument's `each`
103
+ # is the pure core method, so the fold is sound; the hand-rolled
104
+ # allow-list is the right tool, mirroring the bit-test predicates.
105
+ # (The other binary set ops keep folding through the catalog.)
106
+ SET_BINARY = Set[:&, :intersection].freeze
107
+
89
108
  # v0.0.3 C — pure unary catalogue. Each method must:
90
109
  # - take zero arguments,
91
110
  # - have no side effects,
@@ -130,6 +149,19 @@ module Rigor
130
149
  :abs, :magnitude, :floor, :ceil, :round, :truncate,
131
150
  :next_float, :prev_float,
132
151
  :to_s, :to_i, :to_int, :to_f, :to_r, :rationalize,
152
+ # `numerator` / `denominator` expose the rational
153
+ # decomposition of the float (`2.5.numerator → 5`,
154
+ # `.denominator → 2`) — pure arithmetic, the Float siblings
155
+ # of the already-folded Rational accessors. The non-finite
156
+ # edges stay sound: `Infinity.numerator → Infinity` /
157
+ # `.denominator → 1` fold to the same value Ruby returns, and
158
+ # `NaN.numerator → NaN` is declined by `foldable_constant_value?`.
159
+ :numerator, :denominator,
160
+ # `arg` / `angle` / `phase` (aliases) return the complex
161
+ # argument of the real number: `0` for `self >= 0`, `Math::PI`
162
+ # for `self < 0`. Pure sign test, deterministic; a NaN
163
+ # receiver yields NaN which `foldable_constant_value?` declines.
164
+ :arg, :angle, :phase,
133
165
  :inspect, :-@, :+@
134
166
  ].freeze
135
167
  STRING_UNARY = Set[
@@ -138,7 +170,12 @@ module Rigor
138
170
  :empty?, :strip, :lstrip, :rstrip, :chomp, :chop, :squeeze,
139
171
  :to_s, :to_str, :to_sym, :intern,
140
172
  :to_i, :to_f, :ord, :chr, :hex, :oct, :succ, :next,
141
- :sum, :inspect
173
+ :sum, :inspect,
174
+ # `shellescape` is the String-receiver twin of the already-folded
175
+ # `Shellwords.escape` — deterministic shell-quoting, no global
176
+ # state. The `shellwords` library is loaded process-wide via
177
+ # `shellwords_folding`, so the method is always defined here.
178
+ :shellescape
142
179
  ].freeze
143
180
  SYMBOL_UNARY = Set[
144
181
  :to_s, :to_sym, :to_proc, :length, :size,
@@ -386,26 +423,8 @@ module Rigor
386
423
  end
387
424
 
388
425
  def try_fold_unary_set(receiver_values, method_name)
389
- range_lift = try_fold_range_constant_unary(receiver_values, method_name)
390
- return range_lift if range_lift
391
-
392
- string_lift = try_fold_string_array_unary(receiver_values, method_name)
393
- return string_lift if string_lift
394
-
395
- pathname_lift = try_fold_pathname_unary(receiver_values, method_name)
396
- return pathname_lift if pathname_lift
397
-
398
- regexp_lift = try_fold_regexp_array_unary(receiver_values, method_name)
399
- return regexp_lift if regexp_lift
400
-
401
- set_lift = try_fold_set_array_unary(receiver_values, method_name)
402
- return set_lift if set_lift
403
-
404
- integer_lift = try_fold_integer_array_unary(receiver_values, method_name)
405
- return integer_lift if integer_lift
406
-
407
- numeric_lift = try_fold_numeric_array_unary(receiver_values, method_name)
408
- return numeric_lift if numeric_lift
426
+ special = try_fold_unary_special(receiver_values, method_name)
427
+ return special if special
409
428
 
410
429
  # Type-level allow check on every receiver. If one member's
411
430
  # type does not have the method in its allow list (e.g.
@@ -420,6 +439,23 @@ module Rigor
420
439
  end
421
440
  build_constant_type(results, source: receiver_values)
422
441
  end
442
+
443
+ # The carrier-specific unary lifts — Range-to-Tuple, the
444
+ # Array-returning String / Pathname / Regexp / Set / Integer /
445
+ # Numeric folds — that produce a precise structural type before
446
+ # the generic scalar `invoke_unary` path. The first match wins;
447
+ # nil means none applied and the caller falls through to the
448
+ # scalar allow-list path.
449
+ def try_fold_unary_special(receiver_values, method_name)
450
+ try_fold_range_constant_unary(receiver_values, method_name) ||
451
+ try_fold_string_array_unary(receiver_values, method_name) ||
452
+ try_fold_pathname_unary(receiver_values, method_name) ||
453
+ try_fold_pathname_array_unary(receiver_values, method_name) ||
454
+ try_fold_regexp_array_unary(receiver_values, method_name) ||
455
+ try_fold_set_array_unary(receiver_values, method_name) ||
456
+ try_fold_integer_array_unary(receiver_values, method_name) ||
457
+ try_fold_numeric_array_unary(receiver_values, method_name)
458
+ end
423
459
  # v0.0.7 — `Constant<Range>#to_a` and the no-arg
424
460
  # `first` / `last` / `min` / `max` short-circuit through a
425
461
  # Range-specific arm that catalog dispatch cannot reach:
@@ -436,10 +472,13 @@ module Rigor
436
472
  RANGE_FOLD_METHODS = Set[:to_a, :first, :last, :min, :max, :count, :size, :length, :entries, :minmax,
437
473
  :sum].freeze
438
474
  # 1-arg head/tail projections on a `Constant<Range>`. `first(n)` /
439
- # `take(n)` return the first `n` elements, `last(n)` the final `n` —
440
- # each lifts to a per-position `Tuple[Constant[Integer]…]`. The
441
- # no-arg `first` / `last` stay on the unary path (single Integer).
442
- RANGE_FOLD_BINARY_METHODS = Set[:first, :last, :take].freeze
475
+ # `take(n)` return the first `n` elements, `last(n)` the final `n`,
476
+ # and `min(n)` / `max(n)` the n smallest / largest (for an ascending
477
+ # integer range `min(n) == first(n)` and `max(n) == last(n).reverse`)
478
+ # each lifts to a per-position `Tuple[Constant[Integer]…]`. The
479
+ # no-arg `first` / `last` / `min` / `max` stay on the unary path
480
+ # (single Integer endpoint).
481
+ RANGE_FOLD_BINARY_METHODS = Set[:first, :last, :take, :min, :max].freeze
443
482
  RANGE_TO_A_LIMIT = 16
444
483
  private_constant :RANGE_FOLD_METHODS, :RANGE_FOLD_BINARY_METHODS, :RANGE_TO_A_LIMIT
445
484
 
@@ -518,17 +557,31 @@ module Rigor
518
557
 
519
558
  def range_take_tuple(range, method_name, count)
520
559
  return nil unless count.is_a?(Integer) && !count.negative?
521
- # `first(n)`/`last(n)`/`take(n)` materialise at most `min(n, size)`
522
- # elements; cap that count so a huge `n` (or range) never blows up
523
- # the Constant. `Range#size` is O(1) for integer endpoints.
560
+ # `first(n)`/`last(n)`/`take(n)`/`min(n)`/`max(n)` materialise at
561
+ # most `min(n, size)` elements; cap that count so a huge `n` (or
562
+ # range) never blows up the Constant. `Range#size` and the head/
563
+ # tail projections are O(n) for integer endpoints (no full
564
+ # materialisation).
524
565
  return nil if [count, range.size].min > RANGE_TO_A_LIMIT
525
566
 
526
- values = method_name == :last ? range.last(count) : range.first(count)
567
+ values = range_head_tail(range, method_name, count)
527
568
  return Type::Combinator.tuple_of if values.empty?
528
569
 
529
570
  Type::Combinator.tuple_of(*values.map { |v| Type::Combinator.constant_of(v) })
530
571
  end
531
572
 
573
+ # The n elements a head/tail projection selects, in Ruby's order.
574
+ # For an ascending integer range `min(n)` is the leading `n`
575
+ # (`first(n)`) and `max(n)` the trailing `n` reversed (descending),
576
+ # so neither needs the full sort `Array#min`/`#max` would do.
577
+ def range_head_tail(range, method_name, count)
578
+ case method_name
579
+ when :last then range.last(count)
580
+ when :max then range.last(count).reverse
581
+ else range.first(count) # :first, :take, :min
582
+ end
583
+ end
584
+
532
585
  def try_fold_binary_set(receiver_values, method_name, arg_values)
533
586
  range_lift = try_fold_range_constant_binary(receiver_values, method_name, arg_values)
534
587
  return range_lift if range_lift
@@ -559,7 +612,13 @@ module Rigor
559
612
  # bounded for long strings. (`codepoints` yields per-character
560
613
  # Integer codepoints, the sibling of the byte-valued `bytes`;
561
614
  # `grapheme_clusters` is the extended-grapheme sibling of `chars`.)
562
- STRING_ARRAY_UNARY_METHODS = Set[:chars, :bytes, :codepoints, :grapheme_clusters, :lines, :split].freeze
615
+ # `shellsplit` is the String-receiver twin of the already-folded
616
+ # `Shellwords.split` — lifts the token Array to a Tuple. Raises
617
+ # `ArgumentError` on unmatched quotes, which `try_fold_string_array_unary`
618
+ # rescues to nil (RBS tier widens). `shellwords` is loaded process-wide
619
+ # via `shellwords_folding`.
620
+ STRING_ARRAY_UNARY_METHODS = Set[:chars, :bytes, :codepoints, :grapheme_clusters,
621
+ :lines, :split, :shellsplit].freeze
563
622
  # `partition` / `rpartition` always return a fixed 3-element
564
623
  # `[head, separator, tail]` Array whose members are substrings of
565
624
  # the receiver (bounded by the input), so they lift to a precise
@@ -623,10 +682,26 @@ module Rigor
623
682
  ].freeze
624
683
  PATHNAME_PURE_BINARY = Set[
625
684
  :+, :join, :sub_ext, :<=>, :==, :eql?, :===,
626
- :relative_path_from
685
+ :relative_path_from,
686
+ # `/` is the exact alias of `+` (`def /(other) = self + other`),
687
+ # the idiomatic path-join operator (`dir / "file"`). `basename`'s
688
+ # 1-arg suffix-stripping form (`path.basename(".rb")` → the stem)
689
+ # is the binary sibling of the already-folded no-arg `basename` —
690
+ # both are pure `@path` string manipulation, no filesystem read.
691
+ :/, :basename
627
692
  ].freeze
628
693
  private_constant :PATHNAME_PURE_UNARY, :PATHNAME_PURE_BINARY
629
694
 
695
+ # `Constant<Pathname>#split` returns the fixed 2-element
696
+ # `[dirname, basename]` pair (both Pathname), the path-string
697
+ # split of `File.split`. Lifted to `Tuple[Constant[Pathname],
698
+ # Constant[Pathname]]`. Filesystem-independent — reads only
699
+ # `@path` — so it is deterministic at fold time, the
700
+ # Array-returning sibling of the scalar `basename` / `dirname`
701
+ # folds (which `try_fold_pathname_unary` already covers).
702
+ PATHNAME_ARRAY_UNARY_METHODS = Set[:split].freeze
703
+ private_constant :PATHNAME_ARRAY_UNARY_METHODS
704
+
630
705
  def try_fold_pathname_unary(receiver_values, method_name)
631
706
  return nil unless PATHNAME_PURE_UNARY.include?(method_name)
632
707
  return nil unless receiver_values.size == 1
@@ -659,6 +734,22 @@ module Rigor
659
734
  nil
660
735
  end
661
736
 
737
+ # `Constant<Pathname>#split` — lift the `[dirname, basename]`
738
+ # Pathname pair to a Tuple[Constant[Pathname], Constant[Pathname]].
739
+ # Pure path-string manipulation (no filesystem read); both
740
+ # elements are Pathname, a foldable Constant class.
741
+ def try_fold_pathname_array_unary(receiver_values, method_name)
742
+ return nil unless PATHNAME_ARRAY_UNARY_METHODS.include?(method_name)
743
+ return nil unless receiver_values.size == 1
744
+
745
+ receiver = receiver_values.first
746
+ return nil unless receiver.is_a?(Pathname)
747
+
748
+ lift_array_result(receiver.split)
749
+ rescue StandardError
750
+ nil
751
+ end
752
+
662
753
  def try_fold_string_array_unary(receiver_values, method_name)
663
754
  return nil unless STRING_ARRAY_UNARY_METHODS.include?(method_name)
664
755
  return nil unless receiver_values.size == 1
@@ -1500,6 +1591,7 @@ module Rigor
1500
1591
  when nil then NIL_BINARY
1501
1592
  when Rational then RATIONAL_BINARY
1502
1593
  when Complex then COMPLEX_BINARY
1594
+ when ::Set then SET_BINARY
1503
1595
  else Set.new
1504
1596
  end
1505
1597
  end
@@ -88,6 +88,7 @@ module Rigor
88
88
  to_h: :tuple_to_h,
89
89
  zip: :tuple_zip,
90
90
  :[] => :tuple_index,
91
+ slice: :tuple_index,
91
92
  fetch: :tuple_index,
92
93
  dig: :tuple_dig,
93
94
  values_at: :tuple_values_at,
@@ -842,7 +843,10 @@ module Rigor
842
843
 
843
844
  # `tuple.min` / `tuple.max` — fold when every element is
844
845
  # a `Constant` whose values share a Ruby-comparable
845
- # domain. Empty tuples fold to `Constant[nil]`.
846
+ # domain. Empty tuples fold to `Constant[nil]`. The 1-arg
847
+ # `min(n)` / `max(n)` form folds to a `Tuple` of the n
848
+ # edge-most values in Ruby's order (`min(n)` ascending,
849
+ # `max(n)` descending) — the n-arg sibling of `first(n)`.
846
850
  def tuple_min(tuple, _method_name, args)
847
851
  tuple_minmax(tuple, args, :min)
848
852
  end
@@ -852,7 +856,7 @@ module Rigor
852
856
  end
853
857
 
854
858
  def tuple_minmax(tuple, args, edge)
855
- return nil unless args.empty?
859
+ return tuple_minmax_n(tuple, args.first, edge) unless args.empty?
856
860
  return Type::Combinator.constant_of(nil) if tuple.elements.empty?
857
861
 
858
862
  values = constant_values(tuple.elements)
@@ -864,6 +868,25 @@ module Rigor
864
868
  nil
865
869
  end
866
870
 
871
+ # `tuple.min(n)` / `tuple.max(n)` — a `Tuple` of the n
872
+ # edge-most element values, delegating to Ruby's
873
+ # `Array#min` / `#max` for the ordering. Declines on a
874
+ # non-static / negative count or non-Constant elements.
875
+ # The result is bounded by the tuple's known arity, so no
876
+ # extra size cap is needed.
877
+ def tuple_minmax_n(tuple, arg, edge)
878
+ return nil unless arg.is_a?(Type::Constant) && arg.value.is_a?(Integer)
879
+ return nil if arg.value.negative?
880
+
881
+ values = constant_values(tuple.elements)
882
+ return nil if values.nil?
883
+
884
+ picked = values.public_send(edge, arg.value)
885
+ Type::Combinator.tuple_of(*picked.map { |v| Type::Combinator.constant_of(v) })
886
+ rescue StandardError
887
+ nil
888
+ end
889
+
867
890
  # `tuple.minmax` — the `[min, max]` pair as a 2-slot
868
891
  # `Tuple[Constant[min], Constant[max]]`, mirroring the
869
892
  # `Range#minmax` fold. Every element must be a `Constant`
@@ -1201,6 +1224,13 @@ module Rigor
1201
1224
  # indices still fall through because the same handler serves
1202
1225
  # `fetch`, while statically nil slices can be represented
1203
1226
  # precisely for `[]`.
1227
+ # `[]` and its exact alias `slice` share the index / Range /
1228
+ # start-length folding. `fetch` routes here too but stays
1229
+ # integer-index-only: the Range and start-length branches gate
1230
+ # on this selector set, which `fetch` is deliberately not in.
1231
+ SLICE_SELECTORS = Set[:[], :slice].freeze
1232
+ private_constant :SLICE_SELECTORS
1233
+
1204
1234
  def tuple_index(tuple, method_name, args)
1205
1235
  case args.size
1206
1236
  when 1 then tuple_single_index(tuple, method_name, args.first)
@@ -1211,17 +1241,18 @@ module Rigor
1211
1241
  def tuple_single_index(tuple, method_name, arg)
1212
1242
  return nil unless arg.is_a?(Type::Constant)
1213
1243
 
1214
- return tuple_range_slice(tuple, arg.value) if method_name == :[] && arg.value.is_a?(Range)
1215
- return nil unless arg.value.is_a?(Integer)
1244
+ value = arg.value
1245
+ return tuple_range_slice(tuple, value) if SLICE_SELECTORS.include?(method_name) && value.is_a?(Range)
1246
+ return nil unless value.is_a?(Integer)
1216
1247
 
1217
- idx = normalise_index(arg.value, tuple.elements.size)
1248
+ idx = normalise_index(value, tuple.elements.size)
1218
1249
  return nil unless idx
1219
1250
 
1220
1251
  tuple.elements[idx]
1221
1252
  end
1222
1253
 
1223
1254
  def tuple_start_length_slice(tuple, method_name, args)
1224
- return nil unless method_name == :[]
1255
+ return nil unless SLICE_SELECTORS.include?(method_name)
1225
1256
 
1226
1257
  start, length = args
1227
1258
  return nil unless start.is_a?(Type::Constant) && length.is_a?(Type::Constant)
@@ -118,10 +118,12 @@ module Rigor
118
118
  # table to suppress false positives for methods the
119
119
  # user has defined but no RBS sig describes. Merged
120
120
  # UNDER the cross-file pre-pass seed; details: merge_project_method_indexes.
121
- discovered_methods = deep_merge_class_methods(
122
- default_scope.discovered_methods, build_discovered_methods(root)
123
- )
124
- seeded_scope = seeded_scope.with_discovery(seeded_scope.discovery.with(discovered_methods: discovered_methods))
121
+ # One combined descent yields both the discovered-methods existence
122
+ # table and the instance def-node table — see
123
+ # {#build_methods_and_def_nodes}. `seed_discovered_methods` seeds the
124
+ # former onto the scope and returns the def-node table for
125
+ # `merge_project_method_indexes` below.
126
+ seeded_scope, file_def_nodes = seed_discovered_methods(seeded_scope, default_scope, root)
125
127
 
126
128
  # v0.0.2 #5 + ADR-24 slice 2 — record per-instance-method
127
129
  # def nodes, the class -> superclass map, and the
@@ -134,7 +136,7 @@ module Rigor
134
136
  # table. Seeded inside `merge_project_method_indexes` so the
135
137
  # per-file visibilities merge OVER the cross-file project seed
136
138
  # rather than overwriting it.
137
- seeded_scope = merge_project_method_indexes(seeded_scope, default_scope, root)
139
+ seeded_scope = merge_project_method_indexes(seeded_scope, default_scope, root, file_def_nodes)
138
140
 
139
141
  table = {}.compare_by_identity
140
142
  table.default = seeded_scope
@@ -160,6 +162,19 @@ module Rigor
160
162
  table
161
163
  end
162
164
 
165
+ # Runs the combined methods/def-nodes descent (one walk of the file),
166
+ # seeds the discovered-methods existence table onto `seeded_scope`
167
+ # (merged UNDER the cross-file pre-pass seed `default_scope` carries),
168
+ # and returns `[scope, file_def_nodes]` so the caller can thread the
169
+ # def-node table into {#merge_project_method_indexes} without walking
170
+ # the file a second time.
171
+ def seed_discovered_methods(seeded_scope, default_scope, root)
172
+ file_methods, file_def_nodes = build_methods_and_def_nodes(root)
173
+ discovered_methods = deep_merge_class_methods(default_scope.discovered_methods, file_methods)
174
+ scope = seeded_scope.with_discovery(seeded_scope.discovery.with(discovered_methods: discovered_methods))
175
+ [scope, file_def_nodes]
176
+ end
177
+
163
178
  # ADR-48 Struct slice 3 — installs the top-level fold-safe-local set
164
179
  # ({Inference::StructFoldSafety}). Struct member layouts of constant
165
180
  # receivers are resolved through the side-table the seeded scope carries.
@@ -179,9 +194,9 @@ module Rigor
179
194
  # `discovered_def_index_for_paths` seed carried on
180
195
  # `default_scope` — same-file declarations win per entry,
181
196
  # the cross-file seed supplies sibling-file ancestors.
182
- def merge_project_method_indexes(seeded_scope, default_scope, root)
197
+ def merge_project_method_indexes(seeded_scope, default_scope, root, file_def_nodes)
183
198
  def_nodes = default_scope.discovered_def_nodes.merge(
184
- build_discovered_def_nodes(root)
199
+ file_def_nodes
185
200
  ) { |_class, cross_file, per_file| cross_file.merge(per_file) }
186
201
  singleton_def_nodes = default_scope.discovered_singleton_def_nodes.merge(
187
202
  build_discovered_singleton_def_nodes(root)
@@ -1406,16 +1421,29 @@ module Rigor
1406
1421
  Type::Combinator.singleton_of(full)
1407
1422
  end
1408
1423
 
1409
- # Slice 7 phase 12 — in-source method discovery pre-pass.
1410
- # Walks every class/module body and records the methods
1411
- # introduced via `Prism::DefNode` (instance + singleton)
1412
- # and via recognised `define_method(:name) { ... }` calls.
1413
- # The returned table maps qualified class name to a
1414
- # `Hash[Symbol, :instance | :singleton]`.
1415
- def build_discovered_methods(root)
1416
- accumulator = {}
1417
- walk_methods(root, [], false, accumulator)
1418
- accumulator.transform_values(&:freeze).freeze
1424
+ # Slice 7 phase 12 — in-source method discovery pre-pass, fused with
1425
+ # the instance-method def-node pre-pass (v0.0.2 #5). One descent
1426
+ # produces BOTH tables the per-file `index` and the cross-file
1427
+ # pre-pass each need together:
1428
+ #
1429
+ # - `methods` : `{class_name => {method => :instance | :singleton}}`
1430
+ # for every `def` / `define_method(:name)` / `attr_*` / `alias` /
1431
+ # Data/Struct-member reader (the undefined-method existence table).
1432
+ # - `def_nodes` : `{class_name => {method => Prism::DefNode}}` for
1433
+ # every instance-side `def` (the inter-procedural return-inference
1434
+ # table; singleton defs and `define_method` are intentionally
1435
+ # skipped — `record_def_node` filters them).
1436
+ #
1437
+ # `walk_methods` and `walk_def_nodes` had byte-identical class /
1438
+ # module / singleton / meta-block descents (both stop at `DefNode`),
1439
+ # so a single combined walk records both accumulators at once instead
1440
+ # of traversing every file twice.
1441
+ def build_methods_and_def_nodes(root)
1442
+ methods = {}
1443
+ def_nodes = {}
1444
+ walk_methods_and_def_nodes(root, [], false, methods, def_nodes)
1445
+ apply_alias_def_nodes(root, def_nodes)
1446
+ [methods.transform_values(&:freeze).freeze, def_nodes.transform_values(&:freeze).freeze]
1419
1447
  end
1420
1448
 
1421
1449
  # Merges two `class_name => { method => kind }` tables, unioning
@@ -1431,7 +1459,14 @@ module Rigor
1431
1459
  end
1432
1460
 
1433
1461
  # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity
1434
- def walk_methods(node, qualified_prefix, in_singleton_class, accumulator)
1462
+ # Combined `walk_methods` + `walk_def_nodes` descent. The two walks
1463
+ # had identical class / module / singleton-class / meta-block
1464
+ # traversals and both stopped at `DefNode`; the only divergences are
1465
+ # leaf actions (recorded into the right accumulator) and the original
1466
+ # `walk_methods` returning at `AliasMethodNode` (its symbol-only
1467
+ # children carry no def / class node, so not descending them is
1468
+ # byte-identical for `def_nodes` too). See {#build_methods_and_def_nodes}.
1469
+ def walk_methods_and_def_nodes(node, qualified_prefix, in_singleton_class, methods_acc, def_nodes_acc)
1435
1470
  return unless node.is_a?(Prism::Node)
1436
1471
 
1437
1472
  case node
@@ -1439,40 +1474,40 @@ module Rigor
1439
1474
  name = Source::ConstantPath.qualified_name(node.constant_path)
1440
1475
  if name
1441
1476
  child_prefix = qualified_prefix + [name]
1442
- record_meta_superclass_members(node, child_prefix, accumulator) if node.is_a?(Prism::ClassNode)
1443
- walk_methods(node.body, child_prefix, false, accumulator) if node.body
1477
+ record_meta_superclass_members(node, child_prefix, methods_acc) if node.is_a?(Prism::ClassNode)
1478
+ walk_methods_and_def_nodes(node.body, child_prefix, false, methods_acc, def_nodes_acc) if node.body
1444
1479
  return
1445
1480
  end
1446
1481
  when Prism::SingletonClassNode
1447
1482
  if node.body
1448
1483
  singleton_prefix = singleton_class_prefix(node, qualified_prefix)
1449
1484
  if singleton_prefix
1450
- walk_methods(node.body, singleton_prefix, true, accumulator)
1485
+ walk_methods_and_def_nodes(node.body, singleton_prefix, true, methods_acc, def_nodes_acc)
1451
1486
  return
1452
1487
  end
1453
1488
  end
1454
1489
  when Prism::ConstantWriteNode
1455
1490
  if meta_new_block_body(node)
1456
1491
  child_prefix = qualified_prefix + [node.name.to_s]
1457
- walk_methods(meta_new_block_body(node), child_prefix, false, accumulator)
1492
+ walk_methods_and_def_nodes(meta_new_block_body(node), child_prefix, false, methods_acc, def_nodes_acc)
1458
1493
  return
1459
1494
  end
1460
1495
  when Prism::DefNode
1461
- record_def_method(node, qualified_prefix, in_singleton_class, accumulator)
1496
+ record_def_method(node, qualified_prefix, in_singleton_class, methods_acc)
1497
+ record_def_node(node, qualified_prefix, in_singleton_class, def_nodes_acc)
1462
1498
  return
1463
1499
  when Prism::AliasMethodNode
1464
- record_alias_method(node, qualified_prefix, in_singleton_class, accumulator)
1500
+ record_alias_method(node, qualified_prefix, in_singleton_class, methods_acc)
1465
1501
  return
1466
1502
  when Prism::CallNode
1467
- record_define_method(node, qualified_prefix, in_singleton_class, accumulator) if node.name == :define_method
1503
+ record_define_method(node, qualified_prefix, in_singleton_class, methods_acc) if node.name == :define_method
1468
1504
  if ATTR_MACROS.include?(node.name)
1469
- record_attr_methods(node, qualified_prefix, in_singleton_class,
1470
- accumulator)
1505
+ record_attr_methods(node, qualified_prefix, in_singleton_class, methods_acc)
1471
1506
  end
1472
1507
  end
1473
1508
 
1474
1509
  node.compact_child_nodes.each do |child|
1475
- walk_methods(child, qualified_prefix, in_singleton_class, accumulator)
1510
+ walk_methods_and_def_nodes(child, qualified_prefix, in_singleton_class, methods_acc, def_nodes_acc)
1476
1511
  end
1477
1512
  end
1478
1513
 
@@ -1606,57 +1641,6 @@ module Rigor
1606
1641
  end
1607
1642
  end
1608
1643
 
1609
- # v0.0.2 #5 — instance-side def-node recording. Walks
1610
- # class bodies the same way as `build_discovered_methods`
1611
- # but records the actual `Prism::DefNode` for each
1612
- # **instance** method so `ExpressionTyper` can re-type
1613
- # the body at the call site for inter-procedural return
1614
- # inference. Singleton methods and `define_method` calls
1615
- # are intentionally skipped: the inference path needs a
1616
- # statically introspectable body, and singleton dispatch
1617
- # has its own complications (Class / Module ancestry)
1618
- # the first-iteration rule does not yet model.
1619
- def build_discovered_def_nodes(root)
1620
- accumulator = {}
1621
- walk_def_nodes(root, [], false, accumulator)
1622
- apply_alias_def_nodes(root, accumulator)
1623
- accumulator.transform_values(&:freeze).freeze
1624
- end
1625
-
1626
- def walk_def_nodes(node, qualified_prefix, in_singleton_class, accumulator)
1627
- return unless node.is_a?(Prism::Node)
1628
-
1629
- case node
1630
- when Prism::ClassNode, Prism::ModuleNode
1631
- name = Source::ConstantPath.qualified_name(node.constant_path)
1632
- if name
1633
- child_prefix = qualified_prefix + [name]
1634
- walk_def_nodes(node.body, child_prefix, false, accumulator) if node.body
1635
- return
1636
- end
1637
- when Prism::SingletonClassNode
1638
- if node.body
1639
- singleton_prefix = singleton_class_prefix(node, qualified_prefix)
1640
- if singleton_prefix
1641
- walk_def_nodes(node.body, singleton_prefix, true, accumulator)
1642
- return
1643
- end
1644
- end
1645
- when Prism::ConstantWriteNode
1646
- if meta_new_block_body(node)
1647
- child_prefix = qualified_prefix + [node.name.to_s]
1648
- walk_def_nodes(meta_new_block_body(node), child_prefix, false, accumulator)
1649
- return
1650
- end
1651
- when Prism::DefNode
1652
- record_def_node(node, qualified_prefix, in_singleton_class, accumulator)
1653
- return
1654
- end
1655
-
1656
- node.compact_child_nodes.each do |child|
1657
- walk_def_nodes(child, qualified_prefix, in_singleton_class, accumulator)
1658
- end
1659
- end
1660
1644
  # v0.0.3 A — sentinel key under which `record_def_node`
1661
1645
  # files DefNodes that live outside any class / module
1662
1646
  # body (top-level helpers, `def`s nested inside DSL
@@ -2385,7 +2369,13 @@ module Rigor
2385
2369
  # the override-visibility-reduced rule can read an ancestor's
2386
2370
  # visibility declared in a sibling file.
2387
2371
  def accumulate_project_index(acc, path, root)
2388
- merge_discovered_defs(acc[:def_nodes], acc[:def_sources], path, root)
2372
+ # One combined descent yields both the methods existence table and
2373
+ # the def-node table; the latter is also consumed by
2374
+ # `record_class_sources`, so a def-dense file is walked once here
2375
+ # instead of three times (methods + def-nodes ×2). See
2376
+ # {#build_methods_and_def_nodes}.
2377
+ file_methods, file_def_nodes = build_methods_and_def_nodes(root)
2378
+ merge_discovered_defs(acc[:def_nodes], acc[:def_sources], path, file_def_nodes)
2389
2379
  build_discovered_singleton_def_nodes(root).each do |class_name, methods|
2390
2380
  (acc[:singleton_def_nodes][class_name] ||= {}).merge!(methods)
2391
2381
  end
@@ -2395,20 +2385,28 @@ module Rigor
2395
2385
  includes.each do |class_name, mods|
2396
2386
  acc[:includes][class_name] = ((acc[:includes][class_name] || []) + mods).uniq
2397
2387
  end
2398
- record_class_sources(acc[:class_sources], path, root, superclasses, includes)
2399
- merge_class_keyed_index_tables(acc, root)
2388
+ record_class_sources(acc[:class_sources], path, root, superclasses, includes, file_def_nodes)
2389
+ merge_class_keyed_index_tables(acc, root, file_methods)
2390
+ merge_member_layout_tables(acc, root)
2391
+ end
2392
+
2393
+ # Folds one file's Data + Struct member-layout tables into the
2394
+ # cross-file accumulator (kept out of {#accumulate_project_index} to
2395
+ # hold its ABC budget).
2396
+ def merge_member_layout_tables(acc, root)
2400
2397
  acc[:data_member_layouts].merge!(build_data_member_layouts(root))
2401
2398
  acc[:struct_member_layouts].merge!(build_struct_member_layouts(root))
2402
2399
  end
2403
2400
 
2404
2401
  # Folds the per-class method-visibility and method-existence tables of
2405
2402
  # one file into the cross-file accumulator (kept out of
2406
- # {#accumulate_project_index} to hold its ABC budget).
2407
- def merge_class_keyed_index_tables(acc, root)
2403
+ # {#accumulate_project_index} to hold its ABC budget). `file_methods`
2404
+ # is the existence table from the combined methods/def-nodes descent.
2405
+ def merge_class_keyed_index_tables(acc, root, file_methods)
2408
2406
  build_discovered_method_visibilities(root).each do |class_name, table|
2409
2407
  (acc[:method_visibilities][class_name] ||= {}).merge!(table)
2410
2408
  end
2411
- build_discovered_methods(root).each do |class_name, table|
2409
+ file_methods.each do |class_name, table|
2412
2410
  (acc[:methods][class_name] ||= {}).merge!(table)
2413
2411
  end
2414
2412
  end
@@ -2423,13 +2421,13 @@ module Rigor
2423
2421
  # dependency recording (ADR-46). The class-declaration walk
2424
2422
  # (`collect_class_decls`) catches bodyless / def-less reopenings the
2425
2423
  # other three builders miss.
2426
- def record_class_sources(class_sources, path, root, superclasses, includes)
2424
+ def record_class_sources(class_sources, path, root, superclasses, includes, file_def_nodes)
2427
2425
  names = Set.new
2428
2426
  collect_class_decls(root, [], decls = {})
2429
2427
  names.merge(decls.keys)
2430
2428
  names.merge(superclasses.keys)
2431
2429
  names.merge(includes.keys)
2432
- names.merge(build_discovered_def_nodes(root).keys)
2430
+ names.merge(file_def_nodes.keys)
2433
2431
  names.each { |name| (class_sources[name] ||= Set.new) << path }
2434
2432
  end
2435
2433
 
@@ -2438,8 +2436,8 @@ module Rigor
2438
2436
  # seen `"path:line"` definition site in `def_sources` (ADR-17 —
2439
2437
  # the un-registered-project-patch signal `call.undefined-method`
2440
2438
  # and `rigor triage` key on).
2441
- def merge_discovered_defs(def_nodes, def_sources, path, root)
2442
- build_discovered_def_nodes(root).each do |class_name, methods|
2439
+ def merge_discovered_defs(def_nodes, def_sources, path, file_def_nodes)
2440
+ file_def_nodes.each do |class_name, methods|
2443
2441
  (def_nodes[class_name] ||= {}).merge!(methods)
2444
2442
  sources = (def_sources[class_name] ||= {})
2445
2443
  methods.each do |method_name, def_node|
@@ -12,13 +12,13 @@ module Rigor
12
12
  # `exe/rigor` launcher maps `.rigor.yml`'s `plugins_isolation:` onto it
13
13
  # before re-exec). Three backends behind one interface:
14
14
  #
15
- # - `none` (**default**) — load into the main space and call directly.
16
- # Lowest cost; no isolation. Fine for the common case because the
17
- # invoked library is trusted + pure.
15
+ # - `none` — load into the main space and call directly.
16
+ # Lowest cost; no isolation. Used as the fallback where fork is
17
+ # unavailable; fine because the invoked library is trusted + pure.
18
18
  # - `ruby_box` — call inside a {Box} (`Ruby::Box`, `RUBY_BOX=1`). Isolates
19
19
  # core-class monkey-patches + lets gem versions coexist, but a native
20
20
  # crash in the boxed work still takes the process down (in-process).
21
- # - `process` — call in a forked worker ({Process}); returns data over a
21
+ # - `process` (**default**) — call in a forked worker ({Process}); returns data over a
22
22
  # pipe. The strongest: a child crash (even `SIGSEGV`) is contained —
23
23
  # the parent survives and declines. Higher cost (fork + IPC).
24
24
  #
@@ -73,7 +73,7 @@ module Rigor
73
73
  end
74
74
 
75
75
  # `none` — load the trusted library into the main space and call it
76
- # directly. No isolation; lowest cost; the current default behaviour.
76
+ # directly. No isolation; lowest cost; the fork-unavailable fallback.
77
77
  module Direct
78
78
  module_function
79
79