What Lucid does, and what it costs.

Every capability here runs in the compiler today, and each one lists its limits.

  1. Readable by default

    A 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 or impl blocks. lucid fmt puts every file in one layout, with no settings, so a diff shows only meaning.

    Syntax docs
    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"
  2. Inferred ownership, value semantics

    var b = a gives b its 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 --costs lists every copy and count site, so you can see the cost.
    Ownership and memory docs
    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]
  3. 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 a T? 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 an unsafe block, the guarantees are the author’s job.
    Safety docs
    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 here
  4. Native code through Cranelift

    There is no interpreter. lucid run compiles the program to machine code in memory with Cranelift and runs it. lucid build writes the same code to a standalone program, with the runtime linked in.

    Today lucid build makes 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++.
    CLI reference docs
    lucid run hello.lcd
    Hello, world
    lucid build hello.lcd
    ./hello.exe
    Hello, world
    
  5. WebAssembly for the browser

    The same program compiles to WebAssembly, with its runtime, in a single .js file that loads both modules. A page includes it with a <script> tag.

    std.dom builds interactive pages in the Elm architecture: a model, a view task, and an update task 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.
    WebAssembly docs
    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)
  6. Explicit errors, mutation, and concurrency

    There are no exceptions. A task that can fail returns a union with an error type, and an error that nothing uses does not compile. try passes it up, else gives a fallback, and match handles each case.

    A value sent to another thread is a copy, so safe code has no data races. Threads share state only through a channel or a lock, and when every thread waits, the program traps with a deadlock message.

    Error handling docs
    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 handled
  7. Diagnostics and tools for agents

    Errors come with stable codes that never change meaning. lucid explain prints the cause, an example, and a fix for each one, and lucid fix applies the fixes the compiler is sure of.

    lucid context prints 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.
    Writing Lucid with agents docs
    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`