rigortype 0.1.16 → 0.1.18

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 (180) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -2
  3. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +18 -1
  4. data/lib/rigor/analysis/check_rules/rule_walk.rb +67 -0
  5. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +100 -0
  6. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +226 -0
  7. data/lib/rigor/analysis/check_rules.rb +180 -73
  8. data/lib/rigor/analysis/dependency_recorder.rb +122 -0
  9. data/lib/rigor/analysis/diagnostic.rb +18 -0
  10. data/lib/rigor/analysis/incremental.rb +162 -0
  11. data/lib/rigor/analysis/incremental_session.rb +337 -0
  12. data/lib/rigor/analysis/rule_catalog.rb +48 -0
  13. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +580 -0
  14. data/lib/rigor/analysis/runner/pool_coordinator.rb +569 -0
  15. data/lib/rigor/analysis/runner/project_pre_passes.rb +318 -0
  16. data/lib/rigor/analysis/runner/run_snapshots.rb +46 -0
  17. data/lib/rigor/analysis/runner.rb +477 -1110
  18. data/lib/rigor/analysis/self_call_resolution_recorder.rb +121 -0
  19. data/lib/rigor/analysis/worker_session.rb +47 -8
  20. data/lib/rigor/builtins/static_return_refinements.rb +7 -1
  21. data/lib/rigor/cache/descriptor.rb +50 -49
  22. data/lib/rigor/cache/incremental_snapshot.rb +153 -0
  23. data/lib/rigor/cache/rbs_cache_producer.rb +34 -0
  24. data/lib/rigor/cache/rbs_class_ancestor_table.rb +2 -8
  25. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -8
  26. data/lib/rigor/cache/rbs_constant_table.rb +2 -8
  27. data/lib/rigor/cache/rbs_environment.rb +2 -8
  28. data/lib/rigor/cache/rbs_known_class_names.rb +2 -8
  29. data/lib/rigor/cache/store.rb +145 -14
  30. data/lib/rigor/cli/annotate_command.rb +2 -7
  31. data/lib/rigor/cli/baseline_command.rb +2 -7
  32. data/lib/rigor/cli/check_command.rb +705 -0
  33. data/lib/rigor/cli/ci_detector.rb +94 -0
  34. data/lib/rigor/cli/command.rb +47 -0
  35. data/lib/rigor/cli/coverage_command.rb +3 -23
  36. data/lib/rigor/cli/coverage_renderer.rb +3 -8
  37. data/lib/rigor/cli/diagnostic_formats.rb +345 -0
  38. data/lib/rigor/cli/diff_command.rb +3 -7
  39. data/lib/rigor/cli/explain_command.rb +2 -7
  40. data/lib/rigor/cli/lsp_command.rb +3 -7
  41. data/lib/rigor/cli/mcp_command.rb +3 -7
  42. data/lib/rigor/cli/options.rb +57 -0
  43. data/lib/rigor/cli/plugin_command.rb +3 -7
  44. data/lib/rigor/cli/plugins_command.rb +2 -7
  45. data/lib/rigor/cli/prism_colorizer.rb +10 -3
  46. data/lib/rigor/cli/renderable.rb +26 -0
  47. data/lib/rigor/cli/sig_gen_command.rb +2 -7
  48. data/lib/rigor/cli/skill_command.rb +3 -7
  49. data/lib/rigor/cli/trace_command.rb +143 -0
  50. data/lib/rigor/cli/trace_renderer.rb +310 -0
  51. data/lib/rigor/cli/triage_command.rb +2 -7
  52. data/lib/rigor/cli/type_of_command.rb +5 -38
  53. data/lib/rigor/cli/type_of_renderer.rb +4 -9
  54. data/lib/rigor/cli/type_scan_command.rb +3 -23
  55. data/lib/rigor/cli/type_scan_renderer.rb +4 -9
  56. data/lib/rigor/cli.rb +15 -532
  57. data/lib/rigor/configuration/dependencies.rb +18 -1
  58. data/lib/rigor/configuration/severity_profile.rb +22 -3
  59. data/lib/rigor/configuration.rb +16 -3
  60. data/lib/rigor/environment/rbs_loader.rb +129 -71
  61. data/lib/rigor/environment.rb +1 -1
  62. data/lib/rigor/inference/acceptance.rb +10 -0
  63. data/lib/rigor/inference/block_parameter_binder.rb +1 -2
  64. data/lib/rigor/inference/builtins/array_catalog.rb +2 -5
  65. data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -5
  66. data/lib/rigor/inference/builtins/complex_catalog.rb +2 -5
  67. data/lib/rigor/inference/builtins/date_catalog.rb +2 -5
  68. data/lib/rigor/inference/builtins/encoding_catalog.rb +2 -5
  69. data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -5
  70. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -5
  71. data/lib/rigor/inference/builtins/hash_catalog.rb +2 -5
  72. data/lib/rigor/inference/builtins/method_catalog.rb +15 -0
  73. data/lib/rigor/inference/builtins/numeric_catalog.rb +21 -93
  74. data/lib/rigor/inference/builtins/pathname_catalog.rb +2 -5
  75. data/lib/rigor/inference/builtins/proc_catalog.rb +2 -5
  76. data/lib/rigor/inference/builtins/random_catalog.rb +2 -5
  77. data/lib/rigor/inference/builtins/range_catalog.rb +2 -5
  78. data/lib/rigor/inference/builtins/rational_catalog.rb +2 -5
  79. data/lib/rigor/inference/builtins/re_catalog.rb +2 -5
  80. data/lib/rigor/inference/builtins/set_catalog.rb +2 -5
  81. data/lib/rigor/inference/builtins/string_catalog.rb +2 -5
  82. data/lib/rigor/inference/builtins/struct_catalog.rb +2 -5
  83. data/lib/rigor/inference/builtins/time_catalog.rb +2 -5
  84. data/lib/rigor/inference/expression_typer.rb +149 -63
  85. data/lib/rigor/inference/flow_tracer.rb +180 -0
  86. data/lib/rigor/inference/macro_block_self_type.rb +10 -11
  87. data/lib/rigor/inference/method_dispatcher/block_folding.rb +5 -1
  88. data/lib/rigor/inference/method_dispatcher/call_context.rb +65 -0
  89. data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +11 -10
  90. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +12 -6
  91. data/lib/rigor/inference/method_dispatcher/data_folding.rb +246 -0
  92. data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -2
  93. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -2
  94. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +4 -1
  95. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +4 -1
  96. data/lib/rigor/inference/method_dispatcher/math_folding.rb +6 -6
  97. data/lib/rigor/inference/method_dispatcher/method_folding.rb +12 -7
  98. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +33 -1
  99. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +23 -13
  100. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +9 -9
  101. data/lib/rigor/inference/method_dispatcher/set_folding.rb +6 -6
  102. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +120 -9
  103. data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +12 -12
  104. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +49 -0
  105. data/lib/rigor/inference/method_dispatcher/time_folding.rb +6 -6
  106. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +9 -9
  107. data/lib/rigor/inference/method_dispatcher.rb +185 -84
  108. data/lib/rigor/inference/narrowing.rb +262 -5
  109. data/lib/rigor/inference/scope_indexer.rb +208 -21
  110. data/lib/rigor/inference/statement_evaluator.rb +110 -48
  111. data/lib/rigor/language_server/buffer_resolution.rb +33 -0
  112. data/lib/rigor/language_server/completion_provider.rb +4 -4
  113. data/lib/rigor/language_server/document_symbol_provider.rb +4 -4
  114. data/lib/rigor/language_server/folding_range_provider.rb +4 -4
  115. data/lib/rigor/language_server/hover_provider.rb +4 -4
  116. data/lib/rigor/language_server/selection_range_provider.rb +4 -4
  117. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  118. data/lib/rigor/plugin/additional_initializer.rb +61 -38
  119. data/lib/rigor/plugin/base.rb +302 -45
  120. data/lib/rigor/plugin/node_rule_walk.rb +147 -0
  121. data/lib/rigor/plugin/registry.rb +281 -15
  122. data/lib/rigor/plugin.rb +1 -0
  123. data/lib/rigor/rbs_extended/conformance_checker.rb +293 -0
  124. data/lib/rigor/rbs_extended.rb +39 -0
  125. data/lib/rigor/scope/discovery_index.rb +58 -0
  126. data/lib/rigor/scope.rb +150 -167
  127. data/lib/rigor/sig_gen/observation_collector.rb +6 -6
  128. data/lib/rigor/source/literals.rb +14 -0
  129. data/lib/rigor/type/acceptance_router.rb +19 -0
  130. data/lib/rigor/type/accepts_result.rb +3 -10
  131. data/lib/rigor/type/app.rb +3 -7
  132. data/lib/rigor/type/bot.rb +2 -3
  133. data/lib/rigor/type/bound_method.rb +5 -12
  134. data/lib/rigor/type/combinator.rb +22 -0
  135. data/lib/rigor/type/constant.rb +2 -3
  136. data/lib/rigor/type/data_class.rb +80 -0
  137. data/lib/rigor/type/data_instance.rb +100 -0
  138. data/lib/rigor/type/difference.rb +5 -10
  139. data/lib/rigor/type/dynamic.rb +5 -10
  140. data/lib/rigor/type/hash_shape.rb +5 -15
  141. data/lib/rigor/type/integer_range.rb +5 -10
  142. data/lib/rigor/type/intersection.rb +5 -10
  143. data/lib/rigor/type/nominal.rb +5 -10
  144. data/lib/rigor/type/refined.rb +5 -10
  145. data/lib/rigor/type/singleton.rb +5 -10
  146. data/lib/rigor/type/top.rb +2 -3
  147. data/lib/rigor/type/tuple.rb +5 -10
  148. data/lib/rigor/type/union.rb +5 -10
  149. data/lib/rigor/type.rb +2 -0
  150. data/lib/rigor/value_semantics.rb +77 -0
  151. data/lib/rigor/version.rb +1 -1
  152. data/lib/rigor.rb +1 -1
  153. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -2
  154. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +2 -4
  155. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +70 -32
  156. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -3
  157. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +15 -21
  158. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +1 -1
  159. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -2
  160. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -2
  161. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +12 -2
  162. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +12 -2
  163. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
  164. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +35 -18
  165. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +8 -29
  166. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +17 -1
  167. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +2 -2
  168. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +83 -36
  169. data/sig/rigor/cache.rbs +19 -0
  170. data/sig/rigor/environment.rbs +0 -2
  171. data/sig/rigor/inference.rbs +27 -0
  172. data/sig/rigor/plugin/base.rbs +1 -2
  173. data/sig/rigor/rbs_extended.rbs +2 -0
  174. data/sig/rigor/scope.rbs +42 -25
  175. data/sig/rigor/source.rbs +1 -0
  176. data/sig/rigor/type.rbs +58 -1
  177. data/sig/rigor.rbs +6 -1
  178. data/skills/rigor-ci-setup/SKILL.md +319 -0
  179. metadata +36 -2
  180. data/lib/rigor/cache/rbs_instance_definitions.rb +0 -79
