Program Structure
A Cambrian source file uses the .cam extension. It describes one or more
entities — units of persistent state that communicate through messages —
plus optional shared declarations (types, libraries, events, tests, and so on).
Imports
Cambrian has two unrelated import-style constructs.
Multi-file import "path.cam"
Share declarations across files with a string-literal path. An explicit
./ or ../ path is resolved only relative to the importing .cam file.
A bare path (import "token/core.cam") tries that directory first,
then each library_paths root from project.yaml:
// math.cam — shared declarations only, no entities
error ZeroAmount();
pure fn smaller(a: U256, b: U256) -> U256 {
if a < b { a } else { b }
}
// Vault.cam
import "./math.cam"
using { smaller } for U256;
entity Vault {
const CAP: U256 = 1_000_000
routes {
deposit(amount: U256)
where amount > 0 : throw ZeroAmount()
=> []
}
m_balance: U256 {
in deposit(amount) => (m_balance + amount).smaller(CAP)
}
}
Semantics:
- Imports are transitive. Each file is loaded once, so an import cycle is not an error — already-visited files are simply skipped.
- Importable:
pure fn,record,enum,type,event,error,extern entity,library,using … for …;, and otherimports. Constants live inside anentityorlibrary(there is no top-levelconst). - Not importable:
entity,test,fuzz,invariant. Those stay on project entry sources (sources:inproject.yaml). - Duplicate names across files are the same error as two declarations
in one file.
imports:does not create a package namespace. - In
project.yamlmode, list entry points undersources:; transitive imports are picked up automatically. Optionallibrary_paths:/imports:load shared helpers from extra directories (none are built in). If a listed file cannot be found, the error names the directories that were searched. See Multi-File Projects and the Contract Standard Library.
Namespaces always in scope
std::, msg::, sys::, and (on EVM) evm:: need no import. Call stdlib
helpers with an explicit prefix (std::math::min); bare names are rejected
(V49). See Standard Library and
EVM Intrinsics.
Top-level items
A .cam file may contain, in any order:
| Item | Role |
|---|---|
import "…" | Pull in shared declarations (./ file-relative, or bare + library_paths) |
type / record / enum | Shared types |
pure fn | Stateless helpers |
library / using … for … | Reusable pure helpers and method sugar |
event / error | Log topics and custom errors |
extern entity | Foreign entity surface (other project or host) |
entity | Stateful unit with routes and members |
test / property / fuzz / invariant | Specs and checks (entry sources only) |
File layout example
error ZeroAmount();
event Deposited(indexed who: address, amount: U256);
pure fn larger(a: U256, b: U256) -> U256 {
if a >= b { a } else { b }
}
entity Counter {
routes {
#[factory_only]
init setup(owner: address) => []
deposit(amount: U256)
where amount > 0 : throw ZeroAmount()
=> [
emit Deposited(msg::sender, amount);
]
view get_peak() -> U256 => [
return(m_peak)
]
view get_balance() -> U256 => [
return(m_balance)
]
}
m_owner: address {
in setup(owner) => owner
}
m_balance: U256 {
in setup(_) => 0
in deposit(amount) => m_balance + amount
}
m_peak: U256 {
in deposit(amount) => larger(m_peak, amount)
}
}
Ordering inside an entity
- Routes live in a
routes { }block (several blocks are allowed and are merged, but one is conventional). - Members are declared in the entity body alongside that block.
- Type aliases, records, enums, constants, macros, events, and errors may also appear inside the entity.
Multiple entities
One file (or one project) can define several entities that send to each other
via typed addresses and ~>:
entity Ledger {
routes {
credit(account: address, amount: U256) => []
}
m_balances: HashMap<address, U256> {
in credit(account, amount) => m_balances.insert(account, m_balances[account] + amount)
}
}
entity Shop {
routes {
#[factory_only]
init init_shop(ledger: Address<Ledger>) => []
purchase(item_id: u64)
where msg::value > 0 : throw 1
=> [
credit(msg::sender, msg::value) ~> m_ledger
]
}
m_ledger: Address<Ledger> {
in init_shop(ledger) => ledger
}
}
Naming conventions
These are conventions, not compiler rules (identifiers are
[A-Za-z_][A-Za-z0-9_]*):
| Element | Convention | Example |
|---|---|---|
| Entity names | PascalCase | Counter, Token |
| Route names | snake_case | deposit, get_balance |
| Member names | m_ + snake_case | m_balance, m_owner |
| Pure functions / macros | snake_case | max, is_owner |
| Types / records / enums | PascalCase | Amount, Status |
| Events / errors | PascalCase | Transfer, Unauthorized |
| Constants | SCREAMING_SNAKE_CASE | MIN_DEPOSIT |
Minimal entity
entity Counter {
routes {
increment() => []
}
m_count: u64 {
in increment() => m_count + 1
}
}
Routes declare which entry points the entity exposes; members declare how state changes when those routes run.