This is an statically typed programming language focused on simplicity, safety, and performance. It uses immutable-by-default semantics, pass-by-value for primitives, and a reference-based memory model with automatic reference counting (refcount).
The syntax is inspired by modern languages but does not attempt to replicate any existing one. It is a pragmatic language designed to empower developers without exposing them to low-level memory concerns.
- Immutable by default: Variables and function parameters are immutable unless explicitly marked as
mut. - Clear pass-by-value vs pass-by-reference model.
- No raw pointers, manual memory management, or low-level access.
- Reference-counted memory model with automatic cleanup.
let x = 5; // Immutable variable
let mut y = 10; // Mutable variable
y = 15; // OK: y is mutable
let z: i32 = 20; // Immutable variable with explicit type
let w: mut i32 = 25; // Mutable variable with explicit typeThe language supports immutable constants defined with the const keyword. Constants must have a type annotation and are evaluated at compile time.
const PI: f64 = 3.14159; // Immutable constantThe language includes a minimal of built-in types, covering primitives:
| Type | Description | Default Passing |
|---|---|---|
u8 |
Unsigned 8-bit integer | By value |
u32 |
Unsigned 32-bit integer | By value |
u64 |
Unsigned 64-bit integer | By value |
i8 |
Signed 8-bit integer | By value |
u16 |
Unsigned 16-bit integer | By value |
i16 |
Signed 16-bit integer | By value |
i32 |
Signed 32-bit integer | By value |
i64 |
Signed 64-bit integer | By value |
f32 |
32-bit floating point | By value |
f64 |
64-bit floating point | By value |
bool |
Boolean (true/false) | By value |
If you need more complex types like strings or arrays, they are provided as part of the standard library.
The language supports explicit casting between numeric types:
let a: i32 = 10;
let b: f64 = a as f64; // Cast i32 to f64
let c: u32 = b as u32; // Cast f64 to u32| From → To | Allowed? | Behavior |
|---|---|---|
| Integer → Float | ✅ | Converts with precision loss |
| Float → Integer | ✅ | Truncates |
| Signed ↔ Unsigned | ✅ | Wraps/truncates |
| Float ↔ Float | ✅ | Narrowing may lose precision |
| Integer ↔ Integer | ✅ | Truncates if target is smaller |
Non-compatible types (e.g. String → i32) |
❌ | Disallowed unless via conversion functions |
The language supports classic arithmetic operations on numeric types and follows standard operator precedence:
| Operator | Description | Example |
|---|---|---|
+ |
Addition | x + y |
- |
Subtraction | x - y |
* |
Multiplication | x * y |
/ |
Division | x / y |
% |
Modulus | x % y |
** |
Exponentiation | x ** y |
== |
Equality | x == y |
!= |
Inequality | x != y |
< |
Less than | x < y |
<= |
Less than or equal | x <= y |
> |
Greater than | x > y |
>= |
Greater than or equal | x >= y |
! |
Logical NOT | !x |
&& |
Logical AND | x && y |
|| |
Logical OR | x || y |
& |
Bitwise AND | x & y |
| |
Bitwise OR | x | y |
if x < y {
// do something
} else {
// do something else
}
for i in 1..=5 {
println(i);
}
while condition {
// do something
}
loop {
// infinite loop
if some_condition {
break; // exit the loop
}
}
// Loop with labels
outer: loop {
inner: loop {
if some_condition {
break outer; // exit the outer loop
}
}
}
// This can be used with for and while loops as welldef add(a: i32, b: i32) -> i32 {
return a + b;
}
let sum = add(x, y); // sum = 20- Function parameters are immutable by default so you cannot reassign them.
- You must declare it with the mut keyword to allow internal mutability.
- This only applies to structured types and not primitives.
def mut_arg(a: mut T) {
a.mut_value();
}Passing an immutable variable to a
mutparameter will result in a compile-time error.
The language supports defining custom types using the type keyword. This allows you to create structured data types similar to structs in other languages and provides a way alias existing types.
// This is a type alias for an existing type
type MyType = i32;
// This is a structured type definition
type Point {
x: MyType,
y: MyType,
}
let p = Point { x: 10, y: 20 };
let x_coord = p.x;
let y_coord = p.y;// This is a static method definition
def Point.new(x: i32, y: i32) -> Point {
return {x, y};
}
// This is an inmutable instance method definition
def Point.distance(self) -> f64 {
return ((self.x ** 2 + self.y ** 2) as f64).sqrt();
}
// This is a mutable instance method definition
def Point.set_x(mut self, new_x: i32) {
self.x = new_x;
}- Methods that do not modify the instance can be called on immutable variables.
- Methods that modify the instance require the instance to be
mut.
let mut p = Point.new(5, 5);
p.set_x(100); // OK
let q = Point.new(1, 1);
q.set_x(20); // ❌ Error: q is immutableTraits define shared behavior across different types. They are similar to interfaces in other languages, but follow Rust’s semantics:
- Static dispatch by default (no runtime overhead).
- Behaviors are checked at compile time.
- Optional dynamic dispatch using trait objects.
trait Drawable {
def draw(self);
}Traits may contain one or more method signatures:
trait Shape {
def area(self) -> f64;
def perimeter(self) -> f64;
}To implement a trait for a type, use the following syntax:
def Circle.draw(self) for Drawable {
// implementation
}Or with multiple methods:
def Circle.area(self) -> f64 for Shape {
return 3.14 * self.radius.pow(2);
}
def Circle.perimeter(self) -> f64 for Shape {
return 2 * 3.14 * self.radius;
}Trait methods can be called directly on values as long as they implement the trait:
let c = Circle { radius: 5.0 };
c.draw(); // Calls the draw method from Drawable trait
let area = c.area(); // Calls the area method from Shape trait
let perimeter = c.perimeter(); // Calls the perimeter method from Shape traitStatic dispatch is the default behavior in this language, meaning that method calls are resolved at compile time. This allows for optimizations and avoids runtime overhead.
def draw_shape(shape: Drawable) {
shape.draw(); // Calls the draw method of the specific type implementing Drawable
}Static dispatch ensures that the method call is resolved at compile time, providing better performance.
Dynamic dispatch can be achieved using trait objects, allowing for runtime polymorphism. This is useful when you want to work with different types that implement the same trait. This is done using dyn keyword:
def draw_shapes(shapes: [dyn Drawable]) {
for shape in shapes {
shape.draw(); // Calls the draw method of each specific type
}
}Dynamic dispatch allows for flexibility at the cost of some performance, as method resolution happens at runtime.
If multiple traits define methods with the same name, you can disambiguate which implementation to call using fully qualified syntax:
trait Drawable {
def show(self);
}
trait Debug {
def show(self);
}
def Circle.show(self) for Drawable {
print("Drawing circle");
}
def Circle.show(self) for Debug {
print("Circle { radius: ... }");
}
let c = Circle { radius: 10.0 };
c.show(); // ❌ Error: ambiguous method call
Drawable.show(c); // ✅ Calls the Drawable trait method
Debug.show(c); // ✅ Calls the Debug trait methodWhen multiple traits implement the same method, you must specify which trait's method to call using the trait name.
You can also define generic types using the type keyword:
type Box[T] {
value: T;
}
def Box[T].from(value: T) -> Box[T] {
return { value };
}
def Box[T].get_value(self) -> T {
return self.value;
}
let mut int_box: Box[i32] = Box.from(42);
let mut str_box: Box[String] = Box.from("Hello");
let int_value = int_box.get_value(); // Returns 42
let str_value = str_box.get_value(); // Returns "Hello"The system is based on ownership, lifetime analysis, and reference counting, eliminating the need for a traditional garbage collector and preventing common memory errors.
Stack Allocation by Default: Values are allocated on the stack by default, ensuring optimal performance for short-lived data.
type Point { x: i32, y: i32 }
let p1: Point = Point { x: 10, y: 20 }; // p1 is allocated on the stackUnified References (ref T):
A single reference type, ref T, is used to point to data. The compiler understands the context and handles the reference appropriately—either as a stack borrow or as a counted reference to the heap. References are never null.
let p1_ref: ref Point = p1;Explicit Heap Allocation with new:
For data that must outlive the current scope or be shared, the new keyword is used to allocate it on the heap. This returns a ref T, which is a reference-counted (RC) pointer.
let p2: ref Point = new Point { x: 30, y: 40 };
// p2 is a reference-counted pointer to a Point in the heap
// p2 can be returned from functions or stored in long-lived structures- Reference counting is automatic: it increments when a heap reference is copied and decrements when a reference goes out of scope. The object is deallocated when the count reaches zero.
- There is no implicit promotion from stack to heap; heap allocation is always explicit via
new.
Compiler-Enforced Lifetime Analysis:
The compiler performs static lifetime analysis for all stack references (ref T pointing to stack data). This ensures no reference can outlive the data it points to, preventing dangling pointers.
fn get_dangling_ref() -> ref Point {
let p_local: Point = Point { x: 1, y: 1 };
return p_local; // COMPILATION ERROR: p_local is on the stack and cannot escape as a reference.
// The compiler will suggest returning 'Point' by value or using 'new Point { ... }'.
}Explicit Mutability:
Mutability must be declared explicitly using the mut keyword for both values and references.
let p_mut: mut Point = Point { x: 0, y: 0 };
let p_mut_ref: mut ref Point = p_mut;
p_mut_ref.x = 5; // Modifies p_mut via the mutable reference
let p_heap_mut: mut ref Point = new Point { x: 0, y: 0 };
p_heap_mut.y = 10; // Modifies the heap-allocated PointCopying and Cloning Data (Copy, Clone traits):
- To duplicate the underlying value (not the reference), types can implement the
Copytrait (for simple bitwise-copyable types) or theClonetrait (for more complex duplication). - When using
newwith an existing stack value,CopyorCloneis required to transfer the value to the heap:
// Assuming Point implements Clone
let p_stack: Point = Point { x: 1, y: 2 };
let p_heap_clone: ref Point = new p_stack.clone(); // Clone p_stack into the heap
// If Point implements Copy (and the compiler infers it)
// let p_heap_copy: ref Point = new p_stack; // Copy p_stack into the heap-
For an assignment
let a = b;wherebisref T:- If
bis a stack reference,abecomes another stack reference to the same data. - If
bis a heap reference (RC),abecomes another reference to the same heap object, and the reference count is incremented. - To get a copy of the data pointed to by a heap reference, use
b.clone()orb.copy()(if the type implements the respective trait).
- If
When an object is allocated on the heap using new T, the result is of type mut ref T, a mutable reference to an object in dynamic memory. This reference type automatically carries an internal reference counter (RC) that manages the object’s lifecycle.
The system allows a downcast from a mutable reference to an immutable one (ref T) when mutability is no longer required. This conversion is implicit in contexts where immutability is expected.
When assigning a heap reference (ref T or mut ref T) to a new variable, the reference count is automatically incremented:
let foo: ref T = new T;
let bar = foo; // Reference count is incremented by 1Each copy of the reference to the same heap object shares the counter. It is incremented with each new copy and decremented when references go out of scope or are explicitly dropped.
When the reference count reaches zero, the object is automatically deallocated. Before freeing the memory, the drop function defined by the Drop trait is invoked, allowing for custom cleanup logic (e.g., closing files, freeing external resources, etc.).
Constraint:
Objects can only be allocated on the heap using new T if the type T implements the Drop trait. This ensures that all heap-allocated objects have an explicit destruction path, avoiding resource leaks or undefined behavior.
The base memory model is designed to be simple and safe for most use cases. For more complex scenarios, specialized tools are provided, which programmers may opt into with full awareness of the added responsibilities:
-
Concurrency (
Arc<T>,Mutex<T>):- The default
ref T(when pointing to heap) uses non-atomic reference counting, safe for single-threaded use. - For safe sharing across threads, types like
Arc<T>(Atomic Reference Counting) must be used for shared ownership, andMutex<T>(or other synchronization primitives) for shared mutability. - These types are provided via the standard library and make concurrency intent explicit.
- The default
-
C Interoperability (FFI) and Raw Pointers (
Ptr<T>,unsafe):- For interacting with C code or low-level operations requiring raw pointers, the
Ptr<T>type is provided.
- For interacting with C code or low-level operations requiring raw pointers, the
-
Weak References (
Weak<T>):- To break reference cycles in data structures using reference-counted heap allocations, the
Weak<T>type is provided. AWeak<T>does not increment the reference count and allows checking if the data still exists before accessing it.
- To break reference cycles in data structures using reference-counted heap allocations, the
trait Incrementable {
def increment(mut self);
}
type Counter {
value: i32;
}
def Counter.new() -> Counter {
return { value: 0 };
}
def Counter.increment(mut self) for Incrementable {
self.value += 1;
}
def Counter.get_value(self) -> i32 {
return self.value;
}
let mut counter: Incrementable = Counter.new();
counter.increment(); // Increment the counter
let current_value = counter.get_value(); // Get the current value
print("Current value: {}", current_value); // Output: Current value: 1