What Lucid does, and what it costs.
Every capability here runs in the compiler today, and each one lists its limits.
Readable by default
Syntax docsA statement ends at the end of its line, and a block is indented by exactly four spaces. There are no semicolons or braces around blocks, and no truthiness: a condition must be a
bool.Each common job has one form. Methods are tasks whose first parameter is
self, with no classes orimplblocks.lucid fmtputs every file in one layout, with no settings, so a diff shows only meaning.readable.lcd type User name: text age: int = 0 task is_adult(self: User) -> bool return self.age >= 18 let ada = User(name: "Ada", age: 36) if ada.is_adult() print "{ada.name} may enter"Inferred ownership, value semantics
var b = agivesbits own logical value. There are no stored references, so nothing can dangle, and no lifetime annotations exist.The compiler keeps logical copies cheap: a value used for the last time is moved, a shared buffer is counted, and a write copies once only if the buffer is shared. Parameters are read-only views, so passing a value never copies it.
- Limits
- Heavily shared data pays for reference counting.
lucid check --costslists every copy and count site, so you can see the cost.
values.lcd var a = [1, 2, 3] var b = a # b is its own value b.push(4) print a # [1, 2, 3] print b # [1, 2, 3, 4]Memory safety without a garbage collector
Memory is freed by reference counts that the compiler places, so there is no collector and no pause. Absence is
none, and aT?must be narrowed before use.An out-of-bounds index, a missing map key, overflow, or a zero divisor is a trap: a controlled stop with the source position and a stack trace, never silent corruption.
- Limits
- A reference cycle leaks unless it uses
weak, which is not built yet. Inside anunsafeblock, the guarantees are the author’s job.
safety.lcd task find(names: list[text], wanted: text) -> int? for i in 0..names.len() if names[i] == wanted return i return none let index = find(["Ada", "Linus"], "Linus") if index is not none print "found at {index}" # index is an int hereNative code through Cranelift
There is no interpreter.
lucid runcompiles the program to machine code in memory with Cranelift and runs it.lucid buildwrites the same code to a standalone program, with the runtime linked in.Today
lucid buildmakes a Windows x86-64.exe. Linux x86-64 is the next step of M8.- Limits
- Cranelift compiles fast but optimizes less than LLVM, so some tight numeric loops run slower than in Rust or C++.
lucid run hello.lcd Hello, world lucid build hello.lcd ./hello.exe Hello, worldWebAssembly for the browser
The same program compiles to WebAssembly, with its runtime, in a single
.jsfile that loads both modules. A page includes it with a<script>tag.std.dombuilds interactive pages in the Elm architecture: a model, aviewtask, and anupdatetask that turns each event into a new model.- Limits
- On the web,
spawn,std.net,std.http, and foreign calls are compile errors, since a page has no threads, sockets, or C library.
counter.lcd use std.dom choice Message Increment Decrement task update(count: int, message: Message) -> int return match message Increment -> count + 1 Decrement -> count - 1 task button(label: text, message: Message) -> dom.Node[Message] return dom.Element(tag: "button", attrs: [dom.On(event: "click", message: _ => message)], children: [dom.Text(content: label)]) task view(count: int) -> dom.Node[Message] return dom.Element(tag: "p", children: [button("-", Decrement), dom.Text(content: " {count} "), button("+", Increment)]) dom.app(0, view, update)Explicit errors, mutation, and concurrency
Error handling docsThere are no exceptions. A task that can fail returns a union with an error type, and an error that nothing uses does not compile.
trypasses it up,elsegives a fallback, andmatchhandles each case.A value sent to another thread is a copy, so safe code has no data races. Threads share state only through a
channelor alock, and when every thread waits, the program traps with a deadlock message.explicit.lcd use std.fs task load(path: text) -> text or fs.FileError let content = try fs.read_text(path) # try: may return the error return content.trim() task bump(change total: int, by: int) total += by task square(n: int) -> int return n * n var total = 0 bump(change total, 3) # change: marked at the call let job = spawn square(total) # spawn: runs on a new thread print job.wait() print load("missing.txt") else "no file" # else: the error is handledDiagnostics and tools for agents
Errors come with stable codes that never change meaning.
lucid explainprints the cause, an example, and a fix for each one, andlucid fixapplies the fixes the compiler is sure of.lucid contextprints the public API of a module with its doc comments, and a single guide holds everything that runs today, sized to fit in an agent’s context.- Limits
- Agent-friendliness is not proven. Models have no Lucid training data, so the guide and the diagnostics carry the whole load.
lucid check typo.lcd typo.lcd:2:7: error E0301: name `totl` is not defined fix: rename to `total` lucid fix typo.lcd typo.lcd:2:7: fixed E0301: rename to `total`