rigortype 0.1.16 → 0.1.17

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 (136) hide show
  1. checksums.yaml +4 -4
  2. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +100 -0
  3. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +209 -0
  4. data/lib/rigor/analysis/check_rules.rb +149 -70
  5. data/lib/rigor/analysis/dependency_recorder.rb +122 -0
  6. data/lib/rigor/analysis/diagnostic.rb +18 -0
  7. data/lib/rigor/analysis/incremental.rb +162 -0
  8. data/lib/rigor/analysis/incremental_session.rb +337 -0
  9. data/lib/rigor/analysis/rule_catalog.rb +48 -0
  10. data/lib/rigor/analysis/runner.rb +434 -37
  11. data/lib/rigor/analysis/self_call_resolution_recorder.rb +121 -0
  12. data/lib/rigor/builtins/static_return_refinements.rb +7 -1
  13. data/lib/rigor/cache/descriptor.rb +50 -49
  14. data/lib/rigor/cache/incremental_snapshot.rb +147 -0
  15. data/lib/rigor/cache/rbs_cache_producer.rb +30 -0
  16. data/lib/rigor/cache/rbs_class_ancestor_table.rb +2 -8
  17. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -8
  18. data/lib/rigor/cache/rbs_constant_table.rb +2 -8
  19. data/lib/rigor/cache/rbs_environment.rb +2 -8
  20. data/lib/rigor/cache/rbs_instance_definitions.rb +3 -16
  21. data/lib/rigor/cache/rbs_known_class_names.rb +2 -8
  22. data/lib/rigor/cache/store.rb +99 -1
  23. data/lib/rigor/cli/annotate_command.rb +2 -7
  24. data/lib/rigor/cli/baseline_command.rb +2 -7
  25. data/lib/rigor/cli/command.rb +47 -0
  26. data/lib/rigor/cli/coverage_command.rb +3 -23
  27. data/lib/rigor/cli/coverage_renderer.rb +3 -8
  28. data/lib/rigor/cli/diff_command.rb +3 -7
  29. data/lib/rigor/cli/explain_command.rb +2 -7
  30. data/lib/rigor/cli/lsp_command.rb +3 -7
  31. data/lib/rigor/cli/mcp_command.rb +3 -7
  32. data/lib/rigor/cli/options.rb +57 -0
  33. data/lib/rigor/cli/plugin_command.rb +3 -7
  34. data/lib/rigor/cli/plugins_command.rb +2 -7
  35. data/lib/rigor/cli/renderable.rb +26 -0
  36. data/lib/rigor/cli/sig_gen_command.rb +2 -7
  37. data/lib/rigor/cli/skill_command.rb +3 -7
  38. data/lib/rigor/cli/triage_command.rb +2 -7
  39. data/lib/rigor/cli/type_of_command.rb +5 -38
  40. data/lib/rigor/cli/type_of_renderer.rb +4 -9
  41. data/lib/rigor/cli/type_scan_command.rb +3 -23
  42. data/lib/rigor/cli/type_scan_renderer.rb +4 -9
  43. data/lib/rigor/cli.rb +125 -43
  44. data/lib/rigor/configuration/dependencies.rb +18 -1
  45. data/lib/rigor/configuration/severity_profile.rb +22 -3
  46. data/lib/rigor/configuration.rb +13 -3
  47. data/lib/rigor/environment/rbs_loader.rb +76 -3
  48. data/lib/rigor/inference/block_parameter_binder.rb +1 -2
  49. data/lib/rigor/inference/builtins/array_catalog.rb +2 -5
  50. data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -5
  51. data/lib/rigor/inference/builtins/complex_catalog.rb +2 -5
  52. data/lib/rigor/inference/builtins/date_catalog.rb +2 -5
  53. data/lib/rigor/inference/builtins/encoding_catalog.rb +2 -5
  54. data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -5
  55. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -5
  56. data/lib/rigor/inference/builtins/hash_catalog.rb +2 -5
  57. data/lib/rigor/inference/builtins/method_catalog.rb +15 -0
  58. data/lib/rigor/inference/builtins/numeric_catalog.rb +21 -93
  59. data/lib/rigor/inference/builtins/pathname_catalog.rb +2 -5
  60. data/lib/rigor/inference/builtins/proc_catalog.rb +2 -5
  61. data/lib/rigor/inference/builtins/random_catalog.rb +2 -5
  62. data/lib/rigor/inference/builtins/range_catalog.rb +2 -5
  63. data/lib/rigor/inference/builtins/rational_catalog.rb +2 -5
  64. data/lib/rigor/inference/builtins/re_catalog.rb +2 -5
  65. data/lib/rigor/inference/builtins/set_catalog.rb +2 -5
  66. data/lib/rigor/inference/builtins/string_catalog.rb +2 -5
  67. data/lib/rigor/inference/builtins/struct_catalog.rb +2 -5
  68. data/lib/rigor/inference/builtins/time_catalog.rb +2 -5
  69. data/lib/rigor/inference/expression_typer.rb +140 -20
  70. data/lib/rigor/inference/method_dispatcher/block_folding.rb +5 -1
  71. data/lib/rigor/inference/method_dispatcher/call_context.rb +65 -0
  72. data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +11 -10
  73. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +12 -6
  74. data/lib/rigor/inference/method_dispatcher/data_folding.rb +246 -0
  75. data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -2
  76. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -2
  77. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +4 -1
  78. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +4 -1
  79. data/lib/rigor/inference/method_dispatcher/math_folding.rb +6 -6
  80. data/lib/rigor/inference/method_dispatcher/method_folding.rb +12 -7
  81. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +23 -13
  82. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +9 -9
  83. data/lib/rigor/inference/method_dispatcher/set_folding.rb +6 -6
  84. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +120 -9
  85. data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +12 -12
  86. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +49 -0
  87. data/lib/rigor/inference/method_dispatcher/time_folding.rb +6 -6
  88. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +9 -9
  89. data/lib/rigor/inference/method_dispatcher.rb +99 -59
  90. data/lib/rigor/inference/narrowing.rb +202 -5
  91. data/lib/rigor/inference/scope_indexer.rb +134 -7
  92. data/lib/rigor/inference/statement_evaluator.rb +105 -26
  93. data/lib/rigor/language_server/buffer_resolution.rb +33 -0
  94. data/lib/rigor/language_server/completion_provider.rb +4 -4
  95. data/lib/rigor/language_server/document_symbol_provider.rb +4 -4
  96. data/lib/rigor/language_server/folding_range_provider.rb +4 -4
  97. data/lib/rigor/language_server/hover_provider.rb +4 -4
  98. data/lib/rigor/language_server/selection_range_provider.rb +4 -4
  99. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  100. data/lib/rigor/plugin/base.rb +20 -4
  101. data/lib/rigor/plugin/registry.rb +39 -1
  102. data/lib/rigor/rbs_extended/conformance_checker.rb +208 -0
  103. data/lib/rigor/rbs_extended.rb +39 -0
  104. data/lib/rigor/scope.rb +123 -9
  105. data/lib/rigor/type/acceptance_router.rb +19 -0
  106. data/lib/rigor/type/accepts_result.rb +3 -10
  107. data/lib/rigor/type/app.rb +3 -7
  108. data/lib/rigor/type/bot.rb +2 -3
  109. data/lib/rigor/type/bound_method.rb +5 -12
  110. data/lib/rigor/type/combinator.rb +17 -0
  111. data/lib/rigor/type/constant.rb +2 -3
  112. data/lib/rigor/type/data_class.rb +80 -0
  113. data/lib/rigor/type/data_instance.rb +100 -0
  114. data/lib/rigor/type/difference.rb +5 -10
  115. data/lib/rigor/type/dynamic.rb +5 -10
  116. data/lib/rigor/type/hash_shape.rb +5 -15
  117. data/lib/rigor/type/integer_range.rb +5 -10
  118. data/lib/rigor/type/intersection.rb +5 -10
  119. data/lib/rigor/type/nominal.rb +5 -10
  120. data/lib/rigor/type/refined.rb +5 -10
  121. data/lib/rigor/type/singleton.rb +5 -10
  122. data/lib/rigor/type/top.rb +2 -3
  123. data/lib/rigor/type/tuple.rb +5 -10
  124. data/lib/rigor/type/union.rb +5 -10
  125. data/lib/rigor/type.rb +2 -0
  126. data/lib/rigor/value_semantics.rb +77 -0
  127. data/lib/rigor/version.rb +1 -1
  128. data/lib/rigor.rb +1 -0
  129. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +12 -2
  130. data/sig/rigor/cache.rbs +19 -0
  131. data/sig/rigor/inference.rbs +22 -0
  132. data/sig/rigor/rbs_extended.rbs +2 -0
  133. data/sig/rigor/scope.rbs +5 -0
  134. data/sig/rigor/type.rbs +58 -1
  135. data/sig/rigor.rbs +6 -1
  136. metadata +22 -1
