Syntax basics
Recoil borrows Rebol's surface syntax. If you've never seen Rebol, here is everything you need — there are only a handful of lexical rules.
Words, values, and blocks
Code is made of words (names like print or
add), values (like 42 or
"hi"), and blocks — anything inside square
brackets [ ]. A block is just a grouped sequence of items; Recoil
uses blocks for function bodies, type specifications, and literal data alike.
; a comment starts with a semicolon and runs to end of line
print 42 ; a call: the word `print` applied to the value 42
Calls have no parentheses or commas
A function call is a word followed by its arguments, separated by spaces. Arguments are consumed left to right.
add 2 3 ; add(2, 3) in C terms
print add 2 3 ; print(add(2, 3))
Use parentheses only to group sub-expressions:
x: (10 + 5) * 2 ; 30
Set-words and get-words
A word ending in a colon is a set-word — it binds a value. A word starting with a colon is a get-word — it refers to something without moving it (used for borrowing and for passing functions).
count: 0 ; set-word: bind `count` to 0
greet :name ; get-word: borrow `name` for the call
Refinements and paths
A leading / marks a refinement — a named
variant or sub-call. A / between words or indices forms a
path.
file/open %data.txt ; `open` action in the `file` namespace
p/x ; field access
nums/0 ; index access (0-based)
nums/:idx ; index by a variable
Types end in !
By convention every type name ends with an exclamation mark:
i32!, logic!, string!,
struct!. The make word constructs typed values from a
type and an initializer.
point!: make struct! [x: i32! y: i32!]
Bindings & implicit typing
The idiomatic way to introduce a value is a set-word with a right-hand side. The type is inferred from that right-hand side — you rarely write it out.
count: 0 ; i32!
ratio: 3.14159 ; f64!
ch: #"A" ; char!
flag: true ; logic!
total: add 10 20 ; inferred from the call result → i32!
doubled: count * 2 ; inferred from the operands
When you need make
Use the explicit make Type! value form when inference has nothing
concrete to latch onto, or when you want a non-default type:
- a non-default width or type:
big: make i64! 1 - an owned
string!(a bare"..."literal is ac-string!):msg: make string! "hello" - typed or empty containers and structs:
nums: make [#mutable vector! [i32! 3]] [] - a value-position expression with no concrete type:
x: make i32! if cond [1] - callables and ports:
cb: make fn-ptr! add,fp: make port! file/open %data.txt
Mutability
Bindings are immutable by default. There is no mut
keyword — mutability is part of the value's type spec, written inside
make [ ... ]:
buf: make [#mutable string!] "hello"
buf/0: #"H"
grow: make [#flexible [string! 16]] none
append grow "world"
#mutable— contents may be modified in place (fixed size).#flexible— may also grow/shrink (append,insert,remove).
Datatypes
Recoil has a concrete, statically-known type for every value.
Scalars
- Integers:
i8!i16!i32!i64!and unsignedu8!u16!u32!u64! - Floats:
f32!f64! logic!,char!,size!none!— a safe "absence of value";void!— "no value at all" (function return only, cannot be assigned)
Owned & resource types
string!— the user-facing owned stringurl!,file!,port!,error!,c-pointer!
Structured & container types
vector!(fixed-size, homogeneous),array!,dict!,slice!(zero-copy view)struct!,enum!,sum!(tagged union),fsm!,rule!,bitset!,fn-ptr!
ABI-facing types
Boundary types for FFI and the C ABI: c-string!,
c-struct!, c-array!, c-fn!. In ordinary
code prefer string!; use c-string! only at the C
boundary. See FFI & Systems.
Copy, move, and borrow
How a value behaves on assignment depends on its type:
| Category | Types | On assignment |
|---|---|---|
| Copy | integers, floats, logic!, char!, enum!, size! | duplicated; original stays valid |
| Move | string!, c-string!, c-pointer!, file!, port!, error! | ownership transfers; original becomes inaccessible |
| Borrow | slice! and get-word references | a view; does not take ownership |
See Ownership & borrowing below for the rules the borrow checker enforces.
Indexed paths
voice!: make struct! [phase: f64! active: logic!]
voices: make [#mutable vector! [voice! 2]] []
idx: 1
voices/:idx/phase: 0.5 ; field through a variable index
print voices/:idx/phase
Control flow & expressions
Recoil is expression-oriented: most control forms produce a value.
if and either
Both are expressions. The chosen branch's last value becomes the result. An
expression if usually appears in a typed context such as
make.
if x > 10 [print "big"]
if [x > 10] [print "big"] ; block condition also works
label: either x > 10 [
1
] [
2
]
maybe: make i32! if true [1] ; value-position if needs an explicit type
match over sums
Branch on a sum! value and bind its payload:
status: match value [
some [payload] [1]
none [0]
]
all and any
Short-circuiting boolean expressions:
ok: make logic! all [1 = 1 2 = 2]
some: make logic! any [0 = 1 2 = 2]
Loops
while, repeat, and foreach are also
expressions: each yields its body's last value from the final iteration
(boxed as value!), or a boxed none! if the body never
ran.
i: make [#mutable i32!] 0
while [i < 5] [
print i
i: i + 1
]
repeat n 10 [print n] ; 0..9
foreach [key val] counts [ ; two-variable iteration over a dict
print val
]
Inside loops, break and continue behave as usual.
return expr exits the enclosing function.
go
go [body] queues a task body onto the runtime task queue. It is a
statement only and produces no value; tasks drain in FIFO order.
print "main"
go [
print "task"
]
Functions
Functions are defined with func: a spec block (parameters,
optional doc strings, and a return: type) followed by a body
block. The last expression is the result unless an earlier
return exits first.
add: func [
"Add two signed 32-bit integers."
a [i32!] "Left operand."
b [i32!] "Right operand."
return: [i32!] "The sum."
] [
a + b
]
print add 7 8 ; 15
A function with no return: returns void! and cannot be
assigned.
Borrowed parameters
Mark a parameter with a get-word to borrow it instead of taking ownership. Pass the argument as a get-word too:
greet: func [:name [string!] return: [none!]] [
print name
]
msg: make string! "hello"
greet :msg
print msg ; still valid — msg was borrowed, not moved
Method chaining
A refinement call passes the value on its left as the first argument, so calls chain left to right:
result: 10 /add 5 /mul 2 ; mul(add(10, 5), 2) = 30
Functions used in a chain must not return void!.
Exporting
Mark a function #export to expose it from a generated library
(see Packages & Libraries):
init: func [#export return: [none!]] [
print "ready"
]
Closures & function pointers
Callable values have type fn-ptr! and are introduced with
make fn-ptr!. A closure captures variables from its enclosing
scope via an explicit #capture list in its spec.
A plain function pointer
add: func [x [i32!] y [i32!] return: [i32!]] [
return x + y
]
callback: make fn-ptr! add
print callback 3 4 ; 7
Capture by value
base: 100
bump: make fn-ptr! func [#capture [base] a [i32!] return: [i32!]] [
a + base
]
print bump 7 ; 107
Capture by reference
Capture a mutable binding by get-word to mutate it through the closure:
counter: make [#mutable i32!] 0
inc: make fn-ptr! func [#capture [:counter] return: [i32!]] [
counter: counter + 1
return counter
]
print inc ; 1
print inc ; 2
func: the spec (which holds both
#capture and the typed parameters) and the body. A third block
leaves the body empty.
Ownership & borrowing
Recoil's defining feature is a static borrow checker that enforces memory safety at compile time, with no garbage collector. Every binding is in one of three states: owned (holds valid data), moved (ownership transferred away — using it is an error), or borrowed (referenced without transfer).
Move semantics
s1: make string! "Hello"
s2: s1 ; ownership moves to s2
print s1 ; compile error: 's1' used after move
's1' used after move
Recovering ownership
Assigning a fresh value makes a moved binding owned again:
s1: make string! "Hello"
s2: s1 ; s1 moved away
s1: make string! "Fresh" ; s1 owned again
print s1 ; ok
Borrowing avoids the move
greet: func [:name [string!]] [print name]
msg: make string! "hi"
greet :msg ; borrowed
print msg ; still owned
Branches
If a binding is moved in any branch, it is considered moved afterward:
either condition [
consume s1 ; moves s1
] [
print s1 ; ok in this branch
]
print s1 ; error: 's1' used after move
Mutability is enforced too
s: make string! "hi"
s/0: #"H" ; error: 's' is not mutable
s: make [#mutable string!] "hi"
s/0: #"H" ; ok
Error handling
Recoil reports problems at three layers: parse/type errors
and borrow-checker errors at compile time, and
runtime errors through a structured error!
value.
The error! value
e: make error! "something went wrong"
Automatic runtime checks
Division by zero, integer overflow, and out-of-bounds indexing set a
thread-local error state instead of crashing. Check it with
error?:
result: 10 / 0
if error? [print "a math error occurred"]
if error? e [print "this specific value is an error"]
Cleanup with defer
defer runs a block when the function exits (LIFO).
defer/error runs only when an error is set.
fp: make port! file/open %data.txt
defer [file/close fp] ; always runs on exit
defer/error [print "cleaning up after failure"]
Error categories
| Range | Category | Examples |
|---|---|---|
| 300–399 | Math | division by zero, integer overflow |
| 700–799 | Series | index out of bounds |
| 800–899 | User | custom application errors |
The full taxonomy and the diagnostics format live in the Reference.