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 — library file: declarations only, no entities
pure fn min(a: u256, b: u256) -> u256 { if a < b { a } else { b } }
library SafeMath {
pure fn add(a: u256, b: u256) -> u256
where (a + b >= a) : throw Overflow() { a + b }
}
// Vault.cam
import "./math.cam"
using SafeMath for u256;
entity Vault {
routes {
deposit(amount: u256) => [
m_balance := m_balance.add(amount)
]
}
m_balance: u256 {
in deposit(amount) => m_balance.add(amount)
}
}
Semantics:
- Imports are transitive; cycles are rejected at load time.
- Importable:
pure fn,record,enum,type,const,event,error,extern entity,library,using … for …;, and otherimports. - 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 / const | Shared types and constants |
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
import "./shared.cam"
error Unauthorized();
event Deposited(indexed who: address, amount: U256);
pure fn max(a: u64, b: u64) -> u64 {
if a >= b { a } else { b }
}
entity Counter {
routes {
init setup(owner: address) => []
deposit(amount: U256)
where amount > 0 : throw Unauthorized()
=> [
emit Deposited(msg::sender, amount)
]
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
}
}
Ordering inside an entity
- Routes live in a single
routes { }block. - Members are declared in the entity body alongside that block.
- Type aliases, records, enums, constants, and macros 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) => {
let current = m_balances.get(account).unwrap_or(0);
m_balances.set(account, current + amount)
}
}
}
entity Shop {
routes {
purchase(item_id: u64)
where msg::value > 0 : throw Unauthorized()
=> [
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.