@@ -1,104 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "yaml"
3
+ require_relative "method_catalog"
4
4
 
5
5
  module Rigor
6
6
  module Inference
7
7
  module Builtins
8
- # Read-only loader for the Numeric/Integer/Float built-in method
9
- # catalog at `data/builtins/ruby_core/numeric.yml`. The catalog is
10
- # produced offline by `tool/extract_numeric_catalog.rb` from the
11
- # CRuby reference checkout under `references/ruby` plus the RBS
12
- # core signatures under `references/rbs`.
8
+ # `Numeric` family catalog (Integer, Float, Rational, Complex,
9
+ # Numeric). Singleton load once, consult during dispatch.
13
10
  #
14
- # The loader is the runtime bridge: callers ask "is `Integer#+`
15
- # safe to invoke during constant folding?" and the answer comes
16
- # straight from the offline classification (`leaf`, `trivial`,
17
- # `leaf_when_numeric` are foldable; everything else is not).
11
+ # The catalog is produced offline by `tool/extract_builtin_catalog.rb`
12
+ # from the CRuby reference checkout under `references/ruby` plus the
13
+ # RBS core signatures under `references/rbs`. The loader is the
14
+ # runtime bridge: callers ask "is `Integer#+` safe to invoke during
15
+ # constant folding?" and the answer comes straight from the offline
16
+ # classification (`leaf` / `trivial` / `leaf_when_numeric` are
17
+ # foldable; everything else is not).
18
18
  #
