Skip to content
LogoLogo

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 body

Kinds

DeclarationVisibleMay do
pure functioninternalcompute from its arguments
view functioninternalalso read storage, read Evm context, query
functioninternalalso write storage, emit, defer
external pure functionABI (pure)like pure function
external view functionABI (view)like view function
external functionABI (nonpayable)like function
external payable functionABI (payable)also accept ether and read Evm.value()
external nonreentrant functionABIholds 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.count
declared effect is weaker than the body's: external view function `bad` writes storage
import "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
  0
payable functions must be stateful

Parameters

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 fail

A 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 contract

Recursion

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` loop
storage 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:

DeclarationRuns 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.received
  • receive only exists to take ether, so it is always written payable receive.
  • A fallback that is not payable refuses a call that carries ether. A payable fallback takes it, and every entrypoint that is not payable still refuses value sent to it by its selector.
  • The fallback reads what it was sent with Evm.calldata() and Evm.selector(). A fallback : bytes returns its result as it is, not ABI-encoded, as Solidity's fallback(bytes calldata) returns (bytes memory) does.
  • A receive that is not nonreentrant can run while the contract's own deferred calls do. That is how ether comes back from a call such as WETH's withdraw.
  • In the ABI they are {"type": "receive"} and {"type": "fallback"} entries. lithe footprint and lithe errors list 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:

DeclarationRuns the target's codeCompiles to
fallback delegates to target;as this contract, on its storage, balance, and addressDELEGATECALL
fallback forwards to target;as the target, on its own storageCALL, 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;
}
  • payable decides whether the contract takes ether with the call. forwards to sends it on with the CALL. With delegates to, the target's code sees it as Evm.value().
  • Code run by delegates to can 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, prefer upgradeable, whose layout lithe upgrade-check compares.
  • A forwards to call runs under the contract's reentrancy lock, as a deferred call does. A passing fallback cannot itself be nonreentrant.
  • lithe footprint lists 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.