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
Classes: MethodCatalog, NumericCatalog
Constant Summary collapse
- REGEXP_CATALOG =
Regexp/MatchDatacatalog. Singleton — load once, consult during dispatch.Init_Regexpinreferences/ruby/re.cregisters BOTH classes in a single C init block, so the catalog carries both —Regexp(the pattern carrier) plusMatchData(the result-of-match carrier produced byRegexp#match/String#matchand 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 callrb_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_matchandRegexp.timeout/Regexp.timeout=are class-level (singleton) methods that also touch process-global state, but the dispatcher's catalog lookup only consults:instanceentries today — class-method calls on aSingletonreceiver type take themeta_*path inMethodDispatcherrather than walkingCATALOG_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. - The reader surface on each class (
MethodCatalog.for_topic( "re", 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 =
Setcatalog. 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 inreferences/ruby/set.cwithInit_Setregistering every method directly. There is noset.rbprelude — the trailingrb_provide("set.rb")makesrequire "set"a no-op against the built-in.The blocklist below catches the catalog
:leafentries 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.for_topic( "set", 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/DateTimecatalog. Singleton — load once, consult during dispatch.DateandDateTimeboth come from CRuby's bundleddategem (references/ruby/ext/date/date_core.c). A singleInit_date_corefunction registers them, so the catalog carries both classes —Dateplus theDateTimesubclass whose own Init block extends withhour/min/strftime/iso8601etc. The Ruby-side prelude (lib/date.rb) only contributesDate#infinite?and the nestedDate::Infinityclass; the bulk of the surface is in C.Date / DateTime receivers are not lifted to a
Constantcarrier today (there is no Date literal node — the closest isDate.today/Date.parse(...), which produceNominal[Date]). The catalog wiring therefore mostly governs:- The Integer-typed reader surface (
#year,#month,#day,#wday,#hour,#min,#sec) — RBS-declaredIntegeris 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 futureConstant<Date>carrier, plus a defensive:initialize_copyentry 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-newDateobjects rather than mutating the receiver — they intentionally stay catalog-eligible. The two real mutators (#initialize_copy,#marshal_load) are already classified:mutates_selfby the C-body regex, so they fall out ofMethodCatalog#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. - The Integer-typed reader surface (
MethodCatalog.for_topic( "date", 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 =
Hashcatalog. Singleton — load once, consult during dispatch.Hash mirrors Array's mutation pattern: nearly every iteration method yields through
rb_hash_foreachplus 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:leafdespite being block-dependent. The blocklist below captures every false-positive:leafwe have spotted in the generated YAML — bias toward conservatism so a missed fold is acceptable but a folded mutator/yielder is not. MethodCatalog.for_topic( "hash", 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/UnboundMethodcatalog. Singleton — load once, consult during dispatch.The three callable carriers are imported together because
Init_Procregisters 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:leafbecause the C functions themselves do not callrb_funcall*/rb_yielddirectly (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.for_topic( "proc", 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 =
Timecatalog. Singleton — load once, consult during dispatch.Time is a pure-C built-in: the Init block in
references/ruby/time.cregisters the bulk of the surface, and the Ruby-side preludereferences/ruby/timev.rbcontributes the class-side constructors (Time.now,Time.at,Time.new) through Primitive cexpr stubs.Time receivers are not lifted to a
Constantcarrier today (there is noTimeliteral node — the closest isTime.now/Time.new(...), which produceNominal[Time]). The catalog wiring therefore mostly governs:- The size-projection-equivalent reader surface (
#year,#month,#hour,#sec,#wday, …) — RBS-declaredIntegeris preserved through dispatch. - The blocklist below, which keeps the indirect-mutator methods that the C-body classifier
mis-flagged as
:leaffrom ever folding through a hypothetical futureConstant<Time>carrier.
The blocklist captures the false-positive
:leafentries whose helper functions the regex classifier did not recognise as mutators. - The size-projection-equivalent reader surface (
MethodCatalog.for_topic( "time", 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, # `getlocal` is not a mutator — it returns a fresh Time — but the fresh Time is pinned to the # *analysis machine's* timezone. Folding it through a `Constant[Time]` carrier (which only # ever holds a UTC literal from `Time.utc`) would bake a host-dependent wall clock / # `utc_offset` into the inferred type. Blocked so the fold stays machine-independent; the RBS # tier answers `Nominal[Time]`. `getutc` / `getgm` stay foldable — their result is UTC. :getlocal ] } )
- ARRAY_CATALOG =
Arraycatalog. 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_modifyor 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:leafdespite mutating. MethodCatalog.for_topic( "array", 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 =
Rangecatalog. Singleton — load once, consult during dispatch.Range is largely immutable:
begin,end, andexclare set at construction byrange_initializeand 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:leafdespite yielding to a block. MethodCatalog.for_topic( "range", 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 =
Randomcatalog. Singleton — load once, consult during dispatch.The static classifier marks most Random methods
:leafbecause their C bodies do not callrb_funcall*/rb_yield/rb_check_frozendirectly. Random is the canonical case where that heuristic under-counts: every call to#rand/#bytes/Random.rand/Random.bytesadvances 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_seedandRandom.urandomare 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.for_topic( "random", 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 =
StringandSymbolcatalog. Singleton — load once, consult during dispatch.The blocklist below is the curated set of catalog
:leafentries the C-body classifier mis-attributes (the body ofrb_str_replacecallsstr_modifiable/str_discardwhich 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.for_topic( "string", 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, # `crypt` is not a mutator but is blocked from folding for the same "do not bake a non-pure # result into a Constant" reason: `rb_str_crypt` delegates to the platform `crypt(3)`, whose # output (algorithm and digest) varies by libc / OS, so `"x".crypt("ab")` is not deterministic # across the platforms an analyzed project may target. The catalog classifies it `:leaf` from # its C body; this entry overrides that. :crypt ], "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 =
Structcatalog. Singleton — load once, consult during dispatch.Structis a meta-class:Struct.new(*members)returns a fresh anonymous subclass — never aStructvalue. Today Rigor never produces aConstant<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.for_topic( "struct", 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 =
Complexcatalog. Singleton — load once, consult during dispatch.Complexis a fully-immutable value type in Ruby: once a complex number is constructed (viaComplex(real, imag)orComplex.rect/Complex.polar) itsrealandimagslots are never rewritten. Every public instance method either returnsselfunchanged or builds a freshComplex/Numeric. The C-body classifier already correctly flags the four:dispatchmethods (<=>,to_s,inspect,rationalize) so there are no false-positive:leafentries to override. The blocklist therefore carries only the conventional:initialize_copydefence-in-depth entry so a hypothetical futureConstant<Complex>carrier cannot fold an aliasing copy through the catalog (mirrorsrange_catalog.rb,time_catalog.rb,date_catalog.rb). MethodCatalog.for_topic( "complex", 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 ] } )
- NUMERIC_CATALOG =
Numericfamily catalog (Integer, Float, Rational, Complex, Numeric). Singleton — load once, consult during dispatch.The catalog is produced offline by
tool/extract_builtin_catalog.rbfrom the CRuby reference checkout underreferences/rubyplus the RBS core signatures underreferences/rbs. The loader is the runtime bridge: callers ask "isInteger#+safe to invoke during constant folding?" and the answer comes straight from the offline classification (leaf/trivial/leaf_when_numericare foldable; everything else is not).No mutation blocklist is needed. The numeric classes expose no foldable bang or indirect-mutator method that the static C classifier mis-attributes (every
:leafnumeric method is a pure value computation), so the genericMethodCatalogloader — shared with the eighteen other per-class catalogs — covers it directly. This previously hand-rolled its ownsafe_for_folding?/method_entry/load_catalogcopy ofMethodCatalog; folding it onto the shared loader also picks up alias resolution (e.g.Integer#magnitude→abs,Integer#inspect→to_s), which the old bespoke loader silently dropped. MethodCatalog.for_topic("numeric")
- ENCODING_CATALOG =
Encodingcatalog. 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:leafcorrectly.The blocklist focuses on the singleton surface where the hidden state is the process-wide encoding registry. Every method classified
:leafon the singleton actually reads (or, for the setters, writes) a global, so a hypotheticalConstant<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.for_topic( "encoding", 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 =
Pathnamecatalog. Singleton — load once, consult during dispatch.TODO(blocklist curation): read
data/builtins/ruby_core/pathname.ymland add per-method blocklist entries for any:leafclassifications 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 (seestring_catalog.rb,array_catalog.rb,time_catalog.rbfor the canonical shape). MethodCatalog.for_topic( "pathname", 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 =
Rationalcatalog. Singleton — load once, consult during dispatch.Rational is fully immutable: numerator / denominator slots are written once during
nurat_s_new_internaland the C body never reaches forrb_check_frozen. Every catalog entry classifies cleanly (:leaf,:leaf_when_numeric, or:dispatchfor the two methods that delegate into user-redefinable==/Float()—nurat_eqeq_pandnurat_fdiv). Bang-suffixed mutators do not exist on Rational.The blocklist therefore stays minimal.
initialize_copyis added defensively (mirrors Range / Set) so a hypothetical futureConstant<Rational>carrier cannot fold an aliasing copy through the catalog and surface a shared mutable handle. MethodCatalog.for_topic( "rational", mutating_selectors: { "Rational" => Set[ :initialize_copy ] } )
- EXCEPTION_CATALOG =
Exceptioncatalog. 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.cregisters the entire hierarchy in one pass, so the YAML carries 27 classes — but only the baseExceptionrow is wired intoCATALOG_BY_CLASSfor v0.0.5. ARuntimeErrorreceiver hits the Exception arm viais_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:leafmethod that DOES fold is#cause, a pure accessor. MethodCatalog.for_topic( "exception", 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 =
Comparablemodule catalog. Singleton — load once.Comparableis a Ruby module, not a class, so the catalog is NOT routed throughMethodDispatcher::ConstantFolding::CATALOG_BY_CLASS(which dispatches on the receiver's concrete class). The data is wired intoMODULE_CATALOGSinMethodDispatcher::ConstantFolding(ancestor-chain lookup). MethodCatalog.for_topic( "comparable", mutating_selectors: { "Comparable" => Set[] } )
- ENUMERABLE_CATALOG =
Enumerablemodule catalog. Singleton — load once.Enumerableis a Ruby module, not a class, so the catalog is NOT routed throughMethodDispatcher::ConstantFolding::CATALOG_BY_CLASS(which dispatches on the receiver's concrete class). The data is wired intoMODULE_CATALOGSinMethodDispatcher::ConstantFolding(ancestor-chain lookup). MethodCatalog.for_topic( "enumerable", mutating_selectors: { "Enumerable" => Set[] } )