feat: implement evidence passing for O(1) effect handler lookup

Interpreter changes:
- Add evidence HashMap for O(1) handler lookup instead of O(n) stack search
- Update eval_run to manage evidence when entering/exiting run blocks
- Modify handle_effect to use evidence.get() instead of stack iteration

C backend infrastructure:
- Add handler structs (LuxConsoleHandler, LuxStateHandler, LuxReaderHandler)
- Add LuxEvidence struct containing pointers to all handlers
- Add default handlers that delegate to built-in implementations
- Add Console.readLine built-in implementation

Documentation:
- Create docs/EVIDENCE_PASSING.md explaining design and implementation
- Update docs/C_BACKEND.md with current progress

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-14 11:52:00 -05:00
parent 909dbf7a97
commit ce4ab45651
4 changed files with 336 additions and 28 deletions

View File

@@ -249,35 +249,43 @@ Koka also compiles to C with algebraic effects. Key differences:
--- ---
## Current Progress
### Evidence Passing (Zero-Cost Effects) - PARTIALLY COMPLETE
**Interpreter:** ✅ Complete - O(1) HashMap lookup instead of O(n) stack search.
**C Backend Infrastructure:** ✅ Complete - Handler structs and evidence types generated.
**C Backend Threading:** 🔜 Planned - Passing evidence through function calls.
See [docs/EVIDENCE_PASSING.md](EVIDENCE_PASSING.md) for details.
---
## Future Roadmap ## Future Roadmap
### Phase 1: Evidence Passing (Zero-Cost Effects) ### Phase 1: Complete Evidence Passing in C Backend
**Goal:** Eliminate runtime effect handler lookup. **Goal:** Thread evidence through all effectful function calls.
**Current approach (slow):** **Current state:** Handler types (`LuxConsoleHandler`, etc.) and `LuxEvidence` struct
```rust are generated, but not yet used. Effect calls still use hardcoded `lux_console_print()`.
// O(n) search through handler stack
for handler in self.handler_stack.iter().rev() {
if handler.effect == request.effect {
return handler.invoke(request);
}
}
```
**Evidence passing (fast):** **Required changes:**
```c ```c
typedef struct { // Current (hardcoded):
Console* console; void greet_lux(LuxString name) {
FileIO* fileio; lux_console_print(name);
} Evidence; }
void greet(Evidence* ev, const char* name) { // Target (evidence passing):
ev->console->print(ev, name); // Direct call, no search void greet_lux(LuxEvidence* ev, LuxString name) {
ev->console->print(ev->console->env, name);
} }
``` ```
**Expected speedup:** 10-20x for effect-heavy code. **Expected speedup:** 10-20x for effect-heavy code (based on Koka benchmarks).
### Phase 2: Perceus Reference Counting ### Phase 2: Perceus Reference Counting

210
docs/EVIDENCE_PASSING.md Normal file
View File

