Variants
A variant type lists every case a value can take, and each case may carry a payload. Variants shape entrypoints, where an external function that takes a variant becomes one ABI function per case, and they hold state, where case makes every reader handle every state.
variants.lithe
(* A variant type is a closed set of cases. A case may carry a payload.
An external function that takes a variant becomes one ABI function per case:
this contract exposes `deposit(uint256)`, `send(address,uint256)`, and `reset()`. *)
import "std/evm";
type SendParams is record {
to : address;
amount : uint;
}
type Action is
| Deposit external deposit of uint
| Send external send of SendParams
| Reset external reset
error Insufficient of uint;
storage is record {
balances : map(address, uint);
}
(* `case` must handle every case; leaving one out is a compile error.
Each arm binds the case's payload. *)
external function main(const action : Action)
raises Insufficient
is
case action of {
| Deposit(amount) -> {
storage.balances[Evm.sender()] := storage.balances[Evm.sender()] + amount;
}
| Send(p) -> {
const have : uint := storage.balances[Evm.sender()];
assert(have >= p.amount, Insufficient(have));
storage.balances[Evm.sender()] := have - p.amount;
storage.balances[p.to] := storage.balances[p.to] + p.amount;
}
| Reset -> {
storage.balances[Evm.sender()] := 0;
}
}
external view function balanceOf(const who : address) : uint is
storage.balances[who]casemust cover every case. Delete theResetarm and the compiler reportsmissing case arm `Reset`.- A record payload becomes named ABI parameters, so
Sendissend(address to, uint256 amount). - See Dispatch for nesting and guards.
Stored state
A variant value is written by naming its case. A variant with no payloads is one byte in storage and a uint8 in the ABI, like a Solidity enum:
market.lithe
import "std/evm";
type Phase is
| Open
| Paused
| Settled of uint
error NotOpen;
error NotSettled;
storage is record {
phase : Phase;
bids : map(address, uint);
}
external function bid(const amount : uint) raises NotOpen is {
assert(storage.phase = Open, NotOpen);
storage.bids[Evm.sender()] := storage.bids[Evm.sender()] + amount;
}
external function pause() is {
storage.phase := Paused;
}
external function settle(const price : uint) is {
storage.phase := Settled(price);
}
(* `| Open | Paused ->` is one arm for both, and still exhaustive: a new case
is a compile error here until it is handled. *)
external view function price() : uint raises NotSettled is
case storage.phase of {
| Settled(p) -> p
| Open | Paused -> fail NotSettled
}Settledcarries a price, sophaseis a tag slot and a payload slot. Without it,phasewould take one byte.- A new contract reads
phaseasOpen, the first case. - The Order executor keeps each order's status as a variant.
Payloads may hold strings, bytes, and lists. One arm can bind the payload of several cases when they carry the same type:
review.lithe
type Review is
| Draft
| Rejected of string
| Withdrawn of string
| Approved of list(address)
storage is record {
reviews : map(uint, Review);
}
external function reject(const id : uint; const why : string) is {
storage.reviews[id] := Rejected(why);
}
external function withdraw(const id : uint; const why : string) is {
storage.reviews[id] := Withdrawn(why);
}
external function approve(const id : uint; const by : list(address)) is {
storage.reviews[id] := Approved(by);
}
external view function reason(const id : uint) : string is
case storage.reviews[id] of {
| Rejected(why) | Withdrawn(why) -> why
| Draft | Approved -> ""
}
external view function approvals(const id : uint) : uint is
case storage.reviews[id] of {
| Approved(by) -> by.length
| Draft | Rejected | Withdrawn -> 0
}RejectedandWithdrawnkeep their reason in the same slot, so either case reads it the same way.- Writing
Approvedclears a stored reason, and writingDraftclears both.

