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 — 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 other imports.
  • 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 / enum / constShared types and constants
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

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_]*):

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.