Files
lux/docs/COMPILATION_STRATEGY.md
2026-02-14 00:12:28 -05:00

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?

  1. Portability: C compilers exist for every platform
  2. Performance: Leverage decades of GCC/Clang optimizations
  3. No runtime: Like Koka, compile effects away
  4. 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

References