A step-by-step introduction to writing Covenant programs.
This tutorial assumes you understand:
- Basic programming concepts (functions, variables, types)
- What a compiler does
- Why type systems help catch bugs
No prior Covenant experience is required.
Every Covenant program is made of snippets. A snippet is a self-contained unit of code with an ID and a kind.
snippet id="main.hello" kind="fn"
effects
effect console
end
signature
fn name="main"
returns type="Unit"
end
end
body
step id="s1" kind="call"
fn="println"
arg name="message" lit="Hello, world!"
as="_"
end
end
end
Line 1: Snippet Header
snippet id="main.hello" kind="fn"
snippet- Starts a code unitid="main.hello"- Unique identifier (module.name format)kind="fn"- This is a function
Lines 3-5: Effects
effects
effect console
end
- Declares this function needs console access
- Without this, calling
printlnwould be a compiler error
Lines 7-11: Signature
signature
fn name="main"
returns type="Unit"
end
end
- Function named
main - Returns
Unit(likevoidin other languages)
Lines 13-18: Body
body
step id="s1" kind="call"
fn="println"
arg name="message" lit="Hello, world!"
as="_"
end
end
step id="s1"- First step with ID "s1"kind="call"- This step calls a functionfn="println"- The function to callarg name="message" lit="..."- Pass a literal string argumentas="_"- Discard the return value
Line 20: End
end
- Closes the snippet (every block ends with
end)
Functions without side effects don't need an effects section.
snippet id="math.add" kind="fn"
signature
fn name="add"
param name="a" type="Int"
param name="b" type="Int"
returns type="Int"
end
end
body
step id="s1" kind="compute"
op=add
input var="a"
input var="b"
as="result"
end
step id="s2" kind="return"
from="result"
as="_"
end
end
end
Parameters
param name="a" type="Int"
param name="b" type="Int"
Two integer parameters named a and b.
Compute Step
step id="s1" kind="compute"
op=add
input var="a"
input var="b"
as="result"
end
kind="compute"- Arithmetic/logic operationop=add- Addition (not+!)input var="a"- First operand from variableainput var="b"- Second operand from variablebas="result"- Bind output to nameresult
Return Step
step id="s2" kind="return"
from="result"
as="_"
end
- Return the value bound to
result
Covenant uses add instead of + because:
- Unambiguous - No operator precedence rules needed
- Queryable - Easy to find all additions in the codebase
- Consistent - Same syntax for all operations
When your function needs to do I/O, declare the effects.
snippet id="files.read_config" kind="fn"
effects
effect filesystem
end
signature
fn name="read_config"
returns union
type="String"
type="IoError"
end
end
end
body
step id="s1" kind="call"
fn="read_file"
arg name="path" lit="config.txt"
as="content"
end
step id="s2" kind="return"
from="content"
as="_"
end
end
end
Effects Declaration
effects
effect filesystem
end
This function needs filesystem access.
Union Return Type
returns union
type="String"
type="IoError"
end
Returns either a String (success) or IoError (failure).
Effect Propagation
If function A calls function B, A must declare all of B's effects:
read_filehaseffect filesystem- So
read_configmust also declareeffect filesystem
The compiler enforces this automatically.
Covenant makes errors explicit through union types and pattern matching.
snippet id="parse.parse_port" kind="fn"
signature
fn name="parse_port"
param name="s" type="String"
returns union
type="Int"
type="ParseError"
end
end
end
body
// Try to parse the string
step id="s1" kind="call"
fn="parse_int"
arg name="s" from="s"
as="result"
end
// Handle the result
step id="s2" kind="match"
on="result"
case variant type="Ok" bindings=("value")
// Validate range
step id="s2a" kind="compute"
op=less
input var="value"
input lit=1
as="too_low"
end
step id="s2b" kind="compute"
op=greater
input var="value"
input lit=65535
as="too_high"
end
step id="s2c" kind="compute"
op=or
input var="too_low"
input var="too_high"
as="invalid"
end
step id="s2d" kind="if"
condition="invalid"
then
step id="s2e" kind="return"
variant type="ParseError"
field name="message" lit="Port out of range"
end
as="_"
end
end
else
step id="s2f" kind="return"
from="value"
as="_"
end
end
as="_"
end
end
case variant type="Err" bindings=("err")
step id="s2g" kind="return"
from="err"
as="_"
end
end
as="_"
end
end
end
Match Step
step id="s2" kind="match"
on="result"
case variant type="Ok" bindings=("value")
// handle success
end
case variant type="Err" bindings=("err")
// handle error
end
as="_"
end
on="result"- Match on theresultvariablecase variant type="Ok"- Handle the success casebindings=("value")- Extract the inner value
If Step
step id="s2d" kind="if"
condition="invalid"
then
// true branch
end
else
// false branch
end
as="_"
end
condition="invalid"- The boolean variable to check
Covenant has two query styles: structured (for Covenant types) and dialect (for SQL databases).
snippet id="users.find_active" kind="fn"
effects
effect database
end
signature
fn name="find_active_users"
returns collection of="User"
end
end
body
step id="s1" kind="query"
target="app_db"
select all
from="users"
where
equals field="active" lit=true
end
order by="name" dir="asc"
as="result"
end
step id="s2" kind="return"
from="result"
as="_"
end
end
end
For complex SQL, use a dialect block:
snippet id="analytics.top_customers" kind="fn"
effects
effect database
end
signature
fn name="get_top_customers"
param name="min_orders" type="Int"
returns collection of="CustomerStats"
end
end
body
step id="s1" kind="query"
dialect="postgres"
target="app_db"
body
SELECT
c.id, c.name,
COUNT(o.id) as order_count,
SUM(o.total) as total_spent
FROM customers c
JOIN orders o ON o.customer_id = c.id
GROUP BY c.id, c.name
HAVING COUNT(o.id) >= :min_orders
ORDER BY total_spent DESC
end
params
param name="min_orders" from="min_orders"
end
returns collection of="CustomerStats"
as="result"
end
step id="s2" kind="return"
from="result"
as="_"
end
end
end
Structured Query
target="app_db"- The database to queryselect all- Select all fieldsfrom="users"- From the users tablewhere ... end- Filter conditionsorder by="name" dir="asc"- Sort results
Dialect Query
dialect="postgres"- Use PostgreSQL syntaxbody ... end- Raw SQL (not parsed by Covenant)params ... end- Parameter bindingsreturns- Required type annotation
Covenant programs are organized into modules via snippet IDs.
IDs follow the pattern module.name:
users.find_by_id // users module, find_by_id function
users.create // users module, create function
auth.login // auth module, login function
auth.validate_token // auth module, validate_token function
Call functions by their full ID:
step id="s1" kind="call"
fn="users.find_by_id"
arg name="id" from="user_id"
as="user"
end
When building larger functions, effects compose:
snippet id="app.process_order" kind="fn"
// This function calls database and network functions,
// so it needs both effects
effects
effect database
effect network
end
signature
fn name="process_order"
param name="order_id" type="Int"
returns union
type="OrderResult"
type="ProcessingError"
end
end
end
body
// Get order from database
step id="s1" kind="call"
fn="orders.get_by_id"
arg name="id" from="order_id"
as="order"
end
// Call payment API
step id="s2" kind="call"
fn="payments.charge"
arg name="amount" field="total" of="order"
as="payment_result"
end
// Update order status
step id="s3" kind="update"
target="app_db.orders"
set field="status" lit="paid"
where
equals field="id" var="order_id"
end
as="_"
end
step id="s4" kind="return"
from="payment_result"
as="_"
end
end
end
| Concept | Syntax |
|---|---|
| Define function | snippet id="mod.name" kind="fn" |
| Declare effects | effects ... effect X ... end |
| Function signature | signature ... fn name="X" ... end |
| Implementation | body ... step ... end ... end |
| Computation | step kind="compute" op=add input... |
| Function call | step kind="call" fn="X" arg... |
| Return | step kind="return" from="X" |
| Conditional | step kind="if" condition="X" then... else... |
| Pattern match | step kind="match" on="X" case... |
| Query | step kind="query" target="X" select... from... |
- Read syntax-reference.md for complete keyword reference
- Study patterns.md for common idioms
- Browse examples/ for real-world usage
- Check reading-guide.md for understanding existing code