Command line
lithe compile file.lithe
lithe compile directory
lithe abi file.lithe
lithe abi directory
lithe abi-tree file.lithe
lithe footprint file.lithe
lithe analysis file.lithe
lithe errors file.lithe
lithe specs file.lithe
lithe prove file.lithe
lithe lint [--fix] path...
lithe facets file.lithe
lithe stubs file.lithe Module [Interface...]
lithe storage file.lithe
lithe storage old.lithe new.lithe
lithe upgrade-check old.lithe new.lithe
lithe upgrade 0xProxy file.lithe --migrate name [--impl 0xImpl] --args ...
lithe --standard-json
lithe lsp
lithe fmt path...
lithe fmt --check path...
lithe fmt -
lithe --versionCommands that read one file need a file with exactly one contract. Errors and warnings go to stderr as path:line:column: message, where path is the file the error is in, whether that is the file you named or one it imports, relative to the working directory. The standard library's files are std/.... An error that belongs to no one line, such as a contract too large to assemble, is path: message. The exit status is 0 on success, 1 on a compile error, and 2 on a usage error.
lib/fees.lithe:4:18: unknown name `rate`Build flags
compile, abi, facets, footprint, abi-tree, upgrade, and upgrade-check also take these flags, anywhere after the command. A project can set them in its lithe.toml, and a flag overrides the file:
| Flag | Default | Meaning |
|---|---|---|
--size-limit <bytes> | 24576 | the most runtime code the contract or any facet should have. Code over it still builds, with a warning that lists what takes the space and what marking each module delegated would save |
--create2-deployer <address> | 0x4e59b44847b379578588920cA78FbF26c0B4956C | the CREATE2 factory facets are deployed through |
--evm-version <prague|osaka> | osaka | the fork the code runs on. See Target fork |
What runs from a facet is the source's choice, use M delegated;, never a flag's. See Contract size limit.
Target fork
The default is osaka, the fork Ethereum mainnet has run since December 2025, and solc's default too. Osaka adds CLZ (EIP-7939), a one-byte count of leading zero bits. Uint.clz is that one opcode, and Math.log2, log256, sqrt, and cbrt start from it. Code built for Osaka fails with an invalid opcode on a chain that hasn't adopted it, which some L2s and test chains lag on.
For such a chain, build with --evm-version prague, or set evm-version = "prague" in the project's lithe.toml. Then those functions find the bit length by halving the range of bits instead, which costs 35 to 90 more bytes for each of them a contract uses. Before running the Osaka build on anvil, start anvil with --hardfork osaka. Its default fork is older.
Code reads the target as Evm.osaka.
Building
lithe compile file.lithe
Prints the contract's creation bytecode as 0x-prefixed hex, which is ready to deploy:
cast send --private-key $KEY --create $(lithe compile counter.lithe)A contract with delegated modules prints a JSON deployment plan instead: each facet's CREATE2 transaction, then the router's creation code. It also prints a note on stderr. A contract that creates one with facets prints a plan too, because those facets must exist first. See Deploying.
A contract or facet over the size limit still prints its bytecode or plan, and warns on stderr. See Over the limit.
lithe compile directory
Compiles every .lithe file under the directory and its subdirectories, but hidden and symlinked ones, so point it at your sources, such as src, rather than a project root that vendors dependencies under lib/. Each file builds with its nearest lithe.toml. Library files, which declare no contract, only modules, interfaces, and shared declarations, are skipped. It writes one Foundry-style artifact per contract to ./out/<file>/<Contract>.json:
{
"abi": [ ... ],
"bytecode": { "object": "0x...", "sourceMap": "", "linkReferences": {} },
"deployedBytecode": { "object": "0x...", ... },
"methodIdentifiers": { "inc()": "371303c0" },
"contractName": "Counter"
}A contract that builds immutables into its code has deployedBytecode.immutableReferences, as Solidity's: each immutable block's id, such as lithe.immutable, and the start and length of every place the creation code writes its values. An upgradeable implementation lists its own address as __self. Verifiers such as Sourcify need these to compare the deployed code with the build.
It prints each path it writes. It keeps going after a failing file, reports every failure, and exits 1 if any file failed. Files that define several contracts are fine in this mode. Two contract files with one name in different subdirectories, even differing only in case, would share ./out/<file>, so the second fails and writes nothing.
The artifact of a contract with facets has the whole contract's ABI and a facets field listing each facet's module, address, and the selectors the router forwards to it. Each facet is written once, to ./out/facets/<Module>.json, however many contracts use it. When contracts build one module into different facets, each is ./out/facets/<Module>-<address prefix>.json, the first 4 bytes of its address in lowercase hex, and a <Module>.json from an earlier build is removed. Size warnings go to stderr.
lithe compile --check-specs
Builds the contract to check its specs as it runs, for tests and fuzzing: a call that breaks one reverts with SpecViolation(string), which says which. It reads the contract's spec file, runs every module in the contract, delegated or not, and is bigger and costs more gas than the plain build. Don't deploy it. See Checking specs as the contract runs.
lithe abi file.lithe / lithe abi directory
Prints the Solidity-compatible JSON ABI: functions, events, and errors. Given a directory, it writes ABIs to ./out.
lithe --standard-json
Reads solc's standard JSON on stdin and writes the output to stdout, for verifiers and other tools that build from source text rather than a project directory. Nothing is read from disk, and no lithe.toml: settings takes the [build] options, camelCased, and compilationTarget picks the contracts to build, one name for each path:
{
"language": "Lithe",
"sources": {
"src/bank.lithe": { "content": "import \"./lib/ledger\";\n..." },
"src/lib/ledger.lithe": { "content": "module Ledger is { ... }" }
},
"settings": {
"evmVersion": "osaka",
"sizeLimit": 24576,
"create2Deployer": "0x4e59b44847b379578588920cA78FbF26c0B4956C",
"compilationTarget": { "src/bank.lithe": "Bank" }
}
}Imports resolve among sources by path, and std/... from the standard library. Without compilationTarget, every source that declares a contract is built, and library files are skipped. outputSelection is accepted and ignored, since every contract's whole output is written:
{
"errors": [ { "severity": "info", "type": "Info", "component": "general", "message": "...", "formattedMessage": "..." } ],
"contracts": {
"src/bank.lithe": {
"Bank": {
"abi": [ ... ],
"evm": {
"bytecode": { "object": "6080..." },
"deployedBytecode": { "object": "...", "immutableReferences": { "lithe.immutable": [ { "start": 42, "length": 20 } ] } },
"methodIdentifiers": { "deposit()": "d0e30db0" }
}
},
"Ledger": { ... }
}
}
}Bytecode is hex without 0x, as solc writes it. immutableReferences lists where the creation code writes each immutable block into the runtime code, by id; an upgradeable contract's own address is __self. A contract with delegated modules also lists each facet under the file's path, by module name. errors holds compile errors, warnings, and notes, each with severity error, warning, or info, and is left out when there are none. An unreadable input is a JSONError. The exit status is 0 whenever it writes output, as solc's is.
Inspecting
These commands summarize what each entrypoint does. See Inspecting contracts for their output.
| Command | Shows |
|---|---|
lithe footprint file.lithe | one line per entrypoint: reads, writes, events, deferred calls, errors, and reentrancy status |
lithe abi-tree file.lithe | each entrypoint's dispatch path, guards, calls, and effects |
lithe analysis file.lithe | the same facts as JSON, for tools |
lithe errors file.lithe | each entrypoint's errors, and each error's selector, fields, and raisers |
lithe prove file.lithe | proves the specs with Z3 against the bytecode the build emits, which takes the build's flags: each invariant phase by phase, and each claim of each function spec, with the source's verdict where it explains one; a file of modules is proved as a contract that exposes them all; needs Z3; --solver path, --timeout seconds, --code for deployed code, and --source for the source prover alone |
lithe specs file.lithe | the formal verification specs of the file and its .spec.lithe, checked, and what each claims |
lithe storage file.lithe | the storage and transient layout, slot by slot |
lithe facets file.lithe | the router and each facet: size against the limit, the entrypoints each runs, and the functions the router calls across |
lithe stubs file.lithe Module [Interface...] | the use Module exposing block that declares every external a module has, or the line that exposes the interfaces given, then the hooks those externals reach |
Upgrades
lithe storage old.lithe new.lithe / lithe upgrade-check old.lithe new.lithe
Compares the storage layouts of two versions field by field, and ends with compatible or incompatible. See Upgrades.
When either version has facets, upgrade-check also lists the facets the new version uses, and which of them the old version already has. See Upgrades.
lithe upgrade
lithe upgrade 0xProxy vault.lithe --migrate initSettings --impl 0xNewImpl --args 30 0xTreasuryPrints the to address and the calldata for upgradeToAndCall(newImpl, migrationCall), which runs the named migration with the given arguments. Send it with any wallet or tool:
cast send --private-key $KEY 0xProxy 0x4f1ef286...For a contract with facets, it prints the deployment plan with an upgrade entry holding the proxy's call. Deploy the facets and the router first. --impl is optional here: it defaults to the router's CREATE2 address from the plan.
Formatting
lithe fmt path...
Rewrites each file in place, in the one Lithe style, and prints the path of every file it changes. A directory is every .lithe file under it, outside hidden and symlinked directories. lithe fmt - reads source on stdin and writes the formatted source to stdout.
Line breaks you wrote stay, except inside a raises list, which is refilled so each line holds as many errors as fit. A line over 120 columns, or the width a lithe.toml sets, is broken at its outermost structure until it fits: a long header puts raises and the body on their own lines and one parameter per line; a bracket, record, or block puts one element per line; a long expression breaks before its loosest operators; a case puts one arm per line. Strings and comments are never broken, so a line holding a long one can stay over the width.
lithe fmt --check path... writes nothing. It prints each file that would change and exits 1 if any would, so a CI job can refuse unformatted code.
A lex error is path:line:column: message and exits 1. A usage error exits 2.
Linting
lithe lint reads each file, and every .lithe file under a directory, outside hidden and symlinked directories. Findings go to stderr as path:line:col: warning[rule]: message, then a hint line when there is one. The exit status is 0 when nothing is wrong, 1 when there is a finding or a compile error, and 2 on a usage error. Imports are loaded, but only the files you named are reported.
lithe lint --fix applies the fixes that cannot change behavior or bytecode, prints each file it changed, and reports whatever is left.
| Rule | Reports |
|---|---|
reentry | a deferred call can re-enter an entrypoint that is not nonreentrant |
stale-capture | a continuation reads a storage snapshot from an earlier phase |
unbounded-loop | a loop's trip count depends on storage and has no bound |
prefer-const | a var is never reassigned |
unused-binding | a local const or var, or a parameter of an internal function, is unused |
unused-private | a private function, type, error, or event is unused |
unused-import | the file names nothing through an import; what the imported file uses doesn't count |
unused-ignore | a lithe-ignore comment silences nothing, or names no rule |
Silencing a rule
A lithe-ignore comment names the rules it silences, then an optional reason after :. Written at the end of a line, it covers that line. On a line of its own, it covers the next one:
import "std/evm";
storage is record { n : uint; }
(* lithe-ignore reentry: the recipient is our own vault *)
external function pay(const to : address) is {
storage.n := 1;
defer Evm.transfer(to, 1wei);
}To silence a rule everywhere, list it in the project's lithe.toml:
[lint]
allow = ["reentry", "stale-capture"]lithe compile honors both: a warning that is silenced for lint is silenced when compiling too.
Editors
lithe lsp
Runs a language server on stdin and stdout. As you type, it reports the first error, for each contract in the file, and what lithe lint finds, honoring lithe-ignore comments you have not saved yet. A finding lithe lint --fix can fix is offered as a quick fix, and so is the import a name needs when another file or the standard library declares it. Formatting the document formats it as lithe fmt does. Imports resolve from the file's saved location. An error inside an imported file is shown on the import that brings the file in, or on the first import when it comes in through another file, with the imported file's path, line, and column in the message. The VS Code extension and the Recur bundle in extensions/ run it, and find lithe on your PATH. In VS Code, the lithe.path setting points to a different binary.
It also answers from the last version of the file that checked, so answers survive a half-typed line:
| Request | Answers |
|---|---|
| Hover | an entrypoint's selectors and footprint, and those of each entrypoint it dispatches to; a function's effects, errors, and the entrypoints that reach it; a stored field's slot and the entrypoints that read and write it; an error's selector and raisers; an event's topic and emitters; an interface method's selector; an interface named by use M exposing, with the entrypoints it exposes; a module's external, with the entrypoints that expose it; a built-in's declaration in the standard library, with its documentation |
| Go to definition | where a name is declared, in the file, an import, or the standard library, a built-in included; the module an import path names |
| References and highlights | every use of a name |
| Outline | the file's declarations, with fields, variants, and methods under them |
| Completion | what follows storage., a record, an interface value, a module, or an import alias; events after emit, errors after raises, types after :, every arm of a case, and the names the file's imports bring into scope; module paths inside an import string |
| Signature help | the parameters of the function, method, or built-in being called |
Version
lithe --version
Prints the compiler's version, such as lithe 0.1.0+commit.207ff3c1, and exits 0. The part after + is the git commit the compiler was built from, with .mod after it when the compiler's sources had uncommitted changes. A compiler built outside a git checkout prints just lithe 0.1.0. -V is the same.
Releases
Each release is tagged vX.Y.Z and publishes a compiler for Linux (x86-64 and ARM64, statically linked) and macOS (Apple silicon and Intel) at https://github.com/celestiaorg/lithe/releases/download/vX.Y.Z/lithe-<target>, with a SHA256SUMS file. Every target compiles the same source to the same bytes, and releases are kept forever and never replaced.
Deploy contracts built with a released compiler. Source verifiers download the exact version to rebuild a contract and compare the bytecode, and a compiler built from an unreleased commit, or one whose version ends in .mod, can't be downloaded.
Warnings
The compiler warns, but still compiles, when an entrypoint's deferred calls could let another entrypoint re-enter the contract before the transaction finishes:
shop.lithe:7:18: warning: `pay` performs deferred external operations and may
be re-entered through `pay`
consider `nonreentrant` if the protocol requires serialized
external-effect executionSee Reentrancy. To silence a warning, see Silencing a rule.