@@ -501,6 +501,27 @@ module Rigor
501
501
  # registry rather than crashing.
502
502
  end
503
503
 
504
+ # Like {#each_class_decl_annotation}, but also yields the
505
+ # owning class / module's RBS name as the first block
506
+ # argument: `(class_name, annotation_string, location)`. Used
507
+ # by {Rigor::RbsExtended::ConformanceChecker} to resolve a
508
+ # `rigor:v1:conforms-to` directive back to the class it
509
+ # annotates. Same fail-soft policy as the un-named variant.
510
+ def each_class_decl_annotation_with_name
511
+ return enum_for(:each_class_decl_annotation_with_name) unless block_given?
512
+ return if env.nil?
513
+
514
+ env.class_decls.each do |rbs_name, entry|
515
+ entry.each_decl do |decl|
516
+ next unless decl.respond_to?(:annotations)
517
+
518
+ decl.annotations.each { |a| yield rbs_name.to_s, a.string, a.location }
519
+ end
520
+ end
521
+ rescue ::RBS::BaseError, ::Ractor::IsolationError
522
+ # fail-soft: see #each_class_decl_annotation.
523
+ end
524
+
504
525
  # Returns a frozen `Hash<String, String>` mapping each loaded
505
526
  # class / module name (top-level prefixed) to the file path of
