Ownership and memory
Lucid infers ownership. You write no lifetimes, and there is no garbage collector. It combines three known ideas: mutable value semantics, as in Swift and Hylo; compile-time reference counting, as in Koka and Lean; and the law of exclusivity, as in Swift and Rust.
The rules
Section titled “The rules”- Every value is independent.
var b = agivesbits own logical value. Changingbnever changesa. - There are no stored references. A field, element, or variable holds a value, not a pointer to one. Nothing can dangle.
- Parameters are read-only views by default. Passing a value never copies it.
changegives exclusive access. The task may change the caller’s variable in place, and nothing else can touch it during the call.takegives the task its own value, which it may change. The caller writes nothing at the call, and keeps its own value.
task add_all(take items: list[int], extra: int) -> list[int] items.push(extra) return items
let numbers = [1, 2, 3]let more = add_all(numbers, 4) # last use of numbers: moved, changed in placeprint moreWhat the compiler does
Section titled “What the compiler does”Logical copies are almost never real copies.
| Code | What happens |
|---|---|
| A value used for the last time | Moved, at no cost |
| A heap value used again later | Its count goes up, and the buffer is shared |
| A write to a buffer held once | In place |
| A write to a shared buffer | One copy, then in place |
The program is correct either way. Only its cost changes, and lucid check --costs lists each copy and count site:
lucid check --costs main.lcdExclusivity
Section titled “Exclusivity”While a value is being changed, nothing else may use it. The compiler rejects each of these with error E0326:
swap(change a, change a) # two exclusive uses of af(change a, a) # a is read while it changes
for item in items items.push(item) # items changes while it is visitedSo no code can observe a value halfway through a change, and no iterator can be invalidated.
Threads
Section titled “Threads”A value sent to another thread is a copy, and threads share state only through a channel or a lock. So safe code has no data races. See Concurrency.
Limits
Section titled “Limits”- Heavily shared data pays for reference counting, which hand-tuned Rust can avoid.
- A reference cycle needs
sharedandweak, which parse but are not built yet. Without them, a design that needs a graph uses indexes into a list. - Every
typeandchoicevalue is a counted heap record for now. Storing small types inline is not planned yet.