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
int64value cannot silently becomeint32 - Signedness must match — mixing signed and unsigned requires an explicit
@cast - Safe widening is automatic —
int32toint64with matching signedness converts implicitly - Mismatched signedness requires explicit
@casteven 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:
| Form | Semantics | Can mutate? |
|---|---|---|
&self | Mutable pointer to self | Yes |
&const self | Immutable pointer to self | No |
self | By-value copy | Yes (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;
pubexposes 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
dynamickeyword.
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 included | Reason |
|---|---|
| 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.

