Skip to main content

Variables & Types

This section covers variable declaration, type inspection, and core built-in types: primitives, arrays, maps, and classes.

Declaring Variables​

memory — Temporary Variables​

The memory keyword declares temporary variables that are wiped when the function finishes.

endpoint DeclareVar() -> (output U64):
memory n U64 // Initialized to zero
n = 10

memory m = 11 // Type inferred
memory x U64 = 12 // Explicit type

yield output n + m + x
tip

If no value is assigned, variables are initialized to their zero value (0 for integers, false for booleans).

storage — Efficient Pointers​

The storage keyword creates a pointer to stored data, enabling efficient operations without transferring large structures to memory.

endpoint dynamic RegisterGuardian(new_guardian Identifier):
storage operator Operator // Declare pointer

observe operators <- GuardianRegistry.Logic.Operators:
operator = operators[Sender]

mutate operators <- GuardianRegistry.Logic.Operators:
append(operator.Guardians, new_guardian)
operators[Sender] = operator

Block Syntax (Syntax Grouping)​

Both memory and storage declarations support block syntax to group multiple declarations:

memory:
count = 0
name = "Alice"
limit = 10

storage:
operator Operator
balance U64

const — Constants​

Constants are defined at the module level with an explicit type and value:

const MAX_SUPPLY U64 = 1000000
const TOKEN_NAME String = "MyCoin"

Since v0.9.2, several constants can share one const: block, with the same indented shape as a memory: block. Single-line constants and const: blocks can be mixed in one module:

coco Config

const TOKEN_NAME String = "MyCoin"

const:
MAX_SUPPLY U256 = 500
MIN_BALANCE I64 = -5
ADMIN Identifier = 0x1111111111111111111111111111111111111111111111111111111111111111
ENABLED Bool = true

endpoint Settings() -> (name String, supply U256, min I64, on Bool):
name = TOKEN_NAME
supply = MAX_SUPPLY
min = MIN_BALANCE
on = ENABLED