506
527
  # its FIRST declaration's RBS source. Used by
@@ -571,6 +592,44 @@ module Rigor
571
592
  definition.methods[method_name.to_sym]
572
593
  end
573
594
 
595
+ # @return [Array<Symbol>, nil] every instance-method name on
596
+ # `class_name` — own, inherited, and included — as resolved
597
+ # by `RBS::DefinitionBuilder`. Returns `nil` (NOT `[]`) when
598
+ # the class definition cannot be built so callers can tell
599
+ # "no methods" apart from "unknown class". Used by the
600
+ # `rigor:v1:conforms-to` presence check
601
+ # ({Rigor::RbsExtended::ConformanceChecker}).
602
+ def instance_method_names(class_name)
603
+ definition = instance_definition(class_name)
604
+ return nil unless definition
605
+
606
+ definition.methods.keys
607
+ end
608
+
609
+ # @return [RBS::Definition, nil] the built definition for the RBS
610
+ # interface `interface_name` (`_RewindableStream`), whose `.methods`
611
+ # are the required members (including interface-ancestor members).
612
+ # Returns `nil` when the name does not resolve to a loaded interface
613
+ # (a typo, or the defining library / sig set is not on the load
614
+ # path). Fail-soft on RBS build errors.
615
+ def interface_definition(interface_name)
616
+ rbs_name = parse_type_name(interface_name)
617
+ return nil unless rbs_name
618
+ return nil if env.nil?
619
+ return nil unless env.interface_decls.key?(rbs_name)
620
+
621
+ builder.build_interface(rbs_name)
622
+ rescue ::RBS::BaseError
623
+ nil
624
+ end
625
+
626
+ # @return [Array<Symbol>, nil] every method name required by the RBS
627
+ # interface `interface_name`, or nil when it does not resolve. Thin
628
+ # accessor over {#interface_definition} for the presence check.
629
+ def interface_method_names(interface_name)
630
+ interface_definition(interface_name)&.methods&.keys
631
+ end
632
+
574
633
  # @return [RBS::Definition, nil] the resolved singleton (class
575
634
  # object) definition for `class_name`. The methods on this
576
635
  # definition are the *class methods* of `class_name`, including
@@ -983,14 +1042,28 @@ module Rigor
983
1042
  nil
984
1043
  end
985
1044
 
