From c68694294b0e0949377ce9c3f7736d7af805f159 Mon Sep 17 00:00:00 2001 From: Brandon Lucas Date: Sat, 14 Feb 2026 13:12:40 -0500 Subject: [PATCH] feat: implement closure RC - environments are now memory-managed Closures and their environments are now properly reference-counted: - Allocate closures with lux_rc_alloc(sizeof(LuxClosure), LUX_TAG_CLOSURE) - Allocate environments with lux_rc_alloc(sizeof(LuxEnv_N), LUX_TAG_ENV) - Enable Lambda in expr_creates_rc_value() to track closure variables - Add lux_decref() after List higher-order operations (map, filter, fold, find, any, all) to clean up inline lambdas Test results: - Closure test: [RC] No leaks: 8 allocs, 8 frees - List RC test: [RC] No leaks: 31 allocs, 31 frees - All 263 tests pass Remaining for full memory safety: ADT RC, early returns, conditionals. Co-Authored-By: Claude Opus 4.5 --- docs/C_BACKEND.md | 6 +++--- docs/REFERENCE_COUNTING.md | 31 ++++++++++++++++--------------- src/codegen/c_backend.rs | 38 ++++++++++++++++++++++++++++++++------ 3 files changed, 51 insertions(+), 24 deletions(-) diff --git a/docs/C_BACKEND.md b/docs/C_BACKEND.md index 90ac812..ece905f 100644 --- a/docs/C_BACKEND.md +++ b/docs/C_BACKEND.md @@ -218,9 +218,9 @@ Koka also compiles to C with algebraic effects. Key differences: | Aspect | Koka | Lux (current) | |--------|------|---------------| -| Memory | Perceus RC (full) | Scope-based RC (lists/boxed) | +| Memory | Perceus RC (full) | Scope-based RC (lists/closures) | | Effects | Evidence passing (zero-cost) | Evidence passing (zero-cost) | -| Closures | Environment vectors | Heap-allocated structs (leak) | +| Closures | Environment vectors | Heap-allocated structs (RC) | | Reuse (FBIP) | Yes | Not yet | | Maturity | Production-ready | Experimental | @@ -294,8 +294,8 @@ Inspired by Perceus (Koka), our RC system: - ✅ Dynamic strings use RC allocation - ✅ **Scope tracking** - compiler tracks RC variable lifetimes - ✅ **Automatic decref at scope exit** - verified leak-free +- ✅ **Closure RC** - closures and environments are RC-managed - ⏳ Early return handling (decref before nested returns) -- ⏳ Closure RC (environments still leak) - ⏳ ADT RC (algebraic data types) - ⏳ Last-use optimization / reuse (FBIP) diff --git a/docs/REFERENCE_COUNTING.md b/docs/REFERENCE_COUNTING.md index 60848c5..7a7b385 100644 --- a/docs/REFERENCE_COUNTING.md +++ b/docs/REFERENCE_COUNTING.md @@ -15,6 +15,8 @@ The RC system is now functional for lists and boxed values. - Polymorphic drop function (`lux_drop`) - Lists, boxed values, strings use RC allocation - List operations incref shared elements +- **Closures and environments** - RC-managed with automatic cleanup +- **Inline lambda cleanup** - temporary closures freed after use - **Scope tracking** - compiler tracks RC variable lifetimes - **Automatic decref at scope exit** - variables are freed when out of scope - **Memory tracking** - debug mode reports allocs/frees at program exit @@ -27,7 +29,6 @@ The RC system is now functional for lists and boxed values. ### What's NOT Yet Implemented - Early return handling (decref before return in nested scopes) - Conditional branch handling (complex if/else patterns) -- Closure RC (environments still leak) - ADT RC ## The Problem @@ -403,10 +404,10 @@ Rust's ownership system is fundamentally different: #### Phase A: Complete Coverage (Prevent All Leaks) -1. **Closure RC** - Environments should be RC-managed - - Allocate env with `lux_rc_alloc` - - Drop env when closure is dropped - - ~50 lines in `emit_lambda` +1. ~~**Closure RC**~~ ✅ DONE - Environments are now RC-managed + - Closures allocated with `lux_rc_alloc(sizeof(LuxClosure), LUX_TAG_CLOSURE)` + - Environments allocated with `lux_rc_alloc(sizeof(LuxEnv_N), LUX_TAG_ENV)` + - Inline lambdas freed after use in List operations 2. **ADT RC** - Algebraic data types with heap fields - Track which variants contain RC fields @@ -444,17 +445,17 @@ Rust's ownership system is fundamentally different: ### Estimated Effort -| Phase | Description | Lines | Priority | -|-------|-------------|-------|----------| -| A1 | Closure RC | ~50 | P0 - Closures leak | -| A2 | ADT RC | ~100 | P1 - ADTs leak | -| A3 | Early returns | ~30 | P1 - Edge cases | -| A4 | Conditionals | ~50 | P2 - Uncommon | -| B1 | Last-use opt | ~200 | P3 - Performance | -| B2 | Reuse (FBIP) | ~300 | P3 - Performance | -| B3 | Drop special | ~100 | P3 - Performance | +| Phase | Description | Lines | Priority | Status | +|-------|-------------|-------|----------|--------| +| A1 | Closure RC | ~50 | P0 | ✅ Done | +| A2 | ADT RC | ~100 | P1 - ADTs leak | Pending | +| A3 | Early returns | ~30 | P1 - Edge cases | Pending | +| A4 | Conditionals | ~50 | P2 - Uncommon | Pending | +| B1 | Last-use opt | ~200 | P3 - Performance | Pending | +| B2 | Reuse (FBIP) | ~300 | P3 - Performance | Pending | +| B3 | Drop special | ~100 | P3 - Performance | Pending | -**Phase A total: ~230 lines** - Gets us to "no leaks" +**Phase A remaining: ~180 lines** - Gets us to "no leaks" **Phase B total: ~600 lines** - Gets us to Koka-level performance ### Cycle Detection diff --git a/src/codegen/c_backend.rs b/src/codegen/c_backend.rs index 91dfbda..4a9721f 100644 --- a/src/codegen/c_backend.rs +++ b/src/codegen/c_backend.rs @@ -1583,16 +1583,18 @@ impl CBackend { let temp_env = format!("_env_{}", id); let temp_closure = format!("_closure_{}", id); - // Allocate and initialize environment struct + // Allocate and initialize environment struct (RC-managed) if env_fields.is_empty() { - self.writeln(&format!("LuxClosure* {} = malloc(sizeof(LuxClosure));", temp_closure)); + self.writeln(&format!("LuxClosure* {} = lux_rc_alloc(sizeof(LuxClosure), LUX_TAG_CLOSURE);", temp_closure)); self.writeln(&format!("{}->env = NULL;", temp_closure)); } else { - self.writeln(&format!("LuxEnv_{}* {} = malloc(sizeof(LuxEnv_{}));", id, temp_env, id)); + // Allocate RC-managed environment + self.writeln(&format!("LuxEnv_{}* {} = lux_rc_alloc(sizeof(LuxEnv_{}), LUX_TAG_ENV);", id, temp_env, id)); for (name, _) in &env_fields { self.writeln(&format!("{}->{} = {};", temp_env, name, name)); } - self.writeln(&format!("LuxClosure* {} = malloc(sizeof(LuxClosure));", temp_closure)); + // Allocate RC-managed closure + self.writeln(&format!("LuxClosure* {} = lux_rc_alloc(sizeof(LuxClosure), LUX_TAG_CLOSURE);", temp_closure)); self.writeln(&format!("{}->env = {};", temp_closure, temp_env)); } self.writeln(&format!("{}->fn_ptr = (void*)lambda_{};", temp_closure, id)); @@ -2056,6 +2058,10 @@ impl CBackend { self.indent -= 1; self.writeln("}"); self.writeln(&format!("{}->length = {}->length;", result_var, list)); + // Decref the closure if it was a temporary (inline lambda) + if closure.starts_with("_closure_") { + self.writeln(&format!("lux_decref({});", closure)); + } Ok(result_var) } @@ -2090,6 +2096,10 @@ impl CBackend { self.indent -= 1; self.writeln("}"); self.writeln(&format!("{}->length = {};", result_var, count_var)); + // Decref the closure if it was a temporary (inline lambda) + if closure.starts_with("_closure_") { + self.writeln(&format!("lux_decref({});", closure)); + } Ok(result_var) } @@ -2114,6 +2124,10 @@ impl CBackend { self.writeln(&format!("{} = ((LuxInt(*)(void*, LuxInt, LuxInt)){}->fn_ptr)({}->env, {}, lux_unbox_int({}));", result_var, fn_var, fn_var, result_var, elem_var)); self.indent -= 1; self.writeln("}"); + // Decref the closure if it was a temporary (inline lambda) + if closure.starts_with("_closure_") { + self.writeln(&format!("lux_decref({});", closure)); + } Ok(result_var) } @@ -2144,6 +2158,10 @@ impl CBackend { self.writeln("}"); self.indent -= 1; self.writeln("}"); + // Decref the closure if it was a temporary (inline lambda) + if closure.starts_with("_closure_") { + self.writeln(&format!("lux_decref({});", closure)); + } Ok(result_var) } @@ -2172,6 +2190,10 @@ impl CBackend { self.writeln("}"); self.indent -= 1; self.writeln("}"); + // Decref the closure if it was a temporary (inline lambda) + if closure.starts_with("_closure_") { + self.writeln(&format!("lux_decref({});", closure)); + } Ok(result_var) } @@ -2200,6 +2222,10 @@ impl CBackend { self.writeln("}"); self.indent -= 1; self.writeln("}"); + // Decref the closure if it was a temporary (inline lambda) + if closure.starts_with("_closure_") { + self.writeln(&format!("lux_decref({});", closure)); + } Ok(result_var) } @@ -2651,8 +2677,8 @@ impl CBackend { // 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 + // Lambdas create RC-managed closures + Expr::Lambda { .. } => true, // Calls to List.* that return lists Expr::Call { func, .. } => {