Skip to content

Commit fc8d0f3

Browse files
authored
Constant lookup recovers the runtime cref in singleton-class bodies (#882)
1 parent ab97735 commit fc8d0f3

8 files changed

Lines changed: 444 additions & 11 deletions

File tree

monoruby/src/bytecodegen.rs

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -792,15 +792,34 @@ impl<'a> BytecodeGen<'a> {
792792
self.is_escaping_block() || self.singleton_classdef
793793
}
794794

795+
/// The lexical chain of the iseq currently being compiled passes
796+
/// through a `class << obj` body. Propagated onto every nested
797+
/// def/classdef/block so runtime constant lookup knows when the
798+
/// statically stamped cref can be stale.
799+
fn in_singleton_lexical(&self) -> bool {
800+
self.singleton_classdef || self.iseq().in_singleton_lexical
801+
}
802+
803+
fn propagate_singleton_lexical(&mut self, fid: FuncId) {
804+
if self.in_singleton_lexical() {
805+
if let Some(iseq) = self.store[fid].is_iseq() {
806+
self.store[iseq].in_singleton_lexical = true;
807+
}
808+
}
809+
}
810+
795811
fn add_method(
796812
&mut self,
797813
name: Option<IdentId>,
798814
compile_info: CompileInfo,
799815
loc: Loc,
800816
) -> Result<FuncId> {
801817
let sourceinfo = self.sourceinfo.clone();
802-
self.store
803-
.new_iseq_method(name, compile_info, loc, sourceinfo, false)
818+
let fid = self
819+
.store
820+
.new_iseq_method(name, compile_info, loc, sourceinfo, false)?;
821+
self.propagate_singleton_lexical(fid);
822+
Ok(fid)
804823
}
805824

806825
fn add_classdef(
@@ -811,20 +830,29 @@ impl<'a> BytecodeGen<'a> {
811830
is_singleton: bool,
812831
) -> Result<FuncId> {
813832
let sourceinfo = self.sourceinfo.clone();
814-
self.store
815-
.new_classdef(name, compile_info, loc, sourceinfo, is_singleton)
833+
let fid = self
834+
.store
835+
.new_classdef(name, compile_info, loc, sourceinfo, is_singleton)?;
836+
self.propagate_singleton_lexical(fid);
837+
Ok(fid)
816838
}
817839

818840
fn add_block(&mut self, outer: ISeqId, compile_info: CompileInfo, loc: Loc) -> Result<FuncId> {
819841
let sourceinfo = self.sourceinfo.clone();
820-
self.store
821-
.new_block(outer, compile_info, true, loc, sourceinfo)
842+
let fid = self
843+
.store
844+
.new_block(outer, compile_info, true, loc, sourceinfo)?;
845+
self.propagate_singleton_lexical(fid);
846+
Ok(fid)
822847
}
823848

824849
fn add_lambda(&mut self, outer: ISeqId, compile_info: CompileInfo, loc: Loc) -> Result<FuncId> {
825850
let sourceinfo = self.sourceinfo.clone();
826-
self.store
827-
.new_block(outer, compile_info, false, loc, sourceinfo)
851+
let fid = self
852+
.store
853+
.new_block(outer, compile_info, false, loc, sourceinfo)?;
854+
self.propagate_singleton_lexical(fid);
855+
Ok(fid)
828856
}
829857

830858
fn loop_push(

monoruby/src/codegen/jitgen/compile/variables.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,27 @@ impl<'a> JitContext<'a> {
7878
if cache.version as u64 != self.const_version() {
7979
return Ok(CompileResult::Recompile(RecompileReason::NotCached));
8080
}
81+
// A self-dependent resolution (singleton-cref frame) is only
82+
// foldable when this compilation's self class matches — the
83+
// dispatch guard then pins it at runtime.
84+
match cache.self_class {
85+
Some(sc) => {
86+
if sc != self.store.const_self_key_for_class(self.self_class()) {
87+
return Ok(CompileResult::Recompile(RecompileReason::NotCached));
88+
}
89+
}
90+
None => {
91+
// Constant sites in singleton-lexical methods are
92+
// always filled with a Some key (resolution there is
93+
// self-class dependent), so a None entry can only
94+
// predate this receiver's resolution — let the VM
95+
// re-resolve rather than fold a value whose validity
96+
// rests on a re-stampable static cref.
97+
if self.iseq().in_singleton_lexical {
98+
return Ok(CompileResult::Recompile(RecompileReason::NotCached));
99+
}
100+
}
101+
}
81102
let base_slot = self.store[id].base;
82103
if let Some(slot) = base_slot {
83104
if let Some(base_class) = cache.base_class {

monoruby/src/codegen/runtime.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -863,11 +863,15 @@ pub(crate) extern "C" fn vm_check_constant(
863863
site_id: ConstSiteId,
864864
const_version: usize,
865865
) -> Option<Value> {
866+
let self_key = const_self_key(vm, globals);
866867
if let Some(cache) = &globals.store[site_id].cache {
867868
let base_class = globals.store[site_id]
868869
.base
869870
.map(|base| unsafe { vm.get_slot(base) }.unwrap());
870-
if cache.version == const_version && cache.base_class == base_class {
871+
if cache.version == const_version
872+
&& cache.base_class == base_class
873+
&& cache.self_class == self_key
874+
{
871875
return Some(cache.value);
872876
};
873877
}
@@ -878,6 +882,7 @@ pub(crate) extern "C" fn vm_check_constant(
878882
globals.store[site_id].cache = Some(ConstCache {
879883
version: const_version,
880884
base_class,
885+
self_class: self_key,
881886
value,
882887
});
883888
Some(value)
@@ -886,17 +891,27 @@ pub(crate) extern "C" fn vm_check_constant(
886891
}
887892
}
888893

894+
/// The self-dependence key for the constant cache — see
895+
/// `Executor::const_lexical_self_key`.
896+
fn const_self_key(vm: &Executor, globals: &Globals) -> Option<ClassId> {
897+
vm.const_lexical_self_key(globals, vm.method_func_id())
898+
}
899+
889900
pub(crate) extern "C" fn vm_get_constant(
890901
vm: &mut Executor,
891902
globals: &mut Globals,
892903
site_id: ConstSiteId,
893904
const_version: usize,
894905
) -> Option<Value> {
906+
let self_key = const_self_key(vm, globals);
895907
if let Some(cache) = &globals.store[site_id].cache {
896908
let base_class = globals.store[site_id]
897909
.base
898910
.map(|base| unsafe { vm.get_slot(base) }.unwrap());
899-
if cache.version == const_version && cache.base_class == base_class {
911+
if cache.version == const_version
912+
&& cache.base_class == base_class
913+
&& cache.self_class == self_key
914+
{
900915
return Some(cache.value);
901916
};
902917
}
@@ -905,6 +920,7 @@ pub(crate) extern "C" fn vm_get_constant(
905920
globals.store[site_id].cache = Some(ConstCache {
906921
version: const_version,
907922
base_class,
923+
self_class: self_key,
908924
value,
909925
});
910926
Some(value)

monoruby/src/executor/constants.rs

Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,8 @@ impl Executor {
355355
} else {
356356
current_func.lexical_class(globals)
357357
};
358+
// Ancestor-phase lookup starts at the *runtime* cref too.
359+
let lexical_class = self.runtime_innermost_cref(globals, lexical_class, current_func);
358360
let module = globals[lexical_class].get_module();
359361
match self.get_constant_superclass(globals, module, name)? {
360362
Some(v) => return Ok(Some(v)),
@@ -482,7 +484,14 @@ impl Executor {
482484
Some(iseq) => iseq,
483485
None => return false,
484486
};
487+
let mut first = true;
485488
for module in globals.store[iseq].lexical_context.iter().rev().copied() {
489+
let module = if first {
490+
first = false;
491+
self.runtime_innermost_cref(globals, module, current_func)
492+
} else {
493+
module
494+
};
486495
if Self::probe_constant_at(globals, module, name) {
487496
return true;
488497
}
@@ -585,6 +594,119 @@ impl Executor {
585594
Self::probe_constant_superclass_qualified(globals, globals.store[parent].get_module(), name)
586595
}
587596

597+
/// Recover the *runtime* innermost cref when the statically stamped
598+
/// one is stale. A `def` inside a re-executed class body (most
599+
/// notably inside `class << obj`, or a `class X` nested in one)
600+
/// shares its iseq across executions while `enter_classdef`
601+
/// re-stamps the static `lexical_context` per execution (last write
602+
/// wins) — so the recorded innermost class may belong to a
603+
/// *different* execution. Detection: the stamped class is no longer
604+
/// on the receiver's ancestor chain. Recovery: the runtime cref is
605+
/// the ancestor that owns the executing method (for a plain `def`
606+
/// in a class body, the innermost cref IS the defining class).
607+
pub(crate) fn runtime_innermost_cref(
608+
&self,
609+
globals: &Globals,
610+
statically: ClassId,
611+
current_func: FuncId,
612+
) -> ClassId {
613+
// Staleness is only possible when the def is lexically nested
614+
// (at any depth) inside a `class << obj` body — every other
615+
// classdef form re-opens the same class on re-execution, so
616+
// the stamp cannot go stale. Never substitute otherwise: a
617+
// singleton def (`def a.meth`) legitimately has a cref that is
618+
// unrelated to the receiver's ancestry.
619+
if !globals.store[current_func]
620+
.is_iseq()
621+
.is_some_and(|iseq| globals.store[iseq].in_singleton_lexical)
622+
{
623+
return statically;
624+
}
625+
let self_val = self.cfp().lfp().self_val();
626+
if Self::static_cref_plausible(globals, self_val, statically) {
627+
return statically;
628+
}
629+
let self_class = self_val.class();
630+
let mut module = Some(globals.store[self_class].get_module());
631+
while let Some(m) = module {
632+
if globals.store.class_owns_func(m.id(), current_func) {
633+
// For a plain `def` the owner IS the cref. For a
634+
// singleton def (`def self.f` in a class body nested
635+
// under `class << obj`) the owner is the metaclass
636+
// while the cref is the attached class itself — the
637+
// stamped entry being a non-singleton class tells the
638+
// two forms apart.
639+
if globals.store[statically].get_module().is_singleton().is_none() {
640+
if let Some(attached) = globals.store[m.id()]
641+
.get_module()
642+
.is_singleton()
643+
.and_then(|v| v.is_class_or_module())
644+
{
645+
return attached.id();
646+
}
647+
}
648+
return m.id();
649+
}
650+
module = m.superclass();
651+
}
652+
statically
653+
}
654+
655+
/// Is `statically` a plausible innermost cref for this receiver?
656+
/// True when it lies on the receiver's class ancestry (a plain
657+
/// `def` in a class body), or — for a class/module receiver — on
658+
/// the receiver's *own* ancestry: a singleton method (`def M.f`,
659+
/// `class << self` inside `module M`) runs with self == M while
660+
/// its cref is M or an enclosing module, neither of which appears
661+
/// among the metaclass's ancestors.
662+
fn static_cref_plausible(globals: &Globals, self_val: Value, statically: ClassId) -> bool {
663+
if globals
664+
.store
665+
.static_cref_plausible_for_class(self_val.class(), statically)
666+
{
667+
return true;
668+
}
669+
// Checked directly on the value as well: a class/module
670+
// receiver whose metaclass has not been materialized has a
671+
// plain class (`Class`/`Module`) that carries no attachment.
672+
if let Some(m) = self_val.is_class_or_module() {
673+
if globals.store.class_in_ancestors(m.id(), statically) {
674+
return true;
675+
}
676+
}
677+
false
678+
}
679+
680+
/// The self-dependence key for the constant cache: `Some(self's
681+
/// class)` for any method lexically nested under `class << obj`.
682+
/// Resolution there goes through [`Self::runtime_innermost_cref`],
683+
/// which depends only on self's class, so the cached result is
684+
/// valid exactly for the same self class. The key must NOT be
685+
/// weakened to `None` when the static stamp happens to be
686+
/// plausible for the current receiver: plausibility is relative
687+
/// to the *current* stamp, and a later re-execution of the
688+
/// `class << obj` body re-stamps it, silently invalidating what a
689+
/// `None`-keyed entry resolved against.
690+
pub(crate) fn const_lexical_self_key(
691+
&self,
692+
globals: &Globals,
693+
current_func: FuncId,
694+
) -> Option<ClassId> {
695+
let iseq = globals.store[current_func].is_iseq()?;
696+
if !globals.store[iseq].in_singleton_lexical {
697+
return None;
698+
}
699+
// For a class/module receiver (a classdef body, or `def
700+
// self.f`), key by the module's own id: its metaclass may be
701+
// unmaterialized, in which case `.class()` is the shared
702+
// `Class`/`Module` and would alias distinct receivers.
703+
let self_val = self.cfp().lfp().self_val();
704+
Some(match self_val.is_class_or_module() {
705+
Some(m) => m.id(),
706+
None => self_val.class(),
707+
})
708+
}
709+
588710
fn search_lexical_stack(
589711
&mut self,
590712
globals: &mut Globals,
@@ -602,12 +724,15 @@ impl Executor {
602724
Some(iseq) => iseq,
603725
None => return Ok(None),
604726
};
605-
let stack = globals.store[iseq]
727+
let mut stack = globals.store[iseq]
606728
.lexical_context
607729
.iter()
608730
.rev()
609731
.cloned()
610732
.collect::<Vec<_>>();
733+
if let Some(first) = stack.first_mut() {
734+
*first = self.runtime_innermost_cref(globals, *first, current_func);
735+
}
611736
for module in stack {
612737
if globals.store.get_constant(module, name).is_some() {
613738
// Trigger autoload / read the value. If the autoload

monoruby/src/globals/store.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,11 @@ impl Store {
514514
sourceinfo,
515515
);
516516
info.singleton_classdef = is_singleton;
517+
// The body itself resolves constants through a per-execution
518+
// singleton class, so it needs the self-keyed constant cache
519+
// just like defs nested inside it (bytecodegen additionally
520+
// propagates the flag onto those).
521+
info.in_singleton_lexical = is_singleton;
517522
let iseq = self.new_iseq(info);
518523
let info = FuncInfo::new_classdef_iseq(name, func_id, iseq);
519524
self.functions.info.push(info);
@@ -1383,6 +1388,12 @@ impl ClassInfoTable {
13831388
pub struct ConstCache {
13841389
pub version: usize,
13851390
pub base_class: Option<Value>,
1391+
/// `Some` when the resolution depended on the receiver (the frame's
1392+
/// `self` had a singleton class — a `def` in a `class << obj` body
1393+
/// resolves constants against the *runtime* singleton cref, see
1394+
/// `Executor::substitute_singleton_cref`). The cache only hits for
1395+
/// the same self class.
1396+
pub self_class: Option<ClassId>,
13861397
pub value: Value,
13871398
}
13881399

0 commit comments

Comments
 (0)