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>
12 KiB
Lux JavaScript/WASM Backend Plan
Goal
Enable Lux to compile to JavaScript and WebAssembly, allowing:
- Browser execution - Run Lux in web browsers
- Self-hosted website - Build lux-lang.org in Lux itself (like Elm)
- 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:
- Lux → C → Emscripten → WASM (easiest, reuse C backend)
- Lux → WASM directly (more control, harder)
- 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
- Correctness: All existing tests pass when targeting JS
- Performance: Within 2x of hand-written JS for benchmarks
- Size: Generated JS is reasonable size (< 2x hand-written equivalent)
- Interop: Easy to call Lux from JS and JS from Lux
- Self-hosting: lux-lang.org runs entirely on Lux-compiled JS