Recoil logo

Recoil

Language Guide

  • Home
  • Getting Started
  • Guide
  • Reference
  • PARSE
  • State Machines
  • FFI & Systems
  • Ports
  • Packages
  • f00 & Embedded
  • Examples
  • Internals

On this page

Loading…

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 a c-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 unsigned u8! 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 string
  • url!, 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:

CategoryTypesOn assignment
Copyintegers, floats, logic!, char!, enum!, size!duplicated; original stays valid
Movestring!, c-string!, c-pointer!, file!, port!, error!ownership transfers; original becomes inaccessible
Borrowslice! and get-word referencesa 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
Only two blocks follow 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
Borrow-checker error: '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

RangeCategoryExamples
300–399Mathdivision by zero, integer overflow
700–799Seriesindex out of bounds
800–899Usercustom application errors

The full taxonomy and the diagnostics format live in the Reference.

↑

Recoil — an ownership-first, statically-typed language with a static borrow checker.

codeberg.org/rebolek/recoil · boleslav@brezovsky.eu · Apache-2.0