C backend now fully threads evidence through effectful function calls:
- Track effectful functions via effectful_functions HashSet
- Add has_evidence flag to track context during code generation
- Add LuxEvidence* ev parameter to effectful function signatures
- Transform effect operations to use ev->console->print() when evidence available
- Update function calls to pass evidence (ev or &default_evidence)
- Update main entry point to pass &default_evidence
Generated code now uses zero-cost evidence passing:
void greet_lux(LuxEvidence* ev) {
ev->console->print(ev->console->env, "Hello!");
}
This completes the evidence passing implementation for both interpreter
(O(1) HashMap lookup) and C backend (direct function pointer calls).
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
234 lines
6.1 KiB
Markdown
234 lines
6.1 KiB
Markdown
# 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 ✅ COMPLETE
|
|
|
|
**Goal:** Thread evidence through effectful function calls.
|
|
|
|
**Changes made to `c_backend.rs`:**
|
|
|
|
1. Track effectful functions in `effectful_functions: HashSet<String>`
|
|
2. Add `has_evidence: bool` flag to track context
|
|
3. For effectful functions:
|
|
- Add `LuxEvidence* ev` parameter to forward declarations
|
|
- Add `LuxEvidence* ev` parameter to function definitions
|
|
4. Transform effect operations:
|
|
- When `has_evidence` is true: `ev->console->print(ev->console->env, msg)`
|
|
- When `has_evidence` is false: `lux_console_print(msg)` (fallback)
|
|
5. Update function calls:
|
|
- Effectful calls inside effectful functions: pass `ev`
|
|
- Effectful calls outside: pass `&default_evidence`
|
|
6. Update main function generation to pass `&default_evidence`
|
|
|
|
**Example generated code:**
|
|
```c
|
|
// Function signature includes evidence
|
|
void greet_lux(LuxEvidence* ev) {
|
|
// Effect operations use evidence
|
|
ev->console->print(ev->console->env, "Hello, World!");
|
|
}
|
|
|
|
int main(int argc, char** argv) {
|
|
// Entry point uses default evidence
|
|
greet_lux(&default_evidence);
|
|
return 0;
|
|
}
|
|
```
|
|
|
|
## 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)
|