@@ -0,0 +1,210 @@
# Evidence Passing for Algebraic Effects
## Overview
Evidence passing is a compilation technique that transforms O(n) runtime effect handler lookup into O(1) direct function calls. This document describes the implementation in Lux.
## Background: The Problem
### Current Approach (Stack-Based)
When an effect operation is performed, Lux searches the handler stack:
```rust
// interpreter.rs:2898-2903
let handler = self
.handler_stack
.iter()
.rev()
.find(|h| h.effect == request.effect) // O(n) linear search
```
For deeply nested handlers or effect-heavy code, this becomes a bottleneck.
### Evidence Passing Solution
Instead of searching at runtime, pass handler references directly to functions:
```
Before (stack lookup):
┌─────────────┐
│ greet() │ ──► search handler_stack for "Console" ──► call handler
└─────────────┘
After (evidence passing):
┌─────────────┐
│ greet(ev) │ ──► ev.console.print() (direct call)
└─────────────┘
```
## Design
### 1. Effect Index Assignment
Each effect type gets a compile-time index:
| Effect | Index |
|--------|-------|
| Console | 0 |
| State | 1 |
| Reader | 2 |
| Fail | 3 |
| ... | ... |
### 2. Evidence Structure
```rust
// Interpreter representation
struct Evidence {
handlers: HashMap<String, Rc<HandlerValue>>,
}
```
```c
// C backend representation
typedef struct {
LuxConsoleHandler* console;
LuxStateHandler* state;
LuxReaderHandler* reader;
// ... other effects
} LuxEvidence;
```
### 3. Handler Structures (C Backend)
Each effect becomes a struct with function pointers:
```c
// Console effect handler
typedef struct {
void (*print)(void* env, LuxString msg);
LuxString (*readLine)(void* env);
LuxInt (*readInt)(void* env);
void* env; // Captured environment
} LuxConsoleHandler;
// State effect handler
typedef struct {
void* (*get)(void* env);
void (*put)(void* env, void* value);
void* env;
} LuxStateHandler;
```
### 4. Function Signature Transformation
Functions that use effects receive an implicit evidence parameter:
**Lux source:**
```lux
fn greet(name: String): Unit with {Console} =
Console.print("Hello, " + name)
```
**Generated C:**
```c
void greet_lux(LuxEvidence* ev, LuxString name) {
LuxString msg = lux_string_concat("Hello, ", name);
ev->console->print(ev->console->env, msg);
}
```
### 5. Handler Installation
The `run ... with {}` expression:
**Lux source:**
```lux
run greet("World") with {
Console {
print(msg) => resume(builtin_print(msg))
}
}
```
**Generated C:**
```c
// Create handler
LuxConsoleHandler console_handler = {
.print = my_print_impl,
.readLine = builtin_readLine,
.readInt = builtin_readInt,
.env = captured_env
};
// Create evidence with this handler
LuxEvidence ev = outer_ev; // Copy outer evidence
ev.console = &console_handler;
// Run body with new evidence
greet_lux(&ev, "World");
```
## Implementation Status
### Phase 1: Interpreter Optimization ✅ COMPLETE
**Goal:** Replace stack search with HashMap lookup.
**Changes made to `interpreter.rs`:**
1. Added `evidence: HashMap<String, Rc<HandlerValue>>` field to Interpreter
2. Modified `handle_effect` to use O(1) evidence lookup instead of O(n) stack search
3. Updated `eval_run` to manage evidence:
- Save previous evidence values before entering run block
- Insert new handlers into evidence
- Restore previous evidence after exiting run block
**Key code change:**
```rust
// Before (O(n) search):
let handler = self.handler_stack.iter().rev().find(|h| h.effect == request.effect)
// After (O(1) lookup via evidence):
let handler = self.evidence.get(&request.effect)
```
### Phase 2: C Backend Evidence Infrastructure ✅ COMPLETE
**Goal:** Generate evidence types for future use.
**Changes made to `c_backend.rs`:**
1. Added handler struct types to prelude:
- `LuxConsoleHandler` with print/readLine function pointers
- `LuxStateHandler` with get/put function pointers
- `LuxReaderHandler` with ask function pointer
2. Added `LuxEvidence` struct containing pointers to handlers
3. Added default handlers that delegate to built-in implementations
4. Added `default_evidence` global with built-in handlers
### Phase 3: C Backend Evidence Threading 🔜 PLANNED
**Goal:** Thread evidence through effectful function calls.
**Required changes:**
1. Add `LuxEvidence* ev` parameter to effectful functions
2. Transform effect operations to use `ev->console->print(...)`
3. Generate handler structs for user-defined handlers in `run` blocks
4. Pass evidence through call chain
## Performance Characteristics
| Metric | Stack-Based | Evidence Passing |
|--------|-------------|------------------|
| Handler lookup | O(n) | O(1) |
| Memory overhead | Stack grows | Fixed-size struct |
| Function call overhead | None | +1 pointer parameter |
| Effect invocation | Search + indirect call | Direct call |
**Expected speedup:** 10-20x for effect-heavy code (based on Koka benchmarks).
## References
- [Generalized Evidence Passing for Effect Handlers](https://xnning.github.io/papers/multip.pdf) - ICFP 2021
- [Effect Handlers, Evidently](https://www.dhil.net/research/papers/effect_handlers_evidently-draft-march2020.pdf) - ICFP 2020
- [Koka Language](https://koka-lang.github.io/koka/doc/book.html)

View File

@@ -379,12 +379,78 @@ impl CBackend {
self.writeln(" return strstr(haystack, needle) != NULL;"); self.writeln(" return strstr(haystack, needle) != NULL;");
self.writeln("}"); self.writeln("}");
self.writeln(""); self.writeln("");
self.writeln("// === Console Effect (built-in) ==="); self.writeln("// === Built-in Effect Implementations ===");
self.writeln(""); self.writeln("");
self.writeln("static void lux_console_print(LuxString msg) {"); self.writeln("static void lux_console_print(LuxString msg) {");
self.writeln(" printf(\"%s\\n\", msg);"); self.writeln(" printf(\"%s\\n\", msg);");
self.writeln("}"); self.writeln("}");
self.writeln(""); self.writeln("");
self.writeln("static LuxString lux_console_readLine(void) {");
self.writeln(" char buffer[4096];");
self.writeln(" if (fgets(buffer, sizeof(buffer), stdin)) {");
self.writeln(" size_t len = strlen(buffer);");
self.writeln(" if (len > 0 && buffer[len-1] == '\\n') buffer[len-1] = '\\0';");
self.writeln(" return strdup(buffer);");
self.writeln(" }");
self.writeln(" return strdup(\"\");");
self.writeln("}");
self.writeln("");
self.writeln("// === Evidence Passing Types ===");
self.writeln("// These enable O(1) effect handler lookup instead of O(n) stack search.");
self.writeln("// See docs/EVIDENCE_PASSING.md for details.");
self.writeln("");
self.writeln("// Handler struct for Console effect");
self.writeln("typedef struct {");
self.writeln(" void (*print)(void* env, LuxString msg);");
self.writeln(" LuxString (*readLine)(void* env);");
self.writeln(" void* env;");
self.writeln("} LuxConsoleHandler;");
self.writeln("");
self.writeln("// Handler struct for State effect");
self.writeln("typedef struct {");
self.writeln(" void* (*get)(void* env);");
self.writeln(" void (*put)(void* env, void* value);");
self.writeln(" void* env;");
self.writeln("} LuxStateHandler;");
self.writeln("");
self.writeln("// Handler struct for Reader effect");
self.writeln("typedef struct {");
self.writeln(" void* (*ask)(void* env);");
self.writeln(" void* env;");
self.writeln("} LuxReaderHandler;");
self.writeln("");
self.writeln("// Evidence struct - passed to effectful functions");
self.writeln("// Contains pointers to current handlers for each effect type");
self.writeln("typedef struct {");
self.writeln(" LuxConsoleHandler* console;");
self.writeln(" LuxStateHandler* state;");
self.writeln(" LuxReaderHandler* reader;");
self.writeln("} LuxEvidence;");
self.writeln("");
self.writeln("// Default Console handler using built-in implementations");
self.writeln("static void default_console_print(void* env, LuxString msg) {");
self.writeln(" (void)env;");
self.writeln(" lux_console_print(msg);");
self.writeln("}");
self.writeln("");
self.writeln("static LuxString default_console_readLine(void* env) {");
self.writeln(" (void)env;");
self.writeln(" return lux_console_readLine();");
self.writeln("}");
self.writeln("");
self.writeln("static LuxConsoleHandler default_console_handler = {");
self.writeln(" .print = default_console_print,");
self.writeln(" .readLine = default_console_readLine,");
self.writeln(" .env = NULL");
self.writeln("};");
self.writeln("");
self.writeln("// Default evidence with built-in handlers");
self.writeln("static LuxEvidence default_evidence = {");
self.writeln(" .console = &default_console_handler,");
self.writeln(" .state = NULL,");
self.writeln(" .reader = NULL");
self.writeln("};");
self.writeln("");
self.writeln("// === List Types ==="); self.writeln("// === List Types ===");
self.writeln(""); self.writeln("");
self.writeln("typedef struct {"); self.writeln("typedef struct {");

View File

@@ -557,8 +557,11 @@ impl std::fmt::Debug for StoredMigration {
pub struct Interpreter { pub struct Interpreter {
global_env: Env, global_env: Env,
/// Stack of active effect handlers /// Stack of active effect handlers (kept for nested handler semantics)
handler_stack: Vec<Rc<HandlerValue>>, handler_stack: Vec<Rc<HandlerValue>>,
/// Evidence map for O(1) handler lookup (evidence passing optimization)
/// Maps effect name -> handler, updated when entering/exiting run blocks
evidence: HashMap<String, Rc<HandlerValue>>,
/// Stored continuations for resumption /// Stored continuations for resumption
continuations: HashMap<usize, Box<dyn FnOnce(Value) -> Result<EvalResult, RuntimeError>>>, continuations: HashMap<usize, Box<dyn FnOnce(Value) -> Result<EvalResult, RuntimeError>>>,
/// Effect tracing for debugging /// Effect tracing for debugging
@@ -609,6 +612,7 @@ impl Interpreter {
Self { Self {
global_env, global_env,
handler_stack: Vec::new(), handler_stack: Vec::new(),
evidence: HashMap::new(),
continuations: HashMap::new(), continuations: HashMap::new(),
trace_effects: false, trace_effects: false,
effect_traces: Vec::new(), effect_traces: Vec::new(),
@@ -2871,15 +2875,36 @@ impl Interpreter {
} }
} }
// Push handlers // Push handlers onto stack (for nested handler semantics)
for h in &handler_values { for h in &handler_values {
self.handler_stack.push(Rc::clone(h)); self.handler_stack.push(Rc::clone(h));
} }
// Update evidence map for O(1) lookup (evidence passing optimization)
// Save previous evidence values for restoration
let mut previous_evidence: Vec<(String, Option<Rc<HandlerValue>>)> = Vec::new();
for h in &handler_values {
let effect_name = h.effect.clone();
let prev = self.evidence.insert(effect_name.clone(), Rc::clone(h));
previous_evidence.push((effect_name, prev));
}
// Evaluate expression // Evaluate expression
let result = self.eval_expr_inner(expr, env); let result = self.eval_expr_inner(expr, env);
// Pop handlers // Restore previous evidence (for proper nested handler support)
for (effect_name, prev) in previous_evidence.into_iter().rev() {
match prev {
Some(h) => {
self.evidence.insert(effect_name, h);
}
None => {
self.evidence.remove(&effect_name);
}
}
}
// Pop handlers from stack
for _ in &handler_values { for _ in &handler_values {
self.handler_stack.pop(); self.handler_stack.pop();
} }
@@ -2895,12 +2920,11 @@ impl Interpreter {
0 0
}; };
// Find a handler for this effect - clone what we need to avoid borrow issues // Find a handler using evidence map (O(1) lookup via evidence passing)
// This replaces the previous O(n) handler_stack search
let handler_data: Option<(Env, crate::ast::Expr, Vec<Ident>)> = self let handler_data: Option<(Env, crate::ast::Expr, Vec<Ident>)> = self
.handler_stack .evidence
.iter() .get(&request.effect)
.rev()
.find(|h| h.effect == request.effect)
.and_then(|handler| { .and_then(|handler| {
handler handler
.implementations .implementations