Files
lux/docs/JS_WASM_BACKEND_PLAN.md
Brandon Lucas 9f9543a881 docs: add website and JS/WASM backend plans
Website Plan (docs/WEBSITE_PLAN.md):
- Research from Elm, Gleam, Rust, Go, Elixir, Zig websites
- Messaging strategy: "Effects you can see, tests you can trust"
- Section structure: Hero, Problem, Solution (3 pillars), Examples
- Self-hosting goal: Build lux-lang.org in Lux itself

JS/WASM Backend Plan (docs/JS_WASM_BACKEND_PLAN.md):
- Type mappings: Lux types → JavaScript equivalents
- Code generation examples for functions, closures, ADTs, effects
- 6-phase implementation: Core → StdLib → Effects → DOM → CLI → WASM
- New Dom effect for browser manipulation
- Timeline: 11-15 weeks for full support

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-14 16:32:39 -05:00

12 KiB

Lux JavaScript/WASM Backend Plan

Goal

Enable Lux to compile to JavaScript and WebAssembly, allowing:

  1. Browser execution - Run Lux in web browsers
  2. Self-hosted website - Build lux-lang.org in Lux itself (like Elm)
  3. Universal deployment - Same code runs on server (native) and client (JS/WASM)

Research Summary

How Gleam Does It

Gleam compiles to JavaScript with these characteristics:

  • No runtime overhead - Generated JS looks like human-written code
  • Promise-based concurrency - Uses native JS promises, not Erlang actors
  • Good interop - Gleam functions callable from JS/TS directly
  • 30% performance improvement in v1.11.0 through optimization

How Elm Does It

Elm compiles to JavaScript with:

  • Virtual DOM - Efficient updates via diffing
  • Ports - Explicit interop boundary with JavaScript
  • Small runtime - Scheduler and virtual DOM
  • Dead code elimination - Only includes used code
  • Name mangling - Prefixes with underscore to avoid collisions

Implementation Strategy

Approach: Parallel to C Backend

Create src/codegen/js_backend.rs mirroring c_backend.rs structure:

src/codegen/
├── c_backend.rs     # Existing: Lux → C → Native
├── js_backend.rs    # New: Lux → JavaScript
└── wasm_backend.rs  # Future: Lux → WASM (via C or direct)

Type Mappings

Lux Type JavaScript Type
Int number (BigInt for large values)
Float number
Bool boolean
String string
Unit undefined
List<T> Array
Option<T> {tag: "Some", value: T} | {tag: "None"}
Result<T, E> {tag: "Ok", value: T} | {tag: "Err", error: E}
Closure function (closure environment captured naturally)
ADT {tag: "VariantName", field0: ..., field1: ...}

Code Generation Examples

Functions

fn add(a: Int, b: Int): Int = a + b

Generates:

function add_lux(a, b) {
    return a + b;
}

Closures

fn makeAdder(x: Int): (Int) -> Int = {
    fn(y: Int): Int => x + y
}

Generates:

function makeAdder_lux(x) {
    return function(y) {
        return x + y;
    };
}

Pattern Matching

fn describe(opt: Option<Int>): String = {
    match opt {
        Some(n) => "Got " + toString(n),
        None => "Nothing"
    }
}

Generates:

function describe_lux(opt) {
    if (opt.tag === "Some") {
        const n = opt.value;
        return "Got " + String(n);
    } else {
        return "Nothing";
    }
}

ADTs

type Tree =
    | Leaf
    | Node(Int, Tree, Tree)

Generates:

// Constructor functions
function Leaf_lux() {
    return { tag: "Leaf" };
}

function Node_lux(value, left, right) {
    return { tag: "Node", field0: value, field1: left, field2: right };
}

Effects

Effects compile to async/await:

fn fetchData(): String with Http = {
    Http.get("https://api.example.com/data")
}

Generates:

async function fetchData_lux(handlers) {
    return await handlers.Http.get("https://api.example.com/data");
}

Implementation Phases

Phase 1: Core Language (2-3 weeks)

