LeanRustLisp (LRL) is an experimental language that combines:
- dependent types and proof-aware checking
- ownership and borrow checking
- hygienic Lisp-style macros
LRL is currently in alpha. The compiler pipeline is usable, but the language surface, prelude, and backend coverage are still evolving.
Implemented in the current tree:
- Workspace crates:
kernel,frontend,mir,codegen,cli - End-to-end pipeline: parse -> macro expansion -> elaboration -> kernel checks -> MIR lowering -> MIR typing/ownership/NLL checks -> Rust codegen
- CLI modes:
- interpret a file
- interactive REPL
- compile to native binary (
typed,dynamic,autobackend modes)
- Macro tooling (
--expand-1,--expand-full,--trace-expand,--trace-macros) - Axiom tracking and opt-in execution for axiom-dependent definitions
- Definitional equality fuel control (
--defeq-fuel,LRL_DEFEQ_FUEL)
Alpha constraints you should expect:
- Syntax is not fully frozen yet; follow the syntax contract doc linked below.
compileandcompile-mirdefault to--backend auto(typed-first, fallback to dynamic with warning when needed).compile-miris a legacy name; it still runs the full compile pipeline and emits a binary.- Prelude loading uses a shared API + backend platform layers:
stdlib/prelude_api.lrl+stdlib/prelude_impl_dynamic.lrl(dynamic / interpreter / REPL)stdlib/prelude_api.lrl+stdlib/prelude_impl_typed.lrl(typed / auto compile)
- You may see startup warnings from prelude axiom/primitive declarations in alpha workflows.
- Milestone-1 core surface is present in shared stdlib modules:
- core: Nat/Bool helpers
- data: List/Option/Result/Pair basics
- control: minimal
Comphelpers (comp_pure,comp_bind)
- Prelude loading now includes shared core/data stdlib modules plus backend platform layer:
- typed/auto:
prelude_api + std/core(nat,bool) + std/data(list,option,result,pair) + prelude_impl_typed - dynamic:
prelude_api + std/core(nat,bool) + std/data(list,option,result,pair) + prelude_impl_dynamic
- typed/auto:
std/control/comp.lrlexists as an alpha helper module but is not in the default prelude stack yet.- Current backend caveat:
- typed backend is the primary target for Option/Result/Pair alpha smoke workflows
- Option/Result helper behavior is currently guaranteed only for the smoke-tested positive paths
- dynamic overlap remains conservative (Nat/Bool/List-first)
- For prebuilt CLI binaries: no repo build step is required.
- For
compile/compile-mir: Rust (rustc) is still required. - For building from source: Rust toolchain (
cargo,rustc).
This repository now includes a host-target package under:
dist/aarch64-apple-darwin/
The package layout is:
cli(prebuilt executable)stdlib/(runtime prelude + stdlib files)
Run from inside the package directory so relative stdlib/... paths resolve:
cd dist/aarch64-apple-darwin
./cli --help
./cli run /absolute/path/to/program.lrl --backend typedIf you use compile or compile-mir from that binary, rustc must be installed because those commands invoke Rust compilation.
GitHub Actions now builds native release binaries with a matrix on:
ubuntu-latestwindows-latestmacos-latest
Workflow file:
.github/workflows/native-binaries.yml
How to trigger:
- Manual run: GitHub -> Actions ->
Native Binary Packages->Run workflow - Release-tag run:
git tag v0.1.0
git push origin v0.1.0Where binaries are published:
- Manual runs: workflow artifacts named
leanrustlisp-<target-triple> - Tag runs: GitHub Release assets (
*.tar.gzbundles per target)
Windows package note: executable name is cli.exe; Linux/macOS use cli.
cargo buildcargo run -p cli -- run tests/simple_test.lrlFor visible print side effects, use typed backend:
cargo run -p cli -- run tests/hello.lrl --backend typedNotes:
rundefaults to--backend dynamic.--backend typedforrunis currently supported for direct.lrlfile targets.- Program entry is a top-level definition (the last deployed
def), for example:
(def main Text
(print "Hello, world!"))A bare top-level expression like (print "Hello, world!") is not used as executable entry.
cargo run -p cli -- tests/simple_test.lrlcargo run -p cli --Inside REPL, run :help to list commands (:load, :type, :eval, :expand-*, :panic-free, :axioms, ...).
cargo run -p cli -- compile tests/simple_test.lrl -o build/simple_test_bin
./build/simple_test_bin# Default (typed-first, dynamic fallback)
cargo run -p cli -- compile tests/simple_test.lrl --backend auto -o build/out_auto
# Force typed backend
cargo run -p cli -- compile tests/simple_test.lrl --backend typed -o build/out_typed
# Force dynamic backend
cargo run -p cli -- compile tests/simple_test.lrl --backend dynamic -o build/out_dynamicUse --backend auto for normal alpha usage. The dynamic path remains a fallback/debug backend and can still differ from typed behavior outside the documented overlap subset.
cargo run -p cli -- compile-mir tests/simple_test.lrl
./outputcompile-mir currently compiles and emits ./output by default (unless you use other compile entry points with -o).
--expand-1,--expand-full,--trace-expand: inspect macro expansion for a file--trace-macros: print macro expansion trace during normal processing--panic-free: reject panic-requiring paths (including interior mutability/runtime borrow checks)--macro-boundary-warn: downgrade macro-boundary violations to warnings (user code)--require-axiom-tags: require axiom tags (classical/unsafe)--allow-axioms: allow running/compiling axiom-dependent definitions--allow-redefine: disable prelude freeze guard (unsafe)--defeq-fuel N: override definitional equality fuel for this run
Example:
LRL_DEFEQ_FUEL=200000 cargo run -p cli -- tests/simple_test.lrl
cargo run -p cli -- --defeq-fuel 200000 tests/simple_test.lrlcargo test --all
cargo test -p cli --test pipeline_golden
cargo test -p cli --test pipeline_negative
cargo test -p cli --test typed_backend
cargo test -p cli --test backend_conformance -- --test-threads=1Snapshot-based suites:
INSTA_UPDATE=always cargo test -p frontend
INSTA_UPDATE=always cargo test -p clikernel/: core calculus, type checker, definitional equality, kernel invariantsfrontend/: parser, macro expander, desugar/elaborator, diagnosticsmir/: MIR, typing/ownership/NLL checks, transforms, codegen layerscodegen/: backend support cratecli/: REPL, compile driver, command-line entrypointstdlib/: prelude API/platform files (prelude_api.lrl,prelude_impl_dynamic.lrl,prelude_impl_typed.lrl)tests/:.lrlprograms for integration/regressioncli/tests/: pipeline, backend, diagnostics, and conformance tests
Language and pipeline:
- Syntax contract (draft):
docs/spec/syntax_contract_0_1.md - Kernel boundary / trust model:
docs/spec/kernel_boundary.md - Core calculus:
docs/spec/core_calculus.md - Ownership model:
docs/spec/ownership_model.md - Function kinds (
Fn/FnMut/FnOnce):docs/spec/function_kinds.md - Macro system:
docs/spec/macro_system.md - Phase boundaries:
docs/spec/phase_boundaries.md - Mode boundaries (Total/Partial/Unsafe):
docs/spec/mode_boundaries.md
MIR and backend:
- MIR overview:
docs/spec/mir/mir.md - MIR borrows/regions:
docs/spec/mir/borrows-regions.md - MIR NLL constraints:
docs/spec/mir/nll-constraints.md - MIR typing:
docs/spec/mir/typing.md - Typed backend spec:
docs/spec/codegen/typed-backend.md - Prelude API contract:
docs/spec/prelude_api.md - Architecture notes:
docs/dev/architecture.md
Diagnostics and examples:
- Diagnostic codes:
docs/diagnostic_codes.md - Example programs:
code_examples/ - CLI integration programs:
tests/
LeanRustLisp (LRL) is an attempt to make three things stop fighting and start cooperating: mathematical truth (Lean), resource reality (Rust), and programmable language (Lisp). The result is a language where you can write high-performance systems code, state the invariants it must satisfy, and then have the compiler refuse to build anything that doesn’t cash those checks.
The basic slogan is: your program is a contract. And LRL’s compiler is a very literal lawyer who insists the contract is internally consistent.
LRL has a small trusted kernel that checks a minimal core calculus (dependent types, universes, inductives). Everything else—parsing, macros, elaboration, borrow checking, optimizations, code generation—is outside the trust boundary. That split matters because it keeps the “truth engine” tiny enough to audit, and it means the fancy parts of the compiler can’t smuggle in unsoundness: they must output core terms the kernel can verify. If you’ve ever watched a compiler become an unreviewable cathedral, you know why this is a civilizational design constraint.
From Lean, LRL takes the idea that types can express theorems. A function’s signature can encode precise invariants: non-empty vectors, dimension-safe matrix multiplication, protocol states, resource budgets. And proofs are not runtime baggage. Proof terms live in Prop and are erased, so you can ship fast binaries that still carry compile-time correctness guarantees. You you can prove sortedness and permutation, then erase the proof and keep the speed.
From Rust, LRL takes the stance that resources are real. Memory, aliasing, mutation, and concurrency are the default failure modes of ambitious software. LRL uses an explicit ownership and borrowing discipline, checked on a dedicated mid-level IR, so safe code can’t create use-after-free, double-free, or data races. The key point is that this safety is part of the language’s semantics, not an accident of one backend. You can transpile to Rust today and target LLVM tomorrow without changing what “safe LRL” means.
From Lisp, LRL takes the refusal to freeze the language. The syntax is designed to be transformed. Macros are first-class: you can define new surface constructs, new notation, tiny DSLs, and new proof automation. But unlike the “wild west” macro cultures of history, LRL aims for hygienic, staged, deterministic macro expansion with good tooling. You get to bend the language without breaking scoping, without breaking error spans, and without turning compilation into a non-reproducible séance.
One of the most important conceptual moves in LRL is the separation of worlds. There is a total fragment—definitions that are allowed to influence types and definitional equality—and a computational fragment—general recursion, IO, allocation, concurrency—that lives under explicit effects. That keeps typechecking decidable and the kernel sane, while still letting you write real programs. The point is to avoid the classic trap: letting “ordinary computation” leak into the logic until the logic becomes a runtime and the runtime becomes a logic and nobody can prove anything without summoning a demon.
Effects are not invisible. In LRL, doing IO shows up in the type. That makes codebases easier to reason about and harder to accidentally corrupt. Also, effects can be capabilities: you hold a token that can be tracked, passed, and restricted. The same machinery can express budgets: fuel, time, memory. At that point, “this computation cannot exceed N steps” stops being a comment and becomes a type-level fact.
What does this buy you in practice? It collapses entire classes of bugs from “things you find in production” into “programs you cannot write.” Out-of-bounds access becomes “type mismatch.” Dangling references become “lifetime violation.” Wrong dimension math becomes “doesn’t compile.” Race conditions become “need a safe concurrency primitive or explicit unsafe.” And the language lets you raise the bar progressively: start with simple static guarantees, then move toward proof-carrying components where it’s worth it.
LRL is also, intentionally, not pretending that safety means “no sharp edges exist.” It means sharp edges are labeled. There is unsafe, and it is explicit. If you cross the boundary into raw pointers or FFI, you’re making a local treaty with chaos, and the compiler will not let you forget where you did that. This is a philosophy as much as a feature: constrain unsafety to small, auditable regions rather than letting it leak everywhere through “temporary hacks” that become permanent.
If Rust is “systems programming without fear,” LRL is “systems programming without fear, plus the option to prove you didn’t lie to yourself.” If Lean is “proofs that compile,” LRL is “proofs that compile into performant programs.” If Lisp is “a language that grows new limbs,” LRL is “a language that grows new limbs and then proves the surgery didn’t break physics.”
Dual licensed under Apache-2.0 OR MIT (LICENSE).