1045
+ # Memoised on `@state` (the per-loader store also holding `:env` /
1046
+ # `:builder`): `RBS::TypeName.parse` is a pure, deterministic
1047
+ # function of the normalised string, and the `RBS::TypeName` it
1048
+ # returns is a frozen value object safe to share across callers
1049
+ # (every consumer only reads it — `env.class_decls.key?` /
1050
+ # `builder.build_*`). The same handful of class names are parsed
1051
+ # on nearly every call-site dispatch, so this was a top allocation
1052
+ # site; caching the immutable result (nil included) removes it.
986
1053
  def parse_type_name(name)
987
1054
  s = name.to_s
988
1055
  return nil if s.empty?
989
1056
 
990
1057
  s = "::#{s}" unless s.start_with?("::")
991
- RBS::TypeName.parse(s)
992
- rescue ::RBS::BaseError
993
- nil
1058
+ cache = (@state[:type_name_cache] ||= {})
1059
+ return cache[s] if cache.key?(s)
1060
+
1061
+ cache[s] =
1062
+ begin
1063
+ RBS::TypeName.parse(s)
1064
+ rescue ::RBS::BaseError
1065
+ nil
1066
+ end
994
1067
  end
995
1068
 
996
1069
  def compute_class_known(name)
@@ -174,8 +174,7 @@ module Rigor
174
174
 
175
175
  def bind_trailing_positionals(params_node, bindings, cursor)
176
176
  params_node.posts.each do |param|
177
- name = required_name(param)
178
- bindings[name] = positional_type_at(cursor) if name
177
+ bind_required_param(param, cursor, bindings)
179
178
  cursor += 1
180
179
  end
181
180
  cursor
@@ -13,11 +13,8 @@ module Rigor
13
13
  # `ary_push_internal`, …) that the classifier does not yet
14
14
  # recognise. The blocklist captures the methods we have
15
15
  # specifically observed flowing as `:leaf` despite mutating.
