Skip to main content

Lux API Guide

Lux is an energy-efficient programming language that replaces the HTML/JS/TS/CSS/DOM ecosystem with a single syntax. Energy is a first-class observable, reported in joules — not tokens or dollars. This guide covers all supported integration paths.

Lux can be consumed as:

InterfaceCrate / PackageUse case
Rust librarylux-langParse, analyze, transform, emit .lux source
C FFIlux-rtLink compiled Lux programs against the runtime
Pythonlux-py (PyO3)Scripting, notebooks, data pipelines
Golux-go (CGo)Backend services, CLI tools
TypeScript/Node@openie/lux (NAPI-RS)Build tooling, editor extensions
JuliaLux.jl (ccall)Scientific computing, energy research
WASM@openie/lux-wasmBrowser playground, in-page tooling
MCPlux-mcpAI agent integration (Claude, Cursor, etc.)

Building apps in Lux (signals, memos, effects, components, forms, data-loading, routing) is covered in §11 Web Application Reference.


1. Rust Library (lux-lang)

The canonical API. Add to Cargo.toml:

[dependencies]
lux-lang = { path = "crates/lux-lang" }

Core functions

use lux_lang::{parse, emit, Module, LuxError, LuxResult};

// Parse .lux source into an AST.
let module: Module = lux_lang::parse(source)?;

// Pretty-print AST back to .lux source (round-trip safe).
let source: String = lux_lang::emit(&module);

Module AST

pub struct Module {
    pub items: Vec<Item>,
    pub tests: Vec<TestDef>,
}

Item variants — top-level declarations:

VariantDescription
App(AppDef)Reactive application with state + view
Server(ServerDef)HTTP server with typed routes
Function(FuncDef)Named function
Style(StyleDef)Scoped CSS-like styling
Use(UseDef)Module import
TypeDef { name, ty, span }Type alias or record definition
Let(Stmt)Top-level binding

Expr variants — expressions:

VariantExample in Lux
Int(i64)42
Float(f64)3.14
Str(String)"hello"
Interpolated(Vec<StringPart>)"Count: {n}"
Bool(bool)true
Nonenone
List(Vec<Expr>)[1, 2, 3]
Record(Vec<(String, Expr)>){name: "Alice", age: 30}
Ident(String)count
Binary { left, op, right }a + b, x and y
Unary { op, expr }-x, not done
Call { func, args }print("hi")
Field { expr, name }user.name
Index { expr, index }items[0]
Lambda { params, body }fn(x) -> x * 2
Signal(Box<Expr>)signal(0)
Memo(Box<Expr>)memo(total())
If { condition, then_expr, else_expr }if x > 0 then x else -x
Match { expr, arms }match n: ...
Pipe { left, right }data |> process
Range { start, end, inclusive }1..10, 1..=10
Await(Box<Expr>)await fetch(url)
Yield(Box<Expr>)yield item
Do { expr, params, body }list do |x|: print(x)

Stmt variants — statements:

VariantExample in Lux
Let { name, mutable, ty, value, span }let mut count = 0
Assign { target, value, span }count = count + 1
Expr(Expr)print("hello")
If { condition, then_body, else_body, span }if x > 0: ...
For { binding, index, iter, body, span }for item, i in list: ...
While { condition, body, span }while running: ...
Match { expr, arms, span }match shape: ...
Return(Option<Expr>, Span)return result
Break(Span)break
Continue(Span)continue
FuncDef(FuncDef)fn helper(): ...
Effect { body, span }effect: ...
Unless { condition, then_body, else_body, span }unless done: ...
Until { condition, body, span }until ready: ...

Serde support

All AST types derive Serialize and Deserialize:

let module = lux_lang::parse("let x = 42\n")?;
let json = serde_json::to_string_pretty(&module)?;
let restored: lux_lang::Module = serde_json::from_str(&json)?;

Error handling

use lux_lang::{LuxError, Diagnostic, format_error};

match lux_lang::parse(source) {
    Ok(module) => { /* use module */ }
    Err(e) => {
        // Elm-quality diagnostic with source snippet, underline, and hints.
        let diag: Diagnostic = format_error(source, "myapp.lux", &e);
        eprintln!("{diag}");
    }
}

