From ce4ab456511dcdb479912f3302945e6ab17c69a6 Mon Sep 17 00:00:00 2001 From: Brandon Lucas Date: Sat, 14 Feb 2026 11:52:00 -0500 Subject: [PATCH] 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 --- docs/C_BACKEND.md | 46 +++++---- docs/EVIDENCE_PASSING.md | 210 +++++++++++++++++++++++++++++++++++++++ src/codegen/c_backend.rs | 68 ++++++++++++- src/interpreter.rs | 40 ++++++-- 4 files changed, 336 insertions(+), 28 deletions(-) create mode 100644 docs/EVIDENCE_PASSING.md diff --git a/docs/C_BACKEND.md b/docs/C_BACKEND.md index f5fd4e1..65b6f41 100644 --- a/docs/C_BACKEND.md +++ b/docs/C_BACKEND.md @@ -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 diff --git a/docs/EVIDENCE_PASSING.md b/docs/EVIDENCE_PASSING.md new file mode 100644 index 0000000..2c2a4f8 --- /dev/null +++ b/docs/EVIDENCE_PASSING.md @@ -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>, +} +``` + +```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>` 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) diff --git a/src/codegen/c_backend.rs b/src/codegen/c_backend.rs index 67a77fb..174a01b 100644 --- a/src/codegen/c_backend.rs +++ b/src/codegen/c_backend.rs @@ -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 {"); diff --git a/src/interpreter.rs b/src/interpreter.rs index 87af7ab..d96e1fc 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -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>, + /// Evidence map for O(1) handler lookup (evidence passing optimization) + /// Maps effect name -> handler, updated when entering/exiting run blocks + evidence: HashMap>, /// Stored continuations for resumption continuations: HashMap Result>>, /// 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>)> = 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)> = self - .handler_stack - .iter() - .rev() - .find(|h| h.effect == request.effect) + .evidence + .get(&request.effect) .and_then(|handler| { handler .implementations