Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 other imports. Constants live inside an entity or library (there is no top-level const).
  • Not importable: entity, test, fuzz, invariant. Those stay on project entry sources (sources: in project.yaml).
  • Duplicate names across files are the same error as two declarations in one file. imports: does not create a package namespace.
  • In project.yaml mode, list entry points under sources:; transitive imports are picked up automatically. Optional library_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:

ItemRole
import "…"Pull in shared declarations (./ file-relative, or bare + library_paths)
type / record / enumShared types
pure fnStateless helpers
library / using … for …Reusable pure helpers and method sugar
event / errorLog topics and custom errors
extern entityForeign entity surface (other project or host)
entityStateful unit with routes and members
test / property / fuzz / invariantSpecs 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_]*):

ElementConventionExample
Entity namesPascalCaseCounter, Token
Route namessnake_casedeposit, get_balance
Member namesm_ + snake_casem_balance, m_owner
Pure functions / macrossnake_casemax, is_owner
Types / records / enumsPascalCaseAmount, Status
Events / errorsPascalCaseTransfer, Unauthorized
ConstantsSCREAMING_SNAKE_CASEMIN_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.