feat: implement automatic RC cleanup at scope exit
Add scope tracking for reference-counted variables in the C backend: - Add RcVariable struct and rc_scopes stack to CBackend - Track RC variables when assigned in let bindings - Emit lux_decref() calls when scopes exit (functions, blocks) - Add memory tracking counters (alloc/free) for leak detection - Fix List.filter to incref elements before copying (prevents double-free) - Handle return values by incref/decref to keep them alive through cleanup The RC system now properly frees memory at scope exit. Verified with test showing "[RC] No leaks: 28 allocs, 28 frees". Remaining work: early returns, complex conditionals, closures, ADTs. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -63,6 +63,13 @@ struct ClosureInfo {
|
||||
body: Expr,
|
||||
}
|
||||
|
||||
/// Information about an RC-managed variable in scope
|
||||
#[derive(Debug, Clone)]
|
||||
struct RcVariable {
|
||||
name: String, // Variable name in generated C code
|
||||
c_type: String, // C type (for documentation/debugging)
|
||||
}
|
||||
|
||||
impl std::fmt::Display for CGenError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "C codegen error: {}", self.message)
|
||||
@@ -99,6 +106,10 @@ pub struct CBackend {
|
||||
effectful_functions: HashSet<String>,
|
||||
/// Whether we're currently inside an effectful function (has evidence available)
|
||||
has_evidence: bool,
|
||||
/// Stack of scopes for RC management - each scope contains variables that need decref
|
||||
rc_scopes: Vec<Vec<RcVariable>>,
|
||||
/// Whether to emit memory tracking code for debugging
|
||||
debug_rc: bool,
|
||||
}
|
||||
|
||||
impl CBackend {
|
||||
@@ -117,6 +128,8 @@ impl CBackend {
|
||||
variant_field_types: HashMap::new(),
|
||||
effectful_functions: HashSet::new(),
|
||||
has_evidence: false,
|
||||
rc_scopes: Vec::new(),
|
||||
debug_rc: true, // Enable memory tracking for now
|
||||
}
|
||||
}
|
||||
|
||||
@@ -403,12 +416,24 @@ impl CBackend {
|
||||
self.writeln("// Forward declaration of polymorphic drop");
|
||||
self.writeln("static void lux_drop(void* ptr, int32_t tag);");
|
||||
self.writeln("");
|
||||
|
||||
// Memory tracking counters (must be before lux_rc_alloc which uses them)
|
||||
if self.debug_rc {
|
||||
self.writeln("// Memory tracking counters");
|
||||
self.writeln("static int64_t lux_rc_alloc_count = 0;");
|
||||
self.writeln("static int64_t lux_rc_free_count = 0;");
|
||||
self.writeln("");
|
||||
}
|
||||
|
||||
self.writeln("// Allocate RC-managed memory with initial refcount of 1");
|
||||
self.writeln("static void* lux_rc_alloc(size_t size, int32_t tag) {");
|
||||
self.writeln(" LuxRcHeader* hdr = (LuxRcHeader*)malloc(sizeof(LuxRcHeader) + size);");
|
||||
self.writeln(" if (!hdr) return NULL;");
|
||||
self.writeln(" hdr->rc = 1;");
|
||||
self.writeln(" hdr->tag = tag;");
|
||||
if self.debug_rc {
|
||||
self.writeln(" lux_rc_alloc_count++;");
|
||||
}
|
||||
self.writeln(" return hdr + 1; // Return pointer after header");
|
||||
self.writeln("}");
|
||||
self.writeln("");
|
||||
@@ -432,6 +457,23 @@ impl CBackend {
|
||||
self.writeln(" return ptr ? LUX_RC_HEADER(ptr)->rc : 0;");
|
||||
self.writeln("}");
|
||||
self.writeln("");
|
||||
|
||||
// Memory leak check function (only if debug_rc is enabled)
|
||||
if self.debug_rc {
|
||||
self.writeln("// === Memory Tracking (Debug) ===");
|
||||
self.writeln("static void lux_rc_check_leaks(void) {");
|
||||
self.writeln(" if (lux_rc_alloc_count != lux_rc_free_count) {");
|
||||
self.writeln(" fprintf(stderr, \"[RC] LEAK DETECTED: %lld allocs, %lld frees, %lld leaked\\n\",");
|
||||
self.writeln(" (long long)lux_rc_alloc_count, (long long)lux_rc_free_count,");
|
||||
self.writeln(" (long long)(lux_rc_alloc_count - lux_rc_free_count));");
|
||||
self.writeln(" } else {");
|
||||
self.writeln(" fprintf(stderr, \"[RC] No leaks: %lld allocs, %lld frees\\n\",");
|
||||
self.writeln(" (long long)lux_rc_alloc_count, (long long)lux_rc_free_count);");
|
||||
self.writeln(" }");
|
||||
self.writeln("}");
|
||||
self.writeln("");
|
||||
}
|
||||
|
||||
self.writeln("// === String Operations ===");
|
||||
self.writeln("// Dynamically created strings are RC-managed.");
|
||||
self.writeln("// Static string literals from source code are NOT RC-managed.");
|
||||
@@ -1080,6 +1122,9 @@ impl CBackend {
|
||||
self.writeln(" break;");
|
||||
self.writeln(" }");
|
||||
self.writeln(" // Free the object and its RC header");
|
||||
if self.debug_rc {
|
||||
self.writeln(" lux_rc_free_count++;");
|
||||
}
|
||||
self.writeln(" free(LUX_RC_HEADER(ptr));");
|
||||
self.writeln("}");
|
||||
self.writeln("");
|
||||
@@ -1273,14 +1318,34 @@ impl CBackend {
|
||||
self.has_evidence = true;
|
||||
}
|
||||
|
||||
// Push function scope for RC tracking
|
||||
self.push_rc_scope();
|
||||
|
||||
// Emit function body
|
||||
let result = self.emit_expr(&func.body)?;
|
||||
|
||||
// Restore previous evidence state
|
||||
self.has_evidence = prev_has_evidence;
|
||||
|
||||
if ret_type != "void" {
|
||||
self.writeln(&format!("return {};", result));
|
||||
// For non-void functions, we need to save result, decref locals, then return
|
||||
if ret_type != "void" && ret_type != "LuxUnit" {
|
||||
// Check if result is an RC type that we need to keep alive
|
||||
let is_rc_result = self.is_rc_type(&ret_type);
|
||||
if is_rc_result && !self.rc_scopes.last().map_or(true, |s| s.is_empty()) {
|
||||
// Save result, incref to keep it alive through cleanup
|
||||
self.writeln(&format!("{} _result = {};", ret_type, result));
|
||||
self.writeln("lux_incref(_result);");
|
||||
self.pop_rc_scope(); // Emit decrefs for all local RC vars
|
||||
self.writeln("lux_decref(_result); // Balance the incref");
|
||||
self.writeln("return _result;");
|
||||
} else {
|
||||
// No RC locals or non-RC result - simple cleanup
|
||||
self.pop_rc_scope();
|
||||
self.writeln(&format!("return {};", result));
|
||||
}
|
||||
} else {
|
||||
// Void function - just cleanup
|
||||
self.pop_rc_scope();
|
||||
}
|
||||
|
||||
self.indent -= 1;
|
||||
@@ -1536,6 +1601,9 @@ impl CBackend {
|
||||
}
|
||||
|
||||
Expr::Block { statements, result, .. } => {
|
||||
// Push a scope for this block's local variables
|
||||
self.push_rc_scope();
|
||||
|
||||
for stmt in statements {
|
||||
match stmt {
|
||||
Statement::Let { name, value, .. } => {
|
||||
@@ -1553,6 +1621,11 @@ impl CBackend {
|
||||
"LuxInt".to_string()
|
||||
};
|
||||
self.writeln(&format!("{} {} = {};", typ, name.name, val));
|
||||
|
||||
// Register RC variable if it creates a new RC value
|
||||
if self.expr_creates_rc_value(value) {
|
||||
self.register_rc_var(&name.name, &typ);
|
||||
}
|
||||
}
|
||||
Statement::Expr(e) => {
|
||||
// Emit expression - if it's a function call that returns void/unit,
|
||||
@@ -1574,7 +1647,15 @@ impl CBackend {
|
||||
}
|
||||
}
|
||||
}
|
||||
self.emit_expr(result)
|
||||
|
||||
// Emit the result expression
|
||||
let result_val = self.emit_expr(result)?;
|
||||
|
||||
// Pop scope and emit decrefs for block-local variables
|
||||
// Note: We don't decref the result variable itself if it's being returned
|
||||
self.pop_rc_scope();
|
||||
|
||||
Ok(result_val)
|
||||
}
|
||||
|
||||
Expr::EffectOp { effect, operation, args, .. } => {
|
||||
@@ -2001,6 +2082,8 @@ impl CBackend {
|
||||
self.writeln(&format!("LuxBool {} = ((LuxBool(*)(void*, LuxInt)){}->fn_ptr)({}->env, lux_unbox_int({}));", keep_var, fn_var, fn_var, elem_var));
|
||||
self.writeln(&format!("if ({}) {{", keep_var));
|
||||
self.indent += 1;
|
||||
// Incref the element since it's now shared between lists
|
||||
self.writeln(&format!("lux_incref({});", elem_var));
|
||||
self.writeln(&format!("{}->elements[{}++] = {};", result_var, count_var, elem_var));
|
||||
self.indent -= 1;
|
||||
self.writeln("}");
|
||||
@@ -2450,6 +2533,11 @@ impl CBackend {
|
||||
}
|
||||
}
|
||||
|
||||
// Check for memory leaks in debug mode
|
||||
if self.debug_rc {
|
||||
self.writeln("lux_rc_check_leaks();");
|
||||
}
|
||||
|
||||
self.writeln("return 0;");
|
||||
self.indent -= 1;
|
||||
self.writeln("}");
|
||||
@@ -2509,6 +2597,101 @@ impl CBackend {
|
||||
self.name_counter
|
||||
}
|
||||
|
||||
// === RC Scope Management ===
|
||||
|
||||
/// Push a new scope onto the RC scope stack
|
||||
fn push_rc_scope(&mut self) {
|
||||
self.rc_scopes.push(Vec::new());
|
||||
}
|
||||
|
||||
/// Pop the current scope and emit decref calls for all variables
|
||||
fn pop_rc_scope(&mut self) {
|
||||
if let Some(scope) = self.rc_scopes.pop() {
|
||||
// Decref in reverse order (LIFO - last allocated, first freed)
|
||||
for var in scope.iter().rev() {
|
||||
self.writeln(&format!("lux_decref({});", var.name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Register an RC-managed variable in the current scope
|
||||
fn register_rc_var(&mut self, name: &str, c_type: &str) {
|
||||
if let Some(scope) = self.rc_scopes.last_mut() {
|
||||
scope.push(RcVariable {
|
||||
name: name.to_string(),
|
||||
c_type: c_type.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Emit decrefs for all variables in all scopes (for early return)
|
||||
fn emit_all_scope_cleanup(&mut self) {
|
||||
// Collect all decrefs first to avoid borrow issues
|
||||
let decrefs: Vec<String> = self.rc_scopes.iter().rev()
|
||||
.flat_map(|scope| scope.iter().rev())
|
||||
.map(|var| format!("lux_decref({});", var.name))
|
||||
.collect();
|
||||
|
||||
for decref in decrefs {
|
||||
self.writeln(&decref);
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a C type needs RC management
|
||||
fn is_rc_type(&self, c_type: &str) -> bool {
|
||||
// Pointer types that are RC-managed
|
||||
matches!(c_type, "LuxList*" | "LuxClosure*" | "void*")
|
||||
|| c_type.ends_with("*") && c_type != "LuxString"
|
||||
// Note: LuxString (char*) needs special handling - only dynamic strings are RC
|
||||
}
|
||||
|
||||
/// Check if an expression creates a new RC-managed value that needs tracking
|
||||
fn expr_creates_rc_value(&self, expr: &Expr) -> bool {
|
||||
match expr {
|
||||
// List literals create new RC lists
|
||||
Expr::List { .. } => true,
|
||||
|
||||
// Lambdas create closures (though we don't RC closures yet)
|
||||
Expr::Lambda { .. } => false, // TODO: enable when closures are RC
|
||||
|
||||
// Calls to List.* that return lists
|
||||
Expr::Call { func, .. } => {
|
||||
if let Expr::Field { object, field, .. } = func.as_ref() {
|
||||
if let Expr::Var(module) = object.as_ref() {
|
||||
if module.name == "List" {
|
||||
// These List operations return new lists
|
||||
return matches!(field.name.as_str(),
|
||||
"map" | "filter" | "concat" | "reverse" |
|
||||
"take" | "drop" | "range"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
// Effect operations that return lists
|
||||
Expr::EffectOp { effect, operation, .. } => {
|
||||
if effect.name == "List" {
|
||||
matches!(operation.name.as_str(),
|
||||
"map" | "filter" | "concat" | "reverse" |
|
||||
"take" | "drop" | "range"
|
||||
)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// Variable references don't create new values - they borrow
|
||||
Expr::Var(_) => false,
|
||||
|
||||
// Literals don't need RC (primitives or static strings)
|
||||
Expr::Literal(_) => false,
|
||||
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if an expression is a call to a function that returns a closure
|
||||
fn is_closure_returning_call(&self, expr: &Expr) -> bool {
|
||||
match expr {
|
||||
|
||||
Reference in New Issue
Block a user