From 727612b138f7a0483d4b747dc1ee917dbe5da700 Mon Sep 17 00:00:00 2001 From: Randy Stauner Date: Sat, 21 Feb 2026 05:58:46 -0700 Subject: [PATCH] ZJIT: Stabilize anonymous module/class names in stat keys and display (#16176) Anonymous modules/classes get names like `"#"` from `rb_class2name`, which include a memory address that changes between runs. This makes stat keys unstable for diffing across runs and is less informative than it could be for display. Add three variants for class naming: - get_class_name: raw rb_class2name (unchanged, for internal use) - get_stable_class_name: strips addresses, includes superclass for anonymous classes (e.g. `"#"`). For stat keys. - get_debug_class_name: like stable but preserves address when the name can't be fully resolved (e.g. `"#"`, `"#"`). For display where disambiguation matters. Use get_stable_class_name in stat key paths (qualified_method_name and count_call_to). Use get_stable_class_name in display paths that already have a separate pointer via ptr_map. Use get_debug_class_name in display paths without a separate pointer (type specializations, unreachable messages). I did this when trying to identify the source of the `##object_id` stat we get from lobsters. None of these cases handled that (which as we identified was Tempfile using Delegate), so I'm not sure how useful it might be, but it still seemed potentially useful enough to propose and discuss. --- zjit/src/cruby.rs | 67 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 64 insertions(+), 3 deletions(-) diff --git a/zjit/src/cruby.rs b/zjit/src/cruby.rs index 0eb47062c9f8d0..49a3ccb08d593b 100644 --- a/zjit/src/cruby.rs +++ b/zjit/src/cruby.rs @@ -1334,16 +1334,77 @@ pub mod test_utils { #[cfg(test)] pub use test_utils::*; -/// Get class name from a class pointer. +/// Get class name from a class pointer. For anonymous classes, includes the +/// superclass name for context (e.g. `#`). pub fn get_class_name(class: VALUE) -> String { // type checks for rb_class2name() - if unsafe { RB_TYPE_P(class, RUBY_T_MODULE) || RB_TYPE_P(class, RUBY_T_CLASS) } { + let name = if unsafe { RB_TYPE_P(class, RUBY_T_MODULE) || RB_TYPE_P(class, RUBY_T_CLASS) } { Some(class) } else { None }.and_then(|class| unsafe { cstr_to_rust_string(rb_class2name(class)) - }).unwrap_or_else(|| "Unknown".to_string()) + }).unwrap_or_else(|| "Unknown".to_string()); + + // For anonymous classes, include the superclass name for context. + // Use rb_class_real to resolve through iclasses (internal include/prepend + // wrappers) before checking rb_mod_name, which returns Qnil for anonymous classes. + // e.g. "#" with superclass String => "#" + if unsafe { RB_TYPE_P(class, RUBY_T_CLASS) && rb_mod_name(rb_class_real(class)) == Qnil } { + let super_class = unsafe { rb_class_get_superclass(class) }; + if super_class != Qnil { + let super_name = get_class_name(super_class); + return format!("#", class.0); + } + } + + name +} + + +#[cfg(test)] +mod class_name_tests { + use super::*; + use test_utils::{eval, with_rubyvm}; + + #[test] + fn named_class() { + with_rubyvm(|| { + assert_eq!(get_class_name(eval("String")), "String"); + }); + } + + #[test] + fn named_module() { + with_rubyvm(|| { + assert_eq!(get_class_name(eval("Kernel")), "Kernel"); + }); + } + + #[test] + fn anonymous_class_includes_superclass() { + with_rubyvm(|| { + let name = get_class_name(eval("Class.new(String)")); + assert!(name.starts_with("# bool {