19
- # The catalog is loaded lazily on first access and memoised for
20
- # the lifetime of the process. If the file is missing (e.g. in a
21
- # bare gem install where the consumer opted out of shipping data
22
- # files, or in a development checkout that has not yet generated
23
- # the catalog) the loader degrades to an empty catalog so calls
24
- # uniformly return `false` and the rest of the dispatcher
25
- # continues with its hand-rolled allow lists.
26
- module NumericCatalog
27
- # Purity tags from the catalog that are safe for the analyzer
28
- # to invoke against concrete literal receivers/arguments.
29
- # `leaf_when_numeric` is included because `ConstantFolding`
30
- # only lets it through when every argument is itself a
31
- # `Constant<Numeric>` or `IntegerRange` — exactly the gate
32
- # the catalog tag is named for.
33
- FOLDABLE_PURITIES = Set["leaf", "trivial", "leaf_when_numeric"].freeze
34
-
35
- EMPTY_CATALOG = { "classes" => {} }.freeze
36
- private_constant :EMPTY_CATALOG
37
-
38
- # Path resolved relative to this file. The catalog ships under
39
- # `data/builtins/ruby_core/numeric.yml` at the gem root.
40
- CATALOG_PATH = File.expand_path(
41
- "../../../../data/builtins/ruby_core/numeric.yml",
42
- __dir__
43
- )
44
- private_constant :CATALOG_PATH
45
-
46
- class << self
47
- # @param class_name [String] e.g. "Integer", "Float"
48
- # @param selector [Symbol, String]
49
- # @param kind [Symbol] :instance (default) or :singleton
50
- # @return [Boolean]
51
- def safe_for_folding?(class_name, selector, kind: :instance)
52
- entry = method_entry(class_name, selector, kind: kind)
53
- return false unless entry
54
-
55
- FOLDABLE_PURITIES.include?(entry["purity"])
56
- end
57
-
58
- # @return [Hash, nil] catalog entry for the given method, or
59
- # nil when the method is not registered.
60
- def method_entry(class_name, selector, kind: :instance)
61
- klass = catalog.dig("classes", class_name.to_s)
62
- return nil unless klass
63
-
64
- bucket_key = kind == :singleton ? "singleton_methods" : "instance_methods"
65
- klass.dig(bucket_key, selector.to_s)
66
- end
67
-
68
- # Used by tests to drop the cached catalog so a different
69
- # path or content can be exercised. Production code MUST
70
- # NOT call this during normal operation.
71
- #
72
- # ADR-15 Phase 4b.x — reset re-loads eagerly so the
73
- # singleton-class `@catalog` ivar stays populated, and
74
- # the loaded Hash is deep-shared via `Ractor.make_shareable`
75
- # so a worker Ractor reading the ivar via `catalog.dig(...)`
76
- # does not trip `Ractor::IsolationError`. Plain `.freeze`
77
- # is insufficient: YAML parses to a nested Hash/Array/String
78
- # graph and only the outer Hash would be frozen.
79
- def reset!
80
- @catalog = Ractor.make_shareable(load_catalog)
81
- end
82
-
83
- private
84
-
85
- attr_reader :catalog
86
-
87
- def load_catalog
88
- return EMPTY_CATALOG unless File.exist?(CATALOG_PATH)
89
-
90
- data = YAML.safe_load_file(CATALOG_PATH, permitted_classes: [Symbol])
91
- data.is_a?(Hash) ? data : EMPTY_CATALOG
92
- rescue Psych::SyntaxError
93
- EMPTY_CATALOG
94
- end
95
- end
96
-
97
- # ADR-15 Phase 4b.x — eager-load on the main Ractor at
98
- # module-load time so worker Ractors only READ the
99
- # populated singleton-class `@catalog` ivar.
100
- reset!
101
- end
19
+ # No mutation blocklist is needed. The numeric classes expose no
20
+ # foldable bang or indirect-mutator method that the static C
21
+ # classifier mis-attributes (every `:leaf` numeric method is a pure
22
+ # value computation), so the generic `MethodCatalog` loader shared
23
+ # with the eighteen other per-class catalogs covers it directly.
24
+ # This previously hand-rolled its own `safe_for_folding?` /
25
+ # `method_entry` / `load_catalog` copy of `MethodCatalog`; folding
26
+ # it onto the shared loader also picks up alias resolution (e.g.
27
+ # `Integer#magnitude` `abs`, `Integer#inspect` `to_s`), which the
28
+ # old bespoke loader silently dropped.
29
+ NUMERIC_CATALOG = MethodCatalog.for_topic("numeric")
102
30
  end
