Endpoints and Functions
Endpoints are the externally callable entry points of a Coco logic, while functions are local helpers you call from endpoints. Endpoints define named inputs and named return values, and may be marked dynamic when they mutate state (any block that uses mutate must be in a dynamic endpoint). Special qualifiers deploy and enlist handle one-time logic initialization and per-actor initialization, respectively. Aside from visibility and these qualifiers, endpoint and function syntax is the same; Coco favors explicit, named arguments and returns for clarity and auditability.
Endpoint Syntax
endpoint [endpoint_qualifier] [state_qualifier] <name> [argument, ...] -> [return_value, ...]
endpont_qualifier: deploy | enlist
state_qualifier: dynamic | static | asset
name: Identifier
Identifier: string, the first character is a letter (A-Z|a-z) the rest are alphanumerics; underscore _ can be anywhere
argument: NamedValue
return_value: NamedValue
NamedValue: name: type
type: Identifier (in case of a class name or event name) | primitive_type | Map[primitive_type]type | []type | [number]type
primitive_type: Bool | Bytes | Ptr | String | U64 | I64 | U256 | Identifier
An endpoint in Coco is a callable element that can be invoked ("called") on the blockchain. A single endpoint is expected to perform simple and singular operations. function has the same syntax as endpoint, but it can only be called from within the logic from another endpoint, it can't be invoked on the blockchain directly.
An endpoint can accept zero or more arguments and return some outputs. All arguments and return variables for an endpoint must be named. All return variables of the endpoint are automatically initialized at the beginning of its execution. See Functions.DoSomething() If multiple consecutive endpoint parameters are of the same data type, the type can be omitted from all but the last parameter.
An endpoint can have an endpoint qualifier deploy or enlist. These are used by endpoints that initialize logic's state on logic itself (deploy) or on an actor (enlist) and these endpoints can only be called once when the logic is deployed. More details are in the section below.
Some endpoint names have state qualifiers dynamic, static or asset. dynamic emphasizes that the endpoint modifies the logic state. It is mandatory for any endpoint that uses the mutate keyword in its block. static is the default value (an can be omitted) and means the endpoint doesn't mutate the logic's state. asset can only be used in asset logics and it means the endpoint mutates asset description (e.g. it changes circulating supply when tokens are minted). asset also means the endpoint is dynamic, so it can mutate the logic's state. State qualifiers of the endpoint are mutually exclusive, only one can be used (if it's not omitted).
coco Functions
endpoint Combine(x,y String) -> (z String):
memory w = join(y, (output) <- DoSomething())
yield z join(x, w)
function DoSomething() -> (output String):
yield output "done!"
Functions and Calls
function keyword is used for callables that can only be called from endpoints locally. They
can't be invoked on the blockchain. On the other hand, endpoint can't invoke another endpoint in the same logic
so function is a conventional reusable encapsulation of some functionality. So the difference between endpoint and function is just their visibility (endpoints can be called from blockchain and function from endpoints), everything else in the syntax is the same.
The syntax of a function call in Coco is uniquely verbose to maximize readability. It requires explicit naming if arguments and return values, though this may be reduces in case target variables have the same name as arguments. The formal syntax definition for the call is:
[(return_name,...) <-] [scope::]<name> ( [argument...] )
return_name: Identifier
scope: Identifier
name: Identifier
argument: NamedExpression
NamedExpression: name: expression
expression: any valid Coco expression with one value
While passing arguments to and returning the return values from endpoints or functions in Coco is done in the same way as in most other programming languages, Coco requires a more verbose syntax for calling functions: a function call needs to include all argument and return value names. These names can only be omitted in case the argument has exactly the same name as the variable, so we can omit the name to avoid stuttering.
There's another strict rule regarding arguments and return values: arguments are read-only and return values are write-only.
When we're calling a function from another package, the function name has to be prepended with scope, e.g. math::four() in the secton on packages.
coco FCall
endpoint A():
memory x = (out) <- Double(num: 10)
memory num = 20
memory out = Double(num) // compared to the first line, we can omit names when variable names match
memory a, b = (x, y) <- Pair()
function Double(num U64) -> (out U64):
out = num * 2
function Pair() -> (x, y U64):
x, y = 3, 4
function X(a, b U64) -> (out U64):
a += 1 // can't assign to an argument
out = a + b
// we can't read "out" at the right hand side
out += 1 // this is short notation for out = out + 1
Deploy and Enlist
Every endpoint can get invoked, but two special qualifiers are available for deploying and enlisting. deploy endpoint is used when the logic is first deployed on the blockchain and it initializes the logic's state. If there's no state logic in the logic, deploy endpoints are not needed.
When we have state actor we can have enlist endpoints that initialize actor's state.
In modules having a logic state, it is necessary to have at least one deploy endpoint to initialize this state (there can be multiple if there are different ways to deploy a logic). Modules with actor states need at least one enlist endpoint, but if a module has both logic and actor states, only deploy is mandatory.
deploy endpoint can be called only once at deployment of the logic, while there's no limit for other endpoints (including enlist).
coco Client
state logic:
supply U64
state actor:
balance U64
endpoint deploy SeedSupply(): // initialize state when deploying logic
mutate 100 -> Client.Logic.supply
endpoint enlist Alms():
mutate 2 -> Client.Sender.balance // some small initial balance
// adds 1 to the Sender's balance, if there's still some supply
endpoint dynamic GimmeGimme():
mutate sup <- Client.Logic.supply:
sup -= 1 // if supply is gone, this throws an error
mutate cs <- Client.Sender.balance:
cs += 1