vextoly/cryn
A compiled, statically typed systems language with a tiny core.
$ git clone https://gitman.io/git/vextoly/cryn
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:
define main
set 10
push
set 20
add
output
return
end
This compiles to a native executable through either of two backends:
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, orpointerkeywords. 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 for
the formal language reference
(Language Reference),
a course from first principles
(Learning Cryn),
and the compiler internals
(Architecture).
The wiki/ folder holds the sources of those pages.
Repository layout
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
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
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
- Lexing (
lex.c): comments are stripped as[# ... #]; whitespace (including newlines) is skipped everywhere; line/column tracked for diagnostics. - 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. - 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 declareresult <type>. - IR (
ir.h): 50 typed instructions (navigation, value ops, memory access, comparisons, calls, external operations, atomics). - Optimizer (
opt.c): removes unreachable code and unreferenced labels. (Deliberately small; the C compiler performs the heavy lifting on the readable C we emit.) - C backend (
be_c.c): each transformation becomes a C function; frame cells become C locals;movebecomes 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.cfile. - Native backend (
be_x64.c): the same IR lowered straight to x86-64. Each transformation gets a frame of callee-saved machine state (r12comparison,r13focus,r14context,r15working 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, andsnprintf/strtodso 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) |
| 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
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.
spawnstarts a thread running a zero-parameter transformation;waitjoins 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 arewait(join) andexchange(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.
openreturns a handle (1 to 256 per context, -1 on failure;read,write,both, andappendmodes).useroutes the context's streams through a handle: the readable side to input, the writable side to output;use 0returns 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
transformreferences) 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> ofallocation 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
.crynfile 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, andwiki/TUTORIAL.md, published as Learning-Cryn), and a note in this README if it affects the runtime surface. - Run
make test,make test-release, andmake test-nativebefore committing.
Future work
- Self-hosting: rewrite the compiler in Cryn (the C backend bootstraps the port; the native backend then keeps the toolchain C-free).
- 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 is the proof), so no new
primitive was added.