Recoil — statically-typed language enforcing ownership-based memory safety with a static borrow checker
  • Rebol 58.7%
  • C 34.9%
  • HTML 3.1%
  • C++ 2.6%
  • CSS 0.3%
  • Other 0.4%
Find a file
Boleslav Březovský 528857209c
All checks were successful
Linux (fast) / RUT subset (linux-gcc) (push) Successful in 15m41s
docs(f00): note that arena sizing must account for total call/allocation count
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 15:17:04 +02:00
.claude/skills skills: require docstrings on every function, arg, and refinement 2026-06-28 21:18:27 +02:00
.cursor/skills/compact DOCS: add /compact skill for fresh-start context briefs 2026-04-28 18:51:17 +02:00
.forgejo/workflows ci: put the Linux workflows on master so the nightly schedule fires 2026-07-31 09:00:24 +02:00
.github/workflows Run Windows CI on this branch + add SChannel TLS runtime smoke 2026-06-08 11:33:50 +02:00
docs docs(f00): note that arena sizing must account for total call/allocation count 2026-09-18 15:17:04 +02:00
examples ext/sqlite: fix null db handle and invalid-free crash; wire examples into RUT 2026-07-02 11:47:25 +02:00
ext ext/sqlite: fix null db handle and invalid-free crash; wire examples into RUT 2026-07-02 11:47:25 +02:00
lib Commit remaining meaningful workspace changes 2026-06-27 12:43:05 +02:00
make Add standalone RUT build 2026-06-25 16:09:12 +02:00
packages std/port: drop legacy URL/compat helpers, migrate callers, fix read-url query keyword clash 2026-06-28 09:14:48 +02:00
src parser: zero-arg call is valid as if/either condition 2026-07-02 11:47:26 +02:00
std std/f00: report copy/compress/decompress allocation failure as out-of-memory 2026-09-18 13:20:29 +02:00
support Add standalone RUT build 2026-06-25 16:09:12 +02:00
tests test(f00): cover copy out-of-memory reporting 2026-09-18 15:04:15 +02:00
tools Prefer direct find truthiness 2026-06-25 21:35:53 +02:00
vendor Add raylib-terrain browser-WASM build with c-fn! callback lowering 2026-06-26 09:30:49 +02:00
web Add 0.11.0 release notes 2026-07-18 18:42:12 +02:00
.gitignore Add standalone RUT build 2026-06-25 16:09:12 +02:00
AGENTS.md Fix ownership transfer clears, Rebol extension OS_Free, and FFI param ownership 2026-06-24 08:50:19 +02:00
BUILD Add standalone RUT build 2026-06-25 16:09:12 +02:00
CLAUDE.md Fix parser diagnostics and update stale AST format tests 2026-04-01 20:26:02 +02:00
lib-rebol3-bulk-macos-x64.dylib release: 0.10.0 2026-03-09 06:30:44 +01:00
LICENSE Initial commit 2026-02-17 17:20:32 +01:00
README.md Add 0.11.0 release notes 2026-07-18 18:42:12 +02:00
recoil-do.reb Rewrite REPL using Rebol 3 line-editor! framework 2026-06-25 10:30:44 +02:00
recoil.r3 Add raylib-terrain browser-WASM build with c-fn! callback lowering 2026-06-26 09:30:49 +02:00
rut.r3 RUT: fix two more dead object? config guards in git-root detection 2026-06-30 00:01:19 +02:00

Recoil

A statically-typed language enforcing ownership-based memory safety with a static borrow checker with a rich datatypeset and simple syntax.

Installation and usage

Recoil is transpiled to C so to compile it you need C compiler. So far, Recoil was tested with GCC.

Recoil's transpiler to C is written in Rebol, so you need Rebol also. Rebol was originally written by Carl Sassenrath but he's not developing anymore, so http://rebol.com and http://rebol.net are good only as historical reference. Rebol is currently maintained by @oldes (David Oliva) and you can grab latest version from Github. Grab binary, there are three versions, base, core and bulk which differs how much batteries are included. Personally, I use bulk version. Rebol is available for Linux, MacOS, FreeBSD, OpenBSD, DragonFlyBSD, Haiku and even for Windows.

