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 <noreply@anthropic.com>
This commit is contained in:
2026-02-14 13:12:40 -05:00
parent b2f4beeaa2
commit c68694294b
3 changed files with 51 additions and 24 deletions

View File

@@ -218,9 +218,9 @@ Koka also compiles to C with algebraic effects. Key differences:
| Aspect | Koka | Lux (current) | | 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) | | 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 | | Reuse (FBIP) | Yes | Not yet |
| Maturity | Production-ready | Experimental | | Maturity | Production-ready | Experimental |
@@ -294,8 +294,8 @@ Inspired by Perceus (Koka), our RC system:
- ✅ Dynamic strings use RC allocation - ✅ Dynamic strings use RC allocation
-**Scope tracking** - compiler tracks RC variable lifetimes -**Scope tracking** - compiler tracks RC variable lifetimes
-**Automatic decref at scope exit** - verified leak-free -**Automatic decref at scope exit** - verified leak-free
-**Closure RC** - closures and environments are RC-managed
- ⏳ Early return handling (decref before nested returns) - ⏳ Early return handling (decref before nested returns)
- ⏳ Closure RC (environments still leak)
- ⏳ ADT RC (algebraic data types) - ⏳ ADT RC (algebraic data types)
- ⏳ Last-use optimization / reuse (FBIP) - ⏳ Last-use optimization / reuse (FBIP)

View File

@@ -15,6 +15,8 @@ The RC system is now functional for lists and boxed values.
- Polymorphic drop function (`lux_drop`) - Polymorphic drop function (`lux_drop`)
- Lists, boxed values, strings use RC allocation - Lists, boxed values, strings use RC allocation
- List operations incref shared elements - 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 - **Scope tracking** - compiler tracks RC variable lifetimes
- **Automatic decref at scope exit** - variables are freed when out of scope - **Automatic decref at scope exit** - variables are freed when out of scope
- **Memory tracking** - debug mode reports allocs/frees at program exit - **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 ### What's NOT Yet Implemented
- Early return handling (decref before return in nested scopes) - Early return handling (decref before return in nested scopes)
- Conditional branch handling (complex if/else patterns) - Conditional branch handling (complex if/else patterns)
- Closure RC (environments still leak)
- ADT RC - ADT RC
## The Problem ## The Problem
@@ -403,10 +404,10 @@ Rust's ownership system is fundamentally different:
#### Phase A: Complete Coverage (Prevent All Leaks) #### Phase A: Complete Coverage (Prevent All Leaks)
1. **Closure RC** - Environments should be RC-managed 1. ~~**Closure RC**~~ ✅ DONE - Environments are now RC-managed
- Allocate env with `lux_rc_alloc` - Closures allocated with `lux_rc_alloc(sizeof(LuxClosure), LUX_TAG_CLOSURE)`
- Drop env when closure is dropped - Environments allocated with `lux_rc_alloc(sizeof(LuxEnv_N), LUX_TAG_ENV)`
- ~50 lines in `emit_lambda` - Inline lambdas freed after use in List operations
2. **ADT RC** - Algebraic data types with heap fields 2. **ADT RC** - Algebraic data types with heap fields
- Track which variants contain RC fields - Track which variants contain RC fields
@@ -444,17 +445,17 @@ Rust's ownership system is fundamentally different:
### Estimated Effort ### Estimated Effort
| Phase | Description | Lines | Priority | | Phase | Description | Lines | Priority | Status |
|-------|-------------|-------|----------| |-------|-------------|-------|----------|--------|
| A1 | Closure RC | ~50 | P0 - Closures leak | | A1 | Closure RC | ~50 | P0 | ✅ Done |
| A2 | ADT RC | ~100 | P1 - ADTs leak | | A2 | ADT RC | ~100 | P1 - ADTs leak | Pending |
| A3 | Early returns | ~30 | P1 - Edge cases | | A3 | Early returns | ~30 | P1 - Edge cases | Pending |
| A4 | Conditionals | ~50 | P2 - Uncommon | | A4 | Conditionals | ~50 | P2 - Uncommon | Pending |
| B1 | Last-use opt | ~200 | P3 - Performance | | B1 | Last-use opt | ~200 | P3 - Performance | Pending |
| B2 | Reuse (FBIP) | ~300 | P3 - Performance | | B2 | Reuse (FBIP) | ~300 | P3 - Performance | Pending |
| B3 | Drop special | ~100 | P3 - Performance | | 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 **Phase B total: ~600 lines** - Gets us to Koka-level performance
### Cycle Detection ### Cycle Detection

View File

@@ -1583,16 +1583,18 @@ impl CBackend {
let temp_env = format!("_env_{}", id); let temp_env = format!("_env_{}", id);
let temp_closure = format!("_closure_{}", id); let temp_closure = format!("_closure_{}", id);
// Allocate and initialize environment struct // Allocate and initialize environment struct (RC-managed)
if env_fields.is_empty() { 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)); self.writeln(&format!("{}->env = NULL;", temp_closure));
} else { } 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 { for (name, _) in &env_fields {
self.writeln(&format!("{}->{} = {};", temp_env, name, name)); 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!("{}->env = {};", temp_closure, temp_env));
} }
self.writeln(&format!("{}->fn_ptr = (void*)lambda_{};", temp_closure, id)); self.writeln(&format!("{}->fn_ptr = (void*)lambda_{};", temp_closure, id));
@@ -2056,6 +2058,10 @@ impl CBackend {
self.indent -= 1; self.indent -= 1;
self.writeln("}"); self.writeln("}");
self.writeln(&format!("{}->length = {}->length;", result_var, list)); 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) Ok(result_var)
} }
@@ -2090,6 +2096,10 @@ impl CBackend {
self.indent -= 1; self.indent -= 1;
self.writeln("}"); self.writeln("}");
self.writeln(&format!("{}->length = {};", result_var, count_var)); 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) 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.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.indent -= 1;
self.writeln("}"); 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) Ok(result_var)
} }
@@ -2144,6 +2158,10 @@ impl CBackend {
self.writeln("}"); self.writeln("}");
self.indent -= 1; self.indent -= 1;
self.writeln("}"); 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) Ok(result_var)
} }
@@ -2172,6 +2190,10 @@ impl CBackend {
self.writeln("}"); self.writeln("}");
self.indent -= 1; self.indent -= 1;
self.writeln("}"); 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) Ok(result_var)
} }
@@ -2200,6 +2222,10 @@ impl CBackend {
self.writeln("}"); self.writeln("}");
self.indent -= 1; self.indent -= 1;
self.writeln("}"); 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) Ok(result_var)
} }
@@ -2651,8 +2677,8 @@ impl CBackend {
// List literals create new RC lists // List literals create new RC lists
Expr::List { .. } => true, Expr::List { .. } => true,
// Lambdas create closures (though we don't RC closures yet) // Lambdas create RC-managed closures
Expr::Lambda { .. } => false, // TODO: enable when closures are RC Expr::Lambda { .. } => true,
// Calls to List.* that return lists // Calls to List.* that return lists
Expr::Call { func, .. } => { Expr::Call { func, .. } => {