# The Cryn Programming Language
Cryn is a compiled systems programming language built on a very small and
unusual core: **cells, connections, focus, a working value, and a value
stack**. There are no symbolic operators, no braces, no semicolons, and no
indentation rules. Programs read like small English sentences describing
operations on a machine:
```text
define main
set 10
push
set 20
add
output
return
end
```
This compiles to a native executable through either of two backends:
```text
Cryn source -> lexer -> parser -> AST -> semantic analysis -> Cryn IR
-> optimizer -> C backend -> host C compiler -> executable
-> native backend -> ELF (x86-64) -> executable
```
The C backend emits a self-contained C11 translation unit and hands it to
the host C compiler (the portable reference path). The native backend
lowers the IR directly to x86-64 machine code and writes an ELF executable
itself. No C compiler, linker, or CRT objects are involved at program
build time; the host C library is linked dynamically as the system-call
interface.
The bootstrap compiler (`cryn`) is written in portable ISO C11 and has no
dependencies beyond a C compiler, pthreads, and libm (`fmod`, used by
float `remainder`).
---
## Design philosophy
> C-level power through a radically small set of composable primitives.
* **38 commands total.** No `if`, `while`, `for`, `switch`, `array`,
`struct`, `class`, or `pointer` keywords. Control flow is explicit
jumps; arrays are indexed connections; structures are blocks of memory;
pointers are address values.
* **Whitespace has no meaning.** Every program can be written on one line.
Blocks end with `end`.
* **One comment syntax:** `[# ... #]`, possibly spanning lines.
* **English words instead of symbols:** `add`, `compare`, `jump ... when
less`, `move`, `write`.
* **The graph model is the semantics; the compiler lowers it.** Cells and
connections are what the language means; the compiler compiles them into
ordinary locals, arrays, and pointer arithmetic, so Cryn code runs at
native speed with no graph overhead.
* **Static typing with defined behavior.** Fixed-width integer and float
types, defined overflow (wrap), defined division by zero (diagnostic
abort), saturating float-to-int conversion, bounds-checked indexing.
* **No hidden runtime.** The runtime support layer is a few hundred lines:
allocation headers, context structs, a per-context file-handle table,
thread spawn/join, atomic access helpers, and output formatting. Every
dependency is documented in this README.
See the [project wiki](https://vextoly.lgbt.sh/cryn/wiki/) for
the formal language reference
([Language Reference](https://vextoly.lgbt.sh/cryn/wiki/language-reference.php)),
a course from first principles
([Learning Cryn](https://vextoly.lgbt.sh/cryn/wiki/learning-cryn.php)),
and the compiler internals
([Architecture](https://vextoly.lgbt.sh/cryn/wiki/architecture.php)).
The `wiki/` folder holds the sources of those pages.
## Repository layout
```text
src/
main.c CLI entry point (build / run / check / emit-c / emit-ir)
util.[ch] buffers, lists, file I/O
diag.[ch] diagnostics (file:line:col reporting)
lex.[ch] lexer: keywords, literals, comments, locations
ast.h AST node definitions
parse.[ch] recursive-descent parser (formatting independent)
types.[ch] interned type objects, sizes, classifications
check.[ch] semantic analysis: abstract machine interpretation,
produces fully typed IR (src/check.c is the semantic heart)
ir.h Cryn IR instruction definitions
opt.[ch] IR cleanup: unreachable code and dead label removal
be_c.[ch] C backend: emits a self-contained C11 translation unit
be_x64.[ch] native x86-64 backend: lowers the IR to machine code and
writes an ELF executable directly (host libc via a
hand-built GOT; musl and glibc `_start` conventions)
rt/
cryn_rt.h minimal runtime interface (embedded into output)
cryn_rt.c runtime implementation
rt_src.h generated: runtime sources as strings (tools/embed)
tools/
embed.c turns runtime sources into string headers
wiki/ sources of the published wiki pages: Home, Learning-Cryn
(TUTORIAL.md), Language-Reference (LANGUAGE.md),
Architecture (ARCHITECTURE.md)
tests/
*.cryn test programs
expected/ expected outputs
input/ stdin fixtures
errors/ programs that must fail to compile
run.sh test runner
examples/ hello, fib, fizzbuzz, sorting, wordcount, linkedlist,
allocator
```
The frontend (lexer/parser/AST), the semantic layer (types/check), the IR,
and the backend (be_c) are cleanly separated. A native code backend can be
added by translating `src/ir.h` instructions directly; nothing in the
semantic layer is tied to C generation.
## Building
```sh
make # builds ./cryn
make test # runs the test suite (C backend)
make test-release # runs the test suite in release mode (no bounds checks)
make test-native # runs the test suite through the native x86-64 backend
make clean
```
Requirements: any ISO C11 compiler and POSIX `make`. pthreads is required
for `spawn` / `wait` support.
## Using the compiler
```sh
cryn hello.cryn # compile to executable "hello"
cryn build hello.cryn -o out # compile to executable "out"
cryn run hello.cryn # compile and execute
cryn check hello.cryn # type check only
cryn emit-c hello.cryn -o out.c # write the generated C
cryn emit-ir hello.cryn # print the Cryn IR
cryn help
cryn version
```
Both `build` and `run` accept `--release` (no runtime bounds checks;
statically provable checks are always eliminated at compile time) and
`--native` (emit an x86-64 ELF executable directly instead of generated C;
Linux, System V ABI, dynamic host libc).
`cryn` invokes the host C compiler (`$CC`, default `cc`) with
`-O2 -std=c11 -pthread` to turn generated C into an executable. Generated
programs are normal executables and need no Cryn runtime at runtime; the
runtime is compiled into every generated translation unit. With `--native`
no host C compiler is needed at all.
## How the compiler works
1. **Lexing** (`lex.c`): comments are stripped as `[# ... #]`; whitespace
(including newlines) is skipped everywhere; line/column tracked for
diagnostics.
2. **Parsing** (`parse.c`): recursive descent over the token stream.
Transformation parameters are `{ type name }` pairs terminated by the
first non-type token, which keeps parsing unambiguous without any
punctuation.
3. **Semantic analysis** (`check.c`): an abstract interpreter over the
Cryn machine. It tracks the static type of the working value, the
abstract focus stack, the value stack shape, and comparison state, and
merges these states at every label. All merges must agree exactly
(integer literals widen to the type they fit). It also infers result
types, resolves all names, checks argument counts/types, and emits the
fully typed IR in one pass. Recursive transformations with unclear
results must declare `result <type>`.
4. **IR** (`ir.h`): 50 typed instructions (navigation, value ops, memory
access, comparisons, calls, external operations, atomics).
5. **Optimizer** (`opt.c`): removes unreachable code and unreferenced
labels. (Deliberately small; the C compiler performs the heavy lifting
on the readable C we emit.)
6. **C backend** (`be_c.c`): each transformation becomes a C function;
frame cells become C locals; `move` becomes pointer assignment; value
stack and focus history live in a per-thread context struct. The
runtime sources are embedded and emitted into every translation unit,
making the output a single self-contained `.c` file.
7. **Native backend** (`be_x64.c`): the same IR lowered straight to
x86-64. Each transformation gets a frame of callee-saved machine state
(`r12` comparison, `r13` focus, `r14` context, `r15` working value);
the runtime helpers (value stack, focus, blocks with headers, streams,
threads, float formatting) are emitted as machine code, and the writer
produces a static ELF with a hand-built GOT and SysV hash table that
links against the host C library for syscalls (stdio, malloc, pthreads,
`fmod`, and `snprintf`/`strtod` so float output is byte-identical to
the C backend). File handles, contexts, and block headers match the C
runtime's semantics.
## Runtime dependencies (complete list)
The runtime (`src/rt/cryn_rt.*`) provides only:
| Facility | Used by | Notes |
|-----------------|----------------------------------|--------------------------------|
| `malloc`/`free` | `make <size> of`, anonymous `make`, `free` | 16-byte header for sizes plus a mutex-protected registry of live blocks; validation of stale addresses is defined, never undefined reads |
| value stack | `push`, `pop`, `swap`, `call` | growable array, per thread |
| focus history | `move` (relative), `back`, `home`| fixed 1024 entries per context (shared across active frames) |
| FILE* streams | `input`, `output`, `open`, `use`, `close`, `flush`, `seek`| stdin/stdout by default; `use` routes the context's streams through a handle; `seek` positions handle streams at a byte offset |
| `system()` | `system` | shell escape |
| pthreads | `spawn`, `wait` | join is an ordering guarantee |
| formatting | `output` | shortest round-trip float printing |
| relaxed atomics | all scalar loads/stores | races yield unspecified values and never tear ([Language Reference](https://vextoly.lgbt.sh/cryn/wiki/language-reference.php)) |
| acq_rel atomics | `exchange` | indivisible swap of working value and focused cell; the only ordering primitive besides `wait` |
There is no garbage collector, no interpreter, and no mandatory runtime
library beyond the above.
## Testing
```sh
make test # C backend
make test-release # release mode: bounds checks removed
make test-native # native x86-64 backend
```
48 checks each: arithmetic (including overflow wrap and division edge
cases), unsigned arithmetic, floats (including `remainder`/fmod and
NaN), all six comparison conditions, branches, loops, nested loops,
functions, recursion, mutual recursion, the value stack, cells and focus
navigation, arrays (sieve of Eratosthenes), strings, manual memory,
pointer arithmetic, linked lists, callbacks through transform references,
file I/O with first-class handles (two files interleaved, append mode,
flush), `seek` repositioning an `open both` stream (interleaved reads and
writes on one file), threads (`spawn`/`wait`), a spinlock built from
`exchange`, compare-and-swap built from `exchange` and `wait` (the
evaluation that closed further atomic primitives), a word counter, bubble
sort, a free-list allocator, a limits stress test (301 cells, 300 labels,
302-deep focus, 1100-deep value stack), and sixteen programs that must
fail to compile. Every run test passes under all three modes.
## Current limitations
These are honest boundaries of the first implementation, not pretenses:
* **Two backends.** The C backend is the portable reference and needs a
host C compiler at build time. The native backend emits x86-64 ELF
directly (Linux, System V ABI, musl or glibc host libc; the interpreter
path and the libc SONAME are detected at compile time, override with
`CRYN_INTERP`). Other targets stay on the C backend.
* **Concurrency is minimal.** `spawn` starts a thread running a
zero-parameter transformation; `wait` joins all spawned contexts of the
current one. Shared world cells may be accessed concurrently; a race
on an aligned ≤ 8-byte cell yields an unspecified value of the cell's
type and never tears (scalar access is lowered as relaxed atomics).
The only ordering guarantees are `wait` (join) and `exchange`
(an acquire-release atomic swap of the working value with the focused
cell; enough to build locks). Communication of results happens
through shared world cells.
* **File handles are plain integers scoped to one context.** `open`
returns a handle (1 to 256 per context, -1 on failure; `read`, `write`,
`both`, and `append` modes). `use` routes the context's streams
through a handle: the readable side to input, the writable side to
output; `use 0` returns to the standard streams. A spawned context
cannot use its parent's handles, and closing a handle frees its
number for reuse.
* **Indirect calls** (through `transform` references) are only allowed to
zero-parameter transformations; checked at runtime when the reference is
not statically known. The callee's result is discarded and the caller's
working value (the reference) is preserved; results flow through world
cells or addresses.
* **Bounds checks abort** with a diagnostic in the default mode. Release
mode (`--release`) removes them; statically provable checks (literal
indices into static arrays, and literal indices provably within a
block's size) are eliminated at compile time in every mode.
* **Fixed limits:** 1024 focus history entries, 4096 labels, 4096 cells
per transformation, 4096 provable value stack depth, 256 concurrent
spawns per context, 256 open files per context. A single
`make <size> of` allocation is capped at 16 TiB (the cap is
overflow-safe in bytes, not elements); the type table is bounded only
by memory.
* **No separate compilation** yet: one `.cryn` file per program.
* **The compiler itself is not yet written in Cryn.** Self-hosting is the
remaining long-term goal; the architecture (small passes, plain data
structures, no frameworks) was chosen for it.
## Contributing
* Keep the command count at or below 40. Before adding anything, prove it
cannot be expressed with the existing primitives.
* Keep the compiler portable ISO C11; isolate platform specifics in
`src/rt/`.
* Every language change needs: a test in `tests/`, updated wiki sources
(`wiki/LANGUAGE.md`, published as
[Language-Reference](https://vextoly.lgbt.sh/cryn/wiki/language-reference.php),
and `wiki/TUTORIAL.md`, published as
[Learning-Cryn](https://vextoly.lgbt.sh/cryn/wiki/learning-cryn.php)),
and a note in this README if it affects the runtime surface.
* Run `make test`, `make test-release`, and `make test-native` before
committing.
## Future work
1. Self-hosting: rewrite the compiler in Cryn (the C backend bootstraps
the port; the native backend then keeps the toolchain C-free).
2. Extend the native backend beyond Linux/x86-64 (static PIE, more
libc targets) as demand appears; the C backend covers everything
else.
Done from the original list: the native x86-64 backend (`--native`),
release mode with compile-time elimination of statically provable bounds
checks (`--release`), file positioning (`seek`) for `open both` streams,
and the atomic-primitives evaluation: compare-and-swap and atomic
arithmetic build from `exchange` and `wait` alone
([tests/32_cas.cryn](tests/32_cas.cryn) is the proof), so no new
primitive was added.