Error variants:

VariantWhen
LuxError::Parse { line, col, message }Syntax error at a known location
LuxError::UnexpectedEofSource ended mid-expression
LuxError::IndentError { line }Inconsistent indentation
LuxError::NestingTooDeep { max }Expression depth exceeded limit

Example: parse, inspect, modify, emit

use lux_lang::{parse, emit, ast::*};

let mut module = parse("fn double(x):\n  return x * 2\n")?;

// Inspect the first item.
if let Item::Function(ref f) = module.items[0] {
    println!("Function: {} with {} params", f.name, f.params.len());
}

// Add a test block.
module.tests.push(TestDef {
    name: "double works".into(),
    body: vec![Stmt::Expr(Expr::Call {
        func: Box::new(Expr::Ident("assert".into())),
        args: vec![Expr::Binary {
            left: Box::new(Expr::Call {
                func: Box::new(Expr::Ident("double".into())),
                args: vec![Expr::Int(5)],
            }),
            op: BinOp::Eq,
            right: Box::new(Expr::Int(10)),
        }],
    })],
    span: Span { line: 0, col: 0 },
});

let output = emit(&module);
// fn double(x):
//   return x * 2
//
// test "double works":
//   assert(double(5) == 10)

2. C FFI (lux-rt)

The Lux runtime is a C-ABI library. Compiled Lux programs link against liblux_rt.a (static) or liblux_rt.{dylib,so} (dynamic). In JIT mode the same symbols are registered in the Cranelift JIT module.

Build:

cargo build --release -p lux-rt
# Produces: target/release/liblux_rt.a and liblux_rt.dylib

Memory model

Every heap-allocated Lux value begins with a LuxHeader:

typedef struct {
    _Atomic int64_t rc;      // reference count, starts at 1
    uint8_t         type_tag; // 1=String, 2=List, 3=Record, 4=Signal, 5=Closure, 6=HttpServer
} LuxHeader;

The compiler emits lux_rt_retain(ptr) and lux_rt_release(ptr) at assignment and scope exit. When the count reaches 0, the value is freed. Manual FFI consumers must follow the same protocol.

Exported functions by category

Energy metering (3 functions):

void     lux_rt_energy_reset();               // zero the energy counter
uint64_t lux_rt_energy_total();               // read accumulated ticks
void     lux_rt_energy_report(int64_t ticks, const uint8_t* name, int64_t name_len);

Reference counting (2):

void lux_rt_retain(LuxHeader* ptr);   // increment rc
void lux_rt_release(LuxHeader* ptr);  // decrement rc, free at 0

Strings (28):

LuxString* lux_rt_str_new(const uint8_t* ptr, int64_t len);
LuxString* lux_rt_str_concat(LuxString* a, LuxString* b);
LuxString* lux_rt_str_empty();
int64_t    lux_rt_str_len(LuxString* s);
int8_t     lux_rt_str_eq(LuxString* a, LuxString* b);
int64_t    lux_rt_str_cmp(LuxString* a, LuxString* b);
LuxList*   lux_rt_str_split(LuxString* s, LuxString* delim);
LuxString* lux_rt_str_replace(LuxString* s, LuxString* old, LuxString* new_str);
LuxString* lux_rt_str_upper(LuxString* s);
LuxString* lux_rt_str_lower(LuxString* s);
LuxString* lux_rt_str_trim(LuxString* s);
int8_t     lux_rt_str_contains(LuxString* s, LuxString* sub);
int8_t     lux_rt_str_starts_with(LuxString* s, LuxString* prefix);
int8_t     lux_rt_str_ends_with(LuxString* s, LuxString* suffix);
LuxString* lux_rt_str_slice(LuxString* s, int64_t start, int64_t end);
LuxString* lux_rt_str_char_at(LuxString* s, int64_t idx);
int64_t    lux_rt_str_index_of(LuxString* s, LuxString* sub);
LuxString* lux_rt_str_repeat(LuxString* s, int64_t n);
LuxString* lux_rt_str_reverse(LuxString* s);
LuxList*   lux_rt_str_lines(LuxString* s);
LuxList*   lux_rt_str_chars(LuxString* s);
LuxString* lux_rt_str_join(LuxList* list, LuxString* delim);
int64_t    lux_rt_parse_int(LuxString* s);
double     lux_rt_parse_float(LuxString* s);
LuxString* lux_rt_bool_to_string(int8_t val);
LuxString* lux_rt_type_name(int64_t tag);
LuxString* lux_rt_to_string(int64_t val);
LuxString* lux_rt_float_to_string(double val);