103
31
  end
104
32
  end
@@ -16,11 +16,8 @@ module Rigor
16
16
  # helper that triggered the false positive (see
17
17
  # `string_catalog.rb`, `array_catalog.rb`, `time_catalog.rb`
18
18
  # for the canonical shape).
19
- PATHNAME_CATALOG = MethodCatalog.new(
20
- path: File.expand_path(
21
- "../../../../data/builtins/ruby_core/pathname.yml",
22
- __dir__
23
- ),
19
+ PATHNAME_CATALOG = MethodCatalog.for_topic(
20
+ "pathname",
24
21
  mutating_selectors: {
25
22
  "Pathname" => Set[
26
23
  # initialize_copy is blocklisted by convention so a
@@ -31,11 +31,8 @@ module Rigor
31
31
  # `#source_location`, `#name`, `#owner`, `#receiver`) remain
32
32
  # foldable; the RBS tier still resolves return types for
33
33
  # the blocklisted methods so callers do not lose precision.
34
- PROC_CATALOG = MethodCatalog.new(
35
- path: File.expand_path(
36
- "../../../../data/builtins/ruby_core/proc.yml",
37
- __dir__
38
- ),
34
+ PROC_CATALOG = MethodCatalog.for_topic(
35
+ "proc",
39
36
  mutating_selectors: {
40
37
  "Proc" => Set[
41
38
  # `#call` / `#[]` / `#===` / `#yield` invoke the proc
@@ -21,11 +21,8 @@ module Rigor
21
21
  # functionally pure they would produce a misleading constant
22
22
  # at fold time. The whole class is conservative-by-default
23
23
  # at the catalog tier; precision flows through the RBS layer.
24
- RANDOM_CATALOG = MethodCatalog.new(
25
- path: File.expand_path(
26
- "../../../../data/builtins/ruby_core/random.yml",
27
- __dir__
28
- ),
24
+ RANDOM_CATALOG = MethodCatalog.for_topic(
25
+ "random",
29
26
  mutating_selectors: {
30
27
  "Random" => Set[
31
28
  # `rand_random` -> `random_real` / `random_ulong_limited`
@@ -15,11 +15,8 @@ module Rigor
15
15
  # routes through a helper the block/yield regex does not
16
16
  # recognise, so the classifier mis-flags them as `:leaf`
17
17
  # despite yielding to a block.
18
- RANGE_CATALOG = MethodCatalog.new(
19
- path: File.expand_path(
20
- "../../../../data/builtins/ruby_core/range.yml",
21
- __dir__
22
- ),
18
+ RANGE_CATALOG = MethodCatalog.for_topic(
19
+ "range",
23
20
  mutating_selectors: {
24
21
  "Range" => Set[
25
22
  # `range_initialize` / `range_initialize_copy` write
@@ -22,11 +22,8 @@ module Rigor
22
22
  # hypothetical future `Constant<Rational>` carrier cannot
23
23
  # fold an aliasing copy through the catalog and surface a
24
24
  # shared mutable handle.
25
- RATIONAL_CATALOG = MethodCatalog.new(
26
- path: File.expand_path(
27
- "../../../../data/builtins/ruby_core/rational.yml",
28
- __dir__
29
- ),
25
+ RATIONAL_CATALOG = MethodCatalog.for_topic(
26
+ "rational",
30
27
  mutating_selectors: {
31
28
  "Rational" => Set[
32
29
  :initialize_copy
@@ -38,11 +38,8 @@ module Rigor
38
38
  # signatures already widen the answer enough to keep the
39
39
  # behaviour sound; revisit if the dispatcher ever grows a
40
40
  # singleton-aware catalog path.
41
- REGEXP_CATALOG = MethodCatalog.new(
42
- path: File.expand_path(
43
- "../../../../data/builtins/ruby_core/re.yml",
44
- __dir__
45
- ),
41
+ REGEXP_CATALOG = MethodCatalog.for_topic(
42
+ "re",
46
43
  mutating_selectors: {
47
44
  "Regexp" => Set[
48
45
  # Defensive: aliasing-copy semantics already covered
@@ -19,11 +19,8 @@ module Rigor
19
19
  # (`set_iter`, `RETURN_SIZED_ENUMERATOR`) and its identity-
20
20
  # mode and reset paths drive into helpers the regex classifier
21
21
  # does not yet recognise as block-yielding or mutating.
22
- SET_CATALOG = MethodCatalog.new(
23
- path: File.expand_path(
24
- "../../../../data/builtins/ruby_core/set.yml",
25
- __dir__
26
- ),
22
+ SET_CATALOG = MethodCatalog.for_topic(
23
+ "set",
27
24
  mutating_selectors: {
28
25
  "Set" => Set[
29
26
  # Indirect mutators classified `:leaf` because the C
@@ -15,11 +15,8 @@ module Rigor
15
15
  # mutation primitives). Adding to the blocklist is the
16
16
  # corrective surface for false positives until the
17
17
  # classifier learns the helper functions.
18
- STRING_CATALOG = MethodCatalog.new(
19
- path: File.expand_path(
20
- "../../../../data/builtins/ruby_core/string.yml",
21
- __dir__
22
- ),
18
+ STRING_CATALOG = MethodCatalog.for_topic(
19
+ "string",
23
20
  mutating_selectors: {
24
21
  "String" => Set[
25
22
  :replace, :initialize, :initialize_copy, :clear, :<<, :concat, :insert,
@@ -23,11 +23,8 @@ module Rigor
23
23
  # member but the answer depends on the subclass's member
24
24
  # definition, which the catalog does not see, so we blocklist
25
25
  # it defensively.
26
- STRUCT_CATALOG = MethodCatalog.new(
27
- path: File.expand_path(
28
- "../../../../data/builtins/ruby_core/struct.yml",
29
- __dir__
30
- ),
26
+ STRUCT_CATALOG = MethodCatalog.for_topic(
27
+ "struct",
31
28
  mutating_selectors: {
32
29
  "Struct" => Set[
33
30
  # Defensive: aliasing-copy semantics on a hypothetical
@@ -29,11 +29,8 @@ module Rigor
29
29
  # The blocklist captures the false-positive `:leaf` entries
30
30
  # whose helper functions the regex classifier did not
31
31
  # recognise as mutators.
32
- TIME_CATALOG = MethodCatalog.new(
33
- path: File.expand_path(
34
- "../../../../data/builtins/ruby_core/time.yml",
35
- __dir__
36
- ),
32
+ TIME_CATALOG = MethodCatalog.for_topic(
33
+ "time",
37
34
  mutating_selectors: {
38
35
  "Time" => Set[
39
36
  # `time_init_copy` writes the `timew` and `vtm` slots on
@@ -4,9 +4,11 @@ require "prism"
4
4
 
5
5
  require_relative "../type"
6
6
  require_relative "../ast"
7
+ require_relative "../analysis/self_call_resolution_recorder"
7
8
  require_relative "block_parameter_binder"
8
9
  require_relative "budget_trace"
9
10
  require_relative "fallback"
11
+ require_relative "flow_tracer"
10
12
  require_relative "indexed_narrowing"
11
13
  require_relative "macro_block_self_type"
12
14
  require_relative "method_dispatcher"
@@ -111,7 +113,7 @@ module Rigor
111
113
  Prism::GlobalVariableOperatorWriteNode => :type_of_assignment_write,
112
114
  Prism::GlobalVariableOrWriteNode => :type_of_assignment_write,
113
115
  Prism::GlobalVariableAndWriteNode => :type_of_assignment_write,
114
- # Compound writes that share the .value rvalue protocol
116
+ # Compound writes that share the `.value` rvalue accessor
115
117
  Prism::LocalVariableOperatorWriteNode => :type_of_assignment_write,
116
118
  Prism::LocalVariableOrWriteNode => :type_of_assignment_write,
117
119
  Prism::LocalVariableAndWriteNode => :type_of_assignment_write,
@@ -218,6 +220,15 @@ module Rigor
218
220
  end
219
221
 
220
222
  def type_of(node)
223
+ return untraced_type_of(node) unless FlowTracer.active?
224
+
225
+ # `rigor trace` — bracket the recursion with enter/result events.
226
+ # The tracer is observational only: the inferred type flows
227
+ # through unchanged (see FlowTracer's contract).
228
+ FlowTracer.trace_node(node) { untraced_type_of(node) }
229
+ end
230
+
231
+ def untraced_type_of(node)
221
232
  # Slice A-declarations. ScopeIndexer pre-fills
222
233
  # `scope.declared_types` for declaration-position nodes
223
234
  # (`module Foo` / `class Bar` headers) with the qualified
@@ -443,8 +454,12 @@ module Rigor
443
454
  candidates = []
444
455
  while prefix && !prefix.empty?
445
456
  candidates << "#{prefix}::#{name}"
446
- prefix = prefix.rpartition("::").first
447
- prefix = nil if prefix.empty?
457
+ # Strip the last `::` segment without `rpartition`'s throwaway
458
+ # 3-element array + extra substrings (this loop is the sole
459
+ # caller of the `String#rpartition` allocation seen in the
460
+ # profile): `rindex` + slice gives the same prefix, or nil.
461
+ idx = prefix.rindex("::")
462
+ prefix = idx ? prefix[0, idx] : nil
448
463
  end
449
464
  candidates << name
450
465
  candidates
@@ -663,26 +678,17 @@ module Rigor
663
678
  end
664
679
 
665
680
  # Returns `:truthy`, `:falsey`, or `nil` for an arbitrary
666
- # predicate expression under three-valued logic. Uses the
667
- # same {Narrowing} probe as `StatementEvaluator#eval_if`:
668
- # the predicate is truthy when its falsey fragment is `Bot`,
669
- # falsey when its truthy fragment is `Bot`. So
670
- # `Nominal[Integer]` (always truthy in Ruby), `Constant[nil]`,
671
- # and `Constant[false]` fold one branch; `Union[true, false]`,
672
- # `Dynamic[T]`, and `Top` keep both branches live.
681
+ # predicate expression under three-valued logic.
682
+ # {Narrowing.predicate_certainty} owns the judgment (the same
683
+ # one `StatementEvaluator#live_branch_for_if` reads on the
684
+ # scope side): `Nominal[Integer]` (always truthy in Ruby),
685
+ # `Constant[nil]`, and `Constant[false]` fold one branch;
686
+ # `Union[true, false]`, `Dynamic[T]`, and `Top` keep both
687
+ # branches live.
673
688
  def constant_predicate_polarity(predicate)
674
689
  return nil if predicate.nil?
675
690
 
676
- type = type_of(predicate)
677
- return nil if type.nil? || type.is_a?(Type::Bot)
678
-
679
- truthy_bot = Narrowing.narrow_truthy(type).is_a?(Type::Bot)
680
- falsey_bot = Narrowing.narrow_falsey(type).is_a?(Type::Bot)
681
-
682
- return :falsey if truthy_bot && !falsey_bot
683
- return :truthy if !truthy_bot && falsey_bot
684
-
685
- nil
691
+ Narrowing.predicate_certainty(type_of(predicate))
686
692
  end
687
693
 
688
694
  def type_of_else(node)
@@ -813,29 +819,14 @@ module Rigor
813
819
  # `:maybe` — the existing union fallback handles them.
814
820
  def case_when_pattern_certainty(subject_type, pattern_node)
815
821
  class_name = build_constant_path_name(pattern_node)
816
- return class_pattern_certainty(subject_type, class_name) if class_name
822
+ return Narrowing.class_pattern_certainty(subject_type, class_name, environment: scope.environment) if class_name
817
823
 
818
824
  literal = literal_pattern_value(pattern_node)
819
- return literal_pattern_certainty(subject_type, literal[:value]) if literal
820
-
821
- :maybe
822
- end
823
-
824
- def class_pattern_certainty(subject_type, class_name)
825
- env = scope.environment
826
- truthy_bot = Narrowing.narrow_class(subject_type, class_name, environment: env).is_a?(Type::Bot)
827
- falsey_bot = Narrowing.narrow_not_class(subject_type, class_name, environment: env).is_a?(Type::Bot)
828
-
829
- return :no if truthy_bot && !falsey_bot
830
- return :yes if !truthy_bot && falsey_bot
825
+ return Narrowing.value_pattern_certainty(subject_type, literal[:value]) if literal
831
826
 
832
827
  :maybe
833
828
  end
834
829
 
835
- VALUE_EQUALITY_CLASSES = [Integer, Float, Rational, Complex, String, Symbol,
836
- TrueClass, FalseClass, NilClass].freeze
837
- private_constant :VALUE_EQUALITY_CLASSES
838
-
839
830
  # Returns `{ value: v }` when `pattern_node` types to a
840
831
  # `Constant[v]` of a value-equality-safe class (so `===`
841
832
  # reduces to `==`), else nil. Wrapped in a hash so a literal
@@ -844,18 +835,11 @@ module Rigor
844
835
  def literal_pattern_value(pattern_node)
845
836
  type = type_of(pattern_node)
846
837
  return nil unless type.is_a?(Type::Constant)
847
- return nil unless VALUE_EQUALITY_CLASSES.any? { |klass| type.value.is_a?(klass) }
838
+ return nil unless Narrowing::VALUE_EQUALITY_CLASSES.any? { |klass| type.value.is_a?(klass) }
848
839
 
849
840
  { value: type.value }
850
841
  end
851
842
 
852
- def literal_pattern_certainty(subject_type, pattern_value)
853
- return :maybe unless subject_type.is_a?(Type::Constant)
854
- return :maybe unless VALUE_EQUALITY_CLASSES.any? { |klass| subject_type.value.is_a?(klass) }
855
-
856
- pattern_value == subject_type.value ? :yes : :no
857
- end
858
-
859
843
  # `when` clauses for `case` and `in` clauses for `case ... in` have
860
844
  # the same body shape; we reuse one handler for both Prism node
861
845
  # classes.
@@ -1260,9 +1244,64 @@ module Rigor
1260
1244
  # MUST NOT record a tracer event.
1261
1245
  return dynamic_top if receiver.is_a?(Type::Dynamic)
1262
1246
 
1247
+ # ADR-24 slice 4a — this is the engine choke-point where an
1248
+ # implicit-self call has exhausted every resolution tier (RBS
1249
+ # dispatch + user-class ancestor walk) and falls through to
1250
+ # `Dynamic[top]`. When the slice-4 recorder is active, capture the
1251
+ # miss so a later slice's closed-class gate can flag it. Off by
1252
+ # default: `active?` is a plain integer read.
1253
+ record_unresolved_self_call(node, receiver) if Analysis::SelfCallResolutionRecorder.active?
1254
+
1263
1255
  fallback_for(node, family: :prism)
1264
1256
  end
1265
1257
 
1258
+ # ADR-24 slice 4a — records an unresolved *implicit-self* call (no
1259
+ # explicit receiver) whose `self` types to a concrete user class.
1260
+ # Explicit-receiver misses are out of scope (the existing
1261
+ # `call.undefined-method` rule already owns receiver-typed dispatch);
1262
+ # a non-`Nominal` self (top-level / DSL-block `self`, or a `Dynamic`
1263
+ # self) is skipped so the gradual guarantee is never touched here.
1264
+ def record_unresolved_self_call(node, receiver)
1265
+ return unless node.receiver.nil?
1266
+ return unless receiver.is_a?(Type::Nominal)
1267
+ return if self_call_method_known?(receiver.class_name, node.name)
1268
+
1269
+ location = node.message_loc || node.location
1270
+ Analysis::SelfCallResolutionRecorder.record(
1271
+ class_name: receiver.class_name,
1272
+ method_name: node.name,
1273
+ node: node,
1274
+ path: scope.source_path,
1275
+ line: location&.start_line,
1276
+ column: location ? location.start_column + 1 : nil
1277
+ )
1278
+ end
1279
+
1280
+ # The recorder must capture *existence* misses, not type misses.
1281
+ # Reaching the choke-point means RBS dispatch produced no result, but
1282
+ # a project method can still EXIST without an inferable return type —
1283
+ # a `module_function` sibling whose body the engine can't fully type,
1284
+ # an `attr_reader` / `define_method` / `Data.define` member. Recording
1285
+ # those would reproduce the 135 false positives of slice-4 attempt 1.
1286
+ # So skip any name the engine's own existence signals already know:
1287
+ # a `def` resolvable through the ancestor walk, or an own-class entry
1288
+ # in the discovered-methods table (`def` / `attr_*` / `define_method`
1289
+ # / alias). This reuses the engine's real resolution — the
1290
+ # "collect, don't recompute" lesson — so only a name that exists
1291
+ # nowhere a project signal can see reaches the recorder.
1292
+ # `module_function` records its defs as `:singleton` (an implicit-self
1293
+ # call inside such a method dispatches to the module's singleton
1294
+ # method), while ordinary instance methods record `:instance`. The
1295
+ # recorder cannot tell the two contexts apart from the call node, so
1296
+ # existence under EITHER kind suppresses recording — the FP-safe
1297
+ # choice, since either means the method genuinely exists.
1298
+ def self_call_method_known?(class_name, method_name)
1299
+ return true if resolve_user_def_through_ancestors(class_name, method_name)
1300
+
1301
+ scope.discovered_method?(class_name, method_name, :instance) ||
1302
+ scope.discovered_method?(class_name, method_name, :singleton)
1303
+ end
1304
+
1266
1305
  # v0.0.2 #5 — re-types the body of a user-defined
1267
1306
  # instance method with the call site's argument types
1268
1307
  # bound to the method's parameters. Used as a
@@ -1349,7 +1388,44 @@ module Rigor
1349
1388
  ANCESTOR_WALK_LIMIT = 100
1350
1389
  private_constant :ANCESTOR_WALK_LIMIT
1351
1390
 
1391
+ CLASS_GRAPH_CACHE_KEY = :__rigor_class_graph_cache__
1392
+ private_constant :CLASS_GRAPH_CACHE_KEY
1393
+
1394
+ # Run-scoped memo for the static class-graph resolvers below. They
1395
+ # are pure functions of the *frozen* project index trio
1396
+ # (`discovered_def_nodes` / `discovered_superclasses` /
1397
+ # `discovered_includes`) — `user_def_for` / `superclass_of` /
1398
+ # `includes_of` read nothing else, and never touch the current
1399
+ # scope's locals or narrowings — so a result computed for one
1400
+ # `(class, method)` is valid for every `Scope` that shares those
1401
+ # tables. `ExpressionTyper` is rebuilt per `Scope#type_of`, so the
1402
+ # memo lives on `Thread.current` rather than on `self`. It is keyed
1403
+ # by the *identity* of the three frozen tables (nested
1404
+ # `compare_by_identity` stores): a new analysis generation, or any
1405
+ # `Scope` that swaps an index via `with_discovered_*`, transparently
1406
+ # lands in a fresh bucket while everything sharing the tables shares
1407
+ # the memo. Steady-state cost is three identity-keyed hash reads and
1408
+ # zero allocation — the `||=` chains only allocate on the first miss
1409
+ # of a generation. (Pool mode forks per worker, so the
1410
+ # `Thread.current` store is process-local and never crosses a
1411
+ # project boundary.)
1412
+ def class_graph_buckets
1413
+ store = (Thread.current[CLASS_GRAPH_CACHE_KEY] ||= {}.compare_by_identity)
1414
+ by_def = (store[scope.discovered_def_nodes] ||= {}.compare_by_identity)
1415
+ by_super = (by_def[scope.discovered_superclasses] ||= {}.compare_by_identity)
1416
+ by_super[scope.discovered_includes] ||= { name: {}, user_def: {} }
1417
+ end
1418
+
1352
1419
  def resolve_user_def_through_ancestors(class_name, method_name)
1420
+ cache = class_graph_buckets[:user_def]
1421
+ table = (cache[class_name.to_s] ||= {})
1422
+ key = method_name.to_sym
1423
+ return table[key] if table.key?(key)
1424
+
1425
+ table[key] = compute_user_def_through_ancestors(class_name, method_name)
1426
+ end
1427
+
1428
+ def compute_user_def_through_ancestors(class_name, method_name)
1353
1429
  queue = [class_name.to_s]
1354
1430
  seen = {}
1355
1431
  visited = 0
@@ -1398,6 +1474,14 @@ module Rigor
1398
1474
  # no candidate names a discovered user class (e.g. the
1399
1475
  # superclass is an RBS-known or third-party class).
1400
1476
  def resolve_ancestor_class_name(subclass_qualified, raw_superclass)
1477
+ by_subclass = (class_graph_buckets[:name][subclass_qualified] ||= {})
1478
+ return by_subclass[raw_superclass] if by_subclass.key?(raw_superclass)
1479
+
1480
+ by_subclass[raw_superclass] =
1481
+ compute_ancestor_class_name(subclass_qualified, raw_superclass)
1482
+ end
1483
+
1484
+ def compute_ancestor_class_name(subclass_qualified, raw_superclass)
1401
1485
  segments = subclass_qualified.split("::")
1402
1486
  (segments.length - 1).downto(0) do |i|
1403
1487
  candidate = (segments[0, i] + [raw_superclass]).join("::")
@@ -1460,29 +1544,31 @@ module Rigor
1460
1544
  # nil when the parameter shape is too complex for the
1461
1545
  # first-iteration binder (rest args, keyword args,
1462
1546
  # block params, etc.).
1463
- def build_user_method_body_scope(def_node, receiver, arg_types) # rubocop:disable Metrics/AbcSize
1547
+ def build_user_method_body_scope(def_node, receiver, arg_types)
1464
1548
  params = def_node.parameters
1465
1549
  required = params&.requireds || []
1466
1550
  return nil unless params.nil? || user_method_param_shape_simple?(params)
1467
1551
  return nil unless required.size == arg_types.size
1468
1552
 
1469
- fresh = Scope.empty(environment: scope.environment)
1470
- .with_declared_types(scope.declared_types)
1471
- .with_discovered_classes(scope.discovered_classes)
1472
- .with_in_source_constants(scope.in_source_constants)
1473
- .with_class_ivars(scope.class_ivars)
1474
- .with_class_cvars(scope.class_cvars)
1475
- .with_program_globals(scope.program_globals)
1476
- .with_discovered_methods(scope.discovered_methods)
1477
- .with_discovered_def_nodes(scope.discovered_def_nodes)
1478
- .with_discovered_superclasses(scope.discovered_superclasses)
1479
- .with_discovered_includes(scope.discovered_includes)
1480
- .with_self_type(receiver)
1481
-
1482
- required.each_with_index do |param, index|
1483
- fresh = fresh.with_local(param.name, arg_types[index])
1484
- end
1485
- fresh
1553
+ # Bind required positionals by index. The body scope starts from an
1554
+ # empty fact store and narrowing set, so `with_local`'s fact /
1555
+ # narrowing invalidations would be no-ops here — build the locals
1556
+ # table directly (matching `with_local`'s `name.to_sym` key).
1557
+ locals = {}
1558
+ required.each_with_index { |param, index| locals[param.name.to_sym] = arg_types[index] }
1559
+
1560
+ # Construct the body scope in a SINGLE allocation — the previous
1561
+ # `Scope.empty.with_*.with_*…` chain allocated a fresh frozen Scope
1562
+ # per field, run per user-method-call inference (ADR-44). The
1563
+ # discovery index is inherited whole by reference (ADR-53 Track A);
1564
+ # the hand-copied per-field list this replaces had silently dropped
1565
+ # `data_member_layouts` and `discovered_method_visibilities`.
1566
+ Scope.new(
1567
+ environment: scope.environment,
1568
+ locals: locals.freeze,
1569
+ self_type: receiver,
1570
+ discovery: scope.discovery
1571
+ )
1486
1572
  end
1487
1573
 
1488
1574
  # First iteration accepts only required positional