16
- ARRAY_CATALOG = MethodCatalog.new(
17
- path: File.expand_path(
18
- "../../../../data/builtins/ruby_core/array.yml",
19
- __dir__
20
- ),
16
+ ARRAY_CATALOG = MethodCatalog.for_topic(
17
+ "array",
21
18
  mutating_selectors: {
22
19
  "Array" => Set[
23
20
  # Mutators classified `:leaf` by the C-body heuristic
@@ -13,11 +13,8 @@ module Rigor
13
13
  # (which dispatches on the receiver's concrete class).
14
14
  # The data is consumed by future include-aware lookup —
15
15
  # see `docs/CURRENT_WORK.md` for the planned slice.
16
- COMPARABLE_CATALOG = MethodCatalog.new(
17
- path: File.expand_path(
18
- "../../../../data/builtins/ruby_core/comparable.yml",
19
- __dir__
20
- ),
16
+ COMPARABLE_CATALOG = MethodCatalog.for_topic(
17
+ "comparable",
21
18
  mutating_selectors: {
22
19
  "Comparable" => Set[]
23
20
  }
@@ -21,11 +21,8 @@ module Rigor
21
21
  # so a hypothetical future `Constant<Complex>` carrier cannot
22
22
  # fold an aliasing copy through the catalog (mirrors
23
23
  # `range_catalog.rb`, `time_catalog.rb`, `date_catalog.rb`).
24
- COMPLEX_CATALOG = MethodCatalog.new(
25
- path: File.expand_path(
26
- "../../../../data/builtins/ruby_core/complex.yml",
27
- __dir__
28
- ),
24
+ COMPLEX_CATALOG = MethodCatalog.for_topic(
25
+ "complex",
29
26
  mutating_selectors: {
30
27
  "Complex" => Set[
31
28
  # Defence in depth: `Complex` does not currently expose
@@ -54,11 +54,8 @@ module Rigor
54
54
  # blocklist entry; the entries below are defense-in-depth
55
55
  # against indirect mutators the regex might miss in a future
56
56
  # CRuby bump.
57
- DATE_CATALOG = MethodCatalog.new(
58
- path: File.expand_path(
59
- "../../../../data/builtins/ruby_core/date.yml",
60
- __dir__
61
- ),
57
+ DATE_CATALOG = MethodCatalog.for_topic(
58
+ "date",
62
59
  mutating_selectors: {
63
60
  "Date" => Set[
64
61
  # `d_lite_initialize_copy` is already classed
@@ -22,11 +22,8 @@ module Rigor
22
22
  # against the analyzer process's registry — what UTF-8's
23
23
  # alias list is in the analyzer is not necessarily what it
24
24
  # is in the analysed program.
25
- ENCODING_CATALOG = MethodCatalog.new(
26
- path: File.expand_path(
27
- "../../../../data/builtins/ruby_core/encoding.yml",
28
- __dir__
29
- ),
25
+ ENCODING_CATALOG = MethodCatalog.for_topic(
26
+ "encoding",
30
27
  mutating_selectors: {
31
28
  "Encoding" => Set[
32
29
  # Defence-in-depth: mirrors range_catalog.rb /
@@ -13,11 +13,8 @@ module Rigor
13
13
  # (which dispatches on the receiver's concrete class).
14
14
  # The data is consumed by future include-aware lookup —
15
15
  # see `docs/CURRENT_WORK.md` for the planned slice.
16
- ENUMERABLE_CATALOG = MethodCatalog.new(
17
- path: File.expand_path(
18
- "../../../../data/builtins/ruby_core/enumerable.yml",
19
- __dir__
20
- ),
16
+ ENUMERABLE_CATALOG = MethodCatalog.for_topic(
17
+ "enumerable",
21
18
  mutating_selectors: {
22
19
  "Enumerable" => Set[]
23
20
  }
@@ -26,11 +26,8 @@ module Rigor
26
26
  # already declines) or blocklisted because the static classifier
27
27
  # missed an indirect side effect. The remaining `:leaf` method
28
28
  # that DOES fold is `#cause`, a pure accessor.
29
- EXCEPTION_CATALOG = MethodCatalog.new(
30
- path: File.expand_path(
31
- "../../../../data/builtins/ruby_core/exception.yml",
32
- __dir__
33
- ),
29
+ EXCEPTION_CATALOG = MethodCatalog.for_topic(
30
+ "exception",
34
31
  mutating_selectors: {
35
32
  "Exception" => Set[
36
33
  # `exc_initialize` writes `mesg` / `backtrace` ivars on
@@ -15,11 +15,8 @@ module Rigor
15
15
  # captures every false-positive `:leaf` we have spotted in the
16
16
  # generated YAML — bias toward conservatism so a missed fold is
17
17
  # acceptable but a folded mutator/yielder is not.
18
- HASH_CATALOG = MethodCatalog.new(
19
- path: File.expand_path(
20
- "../../../../data/builtins/ruby_core/hash.yml",
21
- __dir__
22
- ),
18
+ HASH_CATALOG = MethodCatalog.for_topic(
19
+ "hash",
23
20
  mutating_selectors: {
24
21
  "Hash" => Set[
25
22
  # Block-dependent iteration — yields via `rb_hash_foreach`
@@ -27,6 +27,21 @@ module Rigor
27
27
  FOLDABLE_PURITIES = Set["leaf", "trivial", "leaf_when_numeric"].freeze
28
28
  EMPTY_CATALOG = { "classes" => {} }.freeze
29
29
 
30
+ # Shared root for the offline-generated catalogues. Resolving it
31
+ # here keeps the repo-relative `../../../../` hop in one place
32
+ # instead of copying it into every per-topic loader.
33
+ DATA_ROOT = File.expand_path("../../../../data/builtins/ruby_core", __dir__)
34
+ private_constant :DATA_ROOT
35
+
36
+ # Build a catalog for a named topic, resolving its YAML path
37
+ # under {DATA_ROOT}. Equivalent to `new(path: …)` for the common
38
+ # case where the file is `<topic>.yml`; prefer this over passing
39
+ # an explicit `File.expand_path` so the data-root hop stays
40
+ # centralised.
41
+ def self.for_topic(topic, mutating_selectors: {})
42
+ new(path: File.join(DATA_ROOT, "#{topic}.yml"), mutating_selectors: mutating_selectors)
43
+ end
44
+
30
45
  def initialize(path:, mutating_selectors: {})
31
46
  @path = path
32
47
  @mutating_selectors = mutating_selectors.transform_values(&:freeze).freeze
@@ -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