Lists (24):

LuxList* lux_rt_list_new(int64_t capacity);
void     lux_rt_list_push(LuxList* list, int64_t val);
int64_t  lux_rt_index_get(LuxList* list, int64_t idx);
void     lux_rt_index_set(LuxList* list, int64_t idx, int64_t val);
int64_t  lux_rt_list_len(LuxList* list);
int64_t  lux_rt_list_pop(LuxList* list);
int64_t  lux_rt_list_first(LuxList* list);
int64_t  lux_rt_list_last(LuxList* list);
void     lux_rt_list_reverse(LuxList* list);
void     lux_rt_list_sort(LuxList* list);
LuxList* lux_rt_list_uniq(LuxList* list);
LuxList* lux_rt_list_flatten(LuxList* list);
LuxList* lux_rt_list_take(LuxList* list, int64_t n);
LuxList* lux_rt_list_drop(LuxList* list, int64_t n);
LuxList* lux_rt_list_slice(LuxList* list, int64_t start, int64_t end);
int8_t   lux_rt_list_contains(LuxList* list, int64_t val);
int64_t  lux_rt_list_index_of(LuxList* list, int64_t val);
LuxList* lux_rt_list_zip(LuxList* a, LuxList* b);
LuxList* lux_rt_list_map(LuxList* list, LuxClosure* closure);
LuxList* lux_rt_list_filter(LuxList* list, LuxClosure* closure);
int64_t  lux_rt_list_reduce(LuxList* list, int64_t init, LuxClosure* closure);
int64_t  lux_rt_list_find(LuxList* list, LuxClosure* closure);
int8_t   lux_rt_list_any(LuxList* list, LuxClosure* closure);
int8_t   lux_rt_list_all(LuxList* list, LuxClosure* closure);
void     lux_rt_list_each(LuxList* list, LuxClosure* closure);

Records (8):

LuxRecord* lux_rt_record_new();
void       lux_rt_record_set(LuxRecord* rec, LuxString* key, int64_t val);
int64_t    lux_rt_field_get(LuxRecord* rec, LuxString* key, int64_t default_val);
int8_t     lux_rt_record_has(LuxRecord* rec, LuxString* key);
LuxList*   lux_rt_record_keys(LuxRecord* rec);
LuxList*   lux_rt_record_values(LuxRecord* rec);
LuxRecord* lux_rt_record_merge(LuxRecord* a, LuxRecord* b);
LuxRecord* lux_rt_record_delete(LuxRecord* rec, LuxString* key);
int64_t    lux_rt_record_len(LuxRecord* rec);

Signals (3):

LuxSignal* lux_rt_signal_new(int64_t initial);
int64_t    lux_rt_signal_get(LuxSignal* signal);
void       lux_rt_signal_set(LuxSignal* signal, int64_t val);

Math (20):

int64_t lux_rt_math_abs(int64_t val);
double  lux_rt_math_abs_f(double val);
double  lux_rt_math_sqrt(double val);
int64_t lux_rt_math_ceil(double val);
int64_t lux_rt_math_floor(double val);
int64_t lux_rt_math_round(double val);
double  lux_rt_math_sin(double val);
double  lux_rt_math_cos(double val);
double  lux_rt_math_tan(double val);
double  lux_rt_math_pow(double base, double exp);
double  lux_rt_math_log(double val);      // natural log
double  lux_rt_math_log2(double val);
double  lux_rt_math_log10(double val);
double  lux_rt_math_exp(double val);
int64_t lux_rt_math_min(int64_t a, int64_t b);
int64_t lux_rt_math_max(int64_t a, int64_t b);
double  lux_rt_math_min_f(double a, double b);
double  lux_rt_math_max_f(double a, double b);
int64_t lux_rt_math_clamp(int64_t val, int64_t lo, int64_t hi);
double  lux_rt_math_clamp_f(double val, double lo, double hi);
int64_t lux_rt_math_random(int64_t lo, int64_t hi);