Feature Effort Notes
Basic types (Int, Float, Bool, String) 2 days Direct mapping
Arithmetic and comparison operators 1 day
Functions and calls 2 days
Let bindings 1 day
If expressions 1 day Ternary or if/else
Pattern matching (basic) 3 days Tag checks, destructuring
ADT definitions and constructors 2 days Object literals
Closures 2 days Native JS closures
Lists 2 days Map to Array

Milestone: Can compile fib.lux to JS and run in Node.js

Phase 2: Standard Library (1-2 weeks)

Module Effort JS Implementation
Console 1 day console.log
String 2 days Native string methods
List 2 days Array methods
Math 1 day Math.*
Option/Result 1 day Pattern matching
JSON 2 days JSON.parse/stringify

Milestone: Standard library examples work in browser

Phase 3: Effects in JS (2 weeks)

Effect Effort JS Implementation
Console 1 day console.log, prompt()
Http 3 days fetch() API
File 2 days Not available in browser (Node.js only)
Time 1 day Date.now(), setTimeout
Random 1 day Math.random()
DOM (new) 5 days New effect for browser manipulation

Milestone: HTTP requests work in browser

Phase 4: Browser/DOM Support (2-3 weeks)

New Dom effect for browser manipulation:

effect Dom {
    fn querySelector(selector: String): Option<Element>
    fn createElement(tag: String): Element
    fn appendChild(parent: Element, child: Element): Unit
    fn addEventListener(el: Element, event: String, handler: () -> Unit): Unit
    fn setTextContent(el: Element, text: String): Unit
    fn setAttribute(el: Element, name: String, value: String): Unit
    fn getInputValue(el: Element): String
}
Feature Effort Notes
Basic DOM queries 2 days querySelector, getElementById
Element creation 2 days createElement, appendChild
Event handling 3 days addEventListener with closures
Attribute manipulation 2 days setAttribute, classList
Form handling 2 days Input values, form submission
Virtual DOM (optional) 5 days Efficient updates

Milestone: Can build interactive web page in Lux

Phase 5: CLI Integration (1 week)

# Compile to JavaScript
lux compile app.lux --target js -o app.js

# Compile to JavaScript module (ES modules)
lux compile app.lux --target js --module -o app.mjs

# Compile with bundled runtime
lux compile app.lux --target js --bundle -o app.bundle.js

# Run in Node.js
lux run app.lux --target js

Phase 6: WASM Backend (3-4 weeks)

Options:

  1. Lux → C → Emscripten → WASM (easiest, reuse C backend)
  2. Lux → WASM directly (more control, harder)
  3. Lux → AssemblyScript → WASM (middle ground)

Recommended: Start with Emscripten approach, direct WASM later.


Architecture

JS Backend Module Structure

// src/codegen/js_backend.rs

pub struct JsBackend {
    output: String,
    indent: usize,
    functions: HashSet<String>,
    name_counter: usize,
}

impl JsBackend {
    pub fn new() -> Self { ... }

    pub fn compile(program: &[Decl]) -> Result<String, JsGenError> { ... }

    fn emit_decl(&mut self, decl: &Decl) -> Result<(), JsGenError> { ... }
    fn emit_function(&mut self, func: &Function) -> Result<(), JsGenError> { ... }
    fn emit_expr(&mut self, expr: &Expr) -> Result<String, JsGenError> { ... }
    fn emit_pattern(&mut self, pattern: &Pattern, value: &str) -> Result<String, JsGenError> { ... }
    fn emit_adt(&mut self, adt: &TypeDecl) -> Result<(), JsGenError> { ... }

    // JS-specific
    fn emit_runtime(&mut self) { ... }  // Minimal runtime helpers
    fn emit_effect_handlers(&mut self, effects: &[Effect]) { ... }
}

Runtime (Minimal)

// Lux JS Runtime (embedded in generated code)
const Lux = {
    // Option helpers
    Some: (value) => ({ tag: "Some", value }),
    None: () => ({ tag: "None" }),

    // Result helpers
    Ok: (value) => ({ tag: "Ok", value }),
    Err: (error) => ({ tag: "Err", error }),

    // List helpers
    Cons: (head, tail) => [head, ...tail],
    Nil: () => [],

    // Effect handler invoker
    handle: async (computation, handlers) => {
        return await computation(handlers);
    }
};