If you prefer Docker, you can grab Dockerfile from here. There are two versions, Alpine and Debian-slim which differs by used libC. Debian-slim uses more common glibc and Alpine uses musl.

Compile a file

rebol3 -s recoil.r3 myprogram.rcl

Build as shared library

rebol3 -s recoil.r3 --lib mylib.rcl

Build as static library

rebol3 -s recoil.r3 --lib mylib.rcl --static

For the complete command-line reference, including package subcommands, targets, and compiler/linker flag passthrough, see docs/user/cli.md.

Run the test suite

rebol3 -s rut.r3

Documentation

Overview

Recoil is a low-level, statically-typed programming language that uses Rebol's expressive syntax while providing compile-time memory safety. It transpiles to C, making it suitable for:

  • Systems programming with memory safety
  • Embedding in other applications
  • Writing small, dependency-free binaries
  • Interfacing with C libraries via FFI

The compilation pipeline:

.rcl source → Parser → AST → Borrow Checker → IR → Optimize → C → GCC

Compiler Diagnostics

Compile-time failures are reported in two forms:

  • a human-readable string such as TYPE ERROR: ...
  • a structured diagnostic object attached to the raised Rebol error!

The user-facing format, code ranges, and helper API are documented in docs/user/diagnostics.md.

If you are changing compiler stages, do not introduce new raw do make error! sites for user-facing diagnostics. Use the stage-local diagnostic helpers documented in docs/plans/diagnostic-contract.md.

Primitive Types

Recoil C Semantics
i8! i16! i32! i64! int8_t ... int64_t Copy
u8! u16! u32! u64! uint8_t ... uint64_t Copy
f32! f64! float double Copy
logic! int (0 or 1) Copy
char! char Copy
none! void * Safe null value
void! void Function return only
c-string! char * Move
string! struct { char *data; size_t len; } Move
c-pointer! void * Move
file! FILE * Move
error! error_t * Move — structured error value
slice! struct { void *ptr; size_t len; size_t elem; } Zero-copy view
enum! int (named constants) Copy

none! vs void!

  • none! is a safe value representing "absence of value":

    x: none          ; valid — inferred none!
    
  • void! represents "no return value" and cannot be assigned:

    f: func [] []    ; returns void!
    x: f             ; ERROR: Cannot assign void
    

Syntax

Variables

Bindings are implicitly typed — the compiler infers the type from the right-hand side. Use make only for a specific type or a typed container.

x: 42                      ; i32! (inferred)
pi: 3.14159                ; f64!
big: make i64! 42          ; explicit non-default width
msg: make string! "hello"  ; owned string! (a bare "..." literal is a c-string!)

Functions

add: func [
    a [i32!]
    b [i32!]
    return: [i32!]
] [
    return a + b
]

print add 10 20    ; 30

Borrowed parameters use get-word (:name):

greet: func [:name [c-string!] return: [none!]] [print name]
greet :msg         ; borrow — msg stays owned

Vectors

; Heap-allocated (default)
arr: make [vector! [i32! 5]] [1 2 3 4 5]

