Recoil — statically-typed language enforcing ownership-based memory safety with a static borrow checker
  • Rebol 55.3%
  • C 35.2%
  • HTML 5.1%
  • C++ 2.2%
  • JavaScript 1.1%
  • Other 0.8%
Find a file
Boleslav Březovský 5a868f7534
Some checks failed
Linux (fast) / RUT subset (linux-gcc) (push) Failing after 54s
feat(parser): a module header carries typed Requires, Provides, Selectors, Providers, Link, Include, Linkage and Name fields, and an unknown field is a P1200 error, not silently stored
The fields are parsed and shape-checked only; nothing reads them yet. Type: is
limited to module, library and rebol-extension. Documentation fields and Needs:
stay legal. Name: moves from extra-fields to header/name (istar #370).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-06 23:18:52 +01:00
.claude Merge docs/f00-zlib-skills: the f00 skills and parse-cache comments name recoil-zlib, not -lz (#366) 2026-11-06 22:06:16 +01:00
.cursor/skills/compact DOCS: add /compact skill for fresh-start context briefs 2026-04-28 18:51:17 +02:00
.forgejo/workflows docs(web): every project link points at recoil-lang.eu, not rblk.eu 2026-10-30 18:17:15 +01:00
benchmarks perf(markdown): avoid per-character escape allocations 2026-10-07 20:23:18 +02:00
docs feat(parser): a module header carries typed Requires, Provides, Selectors, Providers, Link, Include, Linkage and Name fields, and an unknown field is a P1200 error, not silently stored 2026-11-06 23:18:52 +01:00
examples fix(rut): the gui image lifecycle test waits for its parent window, and compares the output it prints 2026-10-28 19:15:15 +01:00
ext feat(sqlite): expose int64 and double numeric bindings 2026-09-06 20:58:24 +02:00
lib fix(runtime): a connected SChannel port reports readable, and an HTTP response read waits instead of spinning 2026-10-27 11:47:57 +01:00
make refactor(pokuston): the compiler uses the separate product checkout for plug-in builds and tests 2026-09-30 09:46:42 +02:00
packages merge: master's std/f00 out-of-memory reporting lands in the packaged f00 runtime 2026-10-30 17:57:02 +01:00
src feat(parser): a module header carries typed Requires, Provides, Selectors, Providers, Link, Include, Linkage and Name fields, and an unknown field is a P1200 error, not silently stored 2026-11-06 23:18:52 +01:00
std fix(runtime): a connected SChannel port reports readable, and an HTTP response read waits instead of spinning 2026-10-27 11:47:57 +01:00
support docs(release): the comptime bullet and the diagnostics count match the compiler, and stale source pointers are gone 2026-10-31 17:05:01 +01:00
tests feat(parser): a module header carries typed Requires, Provides, Selectors, Providers, Link, Include, Linkage and Name fields, and an unknown field is a P1200 error, not silently stored 2026-11-06 23:18:52 +01:00
tools build(tools): a profiler that times compile/ast inside a RUT capability context 2026-10-01 06:32:22 +02:00
vendor feat(packages): recoil-sqlite vendors the SQLite amalgamation, and a single-file build that lacks sqlite3.h fetches it 2026-10-27 17:36:21 +01:00
web build(web): the f00 web REPL wasm is rebuilt from the runtime with backtraces and out-of-memory reporting 2026-10-31 17:06:51 +01:00
.gitignore chore(git): ignore the standalone macOS recoil and rut builds and AppleDouble files, like the Linux builds 2026-10-16 23:03:28 +02:00
.gitlab-ci.yml docs(web): every project link points at recoil-lang.eu, not rblk.eu 2026-10-30 18:17:15 +01:00
AGENTS.md docs(agents): the skill table carries triggers only for skills hidden from the listing, and the routing prose is one paragraph 2026-11-04 16:40:42 +01:00
BUILD docs(rut): an external suite runs through the relocated runner, and packages document the source form 2026-09-30 09:45:02 +02:00
CLAUDE.md docs: consolidate agent instructions 2026-07-30 09:56:50 +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 docs(web): the docs and web pages describe what the compiler does now, and the CLI examples run 2026-10-30 15:15:10 +01:00
recoil-do.reb Rewrite REPL using Rebol 3 line-editor! framework 2026-06-25 10:30:44 +02:00
recoil.r3 fix(build): the macOS build calls clang directly, not through the xcrun shim 2026-10-29 15:38:57 +01:00
rut.r3 fix(rut): a test that builds in its worker's own target tree no longer queues on the shared one 2026-10-29 21:59:07 +01:00

Recoil

A statically typed language with ownership-based memory safety, a static borrow checker, a rich type system, and Rebol-inspired syntax.

New to Recoil? Start with the quick-start build below, then follow the user guide for the language and package workflow.

On this page

Quick start

Recoil transpiles to C. To build programs from this checkout, install:

  • an Oldes Rebol3 interpreter, 3.22.0 or newer (shown below as r3; use your local executable name if it differs). Earlier builds cannot parse -> in generic constraints and fail to load the test suite;
  • a C toolchain — GCC is the default, and a supported target toolchain may be selected with --target or --cc.

Compile and run a program:

r3 -s recoil.r3 myprogram.rcl

Transpile without compiling the generated C:

r3 -s recoil.r3 --transpile -o myprogram.c myprogram.rcl

Build a library:

r3 -s recoil.r3 --lib mylib.rcl          # shared library
r3 -s recoil.r3 --lib mylib.rcl --static # static library

Run the test suite:

r3 -s rut.r3

-s disables Rebol security for the wrapper process; builds and tests need it to write generated C, artifacts, and cache files. The CLI also supports project and package commands such as init, install tls, install f00, fetch, doctor, and repair. See the command-line reference for the complete syntax, targets, and compiler/linker flag options.

Docker build environments are available in support/docker/: Debian slim uses glibc and Alpine uses musl.

Documentation

Build and run

Learn the language

Contribute or extend

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 → Prepare / parse → AST → Monomorphize generics → Borrow check
→ Fact analysis → IR → Optimize → C → target toolchain / linker

For an explanation of each stage, see the compiler pipeline.

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/user/diagnostic-contract.md.

Types at a glance

The built-in types reference is the authoritative guide to construction, ownership, actions, and C representations. The common groups are:

  • Copy values: numeric types, logic!, char!, size!, none!, file! (a path value), and enum!.
  • Move-sensitive values include error!, port!, c-pointer!, and map!. Normal string!, block!, vector!, and array! values, and typed dict! handles, are reference-counted and shared on assignment; add #unique when a native series must be move-only.
  • Collections: vector!, array!, dict!, map!, block!, binary!, and c-array!.
  • Concurrency: channel! — a typed, bounded queue for passing values between go coroutines (see concurrency).
  • Borrowed views: slice! — a zero-copy view over a series.
  • User-defined types: struct!, enum!, sum!, fn-ptr!, fsm!, and fst!.

c-string!, c-struct!, and c-fn! are ABI-facing forms for FFI and runtime boundaries. Prefer string! for ordinary owned text.

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: "hello"               ; inferred 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 [string!] return: [none!]] [print name]
msg: "hello"
greet :msg         ; borrow — msg stays owned

Vectors

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

; Stack-allocated storage must be local to a function
show-stack-vector: func [
    "Print the first element of a stack-allocated vector."
] [
    stack-arr: make [vector! [#stack i32! 5]] [1 2 3 4 5]
    print stack-arr/0    ; 1
]
show-stack-vector

; 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
x: 12
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
i: mutable 0
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: make i32! 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: make i32! adder 5    ; 15

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

Enums

Color!: make enum! [red green blue]

c: make Color! 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: make i32! 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 the global error state (overflow, div-zero, bounds)
divisor: 0
result: make i32! (10 / divisor)
if errored? [print "math error"]

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

Defer

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

run: func [] [
    defer [print "always runs"]
    defer/error [print "runs only after an error"]
]

run

defer/error runs only when the enclosing function returns with an active runtime error. See Error handling for the complete model.

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

math: foreign <stdlib.h> [
    abs: "abs" [i32! return: i32!]
]

x: make i32! math/abs -7
print x

The header tag emits the matching C #include. For opaque C pointers, callback types, exported symbols, and external globals, see the FFI and ABI reference. Declare imported C globals with #extern:

#extern c-pointer! some_global_from_c

Type Casting

a: 10                    ; i32!
x: make f32! as f32! a  ; explicit cast i32! -> f32!
c: make f32! (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

Shared and move-sensitive values

Native series such as string!, block!, vector!, and array!, and typed dict! handles, are shared by default, so an ordinary assignment keeps the original binding usable. Add #unique to opt a native series into move-only ownership.

Other values, including error!, port!, c-pointer!, and map!, move on an owned transfer:

e1: make error! "oops"
e2: e1              ; ownership moves to e2
if error? e2 [print "handled"]
if error? e1 [print "unreachable: e1 was moved"]  ; ERROR: use after move

Borrowing

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

greet: func [:name [string!]] [print name]
msg: "hello"
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:

ready: true
count: 1
status: either ready ["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

Values moved in any branch are conservatively considered moved afterward:

handle: file/open %data.txt
condition: true
either condition [
    moved: handle
] [
    print "not moved in this branch"
]
file/close handle   ; ERROR: handle might have moved

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/compiler/generics.r3 Generic monomorphization
src/borrow-checker.r3 Ownership analysis (analyze)
src/fact-analysis.r3 Refined-type and constraint flow analysis (analyze-facts)
src/ir.r3 AST → IR (ast-to-ir)
src/compiler/optimize.r3 IR optimization (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, type-decls)
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
r3 -s rut.r3

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

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

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

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

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

# Other options
r3 -s rut.r3 --fail-fast      # Stop on first failure
r3 -s rut.r3 --quiet          # Less output
r3 -s rut.r3 --shuffle 42     # Shuffle with seed
r3 -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.

Continuous integration

Forgejo runs .forgejo/workflows/linux.yml for a fast Linux/GCC subset on pushes and pull requests targeting master or releases/*. .forgejo/workflows/linux-nightly.yml runs the full Linux suite nightly.

GitLab runs .gitlab-ci.yml for a Windows/MSVC smoke build on master only (the free compute minutes do not stretch to every push), via a push mirror at gitlab.com/rebolek/recoil. GitLab is used for this rather than GitHub because it can host a SHA-256 repository. That job uses the core Rebol build: the bulk build imports OPENGL32.dll, which a headless Windows Server image does not have.

macOS has no workflow. It is the primary development platform and is covered by local runs; GitLab's hosted macOS runners require a paid plan.

What Recoil is for

  • Native executables and libraries — Recoil emits C and can build executables plus shared or static libraries.
  • Memory-safe low-level code — the static borrow checker tracks moves and borrows without a garbage collector.
  • C interoperation — foreign declarations, ABI types, and explicit linker metadata provide a direct path to C libraries.
  • Rebol-shaped source — blocks, words, paths, and expression-oriented flow remain central to the language.

License

Apache2 License — see LICENSE