Module: Rigor::Inference::Builtins
- Defined in:
- lib/rigor/inference/builtins/re_catalog.rb,
lib/rigor/inference/builtins/set_catalog.rb,
lib/rigor/inference/builtins/date_catalog.rb,
lib/rigor/inference/builtins/hash_catalog.rb,
lib/rigor/inference/builtins/proc_catalog.rb,
lib/rigor/inference/builtins/time_catalog.rb,
lib/rigor/inference/builtins/array_catalog.rb,
lib/rigor/inference/builtins/range_catalog.rb,
lib/rigor/inference/builtins/method_catalog.rb,
lib/rigor/inference/builtins/random_catalog.rb,
lib/rigor/inference/builtins/string_catalog.rb,
lib/rigor/inference/builtins/struct_catalog.rb,
lib/rigor/inference/builtins/complex_catalog.rb,
lib/rigor/inference/builtins/numeric_catalog.rb,
lib/rigor/inference/builtins/encoding_catalog.rb,
lib/rigor/inference/builtins/pathname_catalog.rb,
lib/rigor/inference/builtins/rational_catalog.rb,
lib/rigor/inference/builtins/exception_catalog.rb,
lib/rigor/inference/builtins/comparable_catalog.rb,
lib/rigor/inference/builtins/enumerable_catalog.rb
Defined Under Namespace
Modules: NumericCatalog Classes: MethodCatalog
Constant Summary collapse
- REGEXP_CATALOG =
‘Regexp` / `MatchData` catalog. Singleton — load once, consult during dispatch.
‘Init_Regexp` in `references/ruby/re.c` registers BOTH classes in a single C init block, so the catalog carries both — `Regexp` (the pattern carrier) plus `MatchData` (the result-of-match carrier produced by `Regexp#match` / `String#match` and consulted via `$~`). The catalog wiring therefore mostly governs:
-
The reader surface on each class (‘Regexp#source`, `Regexp#options`, `Regexp#casefold?`, `MatchData#size`, `MatchData#captures`, etc.) — RBS-declared returns are preserved through dispatch.
-
The blocklist below, which keeps methods that touch process-global state (the ‘$~` backref) from being folded. Regexp matching is observably stateful: `Regexp#=~`, `#===` and `#~` all call `rb_backref_set` (writing `$~` and the `$1..$N` / `$&` / “ $` “ / `$’‘ aliases). A constant-fold that dropped those calls would silently change the visible state of the program, so they MUST decline through to the RBS tier.
‘Regexp.last_match` and `Regexp.timeout` / `Regexp.timeout=` are class-level (singleton) methods that also touch process-global state, but the dispatcher’s catalog lookup only consults ‘:instance` entries today — class-method calls on a `Singleton` receiver type take the `meta_*` path in `MethodDispatcher` rather than walking `CATALOG_BY_CLASS` —so listing them here would be dead code. Their RBS-tier signatures already widen the answer enough to keep the behaviour sound; revisit if the dispatcher ever grows a singleton-aware catalog path.
-
MethodCatalog.new( path: File.( "../../../../data/builtins/ruby_core/re.yml", __dir__ ), mutating_selectors: { "Regexp" => Set[ # Defensive: aliasing-copy semantics already covered # by the `:mutates_self` classifier, listed here for # symmetry with String / Array / Hash / Range / Set. :initialize_copy, # `=~`, `===`, `~` all run `rb_reg_search` (or call # `rb_backref_set(Qnil)` directly) — every successful # OR failing match writes `$~` and the # `$1..$N` / `$&` / `` $` `` / `$'` aliases. Folding # would discard the visible side effect. :=~, :"===", :~, # `match` is already `:block_dependent` (the C body # yields), but it ALSO writes `$~` regardless of the # block. Listed here so a future extractor that # reclassifies it as `:leaf` (because the yield is # behind a helper) does not silently fold it. :match ], "MatchData" => Set[ # Defensive entry mirroring the other catalogs. # `match_init_copy` is already `:leaf` per the # extractor (it copies the regs slot in place but # uses no helper the C-body regex flags as a # mutator); blocked so a future # `Constant<MatchData>` carrier never folds an # aliasing copy through the catalog. :initialize_copy ] } )
- SET_CATALOG =
‘Set` catalog. Singleton — load once, consult during dispatch.
Set was rewritten in C and folded into CRuby for Ruby 3.2+; the reference branch (‘ruby_4_0`) ships the implementation in `references/ruby/set.c` with `Init_Set` registering every method directly. There is no `set.rb` prelude — the trailing `rb_provide(“set.rb”)` makes `require “set”` a no-op against the built-in.
The blocklist below catches the catalog ‘:leaf` entries the C-body classifier mis-attributes. Set’s iteration helpers (‘set_iter`, `RETURN_SIZED_ENUMERATOR`) and its identity- mode and reset paths drive into helpers the regex classifier does not yet recognise as block-yielding or mutating.
MethodCatalog.new( path: File.( "../../../../data/builtins/ruby_core/set.yml", __dir__ ), mutating_selectors: { "Set" => Set[ # Indirect mutators classified `:leaf` because the C # classifier did not follow the helper functions: # # - `initialize_copy` calls `set_copy` to overwrite the # receiver's table. # - `compare_by_identity` swaps the internal hash type # via `set_reset_table_with_type`. # - `reset` rebuilds the internal table to dedup after # element mutation. :initialize_copy, :compare_by_identity, :reset, # Block-dependent methods classified `:leaf` because the # C body uses `set_iter` / `RETURN_SIZED_ENUMERATOR` # rather than calling `rb_yield` directly: :each, :classify, :divide, # `disjoint?` delegates into `set_i_intersect`, which # for non-Set enumerables uses `rb_funcall(other, # :any?, ...)` — that is user-redefinable dispatch the # classifier missed because the call site is in a # sibling function. :disjoint? ] } )
- DATE_CATALOG =
‘Date` / `DateTime` catalog. Singleton — load once, consult during dispatch.
‘Date` and `DateTime` both come from CRuby’s bundled ‘date` gem (`references/ruby/ext/date/date_core.c`). A single `Init_date_core` function registers them, so the catalog carries both classes — `Date` plus the `DateTime` subclass whose own Init block extends with `hour` / `min` / `strftime` / `iso8601` etc. The Ruby-side prelude (`lib/date.rb`) only contributes `Date#infinite?` and the nested `Date::Infinity` class; the bulk of the surface is in C.
Date / DateTime receivers are not lifted to a ‘Constant` carrier today (there is no Date literal node — the closest is `Date.today` / `Date.parse(…)`, which produce `Nominal`). The catalog wiring therefore mostly governs:
-
The Integer-typed reader surface (‘#year`, `#month`, `#day`, `#wday`, `#hour`, `#min`, `#sec`) — RBS-declared `Integer` is preserved through dispatch.
-
The blocklist below, which keeps mutator-style methods that the C-body classifier already flagged (‘mutates_self`) from being missed by a future `Constant<Date>` carrier, plus a defensive `:initialize_copy` entry for symmetry with the other catalogs.
The non-bang ‘#next_day` / `#prev_day` / `#next_month` / `#prev_month` / `#next_year` / `#prev_year` / `#>>` / `#<<` selectors all RETURN brand-new `Date` objects rather than mutating the receiver — they intentionally stay catalog-eligible. The two real mutators (`#initialize_copy`, `#marshal_load`) are already classified `:mutates_self` by the C-body regex, so they fall out of `MethodCatalog#safe_for_folding?` without an explicit blocklist entry; the entries below are defense-in-depth against indirect mutators the regex might miss in a future CRuby bump.
-
MethodCatalog.new( path: File.( "../../../../data/builtins/ruby_core/date.yml", __dir__ ), mutating_selectors: { "Date" => Set[ # `d_lite_initialize_copy` is already classed # `:mutates_self` by the regex (it calls # `rb_check_frozen` and rewrites the receiver's # internal `dat` slots). Listed here for symmetry with # String / Array / Range / Set / Time and to keep the # blocklist self-documenting. :initialize_copy, # `d_lite_fill` is a `#ifndef NDEBUG` debug method that # warms the receiver's cached `simple` / `complex` # fields via the `get_s_*` / `get_c_*` macros. The # macros perform in-place writes on the receiver's # internal `dat` struct but use no helper the C-body # regex recognises, so the classifier mis-flags it # `:leaf`. Blocked so a future `Constant<Date>` carrier # never folds it. :fill ], "DateTime" => Set[ # `DateTime` inherits the bulk of its surface from # `Date`. The dedicated DateTime-side methods are all # readers (`hour`, `min`, …) plus formatting # converters (`strftime`, `iso8601`, …); none mutate # the receiver. The single defensive entry mirrors the # Date side so that the inherited # `Date#initialize_copy` (registered against # `cDateTime` through subclassing) cannot fold through # the catalog if a future `Constant<DateTime>` carrier # ever lands. :initialize_copy ] } )
- HASH_CATALOG =
‘Hash` catalog. Singleton — load once, consult during dispatch.
Hash mirrors Array’s mutation pattern: nearly every iteration method yields through ‘rb_hash_foreach` plus a per-pair static callback (`each_value_i`, `keep_if_i`, …), and the C-body classifier does not follow into the callback so it lands as `:leaf` despite being block-dependent. The blocklist below captures every false-positive `:leaf` we have spotted in the generated YAML — bias toward conservatism so a missed fold is acceptable but a folded mutator/yielder is not.
MethodCatalog.new( path: File.( "../../../../data/builtins/ruby_core/hash.yml", __dir__ ), mutating_selectors: { "Hash" => Set[ # Block-dependent iteration — yields via `rb_hash_foreach` # plus a per-pair callback that the regex classifier does # not follow: :each, :each_pair, :each_key, :each_value, :select, :filter, :reject, :transform_values, # Block-dependent merge — `rb_hash_merge` delegates into # `rb_hash_update`, which yields per conflict when a block # is given: :merge ] } )
- PROC_CATALOG =
‘Proc` / `Method` / `UnboundMethod` catalog. Singleton —load once, consult during dispatch.
The three callable carriers are imported together because ‘Init_Proc` registers them in a single C init block. They share the same fundamental hazard at the catalog tier: most of their public methods invoke the wrapped Ruby code (the proc body, the bound method’s receiver, …) and that code can do anything — read mutable state, call I/O, return different values on successive calls. The static C-body classifier marks these ‘:leaf` because the C functions themselves do not call `rb_funcall*` / `rb_yield` directly (they delegate through the VM’s optimised call paths and method-entry table), but folding any of them at compile time would freeze a value the runtime never actually produces twice.
The blocklist below errs aggressively on the side of caution: a hypothetical future ‘Constant<Proc>` / `Constant<Method>` / `Constant<UnboundMethod>` carrier would have very little to gain from these folds and a great deal to lose if user code ran behind the analyzer’s back. Reflective readers (‘#arity`, `#parameters`, `#source_location`, `#name`, `#owner`, `#receiver`) remain foldable; the RBS tier still resolves return types for the blocklisted methods so callers do not lose precision.
MethodCatalog.new( path: File.( "../../../../data/builtins/ruby_core/proc.yml", __dir__ ), mutating_selectors: { "Proc" => Set[ # `#call` / `#[]` / `#===` / `#yield` invoke the proc # body. The C body routes through # `OPTIMIZED_METHOD_TYPE_CALL` (a VM fast path the # classifier cannot see into); the proc body can do # anything — read globals, mutate captured locals, # raise. MUST decline to fold. :call, :[], :===, :yield, # `#curry` / `#<<` / `#>>` allocate a fresh `Proc` # that closes over the receiver (and, for `<<` / # `>>`, over the argument). Folding would freeze a # specific `Proc` instance whose identity the runtime # never actually produces (object_id differs every # call), so the catalog tier declines. :curry, :<<, :>>, # `#to_proc` returns `self` for `Proc` (cheap), but # blocking it keeps the rule shape uniform across the # three callable carriers (Method#to_proc allocates a # fresh `Proc`). :to_proc, # Identity-based equality and hashing: `#hash` is # derived from the underlying ISeq pointer; `#==` / # `#eql?` compare ISeq + binding. Folding to a # `Constant<Integer>` / `Constant<bool>` would freeze # an answer that depends on memory layout. Defensive. :hash, :==, :eql?, # `initialize_copy` is blocklisted by convention so a # hypothetical future `Constant<Proc>` carrier cannot # fold an aliasing copy through the catalog. :initialize_copy ], "Method" => Set[ # `#call` / `#[]` / `#===` invoke the bound method. # Same hazard as `Proc#call`: arbitrary user code, # arbitrary side effects. :call, :[], :===, # `#curry` / `#<<` / `#>>` allocate a fresh `Proc` # that closes over the bound method. :curry, :<<, :>>, # `#to_proc` allocates a fresh `Proc` wrapping the # bound method — folding would freeze its object_id. # The classifier already marks it `:block_dependent`, # but the explicit entry keeps the intent obvious. :to_proc, # `#unbind` allocates a fresh `UnboundMethod` whose # identity differs every call. :unbind, # Identity-based equality and hashing. :hash, :==, :eql?, # `initialize_copy` is blocklisted by convention. :initialize_copy ], "UnboundMethod" => Set[ # `#bind` allocates a fresh `Method` whose object_id # differs every call; `#bind_call` invokes the bound # method (already classified `:block_dependent`). :bind, :bind_call, # Identity-based equality and hashing. :hash, :==, :eql?, # `initialize_copy` is blocklisted by convention. :initialize_copy ] } )
- TIME_CATALOG =
‘Time` catalog. Singleton — load once, consult during dispatch.
Time is a pure-C built-in: the Init block in ‘references/ruby/time.c` registers the bulk of the surface, and the Ruby-side prelude `references/ruby/timev.rb` contributes the class-side constructors (`Time.now`, `Time.at`, `Time.new`) through Primitive cexpr stubs.
Time receivers are not lifted to a ‘Constant` carrier today (there is no `Time` literal node — the closest is `Time.now` / `Time.new(…)`, which produce `Nominal`). The catalog wiring therefore mostly governs:
-
The size-projection-equivalent reader surface (‘#year`, `#month`, `#hour`, `#sec`, `#wday`, …) — RBS-declared `Integer` is preserved through dispatch.
-
The blocklist below, which keeps the indirect-mutator methods that the C-body classifier mis-flagged as ‘:leaf` from ever folding through a hypothetical future `Constant<Time>` carrier.
The blocklist captures the false-positive ‘:leaf` entries whose helper functions the regex classifier did not recognise as mutators.
-
MethodCatalog.new( path: File.( "../../../../data/builtins/ruby_core/time.yml", __dir__ ), mutating_selectors: { "Time" => Set[ # `time_init_copy` writes the `timew` and `vtm` slots on # the receiver via `time_set_timew` / `time_set_vtm`. # Classed `:leaf` because those setters are not in the # mutator regex's helper list. Blocked for symmetry with # String / Array / Range / Set initialize_copy entries. :initialize_copy, # `time_localtime_m` -> `time_localtime` calls # `time_modify(time)` to mark the receiver mutable # before rewriting its `vtm` cache and `tzmode`. The # docstring is explicit ("converts time to local time # in place"). The C-body classifier mis-flagged it as # `:leaf` because `time_modify` is not in its mutator # regex. :localtime, # `time_gmtime` (registered as both `gmtime` and `utc` # against `rb_cTime`) follows the same in-place pattern # as `time_localtime`: `time_modify(time)` then a # `time_set_vtm` write and `TZMODE_SET_UTC`. Both # selectors share the cfunc, so both must be blocked. :gmtime, :utc ] } )
- ARRAY_CATALOG =
‘Array` catalog. Singleton — load once, consult during dispatch.
Array has more mutation surface than String: every method that logically reshapes the array tends to call ‘rb_ary_modify` or an internal helper (`ary_replace`, `ary_resize`, `ary_pop`, `ary_push_internal`, …) that the classifier does not yet recognise. The blocklist captures the methods we have specifically observed flowing as `:leaf` despite mutating.
MethodCatalog.new( path: File.( "../../../../data/builtins/ruby_core/array.yml", __dir__ ), mutating_selectors: { "Array" => Set[ # Mutators classified `:leaf` by the C-body heuristic :<<, :push, :replace, :clear, :concat, :insert, :"[]=", :unshift, :prepend, :pop, :shift, :delete_at, :slice!, :compact!, :flatten!, :uniq!, :sort!, :reverse!, :rotate!, :keep_if, :delete_if, :select!, :filter!, :reject!, :collect!, :map!, :assoc, :rassoc, :fill, :delete, :transpose, # Methods that yield (block-dependent) — classifier # may mark them leaf when the block call is gated: :each, :each_with_index, :each_index, :each_slice, :each_cons, :each_with_object, # Identity/comparison methods that take a block too :max, :min, :max_by, :min_by, :minmax, :minmax_by, :sort_by, :group_by, :partition, :all?, :any?, :none?, :one?, :find, :detect, :find_all, :find_index, :reduce, :inject, :flat_map, :collect_concat, :zip, :product, :combination, :permutation, :chunk_while, :slice_when, :tally ] } )
- RANGE_CATALOG =
‘Range` catalog. Singleton — load once, consult during dispatch.
Range is largely immutable: ‘begin`, `end`, and `excl` are set at construction by `range_initialize` and never mutated afterwards. The blocklist below therefore stays small. The entries we DO need are the iteration methods whose C body routes through a helper the block/yield regex does not recognise, so the classifier mis-flags them as `:leaf` despite yielding to a block.
MethodCatalog.new( path: File.( "../../../../data/builtins/ruby_core/range.yml", __dir__ ), mutating_selectors: { "Range" => Set[ # `range_initialize` / `range_initialize_copy` write # `begin`/`end`/`excl` slots on the receiver; classed # `:leaf` because the writes go through the struct # accessor not `rb_check_frozen`. Blocked for symmetry # with String / Array. :initialize, :initialize_copy, # `range_reverse_each` yields to its block via # `range_each_func` -> caller's block; the regex # classifier follows direct `rb_yield*` calls only. :reverse_each, # `range_percent_step` returns an Enumerator unless a # block is supplied, in which case it yields. Treated # as block-dependent so the fold tier never invokes it # against a literal Range and tries to materialise an # Enumerator into a Constant. :% ] } )
- RANDOM_CATALOG =
‘Random` catalog. Singleton — load once, consult during dispatch.
The static classifier marks most Random methods ‘:leaf` because their C bodies do not call `rb_funcall*` / `rb_yield` / `rb_check_frozen` directly. Random is the canonical case where that heuristic under-counts: every call to `#rand` / `#bytes` / `Random.rand` / `Random.bytes` advances the receiver’s Mersenne-Twister state through a helper (‘rand_random` -> `random_real` / `random_ulong_limited`), so folding any of them statically is unsound. `Random.new_seed` and `Random.urandom` are non-deterministic (different output every call); even though they are functionally pure they would produce a misleading constant at fold time. The whole class is conservative-by-default at the catalog tier; precision flows through the RBS layer.
MethodCatalog.new( path: File.( "../../../../data/builtins/ruby_core/random.yml", __dir__ ), mutating_selectors: { "Random" => Set[ # `rand_random` -> `random_real` / `random_ulong_limited` # advance the MT state on the receiver (instance #rand) # and on `Random::DEFAULT` (singleton .rand). The # classifier misses the indirect mutator. :rand, # `random_bytes` / `random_s_bytes` consume MT output # the same way #rand does — every call mutates the # underlying generator. :bytes, # Non-deterministic: each call produces a fresh seed # via `with_random_seed` reading platform entropy. Folding # to a constant would freeze a value that the runtime # never actually returns twice. :new_seed, # Non-deterministic: reads from platform CSPRNG (e.g. # /dev/urandom). Folding is unsound for the same reason # as `new_seed`. :urandom, # `initialize_copy` is blocklisted by convention so a # hypothetical future `Constant<Random>` carrier # cannot fold an aliasing copy through the catalog. :initialize_copy ] } )
- STRING_CATALOG =
‘String` and `Symbol` catalog. Singleton — load once, consult during dispatch.
The blocklist below is the curated set of catalog ‘:leaf` entries the C-body classifier mis-attributes (the body of `rb_str_replace` calls `str_modifiable` / `str_discard` which the regex-based classifier does not recognise as mutation primitives). Adding to the blocklist is the corrective surface for false positives until the classifier learns the helper functions.
MethodCatalog.new( path: File.( "../../../../data/builtins/ruby_core/string.yml", __dir__ ), mutating_selectors: { "String" => Set[ :replace, :initialize, :initialize_copy, :clear, :<<, :concat, :insert, :prepend, :force_encoding, :encode, :scrub, :unicode_normalize, :"[]=", :upto, :each_byte, :each_char, :each_codepoint, :each_grapheme_cluster, :each_line, :bytesplice ], "Symbol" => Set[ # Symbol is immutable in Ruby; the classifier mis-flags # `inspect` because `rb_sym_inspect` builds a temporary # mutable buffer. Allow it. ] } )
- STRUCT_CATALOG =
‘Struct` catalog. Singleton — load once, consult during dispatch.
‘Struct` is a meta-class: `Struct.new(*members)` returns a fresh anonymous subclass — never a `Struct` value. Today Rigor never produces a `Constant<Struct>` carrier (a literal struct instance), so the catalog is defensive: it documents the shape and forbids unsafe folds in case a future tier learns to lift literal struct instances into the value lattice.
Subclasses define their own writers (‘name=`) at class-build time, so per-instance member accessors do not appear in this YAML — only the generic `[]` / `[]=` pair on the base class. `[]=` is already classified `:mutates_self`; `[]` reads a member but the answer depends on the subclass’s member definition, which the catalog does not see, so we blocklist it defensively.
MethodCatalog.new( path: File.( "../../../../data/builtins/ruby_core/struct.yml", __dir__ ), mutating_selectors: { "Struct" => Set[ # Defensive: aliasing-copy semantics on a hypothetical # `Constant<Struct>` carrier. Convention across the # other catalogs (Range, Random, Pathname). :initialize_copy, # `rb_struct_hash` mixes member values via # `rb_hash` -> `rb_funcall(:hash, ...)`. The classifier # sees no direct dispatch because the recursion goes # through `rb_hash` (a helper), but the answer depends # on the member values' `#hash` — user-redefinable. # Block to avoid folding a hash that would diverge # from the runtime once a member overrides `#hash`. :hash, # `rb_struct_aref` reads a member by name or index; the # answer depends on the subclass's member layout, which # the catalog does not carry. Folding without knowing # the layout would be unsound. :[] ] } )
- COMPLEX_CATALOG =
‘Complex` catalog. Singleton — load once, consult during dispatch.
‘Complex` is a fully-immutable value type in Ruby: once a complex number is constructed (via `Complex(real, imag)` or `Complex.rect` / `Complex.polar`) its `real` and `imag` slots are never rewritten. Every public instance method either returns `self` unchanged or builds a fresh `Complex` / `Numeric`. The C-body classifier already correctly flags the four `:dispatch` methods (`<=>`, `to_s`, `inspect`, `rationalize`) so there are no false-positive `:leaf` entries to override. The blocklist therefore carries only the conventional `:initialize_copy` defence-in-depth entry so a hypothetical future `Constant<Complex>` carrier cannot fold an aliasing copy through the catalog (mirrors `range_catalog.rb`, `time_catalog.rb`, `date_catalog.rb`).
MethodCatalog.new( path: File.( "../../../../data/builtins/ruby_core/complex.yml", __dir__ ), mutating_selectors: { "Complex" => Set[ # Defence in depth: `Complex` does not currently expose # a public `initialize_copy`, but blocking it keeps the # convention identical to every other catalog so future # CRuby additions cannot leak a copy-mutator through. :initialize_copy ] } )
- ENCODING_CATALOG =
‘Encoding` catalog. Singleton — load once, consult during dispatch.
Encoding instances are deep-frozen value objects: once registered, their ‘name` / `dummy?` / `ascii_compatible?` slots never change and the C bodies for the per-instance methods are pure. The C-body classifier therefore lands every instance method as `:leaf` correctly.
The blocklist focuses on the singleton surface where the hidden state is the process-wide encoding registry. Every method classified ‘:leaf` on the singleton actually reads (or, for the setters, writes) a global, so a hypothetical `Constant<Encoding>`-class receiver MUST NOT fold them against the analyzer process’s registry — what UTF-8’s alias list is in the analyzer is not necessarily what it is in the analysed program.
MethodCatalog.new( path: File.( "../../../../data/builtins/ruby_core/encoding.yml", __dir__ ), mutating_selectors: { "Encoding" => Set[ # Defence-in-depth: mirrors range_catalog.rb / # complex_catalog.rb. Encoding does not currently # expose a public `initialize_copy` (Encoding objects # are deep-frozen and #dup is a no-op), but the # convention keeps the door closed against future # CRuby changes that would leak a copy-mutator. :initialize_copy, :hash, :eql?, # `Encoding.find(name)` walks the global encoding # registry. Pure with respect to its argument but # the registry itself can drift (load-order, locale, # process-wide `default_external=` calls), so a # constant-fold would lock in the analyzer's view. :find, # `Encoding.list` / `Encoding.aliases` / # `Encoding.name_list` enumerate the same global # registry. Same reasoning as `find` — the values # are not guaranteed to match the analysed program's # registry. :list, :aliases, :name_list, # Global-default mutators. `MethodCatalog#blocked?` # only auto-blocks `!`-suffixed selectors, so we MUST # list these explicitly: each writes the process-wide # default-encoding slot read by `default_external` / # `default_internal`. :default_external=, :default_internal= ] } )
- PATHNAME_CATALOG =
‘Pathname` catalog. Singleton — load once, consult during dispatch.
TODO(blocklist curation): read ‘data/builtins/ruby_core/pathname.yml` and add per-method blocklist entries for any `:leaf` classifications that are actually mutators or otherwise unsafe to fold. Each entry SHOULD carry a one-line comment naming the indirect mutator helper that triggered the false positive (see `string_catalog.rb`, `array_catalog.rb`, `time_catalog.rb` for the canonical shape).
MethodCatalog.new( path: File.( "../../../../data/builtins/ruby_core/pathname.yml", __dir__ ), mutating_selectors: { "Pathname" => Set[ # initialize_copy is blocklisted by convention so a # hypothetical future `Constant<Pathname>` carrier # cannot fold an aliasing copy through the catalog. :initialize_copy ] } )
- RATIONAL_CATALOG =
‘Rational` catalog. Singleton — load once, consult during dispatch.
Rational is fully immutable: numerator / denominator slots are written once during ‘nurat_s_new_internal` and the C body never reaches for `rb_check_frozen`. Every catalog entry classifies cleanly (`:leaf`, `:leaf_when_numeric`, or `:dispatch` for the two methods that delegate into user-redefinable `==` / `Float()` — `nurat_eqeq_p` and `nurat_fdiv`). Bang-suffixed mutators do not exist on Rational.
The blocklist therefore stays minimal. ‘initialize_copy` is added defensively (mirrors Range / Set) so a hypothetical future `Constant<Rational>` carrier cannot fold an aliasing copy through the catalog and surface a shared mutable handle.
MethodCatalog.new( path: File.( "../../../../data/builtins/ruby_core/rational.yml", __dir__ ), mutating_selectors: { "Rational" => Set[ :initialize_copy ] } )
- EXCEPTION_CATALOG =
‘Exception` catalog. Singleton — load once, consult during dispatch.
Exception is the base of every Ruby error class (RuntimeError, StandardError, KeyError, …). The Init_Exception block in ‘references/ruby/error.c` registers the entire hierarchy in one pass, so the YAML carries 27 classes — but only the base `Exception` row is wired into `CATALOG_BY_CLASS` for v0.0.5. A `RuntimeError` receiver hits the Exception arm via `is_a?(Exception)` and the catalog answers with the base-class entries; subclass-specific methods (`KeyError#receiver`, `NameError#name`, …) intentionally miss the lookup until a later slice routes per-subclass class_names.
The catalog tier here is *defence in depth* — every base method that could plausibly fold has been weighed against the robustness principle (strict on returns) and either left ‘:dispatch` / `:mutates_self` (in which case the catalog already declines) or blocklisted because the static classifier missed an indirect side effect. The remaining `:leaf` method that DOES fold is `#cause`, a pure accessor.
MethodCatalog.new( path: File.( "../../../../data/builtins/ruby_core/exception.yml", __dir__ ), mutating_selectors: { "Exception" => Set[ # `exc_initialize` writes `mesg` / `backtrace` ivars on # self via `rb_ivar_set` — the C-body classifier missed # the indirect mutator because the helpers are not in # its regex. Blocklisted so a hypothetical future # `Constant<Exception>` carrier cannot fold an aliasing # constructor through the catalog. :initialize, # `exc_exception` either returns self (no-arg) or calls # `rb_obj_clone` + `exc_initialize_internal` on the # clone — the clone branch mutates fresh state through # the same indirect helpers as `:initialize`. Conservative # blocklist; the cost is one folded no-arg call. :exception, # `exc_detailed_message` formats with platform / locale # data (highlight markers depend on `$stderr.tty?` via # the keyword arg default and `rb_io_tty_p`). Folding # would freeze a value that depends on the calling # process's stderr state. :detailed_message, # `exc_backtrace` reads the captured frame list, which # depends on where the exception was raised — context # the static fold tier cannot reproduce. :backtrace, # Same rationale as `:backtrace`; `Thread::Backtrace::Location` # objects are runtime artefacts. :backtrace_locations, # `exc_set_backtrace` mutates the @backtrace ivar via # `rb_ivar_set` — another indirect mutator the classifier # missed. :set_backtrace, # `initialize_copy` is blocklisted by convention so a # hypothetical future `Constant<Exception>` carrier # cannot fold an aliasing copy through the catalog. :initialize_copy, # Defensive entries for the universal mutation surface. # Object-identity hashing on a constant carrier is fine, # but `eql?` on Exception delegates to `==` (dispatch); # blocking both keeps the constant-fold tier honest. :hash, :eql? ], # `Exception.to_tty?` (singleton) calls # `rb_io_tty_p($stderr)`; its return depends on the # process's stderr state at runtime, never on compile-time # arguments. The catalog tier today only consults # `mutating_selectors` for instance-receiver dispatches via # `CATALOG_BY_CLASS`, so this row is documentation-grade — # it records the soundness rationale for any future slice # that wires the singleton path through the catalog. "Exception.singleton" => Set[ :to_tty? ] } )
- COMPARABLE_CATALOG =
‘Comparable` module catalog. Singleton — load once.
‘Comparable` is a Ruby module, not a class, so the catalog is NOT routed through `MethodDispatcher::ConstantFolding::CATALOG_BY_CLASS` (which dispatches on the receiver’s concrete class). The data is consumed by future include-aware lookup —see ‘docs/CURRENT_WORK.md` for the planned slice.
MethodCatalog.new( path: File.( "../../../../data/builtins/ruby_core/comparable.yml", __dir__ ), mutating_selectors: { "Comparable" => Set[] } )
- ENUMERABLE_CATALOG =
‘Enumerable` module catalog. Singleton — load once.
‘Enumerable` is a Ruby module, not a class, so the catalog is NOT routed through `MethodDispatcher::ConstantFolding::CATALOG_BY_CLASS` (which dispatches on the receiver’s concrete class). The data is consumed by future include-aware lookup —see ‘docs/CURRENT_WORK.md` for the planned slice.
MethodCatalog.new( path: File.( "../../../../data/builtins/ruby_core/enumerable.yml", __dir__ ), mutating_selectors: { "Enumerable" => Set[] } )