; Stack-allocated
arr: make [vector! [i32! 5 #stack]] [1 2 3 4 5]

; Mutable
nums: make [#mutable vector! [i32! 5]] [10 20 30 40 50]
nums/0: 42         ; element access
print nums/0       ; 42

Structs

point!: make struct! [x: i32! y: i32!]
p: make [#mutable point!] [x: 10 y: 20]
print p/x          ; 10
p/y: 30            ; field assignment

Control Flow

; If
if x > 10 [print "big"]
if [x > 10] [print "big"]    ; block condition also works

; Either (if-else)
either x > 10 [print "big"] [print "small"]

; While
while [i < 5] [print i  i: i + 1]

; Repeat (0-indexed)
repeat i 10 [print i]    ; prints 0-9

Method Chaining (Refinements)

add: func [a [i32!] b [i32!] return: [i32!]] [return a + b]
mul: func [a [i32!] b [i32!] return: [i32!]] [return a * b]

result: 10 /add 5 /mul 2    ; mul(add(10, 5), 2) = 30

Closures

; Explicit capture (by value)
x: 10
adder: make fn-ptr! func [#capture [x] a [i32!] return: [i32!]] [
    return a + x
]
result: adder 5    ; 15

; By-reference capture (mutation)
count: make [#mutable i32!] 0
counter: make fn-ptr! func [#capture [:count] return: [i32!]] [
    count: count + 1
    return count
]

Enums

Color!: make enum! [red green blue]

c: Color!/red
if c = Color!/blue [print "blue"]

Enum values are integer-backed copy values. By default, variants count up from 0 in declaration order. You can also give explicit integer values; later implicit variants continue from the previous value:

Status!: make enum! [idle 10 busy done]

print Status!/idle  ; 10
print Status!/busy  ; 11
print Status!/done  ; 12

Use Type!/variant path access at call sites. Variant names are scoped by their enum type in generated C, so natural names such as connect, idle, or error are safe inside different enums.

Slices

Zero-copy views into vectors or strings:

nums: make [#mutable vector! [i32! 8]] [1 2 3 4 5 6 7 8]

; slice! s points at nums[2..4]
s: make slice! at nums 2 3

; Read through a slice
val: s/0         ; nums[2]

; Write through a slice
s/1: 99          ; nums[3] = 99

Error Handling

Recoil has a structured error system. Arithmetic overflow, division by zero, and out-of-bounds access all set a global error state automatically.

; Create a user error
e: make error! "something went wrong"

; Check global error state (overflow, div-zero, bounds)
result: 10 / 0
if error? [print "math error"]

; Check a specific error value
if error? e [print "local error"]

Errors are propagated via defer for cleanup:

; Always runs on exit (LIFO)
defer [free-resource r]

; Runs only when an error occurred
defer/error [print "cleaning up after error"]

Defer

defer schedules cleanup code to run at function exit in LIFO order:

f: make port! file/open %data.txt
defer [file/close f]        ; always runs on return

; defer/error runs only when __recoil_last_error is set
defer/error [log-failure]

addr-of

Take the address of a variable:

x: 42
p: addr-of x

Unsafe Namespace

Direct access to C standard library functions without FFI boilerplate:

buf: unsafe/malloc 1024
unsafe/memset buf 0 1024
unsafe/free buf

FFI

#include <curl/curl.h>
#link "-lcurl"

curl: foreign <curl> [
    easy-init: "curl_easy_init" [return: :CURL]
    easy-setopt: "curl_easy_setopt" [:CURL int c-string! return: int]
    easy-perform: "curl_easy_perform" [:CURL return: int]
    easy-cleanup: "curl_easy_cleanup" [:CURL]
]

handle: curl/easy-init
curl/easy-setopt handle 10002 "http://example.com"
result: curl/easy-perform handle
curl/easy-cleanup handle

Declare external C variables with #extern:

#extern c-pointer! some_global_from_c

Type Casting

a: 10                  ; i32!
x: as f32! a           ; explicit cast i32! -> f32!
c: x + as f32! 5       ; mixed numeric types require an explicit cast

Ownership Model

Recoil uses Rust-inspired ownership semantics for memory safety without garbage collection. See Ownership, Borrowing, And Mutability for the detailed current model.

Copy Types

Primitives (i32!, f64!, logic!, etc.) are copied on assignment:

a: 10
b: a             ; a is copied
print a          ; OK — a is still valid

Move Types

Move-sensitive owned values such as string!, block!, map!, dict!, port!, and c-pointer! move on owned transfer:

s1: make string! "hello"
s2: s1              ; ownership moves to s2
print s2            ; OK
print s1            ; ERROR: 's1' used after move

Borrowing

Pass by reference with get-word (:var):

greet: func [:name [string!]] [print name]
greet :msg          ; msg is borrowed, not moved
print msg           ; OK, msg is still owned

print itself borrows owned strings. print msg does not move msg; owned temporary string expressions passed to print are cleaned up after printing.

Mutation

Prefer expression-based flow and use mutation only where an API intentionally updates existing state. Type attributes describe values that need mutation or growth capabilities:

status: either ok? ["ok"] ["failed"]
next-count: count + 1

s: make string! "hello"
s/0: #"H"           ; ERROR: 's' is not mutable

buf: make [#mutable string!] "hello"
buf/0: #"H"         ; OK

grow: make [#flexible [string! 16]] none

Branching

Variables moved in any branch are considered moved afterward:

s1: "hello"
either condition [
    moved: s1
] [
    print s1        ; OK here
]
print s1            ; ERROR: 's1' used after move

Module Reference

File Role
recoil.r3 Entry point - CLI, GCC execution
src/compiler.r3 Orchestrates all stages
src/parser.r3 Source → AST (to-ast)
src/borrow-checker.r3 Ownership analysis (analyze)
src/ir.r3 AST → IR, optimization (ast-to-ir, optimize)
src/c-generator.r3 IR → C code (emit-c)
src/core/ffi.r3 FFI parsing and type mapping
src/core/runtime.r3 Runtime declarations and helper registrations
src/core/tools.r3 Operator table, name mangling utilities
src/core/datatypes.r3 Type-action mappings
src/core/shared.r3 Global state containers
src/parser/*.r3 Parser helper modules (specs, types, grammar, comptime)
src/parser/rules/*.r3 Runtime-loaded parser rule assets (term, grammar, control, statements)
src/codegen/*.r3 Codegen helper modules (expr, statements, decls, parse, module-globals)
src/compiler/*.r3 Compiler helper modules (modules, generics, alpha, optimize, header/C support)

Testing

# Run all tests
rebol3 -s rut.r3

# Run specific group
rebol3 -s rut.r3 --group borrow-checker
rebol3 -s rut.r3 -g types

# Run specific test
rebol3 -s rut.r3 -t "Mandelbrot"

# Filter by tag
rebol3 -s rut.r3 -tg closure -tg fn-ptr

# Fast compiler/C verification pass
rebol3 -s rut.r3 --transpile-only

# Native compile/run from cached transpiled C
rebol3 -s rut.r3 --compile-only

# Other options
rebol3 -s rut.r3 --fail-fast      # Stop on first failure
rebol3 -s rut.r3 --quiet          # Less output
rebol3 -s rut.r3 --shuffle 42     # Shuffle with seed
rebol3 -s rut.r3 --list           # List available tests

RUT source-backed tests now declare a single source: value plus optional transpile: and compile: blocks. The transpile phase caches emitted C with a content-derived cache key, and the compile phase reuses that cached C instead of running the Recoil compiler again. RUT also enforces a single active runner via cache/rut.lock/; overlapping runs fail fast, and stale locks from dead processes are cleared automatically.

Comparison

Feature Recoil Rust C3 Zig Go Nim Rebol Red
Memory safety Ownership Ownership Ownership Allocator GC GC/opt GC GC
Static typing Yes Yes Yes Yes Yes Yes No Yes
Compiles to C Yes No Yes Yes No Yes No No
Garbage collection No No No No Yes Optional Yes Yes
Borrow checker Yes Yes Yes No No No No No
Closure support Yes Yes Yes Yes Yes Yes Yes Yes
FFI to C Yes Yes Yes Yes Yes Yes Yes Yes
Zero-cost abstractions Yes Yes Yes Yes No Partial No No

What is Recoil Good For?

  • Small binaries — No runtime, minimal C output with no dependencies
  • FFI simplicity — Clean, declarative syntax for interfacing with C libraries
  • Memory safety without GC — Ownership model prevents use-after-free and double-free at compile time
  • Embedding — Small C output is ideal for embedding in other applications or scripting engines
  • Rebol syntax — Expressive, declarative syntax for developers who prefer Rebol's style
  • Gradual learning — Simple ownership model without Rust's complexity; easier to learn for those coming from dynamic languages
  • Library building — Built-in support for generating shared/static libraries with header files

License

Apache2 License — see LICENSE