Safety (4):

int64_t lux_rt_safe_div(int64_t a, int64_t b);     // returns 0 on div-by-zero
int64_t lux_rt_safe_mod(int64_t a, int64_t b);
double  lux_rt_safe_div_f(double a, double b);
double  lux_rt_safe_mod_f(double a, double b);
int64_t lux_rt_safe_index(LuxList* list, int64_t idx, int64_t default_val);

File I/O (5):

LuxString* lux_rt_read_file(LuxString* path);
int8_t     lux_rt_write_file(LuxString* path, LuxString* content);
int8_t     lux_rt_append_file(LuxString* path, LuxString* content);
int8_t     lux_rt_file_exists(LuxString* path);
LuxList*   lux_rt_list_dir(LuxString* path);

JSON (1):

LuxString* lux_rt_to_json(int64_t val, int64_t type_tag);

Time (2):

int64_t lux_rt_time_now();   // milliseconds since epoch
void    lux_rt_sleep(int64_t ms);

Environment (2):

LuxString* lux_rt_env_get(LuxString* key);
LuxList*   lux_rt_args();

HTTP client (5):

LuxRecord* lux_rt_http_get(LuxString* url);
LuxRecord* lux_rt_http_post(LuxString* url, LuxString* body);
LuxRecord* lux_rt_http_put(LuxString* url, LuxString* body);
LuxRecord* lux_rt_http_delete(LuxString* url);
LuxRecord* lux_rt_http_post_json(LuxString* url, LuxString* json);

HTTP server (3):

LuxHttpServer* lux_rt_http_server_new(int64_t port);
void           lux_rt_http_server_route(LuxHttpServer* server, const uint8_t* method, int64_t method_len, const uint8_t* path, int64_t path_len, int64_t handler);
void           lux_rt_http_server_start(LuxHttpServer* server);

WebSocket (6):

LuxWsConnection* lux_rt_ws_connect(LuxString* url);
int8_t           lux_rt_ws_send_text(LuxWsConnection* ws, LuxString* text);
int8_t           lux_rt_ws_send_binary(LuxWsConnection* ws, const uint8_t* data, int64_t len);
LuxRecord*       lux_rt_ws_recv(LuxWsConnection* ws);
int8_t           lux_rt_ws_close(LuxWsConnection* ws, int64_t code, LuxString* reason);
int8_t           lux_rt_ws_is_closed(LuxWsConnection* ws);

WebTransport (10):

LuxWtConnection* lux_rt_wt_connect(LuxString* addr);
int64_t          lux_rt_wt_open_bidi(LuxWtConnection* wt);
int64_t          lux_rt_wt_open_uni(LuxWtConnection* wt);
int64_t          lux_rt_wt_stream_write(LuxWtConnection* wt, int64_t stream_id, const uint8_t* data, int64_t len);
LuxString*       lux_rt_wt_stream_read(LuxWtConnection* wt, int64_t stream_id, int64_t max_bytes);
int8_t           lux_rt_wt_stream_finish(LuxWtConnection* wt, int64_t stream_id);
int8_t           lux_rt_wt_send_datagram(LuxWtConnection* wt, const uint8_t* data, int64_t len);
LuxString*       lux_rt_wt_recv_datagram(LuxWtConnection* wt);
int8_t           lux_rt_wt_close(LuxWtConnection* wt, LuxString* reason);
int8_t           lux_rt_wt_is_closed(LuxWtConnection* wt);
int64_t          lux_rt_wt_rtt(LuxWtConnection* wt);

Closures (3):

LuxClosure*  lux_rt_closure_new(int64_t fn_ptr, LuxRecord* env);
int64_t      lux_rt_closure_fn(LuxClosure* c);
LuxRecord*   lux_rt_closure_env(LuxClosure* c);

3. Python (lux-py)

PyO3 bindings wrapping lux-lang and lux-runtime.

pip install lux-lang
import lux

