Introduction
Recoil is an ownership-first, statically-typed language that compiles to clean, readable C. It combines memory safety through static borrow checking with the expressiveness of Rebol-inspired syntax.
Key Features
Memory Safety: Compile-time ownership tracking prevents use-after-free and double-free errors
Static Typing: All variables have explicit types known at compile time
Zero-Cost Abstractions: High-level syntax compiles to efficient low-level code
FFI Support: Call foreign libraries directly from Recoil code
Closures: First-class function closures with captured variables
Method Chaining: Elegant refinement syntax for piped function calls
Built-in Optimizations: Constant folding and dead code elimination
Quick Start
Installation
Recoil requires Rebol 3 (r3) and a C compiler (GCC/Clang). Clone the repository:
git clone https://github.com/anomalyco/recoil.git
cd recoil
Run Tests
Verify your setup by running the test suite:
r3 rut.r3
Compile & Run
Create a file called hello.rcl:
; Hello World in Recoil
print "Hello, Recoil!"
Compile and run it:
r3 recoil.r3 hello.rcl
Try it (copy/download)
Examples
A selection of test programs and examples from the repository.
Primitive Types
Recoil provides a rich set of primitive types. Types ending with ! follow Rebol naming conventions.
Numeric Types
| Recoil Type | C Type | Semantic |
|---|---|---|
i8! i16! i32! i64! |
int8_t ... int64_t | Copy |
u8! u16! u32! u64! |
uint8_t ... uint64_t | Copy |
f32! f64! |
float, double | Copy |
Other Primitives
| Recoil Type | C Type | Semantic |
|---|---|---|
logic! |
int (0 or 1) | Copy |
char! |
char | Copy |
none! |
void * | Copy |
c-string! |
char * | Move |
string! |
struct { char *data; size_t len; } | Move |
c-pointer! |
void * | Move |
file! |
FILE * | Move |
slice! |
struct { void *ptr; size_t len; size_t elem; } | Borrow |
enum! |
int | Copy |
error! |
error_t * | Move |
fn-ptr! |
function pointer / closure wrapper | Copy |
size! |
size_t | Copy |
Operators
Recoil supports standard arithmetic, comparison, and logical operators with defined precedence.
Precedence (Higher = Tighter)
| Level | Operators |
|---|---|
| 20 | * / % |
| 10 | + - |
| 5 | = == <> < > <= >= |
| 3 | and or |
Arithmetic Examples
i32! a: 10 + 5 * 2 ; = 20 (multiplication binds tighter)
i32! b: (10 + 5) * 2 ; = 30
Comparison Examples
logic! eq: 10 = 10 ; true
logic! ne: 10 <> 5 ; true
logic! gt: 10 > 5 ; true
Logical Examples
logic! both: and true false ; false
logic! either: or true false ; true
logic! not-true: not true ; false
Type Casting
f32! x: as f32! 10 ; cast integer to float
Mutability
Recoil enforces immutability by default. Variables cannot be modified unless explicitly declared with mut.
Immutable by Default
i32! x: 10
x: 20 ; OK — reassignment changes the binding
x: x + 1 ; OK — creates new value
Mutable Variables
mut i32! counter: 0
counter: counter + 1 ; OK — mut allows in-place modification
print counter ; prints: 1
Mutable Strings
Strings can be made mutable for path-based character access:
mut c-string! s: "hello"
s/0: 72 ; Change 'h' (ASCII 72)
print s ; prints: cello
Immutable Enforcement
The borrow checker prevents mutation of immutable variables:
string! s: "hello"
s/0: 72 ; ERROR — s is not mutable
BORROW CHECKER ERROR: 's' is not mutable
Functions
Functions are defined using the func keyword with type annotations for parameters and return values.
Basic Function
add: func [
a [i32!]
b [i32!]
return: [i32!]
] [
return a + b
]
i32! result: add 5 10
print result ; prints: 15
Void Function
log-number: func [
val [i32!]
] [
print val
]
log-number 100 ; prints: 100
Borrowed Parameters
Use :name (get-word) to borrow without moving ownership:
greet: func [
:name [string!] ; borrowed parameter
] [
print name
]
string! msg: "Hello"
greet :msg ; borrowed — msg stays alive
print msg ; valid — msg was borrowed, not moved
Function Pointers
adder: func [a [i32!] b [i32!] return: [i32!]] [
return a + b
]
fn-ptr! callback: adder
i32! result: call callback [3 4]
Exporting Functions
Use #export to mark functions for library export:
init: func [
#export
return: [none!]
] [
print "Library initialized"
]
Self-Documenting Headers
Function specs can include documentation strings for the function, parameters, and return value:
add: func [
"Add two integers."
a [i32!] "Left operand."
b [i32!] "Right operand."
return: [i32!] "Sum of a and b."
] [
return a + b
]
Annotations like #export can appear before or after the doc string:
init: func [
#export
"Initialize the library."
return: [none!] "No return value."
] [
print "Ready!"
]
Borrowing & Ownership
Recoil's key innovation is a static borrow checker that enforces memory safety at compile time through ownership rules.
Variable States
A variable can be in one of three states:
Owned: The variable holds valid data and can be read or moved
Moved: Ownership was transferred to another variable; accessing triggers a compile error
Borrowed: Used via a reference without transferring ownership
Move Semantics
string! s1: "Hello"
string! s2: s1 ; s1 is now 'moved'
print s1 ; ERROR: Use of 's1' after move
BORROW CHECKER ERROR: 's1' used after move
Ownership Recovery
Assigning a new value restores ownership:
string! s1: "Hello"
string! s2: s1 ; s1 moved to s2
s1: "Fresh Start" ; s1 is now 'owned' again
print s1 ; OK — prints: Fresh Start
Borrowing with Get-Words
Use :variable to borrow without moving:
greet: func [
:name [string!]
] [
print name
]
string! msg: "Hello"
greet :msg ; borrowed — msg stays alive
print msg ; valid — msg was borrowed
Borrowing in Conditionals
When a variable is moved in one branch of either, it's considered moved in the entire scope:
string! s1: "hello"
logic! condition: true
either condition [
print s1 ; s1 is used here (not moved)
] [
s1: "new" ; s1 is moved here
]
print s1 ; OK — s1 was reassigned in else branch
Error handling
Recoil reports errors at multiple layers: parse/type errors (compile-time), borrow-checker errors (ownership violations), and runtime errors reported via the runtime error API. The compiler emits ir-make-error and ir-error-check nodes so defer/error handlers can run when errors occur.
The generated C uses a thread-local __recoil_last_error and helpers (error_new, error_is_error, error_format, error_free) in the runtime so user code and defers can inspect and format errors.
Common examples
- Division by zero: the emitted C checks for zero and sets a math error (division-by-zero) instead of crashing.
- Out-of-bounds: accessing a vector with an invalid index sets a series/bounds error.
- Borrow errors: the borrow checker emits errors like
BORROW CHECKER ERROR: 'x' used after moveat compile time.
Try these tests
See repo test cases demonstrating error conditions:
- error-test.rcl — generic error handling examples
- void-assign-error.rcl — assigning a void return triggers a compile error
- borrow-test.rcl — borrow/move errors and mutability checks
i32! a: 10
i32! b: 0
i32! c: a / b ; runtime error: division by zero
Defer & defer-error
Use defer to run cleanup code when a function exits. Use defer-error to run a block only when an error has been set. The generated C sets a thread-local __recoil_last_error and the emitter wraps defer-error blocks with a guard such as if (__recoil_last_error != NULL) { ... }.
; Example: cleanup and error-only handler
do-stuff: func [] [
defer [ print "cleanup always runs" ]
defer-error [ print "error occurred" ]
; trigger an error (make-error used by compiler)
return make-error "something went wrong"
]
The error! Type
error! is a type representing runtime errors:
; Create an error value
error! err: make error! "something went wrong"
Error Codes and Categories
| Range | Category | Examples |
|---|---|---|
| 300 | Math | division by zero |
| 301 | Math | integer overflow |
| 700 | Series | index out of bounds |
| 800-899 | User | custom application errors |
The none? Predicate
Use none? to check if a nullable value is none (NULL):
c-pointer! ptr: none
if none? ptr [
print "pointer is null"
]
The error? Predicate
Use error? to check if an error is currently set:
i32! result: 10 / 0
if error? [
print "an error occurred"
]
Control Flow
While Loop
i32! i: 0
while [i < 5] [
print i
i: i + 1
]
Repeat Loop
; Repeat with index variable (0 to count-1)
repeat i 10 [
print i
]
If / Either
i32! a: 10
i32! b: 20
; Bare expression condition
if a > b [
print "a is greater"
]
; Block condition
if [a > b] [
print "a is greater"
]
; Either (if-else)
either a > b [
print "yes"
] [
print "no"
]
Parse
Recoil includes a built-in parse function for string-oriented rule parsing. It returns logic! — true if all rules succeed, false otherwise.
Basic Examples
; Exact string match
print parse "abcd" ["ab" "cd"] ; true
; Using 'to' — advance to value
print parse "abcd" [to "cd" "cd"] ; true
; Using 'thru' — advance past value
print parse "abcd" [thru "ab" "cd"] ; true
; Alternation
print parse "ab" ["ab" | "cd"] ; true
Using rule!
Create reusable parse rules with make rule!:
; Define a reusable rule
rule! ab: make rule! [thru "ab"]
; Use in parse
print parse "ababab" [some ab] ; true — matches "ab" one or more times
Rule Keywords
| Keyword | Description |
|---|---|
to |
Search forward to value, cursor at start of match |
thru |
Search forward to value, cursor past end of match |
some |
Repeat rule one or more times |
| |
Alternation — try left, then right |
parse currently supports string! and c-string! inputs. Rule bindings can be rule!, string!, or c-string!.
Structs
Structs define custom compound types with named fields.
Defining and Using Structs
; Define a struct type
point!: make struct! [x: i32! y: i32!]
; Create an instance
point! p1: [x: 10 y: 20]
; Access fields
print p1/x ; prints: 10
print p1/y ; prints: 20
; Modify fields
p1/x: 42
print p1/x ; prints: 42
Vectors
Vectors are fixed-size arrays with optional heap/stack allocation.
Heap Vectors (Default)
; Heap-allocated vector (default)
vector! [i32! 5] arr: [1 2 3 4 5]
print arr/0 ; prints: 1
print arr/4 ; prints: 5
Stack Vectors
; Stack-allocated vector
vector! [i32! 5 #stack] arr: [1 2 3 4 5]
Mutable Vectors
; Mutable heap vector
mut vector! [i32! 5 #heap] arr: [0 0 0 0 0]
arr/0: 42
print arr/0 ; prints: 42
length? to get vector length at compile time.
Slices
A slice! is a zero-copy view into a vector or string. It does not take ownership of the source data.
Creating Slices with at
; Create a slice view into a vector
vector! [i32! 5] nums: [10 20 30 40 50]
slice! s: at nums 1 3 ; view elements 1-3 (20, 30, 40)
print length? s ; prints: 3
print s/0 ; prints: 20
s/0: 42
print nums/1 ; prints: 42 (slice is a view, not a copy)
Slices are Borrowed
Creating a slice does not move or consume the source. The source remains usable:
string! text: "Hello, World!"
slice! greeting: at text 0 5
print text ; OK — text was not moved
print greeting ; prints: Hello
Enums
Enums define named integer constants grouped into a type. Variants are auto-numbered starting at 0.
Defining Enums
; Define an enum type
Color!: make enum! [red green blue] ; red=0, green=1, blue=2
; Create an enum value
Color! c: Color!/red
; Use in comparisons
either c = Color!/green [
print "green"
] [
print "not green"
]
print c ; prints: 0
Generated C
Enums generate standard C enum definitions:
/* Generated C: */
typedef enum {
red = 0,
green = 1,
blue = 2
} Color;
Color c = red;
EnumName/variant path syntax.
Method Chaining
Recoil supports elegant method chaining using refinement syntax. The value on the left is passed as the first argument to each refinement call.
Basic Chaining
; Define helper functions
add: func [a [i32!] b [i32!] return: [i32!]] [return a + b]
mul: func [a [i32!] b [i32!] return: [i32!]] [return a * b]
sub: func [a [i32!] b [i32!] return: [i32!]] [return a - b]
; Chain calls left-to-right
i32! result: 10 /add 5 /mul 2 /sub 3
print result ; prints: ((10 + 5) * 2) - 3 = 27
How It Works
| Syntax | Equivalent |
|---|---|
x /add 5 |
add(x, 5) |
x /add 5 /mul 2 |
mul(add(x, 5), 2) |
x /add 5 /mul 2 /sub 1 |
sub(mul(add(x, 5), 2), 1) |
With Comparisons
greater?: func [a [i32!] b [i32!] return: [logic!]] [return a > b]
logic! result: 10 /add 5 /greater? 20
either result [
print "yes"
] [
print "no"
] ; prints: yes (15 > 20 is false, so result is false)
void!. The compiler enforces this at compile time.
Type Casting
Recoil provides explicit type casting for safe numeric conversions.
The as Operator
i32! a: 10
f32! b: as f32! a
print b ; prints: 10.0
Generated C Code
The as operator generates a C cast:
; Recoil:
f32! b: as f32! a
; Generated C:
float b = (float)(a);
Type Mixing Errors
Mixing types without explicit casting results in a compile error:
i32! a: 10
f32! b: a + 5.5 ; ERROR: Type mismatch
TYPE ERROR: Mixed types in expression
Closures
Closures capture variables from their enclosing scope, enabling powerful functional programming patterns.
Explicit Capture by Value
i32! x: 10
fn-ptr! adder: func [
#capture [x]
a [i32!]
return: [i32!]
] [
return a + x
]
i32! result: call adder [5]
print result ; prints: 15
Auto-Capture
Free variables are automatically captured:
i32! base: 100
; Closure auto-captures free variable
fn-ptr! inc: func [a [i32!]] [
base: base + a
]
call inc [7]
print base ; prints: 107
By-Reference Capture
Use get-words to capture by reference, allowing mutation:
mut i32! counter: 0
fn-ptr! inc: func [
#capture [:counter]
a [i32!]
] [
counter: counter + a
]
call inc [1]
call inc [1]
call inc [1]
print counter ; prints: 3
Closures Inside Functions
Closures can be declared inside user-defined functions:
make-adder: func [
n [i32!]
return: [fn-ptr!]
] [
return func [
#capture [n]
x [i32!]
return: [i32!]
] [
return x + n
]
]
fn-ptr! add5: make-adder 5
fn-ptr! add10: make-adder 10
print call add5 [3] ; prints: 8
print call add10 [3] ; prints: 13
Built-in Functions
Recoil provides several built-in functions that work across types.
Polymorphic printing for all types:
print 42 ; integers
print "hello" ; strings
print true ; logic values
length?
Get the length of vectors and strings:
vector! [i32! 5] arr: [1 2 3 4 5]
i32! len: length? arr ; returns compile-time known size
string! s: "hello"
i32! n: length? s ; returns string length
allocate
Allocate raw heap memory:
vector! [u8! 100] buf: allocate 100
print length? buf ; prints: 100
call
Call a function pointer:
fn-ptr! fn: some-func
call fn [arg1 arg2]
File I/O
Recoil provides a built-in file namespace for file operations. file/open defaults to read mode, and mut file/open selects write mode.
Opening and Reading Files
; Open a file for reading
file! f: file/open "data.txt"
; Open a file for writing
file! out: mut file/open "out.txt"
; Read content (simplified — actual API may vary)
either f [
print "File opened successfully"
file/close f
] [
print "Failed to open file"
]
File Operations
| Function | Description |
|---|---|
file/open |
Open file; mut file/open uses write mode |
file/read |
Read from file |
file/write |
Write to file |
file/close |
Close file |
file/eof |
Check end-of-file |
Foreign Function Interface (FFI)
Call C libraries directly from Recoil using the foreign keyword.
Defining Foreign Functions
; Declare a foreign namespace
curl: foreign <curl> [
easy-init: "curl_easy_init" [return: :CURL]
easy-setopt: "curl_easy_setopt" [:CURL c-string! i32! return: i32!]
easy-perform: "curl_easy_perform" [:CURL return: i32!]
]
Using Foreign Functions
; Call foreign functions
c-pointer! c: curl/easy-init
i32! result: curl/easy-setopt c "URL" 1
result: curl/easy-perform c
C Includes
Include C headers in the generated code:
; Include C headers
#include <stdio.h>
#include <stdlib.h>
Library Linking
Link against external libraries:
; Link against a library
#link "curl"
Library Building
Recoil can compile to shared and static libraries for use in other programs.
Exporting Functions
Use #export to mark functions for export:
init: func [
#export
return: [none!]
] [
print "Library initialized"
]
add: func [
#export
a [i32!]
b [i32!]
return: [i32!]
] [
return a + b
]
Building Libraries
Compile to shared or static library:
; Build shared library
r3 recoil.r3 lib.rcl --lib
; Build static library
r3 recoil.r3 lib.rcl --static
#include to include necessary C headers in the library.
Optimizations
Recoil includes built-in optimizations that run during compilation.
Constant Folding
Arithmetic expressions are evaluated at compile time:
; Recoil source:
i32! a: 3 + 5
; Generated C:
int a = 8;
Dead Code Elimination
Unreachable branches are removed when conditions are constant:
; Recoil source:
either true [
print "always true"
] [
print "never"
]
; Generated C (dead branch removed):
printf("always true");
Testing
Recoil includes a test runner called RUT for running unit tests.
Running Tests
; Run all tests
r3 -s rut.r3
; Run specific test group
r3 -s rut.r3 --group types
; Run tests matching a tag
r3 -s rut.r3 -t "closure basic"
; List all available tests
r3 -s rut.r3 --list
Test File Organization
Tests are located in the tests/ directory:
tests/tests.reb— test declarationstests/*.rcl— test source files
Transpile Only
To verify generated C code without compiling:
r3 -s rut.r3 --transpile-only
none! vs void!
Recoil distinguishes between two important concepts:
none! — Safe Null Value
The none! type represents a safe null value. It can be assigned and used:
none! x: none
print x ; prints: (null) or similar
void! — No Return Value
The void! type represents "no return value". Functions without a return: spec return void:
log: func [msg [string!]] [
print msg
]
; log returns void — cannot be assigned
log "hello"
Cannot assign void return value
Internals
The Recoil compiler processes source through a multi-stage pipeline:
Pipeline Stages
| Stage | Description |
|---|---|
| Parse | Converts source (.rcl) to an AST using a custom Rebol-like parse dialect. |
| AST | Abstract syntax tree — the initial representation of the code. |
| IR | Intermediate representation — suitable for optimization and analysis. |
| Analyze | Static borrow checker enforces ownership rules and prevents memory errors at compile time. |
| Optimize | Constant folding and dead code elimination. |
| Emit C | Generates clean, optimized C code ready for compilation. |
Module Headers
Recoil source files can start with a module header to declare metadata:
Recoil [
name: "mymodule"
version: 1.0.0
]
; ... module code ...
Contact
If you have questions or issues, please file them on the project tracker or reach out via the channels below.
- Code repository & issues: https://codeberg.org/rebolek/recoil
- Email: boleslav@brezovsky.eu
- Mastodon: @recoil@mastodon.social