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, 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 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

  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)
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. 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, and wiki/TUTORIAL.md, published as Learning-Cryn), 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 is the proof), so no new primitive was added.