Variables & Types
This section describes how to declare variables and constants, inspect types, and work with the core built-in types: primitives, arrays/varrays, maps, and classes.
Declaring variables
Using the memory keyword
The memory keyword is used to declare temporary variables. Once a function finishes executing, any data in memory is wiped. It is used for arithmetic, loops, and temporary data holds.
- Declaration: Start with the
memorykeyword, followed by the variable name and its type - Assignment & Type Inference: You can assign a value in the same statement. If a value is assigned, explicitly writing the type is optional (the compiler will infer it)
- Default Initialization: If no value is assigned during declaration, the variable is automatically initialized with the zero value for that type (e.g.,
0for integers,falsefor booleans)
coco Variable
const VAL U64 = 10
endpoint DeclareVar() -> (output U64):
memory n U64
n = VAL
yield output n
endpoint DeclareValue() -> (output U64):
memory n = 10
memory m U64 = 11
yield output n + m
Using the storage Keyword
A special case of a variable is a pointer to a stored value that allows for very efficient "atomic" handling of storage without transferring large amounts of data between blockhain storage and memory. Such variable is declared using storage keyword instead of memory. While for primitive values using storage is possible, though not necessary, for complex values like arrays, maps or classes, using storage can be more efficient that dispersing large data structures from the storage into memory.
In the example below, we first observe an operator but it's only a pointer to the stored value, we don't transfer the complete data structure into memory. Then we append a new_guardian to the element Guardian of the operator, what is a simple operation that doesn't require transferring the complete array into memory. At the end the pointer is written back into map, what again only transfers the new elements, not the entire structure.
coco GuardianRegistry
state logic:
Operators Map[Identifier]Operator
class Operator:
field Name String
field Guardians []Identifier
endpoint deploy Init():
pass
endpoint dynamic RegisterGuardian(new_guardian Identifier):
// 1. Declare a Storage Pointer
storage operator Operator
// 2. Point to the data
observe operators <- GuardianRegistry.Logic.Operators:
operator = operators[Sender]
// 3. Update only what is needed
mutate operators <- GuardianRegistry.Logic.Operators:
append(operator.Guardians, new_guardian) // Writes directly to the list
operators[Sender] = operator // Commits the change
Defining Constants Using const Keyword
Constants can be defined for the whole module using const keyword. Constant’s definition needs to include both type and value. Contants can only be defined at the top-level, constants can't be local to an endpoint or code blocks.
Get a variable’s type with typeof(expr)
A string description of variable type can be retrieved using typeof(variable) function. Any variable or expression can be the argument, except functions. Calling functions in typeof() produces a compiler error.
Primitive types
Coco uses the following primitive types.
| Type |
|---|
| Bool |
| Bytes |
| String |
| Identifier (32-byte) |
| U64 |
| I64 |
| U256 |
Values can be typecast to other values with typecast operator e.g. U256(5). Here is a type conversion matrix with allowed conversions and methods used for conversion.
Src -> Dest | Bool | Bytes | String | Identifier | U64 | I64 | U256 |
|---|---|---|---|---|---|---|---|
Bool | X | Not Supported | __str__ | Not Supported | .ToU64() | .ToI64() | .ToU256() |
Bytes | __bool__ | X | __str__ | __id__ | .ToU64() | .ToI64() | .ToU256() |
String | __bool__ | .ToBytes() | X | __id__ | .ToU64() | .ToI64() | .ToU256() |
Identifier | __bool__ | .ToBytes() | __str__ | X | Not Supported | Not Supported | .ToU256() |
U64 | __bool__ | .ToBytes() | __str__ | Not Supported | X | .ToI64() | .ToU256() |
I64 | __bool__ | .ToBytes() | __str__ | Not Supported | .ToU64() | X | .ToU256() |
U256 | __bool__ | .ToBytes() | __str__ | id | .ToU64() | .ToI64() | X |
Arrays and Varrays
Coco supports two types of collections: Arrays (Fixed-Length) and Varrays (Variable-Length).
Fixed-Length Arrays
Arrays have a size that is determined when you write the code. You cannot change their size later.
- Declaration:
[Length]Type - Initialization: You can use a literal syntax
[3]U64{...}or themake()function
endpoint TestArray() -> (res1 [3]U64, res2 [3]U64):
// 1. Literal Initialization
memory arr = [3]U64{1, 2, 3}
// 2. Zero-Value Initialization
// Creates [0, 0, 0]
memory arr2 = make([3]U64)
arr2[2] = 4 // Updates index 2 -> [0, 0, 4]
return (res1: arr, res2: arr2)
Variable-Length Arrays (Varrays)
Varrays are dynamic. You can add or remove elements during execution.
- Declaration:
[]Type(Notice the empty brackets) - Initialization: Use
make([]Type, initial_size)to pre-fill with zero values
Operations:
append(varray, item): Adds an item to the endpopend(varray): Removes and returns the last itemmerge(v1, v2): Combines two varrays into a new one
endpoint TestVarray() -> (joined []U64):
// 1. Empty Declaration
memory vrr1 []U64
append(vrr1, 1)
append(vrr1, 2)
// vrr1 is now [1, 2]
// 2. Pre-allocated Declaration
// Creates [0, 0] (Size 2, filled with zeros)
memory vrr2 = make([]U64, 2)
append(vrr2, 3)
// vrr2 is now [0, 0, 3]
// 3. Remove the last element
memory last = popend(vrr2) // Removes 3
// 4. Merge two varrays
// Merges [1, 2] and [0, 0] -> [1, 2, 0, 0]
joined = merge(vrr1, vrr2)
Quick Comparison
| Feature | Array ([N]Type) | Varray ([]Type) |
|---|---|---|
| Size | Fixed at compile time | Dynamic (can grow/shrink) |
| Syntax | [3]U64 | []U64 |
| Analogy | C++ int arr[3] | Java ArrayList / C++ std::vector |
| Use Case | Known data (e.g., coordinates) | Lists, queues, stacks |
Maps
Maps are a collection of key-value pairs available in Coco. Each key in the map must be unique and can be mapped to a single value only. Map keys can only be primitive values, but values can be of any type, including another map
The make command can be used to initialize an empty map.
mp2 variable is initialized with a set of key-value pairs in declaration.
The len operator can be used to get total number of keys present in a map.
Maps can be merged using the merge operator. If the keys overlap, the second map pairs overwrite the first. With this operation joined will contain union of all keys present in 'mp' and 'mp2'.
To remove a key/value from the map, one can use remove function. It doesn't return the removed value, if we try to remove a non-existent key, remove doesn't do anything.
When a collection (map or array) is empty, sweep can be used to completely remove the empty map from the storage.
// 1. Initialize an empty map
// Keys are Strings, Values are Unsigned 64-bit integers
mp = make(Map[String]U64)
// 2. Insert a single key-value pair
mp["Maybe"] = 2
memory:
// 3. Initialize a map with pre-populated literals
mp2 = Map[String]U64{"No":0, "Yes": 1}
// 4. Get the total number of keys in mp2 (Result: 2)
l = len(mp2)
// 5. Merge maps. 'mp2' values will overwrite 'mp' if keys overlap.
// Result: {"Maybe": 2, "No": 0, "Yes": 1}
joined = merge(mp, mp2)
// 6. Delete a key from the map.
// Note: This does not return the value, it only modifies the map.
remove(joined, "No")
// 7. Check if a key exists using the '?' operator
// Result: false (stored in 'exists')
memory exists = joined["No"]?
Membership check with ? operator
To check if a map has an element at some key, one can use the "has" operator ? that returns a boolean "true" if the element exists.
m = Map[String]U64{"hi": 42, "lo": 7}
memory exists = m["hi"]?
// Returns: true (boolean) because "hi" is present in 'm'
generate micro keyword
generate keyword in front an expression, reading from maps, assures the default value is returned if the key is missing. The case to use generate keyword is shown in without_generate.coco.
With generate, we can rewrite the example in a single line:
generate counter[newUserId]++
counter[newUserId]++
// the above is actually
// counter[newUserId] = counter[newUserId] + 1
// so it returns a runtime error when newUserId
// key doesn't exist
// so we'd need to write
if !counter[newUserId]?:
counter[newUserId] = 0
counter[newUserId]++
Classes
Classes in Coco allow you to simplify the handling of complex structures. Each class is made up of fields and methods.
Classes can be declared using the class keyword followed by the name of the class. Methods can be declared within the class block using the method keyword followed by the name of the method, input parameters and output parameters. In methods, self is available without explicitly listing it as an argument of a method.
If the method changes its own values, mutate has to follow the method keyword in the method signature.
To access any field or method of the class the value can be called simply using a dot followed by the name of the method or field.
A maximum of 240 methods can be declared on any class excluding the 16 slots reserved for predefined special methods.
memory person = Person{name: "Sam", age: 20, check: 0}
Here person is initialized with a class of type Person with the values as provided
In length = len(personobj) the total number of fields in the class is stored in the variable length.
coco Class
class Person:
field name String
field age U64
field check U64
method mutate Add():
self.check += 1
endpoint Details(name String, age U64, check U64) -> (nameres String, ageres U64, checkres U64):
memory person Person
person.name = name
person.age = age
person.check = 0
person.Add()
return (nameres: person.name, ageres: person.age, checkres: person.check)
endpoint Literal() -> (length U64, person Person):
memory personobj = Person{name: "Sam", age: 20, check: 0}
length = len(personobj)
yield person personobj
Special methods on classes
Among the 256 method codes, the first 16 (0x0 to 0xF) are reserved for special methods that can be invoked. The special methods are build, throw, event, join, lt, gt, eq, bool, str, id, and len.
Special methods can be defined or overridden for classes. E.g., one can define a __len__ of a class or __bool__ and use a class in boolean expressions.
class Person:
field name String
method __eq__(other Person) -> (is_equal Bool):
is_equal = self.name == other.name
Reserved Method Outputs As these methods are reserved, their output types are predefined. Ensuring your output type matches the method call is vital, as any mismatch will result in failure.
Special Method Signatures
| Special Method | Description | Signature |
|---|---|---|
| build | Build | (<any> other) → (<any> value) |
| throw | Throw | () → (<any> String) |
| event | Event | () → (<any> <event>) |
| join | Join | (<any> X) → (<any> X) |
| lt | Less Than | (<any> X) → (<any> Bool) |
| gt | Greater Than | (<any> X) → (<any> Bool) |
| eq | Equal To | (<any> X) → (<any> Bool) |
| bool | Bool | () → (<any> Bool) |
| str | String | () → (<any> String) |
| id | Identifier | () → (<any> Identifier) |
| len | Length | () → (<any> U64) |
These methods must be called in coco using the double underline (__) at start and end of the method name, e.g. __len__.