# Parse source to AST (returned as JSON-serializable dict).
module = lux.parse("fn main() -> int:\n  println(42)\n  0")
print(module.to_json())

# Run code with energy metering.
result = lux.run("println('hello')")
print(result.energy_uj)   # energy consumed in microjoules
print(result.wall_us)     # wall-clock time in microseconds

# Emit AST back to .lux source.
source = lux.emit(module)

The Python binding exposes parse, check, emit, and run. Foreign-code lifting/auditing is available through the lux CLI (lux lift / lux audit), not the Python binding. Note that run’s energy_uj is a lightweight runtime proxy, not a hardware-measured figure — see §9.


4. Go (lux-go)

CGo bindings over the C FFI.

go get github.com/openie-dev/lux-go
package main

import (
    "fmt"
    lux "github.com/openie-dev/lux-go"
)

func main() {
    // Parse .lux source.
    module, err := lux.Parse("fn main() -> int:\n  println(42)\n  0")
    if err != nil {
        panic(err)
    }
    fmt.Println("Items:", len(module.Items))

    // Run with energy receipt.
    result, err := lux.Run("println('hello')")
    if err != nil {
        panic(err)
    }
    fmt.Printf("Energy: %d uJ\n", result.EnergyUJ)
    fmt.Printf("Wall:   %d us\n", result.WallUS)
}

5. TypeScript / Node (lux-node)

NAPI-RS native addon.

npm install @openie/lux
import { parse, emit, run, lift } from '@openie/lux';

// Parse .lux source to AST.
const module = parse(`fn main() -> int:\n  println(42)\n  0`);
console.log(JSON.stringify(module, null, 2));

// Emit AST back to source.
const source = emit(module);

// Run with energy receipt.
const result = await run("println('hello')");
console.log(result.energyUj);  // microjoules
console.log(result.wallUs);    // microseconds

// Lift JS to Lux.
const lifted = lift('const add = (a, b) => a + b;', 'js');
console.log(lifted.luxSource);

6. Julia (lux-julia)

ccall bindings over the C FFI.

] add Lux
using Lux

# Parse .lux source.
mod = Lux.parse("fn main() -> int:\n  println(42)\n  0")
println("Items: ", length(mod.items))

# Run with energy receipt.
result = Lux.run("println('hello')")
println("Energy: $(result.energy_uj) uJ")
println("Wall:   $(result.wall_us) us")

7. WASM (lux-wasm)

Full toolchain in the browser. Build:

wasm-pack build --target web crates/lux-wasm
import init, {
    lux_parse,
    lux_parse_pretty,
    lux_emit,
    lux_fmt,
    lux_check,
    lux_run,
    lux_lift,
    lux_modules,
    lux_domains,
    lux_modules_in_domain,
    lux_invoke,
    lux_info
} from '@openie/lux-wasm';

await init();

// Parse .lux source to AST JSON.
const ast = lux_parse("let x = 42\n");
const module = JSON.parse(ast);
console.log(module.items[0]);  // { "Let": { ... } }

// Pretty-print AST for debugging.
const pretty = lux_parse_pretty("let x = 42\n");

// Emit AST JSON back to .lux source.
const source = lux_emit(ast);

// Format / canonicalize .lux source.
const formatted = lux_fmt("let   x=42\n");  // "let x = 42\n"

// Validate without executing.
const check = JSON.parse(lux_check("let x = 42\n"));
// { status: "ok", items: 1, tests: 0 }

// Execute in the browser sandbox (lux-eval-core): output captured,
// every AST-node evaluation step-counted, budget-limited. No filesystem,
// network, or clock. The step count is the op count for energy modeling.
const run = JSON.parse(lux_run('fn main():\n    print("hi")\n'));
// { status: "ok", output: "hi\n", steps: 7, items: 1, tests: 0 }
// on failure: { status: "error", message, line?, col?, output, steps }

// Lift foreign code to Lux.
const lifted = JSON.parse(lux_lift("function add(a, b) { return a + b; }", "js"));
console.log(lifted.lux_source);
console.log(lifted.recommendations);