Browser Integration

Entry Point

// app.lux
fn main(): Unit with Dom = {
    let button = Dom.createElement("button")
    Dom.setTextContent(button, "Click me!")
    Dom.addEventListener(button, "click", fn(): Unit => {
        Dom.setTextContent(button, "Clicked!")
    })
    let body = Dom.querySelector("body")
    match body {
        Some(el) => Dom.appendChild(el, button),
        None => ()
    }
}

Compiles to:

async function main_lux(handlers) {
    const button = handlers.Dom.createElement("button");
    handlers.Dom.setTextContent(button, "Click me!");
    handlers.Dom.addEventListener(button, "click", function() {
        handlers.Dom.setTextContent(button, "Clicked!");
    });
    const body = handlers.Dom.querySelector("body");
    if (body.tag === "Some") {
        handlers.Dom.appendChild(body.value, button);
    }
}

// Browser handler
const BrowserDom = {
    createElement: (tag) => document.createElement(tag),
    setTextContent: (el, text) => { el.textContent = text; },
    addEventListener: (el, event, handler) => el.addEventListener(event, handler),
    querySelector: (sel) => {
        const el = document.querySelector(sel);
        return el ? Lux.Some(el) : Lux.None();
    },
    appendChild: (parent, child) => parent.appendChild(child)
};

// Initialize
main_lux({ Dom: BrowserDom });

HTML Integration

<!DOCTYPE html>
<html>
<head>
    <title>Lux App</title>
</head>
<body>
    <script src="app.js"></script>
    <script>
        // Generated initialization code
        Lux.main({ Dom: BrowserDom });
    </script>
</body>
</html>

Self-Hosting the Website

Once the JS backend is complete, the lux-lang.org website can be built in Lux:

// website/src/Main.lux

import Html.{div, h1, p, button, text}
import App.{Model, Msg, init, update, view}

fn main(): Unit with Dom = {
    let model = init()
    let root = Dom.querySelector("#app")
    match root {
        Some(el) => render(el, model),
        None => Console.print("No #app element found")
    }
}

fn render(root: Element, model: Model): Unit with Dom = {
    let html = view(model)
    Dom.setInnerHtml(root, html)
}

Testing Strategy

Unit Tests

#[test]
fn test_js_function_generation() {
    let input = "fn add(a: Int, b: Int): Int = a + b";
    let js = JsBackend::compile(input).unwrap();
    assert!(js.contains("function add_lux(a, b)"));
    assert!(js.contains("return a + b"));
}

Integration Tests

Run generated JS in Node.js and compare output:

#[test]
fn test_fibonacci_js() {
    let lux_code = include_str!("../../examples/fibonacci.lux");
    let js_code = JsBackend::compile(lux_code).unwrap();

    let output = Command::new("node")
        .arg("-e")
        .arg(&js_code)
        .output()
        .unwrap();

    assert!(String::from_utf8_lossy(&output.stdout).contains("fib(10) = 55"));
}

Browser Tests

Use Playwright/Puppeteer to test DOM manipulation:

test('button click works', async ({ page }) => {
    await page.goto('http://localhost:3000/test.html');
    await page.click('button');
    expect(await page.textContent('button')).toBe('Clicked!');
});

Timeline

Phase Duration Milestone
Phase 1: Core Language 2-3 weeks Fibonacci runs in Node.js
Phase 2: Standard Library 1-2 weeks Examples work in browser
Phase 3: Effects 2 weeks HTTP works in browser
Phase 4: DOM Support 2-3 weeks Interactive page in Lux
Phase 5: CLI Integration 1 week lux compile --target js
Phase 6: WASM 3-4 weeks WASM execution

Total: 11-15 weeks for full JS/WASM support


Success Criteria

  1. Correctness: All existing tests pass when targeting JS
  2. Performance: Within 2x of hand-written JS for benchmarks
  3. Size: Generated JS is reasonable size (< 2x hand-written equivalent)
  4. Interop: Easy to call Lux from JS and JS from Lux
  5. Self-hosting: lux-lang.org runs entirely on Lux-compiled JS

References