Skip to content

Commit a5b5bd9

Browse files
committed
gc module internal structure and API
Add gc_state module with GcState, GcGeneration, GcDebugFlags, GcStats. Replace gc module stubs with working API backed by gc_state. Add gc_callbacks and gc_garbage to Context. Add is_gc_tracked, gc_finalized, gc_get_referents to PyObject. Collection is stubbed (returns 0) — actual algorithm to follow.
1 parent 10cbfa9 commit a5b5bd9

File tree

6 files changed

+727
-31
lines changed

6 files changed

+727
-31
lines changed

crates/stdlib/src/gc.rs

Lines changed: 219 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,75 +2,265 @@ pub(crate) use gc::module_def;
22

33
#[pymodule]
44
mod gc {
5-
use crate::vm::{PyResult, VirtualMachine, function::FuncArgs};
5+
use crate::vm::{
6+
PyObjectRef, PyResult, VirtualMachine,
7+
builtins::PyListRef,
8+
function::{FuncArgs, OptionalArg},
9+
gc_state,
10+
};
611

12+
// Debug flag constants
13+
#[pyattr]
14+
const DEBUG_STATS: u32 = gc_state::GcDebugFlags::STATS.bits();
15+
#[pyattr]
16+
const DEBUG_COLLECTABLE: u32 = gc_state::GcDebugFlags::COLLECTABLE.bits();
17+
#[pyattr]
18+
const DEBUG_UNCOLLECTABLE: u32 = gc_state::GcDebugFlags::UNCOLLECTABLE.bits();
19+
#[pyattr]
20+
const DEBUG_SAVEALL: u32 = gc_state::GcDebugFlags::SAVEALL.bits();
21+
#[pyattr]
22+
const DEBUG_LEAK: u32 = gc_state::GcDebugFlags::LEAK.bits();
23+
24+
/// Enable automatic garbage collection.
25+
#[pyfunction]
26+
fn enable() {
27+
gc_state::gc_state().enable();
28+
}
29+
30+
/// Disable automatic garbage collection.
31+
#[pyfunction]
32+
fn disable() {
33+
gc_state::gc_state().disable();
34+
}
35+
36+
/// Return true if automatic gc is enabled.
37+
#[pyfunction]
38+
fn isenabled() -> bool {
39+
gc_state::gc_state().is_enabled()
40+
}
41+
42+
/// Run a garbage collection. Returns the number of unreachable objects found.
43+
#[derive(FromArgs)]
44+
struct CollectArgs {
45+
#[pyarg(any, optional)]
46+
generation: OptionalArg<i32>,
47+
}
48+
49+
#[pyfunction]
50+
fn collect(args: CollectArgs, vm: &VirtualMachine) -> PyResult<i32> {
51+
let generation = args.generation;
52+
let generation_num = generation.unwrap_or(2);
53+
if !(0..=2).contains(&generation_num) {
54+
return Err(vm.new_value_error("invalid generation".to_owned()));
55+
}
56+
57+
// Invoke callbacks with "start" phase
58+
invoke_callbacks(vm, "start", generation_num as usize, 0, 0);
59+
60+
// Manual gc.collect() should run even if GC is disabled
61+
let gc = gc_state::gc_state();
62+
let (collected, uncollectable) = gc.collect_force(generation_num as usize);
63+
64+
// Move objects from gc_state.garbage to vm.ctx.gc_garbage (for DEBUG_SAVEALL)
65+
{
66+
let mut state_garbage = gc.garbage.lock();
67+
if !state_garbage.is_empty() {
68+
let py_garbage = &vm.ctx.gc_garbage;
69+
let mut garbage_vec = py_garbage.borrow_vec_mut();
70+
for obj in state_garbage.drain(..) {
71+
garbage_vec.push(obj);
72+
}
73+
}
74+
}
75+
76+
// Invoke callbacks with "stop" phase
77+
invoke_callbacks(
78+
vm,
79+
"stop",
80+
generation_num as usize,
81+
collected,
82+
uncollectable,
83+
);
84+
85+
Ok(collected as i32)
86+
}
87+
88+
/// Return the current collection thresholds as a tuple.
789
#[pyfunction]
8-
fn collect(_args: FuncArgs, _vm: &VirtualMachine) -> i32 {
9-
0
90+
fn get_threshold(vm: &VirtualMachine) -> PyObjectRef {
91+
let (t0, t1, t2) = gc_state::gc_state().get_threshold();
92+
vm.ctx
93+
.new_tuple(vec![
94+
vm.ctx.new_int(t0).into(),
95+
vm.ctx.new_int(t1).into(),
96+
vm.ctx.new_int(t2).into(),
97+
])
98+
.into()
1099
}
11100

101+
/// Set the collection thresholds.
12102
#[pyfunction]
13-
fn isenabled(_args: FuncArgs, _vm: &VirtualMachine) -> bool {
14-
false
103+
fn set_threshold(threshold0: u32, threshold1: OptionalArg<u32>, threshold2: OptionalArg<u32>) {
104+
gc_state::gc_state().set_threshold(
105+
threshold0,
106+
threshold1.into_option(),
107+
threshold2.into_option(),
108+
);
15109
}
16110

111+
/// Return the current collection counts as a tuple.
17112
#[pyfunction]
18-
fn enable(_args: FuncArgs, vm: &VirtualMachine) -> PyResult {
19-
Err(vm.new_not_implemented_error(""))
113+
fn get_count(vm: &VirtualMachine) -> PyObjectRef {
114+
let (c0, c1, c2) = gc_state::gc_state().get_count();
115+
vm.ctx
116+
.new_tuple(vec![
117+
vm.ctx.new_int(c0).into(),
118+
vm.ctx.new_int(c1).into(),
119+
vm.ctx.new_int(c2).into(),
120+
])
121+
.into()
20122
}
21123

124+
/// Return the current debugging flags.
22125
#[pyfunction]
23-
fn disable(_args: FuncArgs, vm: &VirtualMachine) -> PyResult {
24-
Err(vm.new_not_implemented_error(""))
126+
fn get_debug() -> u32 {
127+
gc_state::gc_state().get_debug().bits()
25128
}
26129

130+
/// Set the debugging flags.
27131
#[pyfunction]
28-
fn get_count(_args: FuncArgs, vm: &VirtualMachine) -> PyResult {
29-
Err(vm.new_not_implemented_error(""))
132+
fn set_debug(flags: u32) {
133+
gc_state::gc_state().set_debug(gc_state::GcDebugFlags::from_bits_truncate(flags));
30134
}
31135

136+
/// Return a list of per-generation gc stats.
32137
#[pyfunction]
33-
fn get_debug(_args: FuncArgs, vm: &VirtualMachine) -> PyResult {
34-
Err(vm.new_not_implemented_error(""))
138+
fn get_stats(vm: &VirtualMachine) -> PyResult<PyListRef> {
139+
let stats = gc_state::gc_state().get_stats();
140+
let mut result = Vec::with_capacity(3);
141+
142+
for stat in stats.iter() {
143+
let dict = vm.ctx.new_dict();
144+
dict.set_item("collections", vm.ctx.new_int(stat.collections).into(), vm)?;
145+
dict.set_item("collected", vm.ctx.new_int(stat.collected).into(), vm)?;
146+
dict.set_item(
147+
"uncollectable",
148+
vm.ctx.new_int(stat.uncollectable).into(),
149+
vm,
150+
)?;
151+
result.push(dict.into());
152+
}
153+
154+
Ok(vm.ctx.new_list(result))
155+
}
156+
157+
/// Return the list of objects tracked by the collector.
158+
#[derive(FromArgs)]
159+
struct GetObjectsArgs {
160+
#[pyarg(any, optional)]
161+
generation: OptionalArg<Option<i32>>,
35162
}
36163

37164
#[pyfunction]
38-
fn get_objects(_args: FuncArgs, vm: &VirtualMachine) -> PyResult {
39-
Err(vm.new_not_implemented_error(""))
165+
fn get_objects(args: GetObjectsArgs, vm: &VirtualMachine) -> PyResult<PyListRef> {
166+
let generation_opt = args.generation.flatten();
167+
if let Some(g) = generation_opt
168+
&& !(0..=2).contains(&g)
169+
{
170+
return Err(vm.new_value_error(format!("generation must be in range(0, 3), not {}", g)));
171+
}
172+
let objects = gc_state::gc_state().get_objects(generation_opt);
173+
Ok(vm.ctx.new_list(objects))
40174
}
41175

176+
/// Return the list of objects directly referred to by any of the arguments.
42177
#[pyfunction]
43-
fn get_referents(_args: FuncArgs, vm: &VirtualMachine) -> PyResult {
44-
Err(vm.new_not_implemented_error(""))
178+
fn get_referents(args: FuncArgs, vm: &VirtualMachine) -> PyListRef {
179+
let mut result = Vec::new();
180+
181+
for obj in args.args {
182+
// Use the gc_get_referents method to get references
183+
result.extend(obj.gc_get_referents());
184+
}
185+
186+
vm.ctx.new_list(result)
45187
}
46188

189+
/// Return the list of objects that directly refer to any of the arguments.
47190
#[pyfunction]
48-
fn get_referrers(_args: FuncArgs, vm: &VirtualMachine) -> PyResult {
49-
Err(vm.new_not_implemented_error(""))
191+
fn get_referrers(args: FuncArgs, vm: &VirtualMachine) -> PyListRef {
192+
// This is expensive: we need to scan all tracked objects
193+
// For now, return an empty list (would need full object tracking to implement)
194+
let _ = args;
195+
vm.ctx.new_list(vec![])
50196
}
51197

198+
/// Return True if the object is tracked by the garbage collector.
52199
#[pyfunction]
53-
fn get_stats(_args: FuncArgs, vm: &VirtualMachine) -> PyResult {
54-
Err(vm.new_not_implemented_error(""))
200+
fn is_tracked(obj: PyObjectRef) -> bool {
201+
// An object is tracked if it has IS_TRACE = true (has a trace function)
202+
obj.is_gc_tracked()
55203
}
56204

205+
/// Return True if the object has been finalized by the garbage collector.
57206
#[pyfunction]
58-
fn get_threshold(_args: FuncArgs, vm: &VirtualMachine) -> PyResult {
59-
Err(vm.new_not_implemented_error(""))
207+
fn is_finalized(obj: PyObjectRef) -> bool {
208+
// Check the per-object finalized flag directly
209+
obj.gc_finalized()
60210
}
61211

212+
/// Freeze all objects tracked by gc.
62213
#[pyfunction]
63-
fn is_tracked(_args: FuncArgs, vm: &VirtualMachine) -> PyResult {
64-
Err(vm.new_not_implemented_error(""))
214+
fn freeze() {
215+
gc_state::gc_state().freeze();
65216
}
66217

218+
/// Unfreeze all objects in the permanent generation.
67219
#[pyfunction]
68-
fn set_debug(_args: FuncArgs, vm: &VirtualMachine) -> PyResult {
69-
Err(vm.new_not_implemented_error(""))
220+
fn unfreeze() {
221+
gc_state::gc_state().unfreeze();
70222
}
71223

224+
/// Return the number of objects in the permanent generation.
72225
#[pyfunction]
73-
fn set_threshold(_args: FuncArgs, vm: &VirtualMachine) -> PyResult {
74-
Err(vm.new_not_implemented_error(""))
226+
fn get_freeze_count() -> usize {
227+
gc_state::gc_state().get_freeze_count()
228+
}
229+
230+
/// gc.garbage - list of uncollectable objects
231+
#[pyattr]
232+
fn garbage(vm: &VirtualMachine) -> PyListRef {
233+
vm.ctx.gc_garbage.clone()
234+
}
235+
236+
/// gc.callbacks - list of callbacks to be invoked
237+
#[pyattr]
238+
fn callbacks(vm: &VirtualMachine) -> PyListRef {
239+
vm.ctx.gc_callbacks.clone()
240+
}
241+
242+
/// Helper function to invoke GC callbacks
243+
fn invoke_callbacks(
244+
vm: &VirtualMachine,
245+
phase: &str,
246+
generation: usize,
247+
collected: usize,
248+
uncollectable: usize,
249+
) {
250+
let callbacks_list = &vm.ctx.gc_callbacks;
251+
let callbacks: Vec<PyObjectRef> = callbacks_list.borrow_vec().to_vec();
252+
if callbacks.is_empty() {
253+
return;
254+
}
255+
256+
let phase_str: PyObjectRef = vm.ctx.new_str(phase).into();
257+
let info = vm.ctx.new_dict();
258+
let _ = info.set_item("generation", vm.ctx.new_int(generation).into(), vm);
259+
let _ = info.set_item("collected", vm.ctx.new_int(collected).into(), vm);
260+
let _ = info.set_item("uncollectable", vm.ctx.new_int(uncollectable).into(), vm);
261+
262+
for callback in callbacks {
263+
let _ = callback.call((phase_str.clone(), info.clone()), vm);
264+
}
75265
}
76266
}

crates/vm/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ repository.workspace = true
1010
license.workspace = true
1111

1212
[features]
13-
default = ["compiler", "wasmbind", "stdio"]
13+
default = ["compiler", "wasmbind", "stdio", "gc"]
1414
stdio = []
1515
importlib = []
1616
encodings = ["importlib"]
@@ -19,6 +19,7 @@ flame-it = ["flame", "flamer"]
1919
freeze-stdlib = ["encodings"]
2020
jit = ["rustpython-jit"]
2121
threading = ["rustpython-common/threading"]
22+
gc = []
2223
compiler = ["parser", "codegen", "rustpython-compiler"]
2324
ast = ["ruff_python_ast", "ruff_text_size"]
2425
codegen = ["rustpython-codegen", "ast"]

0 commit comments

Comments
 (0)