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:
| Interface | Crate / Package | Use case |
|---|---|---|
| Rust library | lux-lang | Parse, analyze, transform, emit .lux source |
| C FFI | lux-rt | Link compiled Lux programs against the runtime |
| Python | lux-py (PyO3) | Scripting, notebooks, data pipelines |
| Go | lux-go (CGo) | Backend services, CLI tools |
| TypeScript/Node | @openie/lux (NAPI-RS) | Build tooling, editor extensions |
| Julia | Lux.jl (ccall) | Scientific computing, energy research |
| WASM | @openie/lux-wasm | Browser playground, in-page tooling |
| MCP | lux-mcp | AI 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:
| Variant | Description |
|---|---|
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:
| Variant | Example in Lux |
|---|---|
Int(i64) | 42 |
Float(f64) | 3.14 |
Str(String) | "hello" |
Interpolated(Vec<StringPart>) | "Count: {n}" |
Bool(bool) | true |
None | none |
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:
| Variant | Example 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:
| Variant | When |
|---|---|
LuxError::Parse { line, col, message } | Syntax error at a known location |
LuxError::UnexpectedEof | Source 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, andrun. Foreign-code lifting/auditing is available through theluxCLI (lux lift/lux audit), not the Python binding. Note thatrun’senergy_ujis 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
| Tool | Description | Required args |
|---|---|---|
lux_run | Execute Lux code, return result + energy receipt | code: string |
lux_check | Parse and validate Lux code, return AST summary | code: string |
lux_audit | Energy audit on foreign code with optimization recs | code: string, language: string |
lux_lift | Convert foreign code to Lux syntax | code: string, language: string |
lux_modules | List standard library modules, optional filter | query?: string |
lux_docs | Get docs for a Lux language construct | topic: 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
}
| Field | Type | Description |
|---|---|---|
total_uj | u64 | Total energy consumed in microjoules |
compute_uj | u64 | CPU/compute portion |
io_uj | u64 | File, network, and system call portion |
wall_us | u64 | Wall-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")
| Builtin | Effect |
|---|---|
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_signal | the 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
| Command | Output |
|---|---|
lux dev | live dev server with hot state |
lux build --target js | interactive HTML page + the client runtime |
lux build --target html | static prerender (no client runtime) |
lux build --target wasm | compiled WebAssembly module |
lux build --target flowg-wasm | app 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.