8.9 KiB
Lux Compilation Strategy
Vision
Lux should compile to native code with zero-cost effects AND compile to JavaScript for frontend development - all while eliminating runtime overhead like Svelte does.
Current State
| Component | Status |
|---|---|
| Interpreter | Full-featured, all language constructs |
| JIT (Cranelift) | Integer arithmetic only, ~160x speedup |
| Targets | Native (via Cranelift JIT) |
Target Architecture
┌──→ C Backend ──→ GCC/Clang ──→ Native Binary (servers, CLI)
│
Lux Source ──→ HIR ──→ MIR ──→ JS Backend ──→ Optimized JS (no runtime, like Svelte)
│
└──→ Wasm Backend ──→ WebAssembly (browsers, edge)
Compilation Targets Comparison
What Other Languages Do
| Language | Backend | Runtime | Effect Strategy |
|---|---|---|---|
| Koka | C (Perceus) | None | Evidence passing |
| Elm | JS | Small runtime | No effects (pure) |
| Gleam | Erlang/JS | BEAM/JS runtime | No effects |
| Svelte | JS | No runtime | Compile-time reactivity |
| ReScript | JS | Minimal | No effects |
| PureScript | JS | Small runtime | Monad transformers |
Key Insight: Svelte's No-Runtime Approach
Svelte compiles components to vanilla JavaScript that directly manipulates the DOM. No virtual DOM diffing, no framework code shipped to the browser.
<!-- Svelte component -->
<script>
let count = 0;
</script>
<button on:click={() => count++}>{count}</button>
Compiles to:
// Direct DOM manipulation, no runtime
function create_fragment(ctx) {
let button;
return {
c() {
button = element("button");
button.textContent = ctx[0];
},
m(target, anchor) {
insert(target, button, anchor);
listen(button, "click", ctx[1]);
},
p(ctx, dirty) {
if (dirty & 1) set_data(button, ctx[0]);
}
};
}
We can do the same for Lux effects on the frontend.
Phase 1: C Backend (Native Compilation)
Goal
Compile Lux to C code that can be compiled by GCC/Clang for native execution.
Why C?
- Portability: C compilers exist for every platform
- Performance: Leverage decades of GCC/Clang optimizations
- No runtime: Like Koka, compile effects away
- Proven path: Koka, Nim, Chicken Scheme all do this successfully
Implementation Plan
Step 1.1: Basic C Code Generation
- Integer arithmetic and comparisons
- Function definitions and calls
- If/else expressions
- Let bindings
Step 1.2: Data Structures
- Records → C structs
- ADTs → Tagged unions
- Lists → Linked lists or arrays
- Strings → UTF-8 byte arrays
Step 1.3: Effect Compilation (Evidence Passing)
Transform:
fn greet(name: String): Unit with {Console} =
Console.print("Hello, " + name)
To:
void greet(Evidence* ev, LuxString name) {
LuxString msg = lux_string_concat("Hello, ", name);
ev->console->print(ev, msg);
}
Step 1.4: Memory Management (Perceus-style)
- Compile-time reference counting insertion
- Reuse analysis for in-place updates
- No garbage collector needed
File Structure
src/
codegen/
mod.rs # Code generation module
c_backend.rs # C code generation
c_runtime.h # Minimal C runtime header
c_runtime.c # Runtime support (RC, strings, etc.)
Phase 2: JavaScript Backend (Frontend)
Goal
Compile Lux to optimized JavaScript with NO runtime, like Svelte.
Effect Mapping
| Lux Effect | JS Compilation |
|---|---|
Console.print |
console.log() |
Dom.getElementById |
document.getElementById() |
Dom.addEventListener |
Direct event binding |
Http.get |
fetch() with async/await |
State.get/set |
Compile-time reactivity |
No-Runtime Strategy
Instead of shipping a runtime, compile effects to direct DOM manipulation:
effect Dom {
fn getElementById(id: String): Element
fn setTextContent(el: Element, text: String): Unit
fn addEventListener(el: Element, event: String, handler: fn(): Unit): Unit
}
fn counter(): Unit with {Dom} = {
let btn = Dom.getElementById("btn")
let count = 0
Dom.addEventListener(btn, "click", fn() => {
count = count + 1
Dom.setTextContent(btn, toString(count))
})
}
Compiles to:
// No runtime needed - direct DOM calls
function counter() {
const btn = document.getElementById("btn");
let count = 0;
btn.addEventListener("click", () => {
count = count + 1;
btn.textContent = String(count);
});
}
Reactive State (Like Svelte)
For reactive state, compile to fine-grained updates:
effect Reactive {
fn signal<T>(initial: T): Signal<T>
fn derived<T>(compute: fn(): T): Signal<T>
}
let count = Reactive.signal(0)
let doubled = Reactive.derived(fn() => count.get() * 2)
Compiles to:
// Compile-time reactivity, no virtual DOM
let count = 0;
let doubled;
const count_subscribers = new Set();
function set_count(value) {
count = value;
doubled = count * 2; // Statically known dependency
count_subscribers.forEach(fn => fn());
}
Phase 3: WebAssembly Backend
Goal
Compile to Wasm for browser and edge deployment.
Strategy
- Reuse C backend:
C → Emscripten → Wasm - Or direct Wasm generation via Cranelift
Phase 4: Zero-Cost Effects (Evidence Passing)
Current Problem
Effects use runtime handler lookup - O(n) per effect operation.
Solution: Evidence Passing
Transform effect operations to direct function calls at compile time.
Before (runtime lookup):
performEffect("Console", "print", [msg])
→ search handler stack
→ invoke handler
After (compile-time):
evidence.console.print(msg)
→ direct function call
Implementation
// Transform AST to evidence-passing form
fn transform_to_evidence(expr: Expr, effects: &[Effect]) -> Expr {
match expr {
Expr::EffectOp { effect, operation, args } => {
// Transform Console.print(x) to ev.console.print(x)
Expr::Call {
func: Expr::Field {
object: Expr::Field {
object: Expr::Var("__evidence__"),
field: effect.to_lowercase(),
},
field: operation,
},
args,
}
}
// ... recurse on other expressions
}
}
Phase 5: FBIP (Functional But In-Place)
Goal
Detect when functional updates can be performed in-place.
Example
fn increment(tree: Tree): Tree =
match tree {
Leaf(n) => Leaf(n + 1),
Node(l, r) => Node(increment(l), increment(r))
}
With FBIP analysis, if tree has refcount 1, reuse the memory:
Tree increment(Tree tree) {
if (tree->tag == LEAF) {
if (is_unique(tree)) {
tree->leaf.value += 1; // In-place!
return tree;
} else {
return make_leaf(tree->leaf.value + 1);
}
}
// ...
}
Performance Targets
| Metric | Target | How |
|---|---|---|
| Effect overhead | 0% | Evidence passing |
| Memory allocation | Minimal | Perceus RC + FBIP |
| JS bundle size | No runtime | Direct compilation |
| Native vs C | <5% overhead | Good C codegen |
Milestones
v0.2.0 - C Backend (Basic)
- Integer/bool expressions → C
- Functions → C functions
- If/else → C conditionals
- Let bindings → C variables
- Basic main() generation
- Build with GCC/Clang
v0.3.0 - C Backend (Full)
- Strings → C strings
- Records → C structs
- ADTs → Tagged unions
- Pattern matching → Switch/if chains
- Lists → Linked structures
- Effect compilation (basic)
v0.4.0 - Evidence Passing
- Effect analysis
- Evidence vector generation
- Transform effect ops to direct calls
- Handler compilation
v0.5.0 - JavaScript Backend
- Basic expressions → JS
- Functions → JS functions
- Effects → Direct DOM/API calls
- No runtime bundle
v0.6.0 - Reactive Frontend
- Reactive primitives
- Fine-grained DOM updates
- Compile-time dependency tracking
- Svelte-like output
v0.7.0 - Memory Optimization
- Reference counting insertion
- Reuse analysis
- FBIP detection
- In-place updates