Functions
There is one function form. Keywords in front of function set its visibility and what it is allowed to do:
[private] [external] [payable] [nonreentrant] [view | pure] function name(params) [: type] [raises errors] is bodyKinds
| Declaration | Visible | May do |
|---|---|---|
pure function | internal | compute from its arguments |
view function | internal | also read storage, read Evm context, query |
function | internal | also write storage, emit, defer |
external pure function | ABI (pure) | like pure function |
external view function | ABI (view) | like view function |
external function | ABI (nonpayable) | like function |
external payable function | ABI (payable) | also accept ether and read Evm.value() |
external nonreentrant function | ABI | holds the reentrancy lock |
Two declarations are not functions and run once each: deploy, when the contract is created, and migrate, inside an upgrade. Two more run for calls that name no entrypoint: receive and fallback.
The compiler checks each body against its declared kind, including everything the body calls:
storage is record {
count : uint;
}
external view function bad() : uint is {
storage.count := 1
} with storage.countdeclared effect is weaker than the body's: external view function `bad` writes storageimport "std/evm";
pure function who() : address is
Evm.sender()declared effect is weaker than the body's: pure function `who` reads `Evm.sender`A view that calls a stateful function, a pure function that calls a view, and a view that emits or defers are all rejected the same way.
Keywords must appear in this order: external, then payable and/or nonreentrant, then view or pure. payable and nonreentrant apply only to stateful external functions. The compiler says what it expected:
external payable view function go() : uint is
0payable functions must be statefulParameters
Parameters are separated by ;. Each is const (a value that never changes), var (a value reassignable within the body), or storage (a reference to storage the function may write):
storage is record {
n : uint;
}
pure function clamp(var x : uint; const lo : uint; const hi : uint) : uint is {
if x < lo then { x := lo; }
if x > hi then { x := hi; }
} with x
external function set(const x : uint) is {
storage.n := clamp(x, 1, 100);
}Arguments are separated by ,.
A storage parameter is passed a storage path, and the function reads and writes that storage in place. A map, or a record holding one, lives only in storage, so it is always a storage parameter. Other types can be either: const xs : list(uint) is a copy, storage xs : list(uint) is the stored list itself. Only internal functions take storage parameters. The writes show up in the calling entrypoint's footprint:
storage is record {
balances : map(address, uint);
total : uint;
}
function credit(storage ledger : map(address, uint); const to : address; const amount : uint) is {
ledger[to] := ledger[to] + amount;
}
external function mint(const to : address; const amount : uint) is {
credit(storage.balances, to, amount);
storage.total := storage.total + amount;
}Passing a local where a storage parameter is expected is an error:
pass a storage path to `ledger`Results
A function with a result declares : type. The body is an expression, or a block followed by with and the result:
storage is record {
count : uint;
}
pure function double(const n : uint) : uint is
n * 2
external function bump() : uint is {
storage.count := storage.count + 1;
} with double(storage.count)A function without : type returns unit. external view and external pure functions must return a value, because deciding to revert is the caller's call: a check returns a bool the caller branches on, and the caller fails if it must. An external stateful function can return one too. The value goes back to the caller after the function's deferred calls have run.
storage is record { limit : uint; }
external view function check(const n : uint) is {
assert(n <= storage.limit);
}gate.lithe:3:46: an `external view` function must return a value: return a `bool` the caller can check, and let the caller failA result can be any ABI value. A record result is flattened into named ABI outputs, one per field, as a Solidity function with several results has. A tuple or tuple record result is one output, a Solidity tuple, as a function returning a struct has. For words alone the two lay out the same bytes.
Errors
raises lists the errors a function can raise, separated by |. External functions must declare them. Internal functions may leave raises off and have their errors inferred. raises none states that a function never raises a declared error. See Errors.
Overloading
External functions can share a name when their parameter types differ, because each gets its own ABI selector:
storage is record {
total : uint;
}
external function add(const a : uint) is {
storage.total := storage.total + a;
}
external function add(const a : uint; const b : uint) is {
storage.total := storage.total + a + b;
}These are add(uint256) and add(uint256,uint256). Two external functions with the same signature are a duplicate entrypoint.
Internal functions overload the same way, generic ones and ones that take storage included:
storage is record {
total : uint;
}
pure function double(const a : uint) : uint is a * 2
pure function double(const a : bool) : uint is 0
external function add(const a : uint) is {
storage.total := storage.total + double(a) + double(true);
}A call picks the overload that takes as many arguments as it passes. If several do, the argument types choose, and literals keep their own types: 1 is a uint. In a contract or a module, overloads of one name are all internal or all external, and no two take the same parameter types.
Calling functions
- Internal functions can call each other and are called with
f(args). - External functions cannot be called from inside the contract. An external function is a whole transition: its storage writes, events, and deferred calls happen after its body finishes, so it can't run partway through another function's body. Share logic through internal functions.
- A module's functions are called as
Module.f(args). - A function with no result is called as a statement:
f(args);.
storage is record {
n : uint;
}
external function g() is { skip }
external function f() is {
g();
}external function `g` cannot be called from inside the contractRecursion
A function cannot call itself, directly or through another function. Every loop in Lithe has a count fixed when it starts, and a recursive call would be a loop with no such count. Write the loop instead:
storage is record {
n : uint;
}
pure function fact(const n : uint) : uint is
if n = 0 then 1 else n * fact(n - 1)
external function set(const n : uint) is {
storage.n := fact(n);
}recursion is not supported: iteration is bounded, write a `for` loopstorage is record {
n : uint;
}
pure function fact(const n : uint) : uint is {
var acc : uint := 1;
for i : uint in 0 .. n do {
acc := acc * (i + 1);
}
} with acc
external function set(const n : uint) is {
storage.n := fact(n);
}See Recursion for more rewrites.
Generic functions
A function can take type parameters, written in parentheses before its value parameters. Every use is specialized at compile time:
storage is record {
owner : address;
}
pure function first(T)(const a : T; const b : T) : T is
a
external view function pick(const other : address) : address is
first(storage.owner, other)See Generics for generic modules.
Module externals
A module can declare external functions, and a contract exposes them by declaring each one's header in use M exposing:
use Token exposing {
function transfer(to : address; amount : uint)
raises InsufficientBalance;
view function balanceOf(account : address) : uint;
}A declaration has no body, and must match the module's external exactly: the same effect, payable and nonreentrant, parameters, result, and errors. See Modules and contracts.
receive and fallback
Every call to an entrypoint names it by its selector, the first four bytes of the calldata. Two declarations answer the calls that don't:
| Declaration | Runs for |
|---|---|
payable receive is { ... } | a call with no calldata, such as a plain ether transfer |
[payable] fallback [: bytes] is { ... } | calldata that matches no entrypoint, and no calldata when there is no receive |
Without them, both kinds of call revert. Each is a transition like any stateful entrypoint: it can write storage, emit, defer, raise the errors its raises lists, and be nonreentrant. Neither takes parameters, and neither can be called from inside the contract.
import "std/evm";
event Received of record {
indexed from : address;
amount : wei;
}
storage is record {
received : wei;
unknown : uint;
}
payable receive is {
storage.received := storage.received + Evm.value();
emit Received { from = Evm.sender(); amount = Evm.value(); };
}
fallback is {
storage.unknown := storage.unknown + 1;
}
external view function received() : wei is
storage.receivedreceiveonly exists to take ether, so it is always writtenpayable receive.- A
fallbackthat is notpayablerefuses a call that carries ether. Apayable fallbacktakes it, and every entrypoint that is not payable still refuses value sent to it by its selector. - The
fallbackreads what it was sent withEvm.calldata()andEvm.selector(). Afallback : bytesreturns its result as it is, not ABI-encoded, as Solidity'sfallback(bytes calldata) returns (bytes memory)does. - A
receivethat is notnonreentrantcan run while the contract's own deferred calls do. That is how ether comes back from a call such as WETH'swithdraw. - In the ABI they are
{"type": "receive"}and{"type": "fallback"}entries.lithe footprintandlithe errorslist them by name.
storage is record {
n : uint;
}
receive is {
skip
}`receive` accepts ether; write `payable receive`Passing a call on
A fallback can pass the whole call to another contract instead of running a body:
| Declaration | Runs the target's code | Compiles to |
|---|---|---|
fallback delegates to target; | as this contract, on its storage, balance, and address | DELEGATECALL |
fallback forwards to target; | as the target, on its own storage | CALL, with the call's value |
The target is an address or an interface value, computed for each call by an expression that only reads: a storage field, a constant, a view function, or a query. The calldata goes to it unchanged, and what it returns, or reverts with, comes back unchanged. A target with no code is refused, since it would answer every call with success and nothing.
storage proxy at "example.proxy" is record {
implementation : address;
}
deploy(const implementation : address) is {
proxy.implementation := implementation;
}
payable fallback delegates to proxy.implementation;To refuse some calls, have a view function choose the target and raise, and list its errors on the fallback. A map from selectors to targets routes like a diamond:
import "std/address";
import "std/evm";
error UnknownSelector of bytes4;
error NotOwner;
storage is record {
owner : address;
routes : map(bytes4, address);
}
view function route(const selector : bytes4) : address is {
const to : address := storage.routes[selector];
assert(to <> Address.zero, UnknownSelector(selector));
} with to
payable fallback raises UnknownSelector delegates to route(Evm.selector());
external function addRoute(const selector : bytes4; const to : address)
raises NotOwner
is {
assert(Evm.sender() = storage.owner, NotOwner);
storage.routes[selector] := to;
}payabledecides whether the contract takes ether with the call.forwards tosends it on with theCALL. Withdelegates to, the target's code sees it asEvm.value().- Code run by
delegates tocan write any of this contract's storage, so what it may change is only as safe as what the target is. For a contract's own upgrades, preferupgradeable, whose layoutlithe upgrade-checkcompares. - A
forwards tocall runs under the contract's reentrancy lock, as a deferred call does. A passingfallbackcannot itself benonreentrant. lithe footprintlists what each passes its call to:delegates to: proxy.implementation.
private
private hides a declaration from files that import this one. It applies to internal functions, types, errors, and events. An external function cannot be private.

