The Lucid language¶
Motivation¶
Lucid is a language for LLMs: a Python-like language sketch designed to be as easy for an LLM to read and write correctly as for a human. The two goals overlap far more than they conflict — an LLM benefits from the same explicitness, predictability, and freedom from hidden state that make code readable to a person, so most of what follows serves both audiences at once. But the two audiences are not identical, and where they pull apart, this spec picks a side deliberately rather than papering over the conflict.
Typing cost is one place they diverge. A human pays, in effort felt at the
keyboard, for every extra character a call site needs; an LLM, or a linter
inserting the same text mechanically, does not. Anonymous
functions's explicit def: expr for a lazy
argument is one consequence: a dedicated auto-thunking parameter would save
a human a few keystrokes, but since the one writing def: out is usually
an LLM or a linter, there is no keystroke cost left to design sugar around.
Naming consistency is another. An LLM has no entrenched habit to offend by
capitalizing str and int the same way Bytes and
ByteArray already are; a human reader does, from decades of
exactly this spelling elsewhere. Lucid keeps bool/int/float/complex/str
lowercase for that reason alone — a concession to the human reader this
spec would have no cause to make if it optimized for an LLM audience only.
Guiding principles¶
Four properties this spec keeps returning to, each one a standard humans already ask of readable code, held here without exception:
Succinct¶
A simple idea is written simply, with nothing carried along out of habit.
- no repeated boilerplate for one job
- Python's
*args,**kwargs, and aParamSpecto forward them typed collapse into one gathered value:***rest: Arguments[str, dict[str, str]]
- Python's
- no cost for the common case
- mutable by default, no
~; visible by default, only a leading_costs anything
- mutable by default, no
- no separate machinery for what the grammar already expresses
functools.partial(score, weights)becomesscore(weights, _)— an ordinary call, with a hole
Clear¶
Nothing about a piece of code's behavior depends on something declared elsewhere the reader never saw.
- no second way to spell the same thing
tuple,namedtuple,dataclass, and a plain class all do the same job in Python; Lucid keeps one — a dataclass, spelled with justclass
- no action at a distance
- no
__getattr__, no descriptors, no metaclasses — attribute access can't be intercepted by code declared somewhere else entirely
- no
- no operator that means two different things
&means only intersection;~marks a read-only view, never&
- no well-known footgun to dodge
assert (x == y, "message")is always true in Python — Lucid'sassertrequires those parens, so there is no bare form left to mis-parenthesize
Checked¶
A mistake is caught where it is made, not learned three calls later at runtime.
- no silent escape hatch out of the type system
- there is no
Any;objectaccepts anything but permits only whatobjectitself promises, until it's narrowed back down
- there is no
- exhaustive matches
- leaving a case out of a
matchover a closed union is a compile-time error, not a silent no-op
- leaving a case out of a
- class shapes closed by default
p.z = 3.0on a class with no declaredzfield is a compile-time error, not a new attribute
- private names enforced rather than merely requested
cache._entriesfrom outsideCacheis a compile-time error — Python's leading underscore is only a convention nothing checks
Capable¶
None of the above is bought by cutting scope, so "easy to get right" never has to mean "too small to use."
- generics carry real, checked variance, not just erased type parameters
trait Cache[K, V]— the checker derivesK's andV's variance from how each is actually used, not a call-site-only or fully erased scheme
- binary operators dispatch on both operand types
a + bpicks an implementation from both operands' types at once — no__radd__, noNotImplementednegotiation
- errors stay structured, not reduced to a bare boolean or
None- a recoverable failure is an ordinary return type, checked
exhaustively;
raisestays for what should never happen
- a recoverable failure is an ordinary return type, checked
exhaustively;
Example¶
trait Scorable[K]:
def score(self, item: K) -> float
def is_confident(self, item: K) -> bool:
return self.score(item) >= 0.8
class InferenceModel[K](Scorable[K]):
weights: Tensor
labels: list[K]
_scores: dict[K, float]
factory from_checkpoint(cls, path: Path, labels: list[K]):
weights = Tensor.load(path)
return construct(weights, labels, {:})
def score(self, item: K) -> float:
if item not in self._scores:
self._scores[item] = self.weights.dot(encode(item))
return self._scores[item]
getter label_count(self) -> int:
return len(self.labels)
def evaluate(model: InferenceModel[str], item: str) -> float:
return model.score(item)
model: InferenceModel[str] = InferenceModel.from_checkpoint("model.bin", ["cat", "dog"])
stable: !InferenceModel[str] = freeze(model)
view: ~InferenceModel[str] = model
view.label_count # fine: a getter is read-only by construction
view.score("cat") # error: score mutates, view is read-only
_scoresis private to the class; everything else here is visible project-wide with no keyword needed- a trait member with no body is a requirement, one with a body is reusable behavior — no marker keyword needed for either
- stored fields are declared in the class body
- factories construct exact, fully initialized objects
- getters expose computed attributes without descriptors, attribute access stays structural rather than programmable through hooks
- mutable, read-only, and immutable views are visible in annotations