The same rules apply to both forms:

  • The type is required. A = 1 inside a const: block is a parse error ("Unrecognized token Assign … Expected one of identifier, "[" or "Map"").
  • The value must be a literal: a number (optionally negative), a string, a hex value, or true/false. An expression such as A U64 = 1 + 2 is a parse error ("Unrecognized token Plus").
  • In a package, pub const: makes every constant in the block public. There is no per-line pub inside a block. Use single-line pub const for a constant that should be public on its own.
limits/limits.coco
coco package limits

pub const:
MAX_TRANSFER U64 = 1000
FEE_BPS U64 = 30

A module that imports the package reads them as limits::MAX_TRANSFER and limits::FEE_BPS.

typeof() — Type Inspection​

Get a string description of any variable's type:

memory x = 42
memory t = typeof(x) // Returns "U64"

Primitive Types​

TypeDescription
BoolBoolean (true/false)
BytesByte sequence
StringText string
Identifier32-byte identifier
U64Unsigned 64-bit integer
I64Signed 64-bit integer
U256Unsigned 256-bit integer

Type Conversion​

Use typecast operators like U256(5) to convert between types:

From ↓ / To →BoolBytesStringIdentifierU64I64U256
Bool—✗__str__✗.ToU64().ToI64().ToU256()
Bytes__bool__—__str____id__.ToU64().ToI64().ToU256()
String__bool__.ToBytes()—__id__.ToU64().ToI64().ToU256()
Identifier__bool__.ToBytes()__str__—✗✗.ToU256()
U64__bool__.ToBytes()__str__✗—.ToI64().ToU256()
I64__bool__.ToBytes()__str__✗.ToU64()—.ToU256()
U256__bool__.ToBytes()__str____id__.ToU64().ToI64()—

Actor Methods​

An Identifier can name any resource. It can be explicitly tagged as an actor with Actor(<identifier>), which allows calling methods on it:

MethodReturnsReturn nameDescription
Actor(id).Exists()BoolexistsWhether the account exists
Actor(id).HasSigned()Boolhas_signedWhether the actor signed the current interaction
Actor(id).Param(name)BytesparamValue of the participant parameter name
endpoint RequireSignature(participant Identifier):
if !Actor(participant).HasSigned():
throw "participant did not sign this interaction"

endpoint Onboard(participant Identifier) -> (ok Bool, tier Bytes):
ok = Actor(participant).Exists()
tier = Actor(participant).Param("tier")

The return name lets you bind the result explicitly, like any other call:

memory signed = (has_signed) <- Actor(participant).HasSigned()

The same Actor(id) tag selects that actor's state — see Logic and Actor State:

observe bal <- MyLogic.Actor(participant).balance
PISA v0.8.0

All three methods require version = "0.8.0" under [target.pisa] in coco.nut. Calling an unknown method reports "<name> is not defined for type identifier".

Arguments are checked by count, type and name. A literal such as Param("tier") carries no name, but a bare variable passes its own name, which must match the parameter: Actor(p).Param(code) fails with "expected argument name 'name' at position 0, called with 'code'". Write Actor(p).Param(name: code) instead.


Arrays​

Fixed-Length Arrays​

Size determined at compile time. Cannot be changed later.

// Literal initialization
memory arr = [3]U64{1, 2, 3}

// Zero-value initialization
memory arr2 = make([3]U64) // [0, 0, 0]
arr2[2] = 4 // [0, 0, 4]

Fixed Array Methods​

Method / FunctionReturnsDescription
len(arr)U64Returns the length of the array
arr[index]TypeAccess element at index (0-based)

Variable-Length Arrays (Varrays)​

Dynamic arrays that can grow or shrink.

memory vrr []U64              // Empty varray
append(vrr, 1) // [1]
append(vrr, 2) // [1, 2]

memory last = popend(vrr) // Removes and returns 2

memory vrr2 = make([]U64, 2) // [0, 0]
memory joined = merge(vrr, vrr2)

Varray Methods​

Method / FunctionReturnsDescription
len(vrr)U64Returns current length
vrr[index]TypeAccess element at index (0-based)
append(vrr, item)—Add element to the end
popend(vrr)TypeRemove and return the last element
merge(vrr1, vrr2)[]TypeCombine two varrays into a new varray
make([]Type, size)[]TypeCreate varray with size zero-value elements
memory items []String
append(items, "first")
append(items, "second")

memory count = len(items) // 2
memory last = popend(items) // "second"
memory first = items[0] // "first"

Array vs Varray Comparison​

FeatureArray [N]TypeVarray []Type
SizeFixed at compile timeDynamic
Syntax[3]U64[]U64
append✗ Not supported✓ Supported
popend✗ Not supported✓ Supported
merge✗ Not supported✓ Supported
Use caseKnown data (coordinates)Lists, queues, stacks

Multi-Dimensional and Nested Literals​

Array types nest directly. In a nested literal the inner element type can be elided to { ... }:

memory grid = [2][3]U64{{1, 2, 3}, {4, 5, 6}}  // grid[1][2] == 6
memory rows = [][]U64{{1}, {2, 3}} // varray of varrays

Maps​

Key-value pairs where keys must be unique primitives.

// Initialize empty map
memory mp = make(Map[String]U64)
mp["key"] = 42

// Initialize with literals
memory mp2 = Map[String]U64{"No": 0, "Yes": 1}

// Operations
memory count = len(mp2) // 2
memory merged = merge(mp, mp2) // Combine maps
remove(merged, "No") // Delete key

Map Methods​

Method / FunctionReturnsDescription
len(map)U64Returns the number of key-value pairs
map[key]ValueAccess value by key
map[key] = value—Set or update a key-value pair
map[key]?BoolCheck if key exists (membership test)
remove(map, key)—Delete a key-value pair
merge(map1, map2)Map[K]VCombine two maps (second overwrites duplicates)
make(Map[K]V)Map[K]VCreate an empty map
Maps in state

A map in logic or actor state can be written from memory with disperse, which merges the local map into the stored one: existing keys that the local map doesn't mention are kept, and disperse make(Map[K]V) -> m clears nothing. A stored map can't be copied into memory with gather, so read it one key at a time. To delete a stored key, use if m[k]?: sweep remove(m, k). See Maps: disperse merges, gather is rejected.

Membership Check with ?​

Use the ? operator to check if a key exists without accessing the value:

memory m = Map[String]U64{"hi": 42}
memory exists = m["hi"]? // true
memory missing = m["lo"]? // false

// Common pattern: check before access
if m["key"]?:
memory val = m["key"]

generate Keyword​

Auto-initialize missing keys with their zero value, avoiding manual existence checks:

// Without generate (requires check)
if !counter[userId]?:
counter[userId] = 0
counter[userId]++

// With generate (single line)
generate counter[userId]++
tip

Use generate when you want to safely increment or modify a map value that may not exist yet. It automatically initializes missing keys to their zero value (0 for integers, "" for strings, etc.).


Classes​

Group fields and methods into reusable structures.

class Person:
field name String
field age U64

method GetInfo() -> (info String):
info = f"{self.name} is {self.age}"

method mutate Birthday():
self.age += 1

Usage​

memory person = Person{name: "Sam", age: 20}
person.Birthday()
memory info = person.GetInfo()
memory fields = len(person) // Number of fields

Omitted and Elided Fields​

Fields left out of a class literal default to their zero value:

memory a = Person{age: 20}   // name == "", age == 20
memory b = Person{} // all fields zero (same as: memory b Person)

In collections of classes, the inner class type can be elided to { ... }:

memory people = []Person{{name: "Alice"}, {age: 30}}
memory byName = Map[String]Person{"Alice": {name: "Alice"}}

Field Name Shorthand​

When a variable already has the same name as the field, write the name once instead of stuttering name: name:

memory name = "Sam"
memory age = 20

memory p = Person{name, age} // same as Person{name: name, age: age}

Shorthand and explicit fields mix freely, in any order:

memory p = Person{name, age: 30}

The shorthand only accepts a bare variable name that matches a field of the class. Anything else still needs an explicit field name:

memory p = Person{other.name}         // error: missing name
memory q = Person{name: other.name} // correct

memory r = Person{nickname} // error: field nickname not found
memory t = Person{name: nickname} // correct

Built-in Class Operations​

Method / FunctionReturnsDescription
len(instance)U64Returns the number of fields in the class
instance.fieldTypeAccess a field by name
instance.method()variesCall a method on the instance
note
  • Use mutate keyword for methods that modify self
  • Maximum 240 custom methods per class

Special Methods (Dunder Methods)​

Override these methods to customize how your class behaves with operators and type conversions:

MethodPurposeTriggered BySignature
__eq__Equality comparisona == b(other T) -> (is_equal Bool)
__lt__Less than comparisona < b(other T) -> (is_less Bool)
__gt__Greater than comparisona > b(other T) -> (is_greater Bool)
__bool__Boolean conversionBool(instance)() -> (result Bool)
__str__String conversionString(instance)() -> (result String)
__len__Custom lengthlen(instance)() -> (result U64)
__id__Identifier conversionIdentifier(instance)() -> (result Identifier)
__join__Merge two instancesjoin(a, b)(other T) -> (joined T)
__event__Convert to eventemit() -> (ev EventType)
__except__Custom errorthrow() -> (err String)
class Person:
field name String
field age U64

method __eq__(other Person) -> (is_equal Bool):
is_equal = self.name == other.name && self.age == other.age
memory p1 = Person{name: "Sam", age: 20}
memory p2 = Person{name: "Sam", age: 20}
memory same = p1 == p2 // true (uses __eq__)

Example: String Conversion​

class Token:
field symbol String
field amount U64

method __str__() -> (result String):
result = f"{self.amount} {self.symbol}"
memory t = Token{symbol: "MOI", amount: 100}
memory s = String(t) // "100 MOI"

Example: Boolean Conversion​

class Balance:
field value U64

method __bool__() -> (result Bool):
result = self.value > 0
memory b = Balance{value: 0}
if !Bool(b):
// balance is empty