// Browse standard library (1,723 modules).
const allModules = JSON.parse(lux_modules());
const domains = JSON.parse(lux_domains());
const chartModules = JSON.parse(lux_modules_in_domain("chart"));

// Invoke a module directly.
const result = JSON.parse(lux_invoke("math.abs", '{"value": -42}'));

// Runtime info.
const info = JSON.parse(lux_info());
// { version: "0.1.0", runtime: "wasm32", modules: 1723, ... }

Supported lift languages: js, ts, html, css, ruby, go, php, dart, sql, python, yaml, json.


8. MCP (lux-mcp)

JSON-RPC 2.0 over stdio. Six tools for AI agent integration.

Build and configure:

cargo build --release -p lux-mcp

Add to your MCP client config (e.g., claude_desktop_config.json):

{
  "mcpServers": {
    "lux": {
      "command": "target/release/lux-mcp"
    }
  }
}

Tools

ToolDescriptionRequired args
lux_runExecute Lux code, return result + energy receiptcode: string
lux_checkParse and validate Lux code, return AST summarycode: string
lux_auditEnergy audit on foreign code with optimization recscode: string, language: string
lux_liftConvert foreign code to Lux syntaxcode: string, language: string
lux_modulesList standard library modules, optional filterquery?: string
lux_docsGet docs for a Lux language constructtopic: string

Supported language values: javascript, typescript, python, ruby, go, rust, c, php, dart, html, css, sql.

Supported topic values: overview, app, server, fn, style, view, use, type, test, signal, memo, match, for, while, effect, let.

Example: lux_run

Request:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "lux_run",
    "arguments": {
      "code": "fn fib(n: int) -> int:\n  if n <= 1:\n    return n\n  fib(n - 1) + fib(n - 2)\nprintln(fib(10))"
    }
  }
}

Response:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [{
      "type": "text",
      "text": "Result: 55\n\n--- Energy Receipt ---\nTotal:      0.000023 J\nOperations: 177\nElapsed:    0.12 ms\nAvg Power:  5.0000 W"
    }]
  }
}

9. Energy Metering

Every Lux operation is tracked. The runtime maintains an atomic energy counter that accumulates ticks across all function calls, I/O, and compute.

Receipt format

All execution APIs return an energy receipt:

{
  "total_uj":   23,
  "compute_uj": 18,
  "io_uj":      5,
  "wall_us":    120
}
FieldTypeDescription
total_uju64Total energy consumed in microjoules
compute_uju64CPU/compute portion
io_uju64File, network, and system call portion
wall_usu64Wall-clock elapsed time in microseconds

C FFI energy API

// Reset the counter before a measurement window.
lux_rt_energy_reset();

// ... run code ...

// Read accumulated ticks.
uint64_t total = lux_rt_energy_total();

// Report energy for a named operation (compiler-generated).
lux_rt_energy_report(ticks, name_ptr, name_len);

Design principle

Energy is a first-class observable, not an afterthought. The runtime’s atomic counter accumulates ticks across calls, I/O, and compute; the figures above are substantiated by hardware sensors (RAPL, NVML, IOKit) at aggregate granularity and by a calibrated per-platform model below the sensors’ resolution floor (~1 mJ for RAPL). The receipt always reflects which source produced a figure. This enables:

  • Setting energy budgets (max_energy_per_query_uj)
  • Auditing foreign code before and after conversion to Lux
  • Reporting a program’s own cost to users in joules instead of opaque token counts

It is a meter on your own program, not a benchmark against someone else’s.


10. Building from Source

Prerequisites: Rust 1.93.1+ (edition 2024).

# Rust library (parser + printer)
cargo build --release -p lux-lang

# C runtime library
cargo build --release -p lux-rt
# Output: target/release/liblux_rt.a, liblux_rt.dylib

# WASM package (browser)
wasm-pack build --target web crates/lux-wasm
# Output: crates/lux-wasm/pkg/ (~398 KB)

# MCP server
cargo build --release -p lux-mcp
# Output: target/release/lux-mcp

# CLI
cargo build --release -p lux-cli
# Output: target/release/lux

# LSP server (editor integration)
cargo build --release -p lux-lsp
# Output: target/release/lux-lsp

