Design Goals

    Cyrus is a high-performance systems programming language with explicit syntax and semantics, minimal abstraction and runtime, and manual memory management.

    Philosophy

    Cyrus is built on a strict principle:

    The developer is in charge. The language should make every control point visible, not hide decisions behind automatic rules.

    It does not attempt to eliminate all bugs through compiler-enforced safety systems, nor does it introduce novel paradigms or heavy abstraction layers. Instead, it selects proven concepts from systems programming and refines them into a consistent, explicit form where every mutability decision, allocation, indirection, and dispatch strategy is declared in the code rather than inferred.

    Cyrus builds on the same machine model as C, designed so that everything happening at the hardware level is apparent from reading the source.

    Explicit Semantics

    Cyrus makes control visible by requiring developers to state intent directly, leaving no ambiguity about what the code does.

    Mutability is Explicit

    Every variable declares its mutability at the point of definition:

    const max_retries = 5; // immutable
    var current_try = 0;   // mutable
    

    A const binding can never be reassigned — the compiler enforces this. A var binding signals to readers that this value changes over its lifetime. No scanning backward to check whether a name was declared let or let mut; the signal is in the first token of every declaration.

    Type Conversions are Visible

    Where C performs silent implicit conversions that can lose information, Cyrus requires explicit casts:

    const a: int32 = @cast(int32, 0xFFFFFFFF);
    const b: int16 = @cast(int16, 0xFFFFFFFF);
    

    Widening conversions (e.g., int32 to int64 with matching signedness) are allowed implicitly because they are always safe. Every narrowing or signedness mismatch requires a visible @cast. This eliminates an entire class of bugs where C silently truncates or reinterprets values.

    Pointer Indirection is Trackable

    Cyrus distinguishes direct access (.) from pointer access (->), making every dereference visible at the call site:

    var box = Box { value: 1 };
    
    var box2 = box.methodA(5)->methodB(50); // thin arrow means we're going through a pointer
    

    The -> operator tells the reader "indirection is happening here" without requiring them to mentally resolve the type of the left-hand side.

    Memory Management is Manual and Visible

    Every allocation and deallocation is explicit in the code:

    import std::mem{Allocator, LibcAllocator};
    
    pub fn main() {
        const allocator = LibcAllocator.new();
        var buffer: int32* = allocator.alloc(64 * @sizeof(int32));
        defer allocator.free(buffer); // cleanup is visible and scoped
    
        buffer[0] = 369;
    }
    

    The defer keyword ensures cleanup runs at scope exit — in reverse declaration order (LIFO) — making resource lifetime deterministic and visible without nesting:

    pub fn main() {
        defer log(100);        // runs last at function exit
    
        {
            defer log(10 + 1); // runs when inner scope exits
            defer log(10 + 2);
            log(2);
        }
    }
    

    Integer Conversions are Strict

    Cyrus enforces strict integer correctness to eliminate the silent truncation and sign-mismatch bugs that plague C codebases:

    • No implicit narrowing conversions — a int64 value cannot silently become int32
    • Signedness must match — mixing signed and unsigned requires an explicit @cast
    • Safe widening is automatic — int32 to int64 with matching signedness converts implicitly
    • Mismatched signedness requires explicit @cast even when widening
    const a: int32 = 300;
    const b: int64 = a;               // OK: safe widening, matching signedness
    const c: int32 = @cast(int32, b); // explicit: narrowing requires cast
    
    const u: uint32 = 10;
    const i: int64 = @cast(int64, u); // explicit: signedness mismatch requires cast
    

    This prevents the classic C bugs where:

    • A loop counter silently wraps because it was stored in a smaller type
    • A comparison between signed and unsigned produces unexpected results
    • A 64-bit file size is truncated when assigned to a 32-bit variable

    What You Can Do

    Cyrus trusts you to understand the system you're working with. Rather than restricting what you can express, it gives you the tools to control the machine directly — and takes responsibility for the consequences.

    Write your own memory allocator — custom allocators are first-class citizens via the Allocator interface, with stack, bump, and libc-backed implementations provided in the standard library:

    pub struct BumpAllocator : Allocator {
        ptr: void*,
        cap: usize,
        offset: usize,
    
        pub fn alloc(&self, size: usize) void* {
            const current = self->offset;
            const next = current + size;
            if (next > self->cap) return null;
            self->offset = next;
            return @cast(void*, @cast(uintptr, self->ptr) + current);
        }
    
        pub fn free(&self, ptr: void*) void {
            // no-op: bump allocators don't free individually
        }
    }
    

    Interpret raw memory as different types — unions allow type punning without casts:

    union IntBytes {
        value: int32;
        bytes: uint8[4];
    }
    
    var data = IntBytes{ value: 0x12345678 };
    // data.bytes[0] == 0x78 on little-endian,
    // same memory, different type
    

    Control layout, alignment, and padding — packed structs, explicit field alignment, and ABI attributes for precise memory mapping.

    Use pointer arithmetic for performance-critical paths — Cyrus fully supports GEP-based indexing and pointer difference:

    const arr: int32[10];
    const ptr: int32* = &arr[0];
    var ptr2: int32* = ptr + 5; // pointer arithmetic
    *ptr2 = 99;
    

    Interface directly with hardware, kernel, or bare-metal targets — inline assembly, extern for C ABI, naked functions, and custom calling conventions are all available:

    extern fn printf(fmt: const uint8*, ...) int32;
    naked fn interrupt_handler() void { /* no prologue/epilogue */ }
    

    The Cost of Control

    Because Cyrus does not interpose a safety layer between you and the machine, the following are still possible:

    • Reading or writing invalid memory
    • Use-after-free and double free
    • Dangling pointers
    • Data races

    These are the cost of control — a consequence of giving you direct access to the machine, not a failure of the language. Runtime sanitizers are available during development to help catch these issues.

    A language that prevented every memory error would have to restrict what you can express. Cyrus chooses the opposite tradeoff.

    Data Model

    Cyrus programs are built from three core data structure primitives — each chosen for a specific purpose, with no hidden object model or class hierarchy.

    Struct

    Named fields, methods, and explicit visibility:

    struct User {
        pub name: uint8*,
        pub age: uint32,
        role: uint8* // private field
    }
    
    pub fn main() {
      var user = User { name: "Cyrus", age: 2500, role: "founder" };
    
      user.name; // OK: public
      // user.role; // ERROR: private outside the struct module
    }
    

    Cyrus also supports unnamed (anonymous) structs for one-off groupings, and structural compatibility between named and unnamed structs with identical field layouts:

    var point = struct { x: 10, y: 20 }; // unnamed struct, type inferred
    
    pub const config = struct { // unnamed struct
        host = "127.0.0.1",
        port = 8080
    };
    
    pub fn main() {
      point.x += 5;
    
      const temp: struct { a: int32, b: float64 } = obj; // compatible with named struct
    }
    

    Enum

    Enums in Cyrus are true algebraic sum types — each variant can carry its own payload, and the compiler enforces exhaustive matching. Four variant forms are supported:

    Unit variants — simple tags with no data:

    enum ConnectionState {
      Disconnected,
      Connected,
      Failed
    }
    

    Tuple variants — positional payload:

    enum Task {
      Delay(uint32),
      Timeout(uint32, const uint8*)
    }
    

    Struct variants — named-field payload:

    enum Error {
        NotFound { id: uint32, msg: const uint8* },
        PermissionDenied { resource: const uint8* }
    }
    

    Valued (scalar) variants — constant-value association:

    enum HttpStatus {
      OK = 200,
      NotFound = 404,
      InternalError = 500
    }
    

    Matching is exhaustive — the compiler checks that all variants are handled:

    switch (err) {
        case .NotFound { id, msg } => printf("%u: %s", id, msg);
        case .PermissionDenied { resource } => printf("denied: %s", resource);
    }
    // compiler error if any variant is missing
    

    This replaces the C pattern of enum + untagged union + manual discriminant tracking with a single construct where the compiler manages the tag and enforces correctness.

    Unnamed enums can be declared inline at the use site without a named type:

    var mode: enum { Off = 0, On = 1 } = .On;
    
    switch (mode) {
        case .On(value) => printf("on: %d", value);
        case .Off(value) => printf("off: %d", value);
    }
    

    Union

    Unions provide unchecked type punning — all fields share the same memory address, and reading a field that was not most recently written is undefined behavior:

    union Payload {
        i: int64;
        s: uint8*;
    }
    
    var data = Payload { s: "Cyrus" };
    // data.i is gone, writing to s now overwrites stale memory
    
    var iptr: int64* = &data.i; // pointer aliasing: same address as data.s
    *iptr = 2500;
    

    Use unions when you need memory-efficient data representation or C interop. For safe tagged unions, use enum instead.

    Unnamed unions can be used inline for temporary layout or initialization of named union types:

    union Payload {
      i: int64,
      s: uint8*
    }
    
    pub fn main() {
        const layout: Payload = union { s: "Cyrus!" };
    
        printf("%s\n", layout.s);
    }
    

    Generics

    Structs, enums, unions, and functions can all be parameterized by type. Generics are monomorphized at compile time with zero runtime overhead — the compiler generates a separate specialized copy for each concrete type combination:

    struct Triple<A, B, C> {
        pub first: A,
        pub second: B,
        pub third: C,
    
        pub fn new(a: A, b: B, c: C) Self {
            return Self { first: a, second: b, third: c };
        }
    }
    
    pub fn main() {
        const triple = Triple.new(3, 4.5, "hello"); // implicit var type
        const triple2: Triple<int32, float64, uint8*> = Triple.new(3, 4.5, "hello"); // explicit var type
    
        printf("%d %f %s\n", triple.first, triple.second, triple.third);
    }
    

    Controlled Polymorphism

    Cyrus provides two dispatch strategies, each explicitly chosen at the use site:

    Static dispatch — guaranteed monomorphization, zero indirection:

    fn process<T: Speaker>(animal: T) {
        printf("%s\n", animal.speak());  // direct call, no vtable
    }
    

    Dynamic dispatch — fat-pointer with vtable, explicitly constructed via dynamic:

    const speakers = Speaker[2]{dynamic dog, dynamic cat};
    printf("%s %s\n", speakers[0].speak(), speakers[1].speak());
    

    The dynamic keyword makes vtable construction visible at the creation site. A reader can see exactly where runtime polymorphism enters the system.

    Methods and Encapsulation

    Methods are functions defined inside a struct body. They follow consistent rules with no inheritance or virtual dispatch by default.

    Instance Methods

    Methods receive the struct instance through an explicit receiver parameter:

    struct SimpleCounter {
        pub count: int32,
    
        pub fn new(initial: int32) Self {
            return Self{ count: initial };
        }
    
        pub fn increment(&self) { // mutable reference receiver
            self->count++;
        }
    
        pub fn describe(&const self) { // const reference receiver
            printf("count: %d\n", self->count);
        }
    
        pub fn into_value(self) int32 { // by-value (consuming) receiver
            return self.count;        // self is a copy
        }
    }
    

    Three receiver forms:

    FormSemanticsCan mutate?
    &selfMutable pointer to selfYes
    &const selfImmutable pointer to selfNo
    selfBy-value copyYes (local copy only)

    Method Calling / Member Access Conventions

    Methods are called using dot syntax on values, or -> on pointers:

    var c = SimpleCounter.new(10);
    c.increment(); // value receiver
    
    var ptr = &c;
    ptr->increment(); // pointer receiver
    

    Static Methods

    Methods without a receiver parameter are called on the type itself:

    pub struct MathUtils {
        pub fn square(x: int32) int32 {
            return x * x;
        }
    }
    
    pub fn main() {
        printf("%d\n", MathUtils.square(5));
    }
    

    Encapsulation

    • Fields and methods are private by default; pub exposes them
    • Methods cannot be added to a struct outside its defining module — no monkey-patching, no scattered behavior
    • No inheritance — composition over hierarchy

    Type System Rules

    Cyrus is statically typed with strict rules designed for predictability.

    Type Inference

    Type inference works within fully declared contexts — every variable must have a known type at the point of declaration, either from an initializer or an explicit annotation:

    const object: Box<int32> = Box.new(10);  // OK: explicit type annotation
    const value = Box.new(10);             // OK: type inferred from initializer
    // const object;                       // ERROR: type cannot be inferred later
    

    Type Aliases

    The type keyword creates named aliases — the compiler expands them transparently:

    type rune = uint32;
    type Handler = fn(int32, int32) void;
    

    Zero Initialization

    Any variable declared without an explicit initial value is automatically zero-initialized:

    var x: int32; // initialized to 0
    var p: void*; // initialized to null
    var arr: int32[10]; // all elements set to 0
    

    Predictable Performance Model

    Cyrus is built on LLVM and compiles ahead-of-time. The performance model is straightforward:

    • No hidden allocations: Cyrus never allocates memory on your behalf. Every allocation is explicit (malloc, custom allocator, stack allocation).
    • No hidden copies: Structure passing semantics are explicit (by value copies, by pointer). The compiler will optimize but the default model is visible.
    • Zero-cost generics: Monomorphization produces code identical to hand-written specializations.
    • Explicit dispatch strategy: Static dispatch is guaranteed for constrained generics; dynamic dispatch requires the dynamic keyword.

    What you write closely reflects what executes. There are no compiler-inserted runtime abstractions, reference counting, or garbage collection cycles to account for.

    Module System

    Cyrus modules are files, resolved by path, with explicit visibility:

    // auth_utils.cyrus
    pub fn login(user: uint8*) { /* visible outside */ }
    fn internal_helper() { /* private to this file */ }
    
    // main.cyrus
    import auth_utils{login};
    
    pub fn main() {
        login("Cyrus"); // imported by name
    }
    

    There is no preprocessor, no textual inclusion, no include guards — just semantic imports with clear dependency boundaries.

    What Cyrus Refuses to Do

    These exclusions are by design, not limitation:

    Not includedReason
    No garbage collector

    GC adds unpredictable pause times and hides allocation from the developer

    No borrow checker

    Borrow checking imposes complex rules that restrict what the developer can express

    No concurrency model

    Under design — evaluating options; libraries and OS primitives serve current needs

    No full OOP system

    Inheritance hierarchies obscure data flow; composition is explicit

    No method extension outside modules

    All behavior attached to a type lives in the same module — no scattering

    No paradigm mixing

    Cyrus is procedural at its core; functional abstractions and dense expressions are avoided

    Where Cyrus Fits

    Every language makes tradeoffs. Cyrus is an alternative philosophy: explicit control, minimal abstraction, maximum authority for the developer.

    • If C frustrates you because it doesn't offer enough language features for modern programming and lacks a proper module system, Cyrus gives you the same level of control with better ergonomics.
    • If Rust's borrow checker solves problems you don't have and the annotation burden doesn't fit your use case, Cyrus gives you control without the lifetime system.
    • If Zig's comptime and metaprogramming is unnecessary complexity for your project, Cyrus keeps the execution model simple and predictable.
    • If Go's runtime and GC is unsuitable for your systems project and you want richer data structures (enums, generics, interfaces) with manual memory control, Cyrus gives you those tools.

    Cyrus is not trying to replace any of these languages. It is a different point on the same spectrum.

    Who Cyrus Is For

    • Systems programmers familiar with C who want better ergonomics without losing control
    • Developers building low-level infrastructure (runtimes, kernels, embedded firmware, networking)
    • Teams that prioritize long-term maintainability and explicit code over convenience features
    • Anyone who prefers to reason about the machine model directly rather than through a safety layer

    Who Cyrus Is Not For

    • Absolute beginners without systems programming background
    • Developers expecting automatic memory management
    • Those seeking strong compile-time safety guarantees without understanding the underlying machine
    • Applications where proving memory safety statically is a hard requirement

    Design Constraint: Proven Ideas

    Cyrus does not pursue novelty for its own sake. Every feature earns its place by solving a real problem in systems programming — proven by decades of use in C-family languages, refined into a consistent and explicit form.

    The goal is not to invent. The goal is to curate: select the best ideas, integrate them coherently, and remove the complexity that comes from ad-hoc evolution.

    Features We're Still Exploring

    Cyrus is under active development. The following features are being researched and designed, and will be shaped according to the same principles when they arrive:

    • Concurrency model — evaluating options including fibers, coroutines, and async I/O; no decision has been made yet
    • Macro/metaprogramming system — exploring compile-time code generation approaches
    • Package management — investigating dependency resolution and distribution mechanisms

    Final Note

    Cyrus is not trying to be the safest language, the most abstract, or the most innovative.

    It is trying to be:

    • Explicit — so you can see what the code does without reading the compiler's mind.
    • Controllable — so you can make the machine do what you need, without fighting the language.
    • Maintainable — so code written today is still understandable years from now.

    A language where the developer remains in charge — without pretending the machine isn't there.

    Cyrus: systems infrastructure, explicit control.