In this section

This guide shows how to build a Lean executable, one with a main, as a Rust binary.

Generate the program

The module that defines main must be a root module. A Lake executable that is a default target already makes its root one, so a build of the package’s default targets needs no configuration. main may have any of Lean’s forms: IO Unit, IO UInt32, List String → IO Unit, or List String → IO UInt32.

// build.rs
fn main() -> lungo_build::Result<()> {
    lungo_build::configure().root_module("Tool.Main").name("program").compile_lean("lean")
}

The generated module then has __lean_main() and __lean_main_with(args).

Call it

// src/main.rs
mod program {
    lungo::include_lean!("program");
}

fn main() {
    std::process::exit(program::__lean_main());
}

__lean_main() passes the process arguments (without the program name) to Lean’s main, runs it on a thread with the stack size Lean programs expect, and returns the exit code: the UInt32 Lean returned, 0 for Unit, or 1 after printing an uncaught IO error as Lean does (uncaught exception: …).

To pass other arguments, call program::__lean_main_with(vec!["--flag".into()]).

Several programs in one binary

Give each program its own name:

// build.rs
fn main() -> lungo_build::Result<()> {
    for (name, root) in [("fmt", "Tool.Fmt"), ("check", "Tool.Check")] {
        lungo_build::configure().root_module(root).name(name).compile_lean("lean")?;
    }
    Ok(())
}

and dispatch on it:

// src/main.rs
mod fmt {
    lungo::include_lean!("fmt");
}
mod check {
    lungo::include_lean!("check");
}

fn main() {
    let mut args = std::env::args().skip(1);
    let code = match args.next().as_deref() {
        Some("fmt") => fmt::__lean_main_with(args.collect()),
        Some("check") => check::__lean_main_with(args.collect()),
        _ => 2,
    };
    std::process::exit(code);
}

compiler-tests/runner in lungo-rs generates every executable of a Lake project this way.