# Run all Lux tests
cargo test -p lux-lang -p lux-rt -p lux-wasm -p lux-runtime

Crate dependency graph

lux-lang          (parser, AST, printer, diagnostics)
  |
  +-- lux-runtime (interpreter, energy tracking)
  +-- lux-lift    (foreign code -> Lux conversion)
  +-- lux-wasm    (browser bindings via wasm-bindgen)
  +-- lux-mcp     (MCP server, uses lux-runtime + lux-lift)
  +-- lux-cli     (command-line interface)
  +-- lux-lsp     (Language Server Protocol)
  |
lux-rt            (C-ABI runtime for AOT/JIT compiled programs)
  +-- lux-codegen (Cranelift code generation, links lux-rt symbols)

11. Web Application Reference

The sections above cover embedding Lux. This section covers building apps in Lux — the reactive surface that runs in the browser engine (lux-eval-core), under lux dev, and compiled with lux build. See also REACTIVITY.md, COMPONENTS.md, and FORMS-DATA-ROUTING.md.

11.1 Signals — reactive state

count = signal(0)              # a reactive cell

Read it in a view binding ({count}) and that binding tracks it; mutate it with count.set(v) / count.update(f) (or count = v for a let mut). Any expression over a signal is reactive — including method calls and field access: {count * 2}, {items.len()}, {user.name} all re-render when their signals change (a signal derefs to its value before the method/field).

11.2 Memos — derived state

total = memo(price * qty)      # re-tracks price & qty
tax   = memo(total * 0.08)     # memos chain

memo(expr) is live derived state (Solid createMemo / Vue computed / Svelte $derived). A plain let x = price * qty is a one-time snapshot.

11.3 Effects — dependency-tracked side effects

effect:
    print("count is {count}")  # re-runs only when count changes

An effect: block re-runs after a state change only when a signal it reads changed value. Effects work app-level and per component instance; object & array dependencies are compared by structural snapshot; a component effect that reads a prop tracks the caller’s signal.

11.4 Two-way binding & reactive class/style

input value="{name}"           # typing writes name; setting name updates the field
input checked="{on}"           # same, for checkboxes
text "Hi {name}" class="{cls}" # reactive class — updates className in place
div style="{boxStyle}":        # reactive style — updates inline style, base props kept

11.5 Components

component Counter(label):
    let mut n = signal(0)       # per-instance state
    view:
        button on:click -> n = n + 1 "{label}: {n}"

Components take props, project caller children via slot, accept callback props, and keep isolated per-instance reactive state. Signal-backed props stay reactive inside the component. See COMPONENTS.md.

11.6 Forms

form on:submit -> greeting.set("Hello " + name):
    input value="{name}"
    button "Greet" type="submit"

on:submit is handled in-page — the default full-page navigation is prevented automatically, so state survives. Fields already wrote their signals via two-way binding, so the handler just reads them.

11.7 Data loading

effect:
    fetch_signal("/api/search?q=" + query, "results")
BuiltinEffect
fetch_signal(url, name)GET url, parse JSON (else text), write name
post_signal(url, name, body)POST a JSON body, write name
put_signal / patch_signal / delete_signalthe matching write verbs

Each also writes name + "_loading" (bool) and name + "_error" (string) signals. Inside an effect: the call re-fetches whenever a signal in the URL changes — the effect’s dependency tracking drives reactive data-loading.

11.8 Routing

route = signal("/")
view:
    a href="/about" "About"             # intercepted: history push, no reload
    button "Go" on:click -> navigate("/about")  # programmatic
    if route == "/about":
        text "About page"

Declare a route signal and it stays in sync with the URL (back/forward via popstate too). route is an ordinary signal, so if route == "…": and any expression over it drive conditional rendering.

11.9 CLI build targets

CommandOutput
lux devlive dev server with hot state
lux build --target jsinteractive HTML page + the client runtime
lux build --target htmlstatic prerender (no client runtime)
lux build --target wasmcompiled WebAssembly module
lux build --target flowg-wasmapp lowered through the flowG dataflow graph to wasm
lux run <file>run a program through the interpreter

Reactivity behaves identically across the browser engine, lux dev, and the compiled targets.