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:
@@ -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
|
||||
|
||||
### 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):**
|
||||
```rust
|
||||
// O(n) search through handler stack
|
||||
for handler in self.handler_stack.iter().rev() {
|
||||
if handler.effect == request.effect {
|
||||
return handler.invoke(request);
|
||||
}
|
||||
}
|
||||
```
|
||||
**Current state:** Handler types (`LuxConsoleHandler`, etc.) and `LuxEvidence` struct
|
||||
are generated, but not yet used. Effect calls still use hardcoded `lux_console_print()`.
|
||||
|
||||
**Evidence passing (fast):**
|
||||
**Required changes:**
|
||||
```c
|
||||
typedef struct {
|
||||
Console* console;
|
||||
FileIO* fileio;
|
||||
} Evidence;
|
||||
// Current (hardcoded):
|
||||
void greet_lux(LuxString name) {
|
||||
lux_console_print(name);
|
||||
}
|
||||
|
||||
void greet(Evidence* ev, const char* name) {
|
||||
ev->console->print(ev, name); // Direct call, no search
|
||||
// Target (evidence passing):
|
||||
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
|
||||
|
||||
|
||||
210
docs/EVIDENCE_PASSING.md
Normal file
210
docs/EVIDENCE_PASSING.md
Normal 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)
|
||||
@@ -379,12 +379,78 @@ impl CBackend {
|
||||
self.writeln(" return strstr(haystack, needle) != NULL;");
|
||||
self.writeln("}");
|
||||
self.writeln("");
|
||||
self.writeln("// === Console Effect (built-in) ===");
|
||||
self.writeln("// === Built-in Effect Implementations ===");
|
||||
self.writeln("");
|
||||
self.writeln("static void lux_console_print(LuxString msg) {");
|
||||
self.writeln(" printf(\"%s\\n\", msg);");
|
||||
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("");
|
||||
self.writeln("typedef struct {");
|
||||
|
||||
@@ -557,8 +557,11 @@ impl std::fmt::Debug for StoredMigration {
|
||||
|
||||
pub struct Interpreter {
|
||||
global_env: Env,
|
||||
/// Stack of active effect handlers
|
||||
/// Stack of active effect handlers (kept for nested handler semantics)
|
||||
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
|
||||
continuations: HashMap<usize, Box<dyn FnOnce(Value) -> Result<EvalResult, RuntimeError>>>,
|
||||
/// Effect tracing for debugging
|
||||
@@ -609,6 +612,7 @@ impl Interpreter {
|
||||
Self {
|
||||
global_env,
|
||||
handler_stack: Vec::new(),
|
||||
evidence: HashMap::new(),
|
||||
continuations: HashMap::new(),
|
||||
trace_effects: false,
|
||||
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 {
|
||||
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
|
||||
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 {
|
||||
self.handler_stack.pop();
|
||||
}
|
||||
@@ -2895,12 +2920,11 @@ impl Interpreter {
|
||||
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
|
||||
.handler_stack
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|h| h.effect == request.effect)
|
||||
.evidence
|
||||
.get(&request.effect)
|
||||
.and_then(|handler| {
|
||||
handler
|
||||
.implementations
|
||||
|
||||
Reference in New Issue
Block a user