Introduction
Warning
This book is currently in the early stages of development.
This book is the primary reference for the Eter programming language. This book does not assume you are reading it sequentially. Each chapter generally can be read standalone, but will cross-link to other chapters for facets of the language they refer to, but do not discuss.
Note
For known bugs and omissions in this book, see our GitHub issues. If you see a case where the compiler behavior and the text here do not agree, file an issue so we can think about which is correct.
Contributing
We welcome contributions of all kinds.
You can contribute to this book by opening an issue or sending a pull request to the Eter repository. If this book does not answer your question, and you think its answer is in scope of it, please do not hesitate to file an issue. Knowing what people use this book for the most helps direct our attention to making those sections the best that they can be. And of course, if you see anything that is wrong or is non-normative but not specifically called out as such, please also file an issue.
Lexical structure
A sequence of Unicode characters is translated into a sequence of tokens. The following rules apply during translation:
- Comments are treated as though they are a single space
U+20. - Spaces are ignored unless they appear between the opening and closing delimiters of a character or string literal. Unicode characters with the “White_Space” property are recognized as spaces.
- New-line delimiters are ignored unless they appear between the opening and closing delimiters of a character or string literal. Unicode characters with the “Line_Break” property are recognized as new-line delimiters.
Comments
The character sequence // starts a single-line comment, which terminates immediately before the next new-line delimiter.
The character sequences /* and */ are multiline comment opening and closing delimiters, respectively. A multiline comment opening delimiter starts a comment that terminates immediately after a matching closing delimiter. Each opening delimiter must have a matching closing delimiter. Multiline comments may nest and need not contain any new-line characters.
Note
The character sequences
//have no special meaning in a multiline comment. The character sequences/*and*/have no special meaning in a single-line comment. The character sequences//and/*have no special meaning in a string literal. String and character literal delimiters have no special meaning in a comment.
Tokens
A token is a terminal symbol of the syntactic grammar. It falls into one of five categories:
| Category | Description | Examples |
|---|---|---|
| Literals | Fixed values in the source code | 42, "hello", true |
| Keywords | Reserved words with special meaning | if, else, while |
| Identifiers | Names given to entities (variables, functions, etc.) | foo, bar, count |
| Operators | Symbols representing computations or logic | +, -, *, == |
| Delimiters | Symbols used for grouping and structure | (, ), {, } |
Note
The input
a << bis translated to a sequence of 4 tokens: identifier, raw-operator, raw-operator, identifier.
Unless otherwise specified, the token recognized at a given lexical position is the one having the longest possible sequence of characters.
Literals
Literals are tokens representing fixed values in the source code. The language supports the following types of literals:
| Literal Type | Description | Standard Examples | Special/Escaped Examples |
|---|---|---|---|
| Integer | Decimal and hexadecimal formats | 123 | 0x1A, 0x_FF_00 |
| Floating-Point | Like an integer literal but includes a decimal point . | 3.14159, 0.5 | |
| Boolean | Represent truth values | true, false | |
| Character | Single Unicode scalar value enclosed in single quotes | 'a', 'R' | '\n', '\t', '\'', '\u{1F600}' |
| String | Sequence of characters enclosed in double quotes | "hello world" | r"C:\Path", r#"He said, "Hello!""# |
| C-Style String | Null-terminated string literal identified by the @c qualifier | @c"hello" | @c"c style string" |
Keywords
Keywords are reserved words that have special meaning in the language. They cannot be used as identifiers. The language defines the following keywords:
| Strict Keywords | |||
|---|---|---|---|
as | enum | match | true |
break | false | mod | type |
const | fn | mut | use |
continue | for | pub | where |
if | ref | while | else |
return | let | struct |
Note: Some keywords might be reserved for future use or macro rules.
Identifiers
An identifier is a name used to identify a variable, function, class, module, or other user-defined item.
Identifiers must follow these rules:
- They must begin with a letter (
a-z,A-Z) or an underscore_. - Subsequent characters may be letters, digits (
0-9), or underscores_. - They cannot match one of the reserved keywords.
| Status | Example | Reason |
|---|---|---|
| Valid | my_var, _private, Count1 | Follows all rules. |
| Invalid | 1stPlace | Cannot start with a digit. |
| Invalid | my-var | Hyphens are not allowed (parsed as subtraction). |
| Invalid | if | if is a reserved keyword. |
Operators
Operators are special symbols or combinations of symbols used to perform operations on values or variables.
| Category | Operators | Description |
|---|---|---|
| Arithmetic | +, -, *, /, %, ++, -- | Standard mathematical operations, increment, and decrement. |
| Bitwise | &, |, ^, <<, >> | Operations on the bit level. |
| Logical | &&, ||, ! | Boolean logic (AND, OR, NOT). |
| Comparison | ==, !=, <, >, <=, >= | Relational equality and ordering. |
| Assignment | =, +=, -=, *=, /=, etc. | Assigns values, optionally compound. |
| Structural | ., => | Member access, matching. |
Delimiters
Delimiters are punctuation characters used to group tokens, separate lists, or define structure.
| Delimiter | Name | Primary Usage |
|---|---|---|
( ) | Parentheses | Function calls, grouping expressions, tuples. |
{ } | Braces | Block expressions, struct definitions, matching bodies. |
[ ] | Brackets | Array indexing, slices, array literals. |
, | Comma | Separating arguments, tuple elements, and list items. |
; | Semicolon | Terminating statements. |
: | Colon | Type annotations, struct field initialization, return types. |
:: | Double Colon | Path separation (namespaces, modules). |
Items
Modules
Constant
External block
Use declarations
Functions
Structs
Enumerations
Unions
Type system
Statically sized types
Statically sized types are those stored with static (immutable) and fixed amount of contiguous space. The amount of space occupied solely depends on the type and not the value represented.
Boolean type
Booleans can represent one of two distinct values: true or false. Each boolean occupies 8 bits.
#![allow(unused)]
fn main() {
let rain: bool = true;
let sunny: bool = false;
}
Numeric types
Numeric types represents numbers. A numeric type must be declared with a character (representing the type of number) and a number (representing the size of the variable)
#![allow(unused)]
fn main() {
let number: cXY = ... // c = the char of the type
// XY = the size to allocate
}
Unsigned types represents absolute numbers (which, by convention are treated as positive numbers). Unsigned numbers are declared with the letter u. The following table shows the possible sizes.
| Type | Byte Size (bit) | Min Value | Max Value |
|---|---|---|---|
u8 | 1 byte (8 bits) | \(0\) | \(2^{8}-1\) |
u16 | 2 bytes (16 bits) | \(0\) | \(2^{16}-1\) |
u32 | 4 bytes (32 bits) | \(0\) | \(2^{32}-1\) |
u64 | 8 bytes (64 bits) | \(0\) | \(2^{64}-1\) |
u128 | 16 bytes (128 bits) | \(0\) | \(2^{128}-1\) |
#![allow(unused)]
fn main() {
let x1: u8 = 10;
let x2: u8 = 300; // compiler error. The variable type has not enough size to represent the value
}
Integer types represents integer numbers, both positive and negative. Integer numbers are declared with the letter i. The following table shows the possible sizes.
| Type | Byte Size (bit) | Min Value | Max Value |
|---|---|---|---|
i8 | 1 byte (8 bits) | \(-2^{7}\) | \(2^{7}-1\) |
i16 | 2 bytes (16 bits) | \(-2^{15}\) | \(2^{15}-1\) |
i32 | 4 bytes (32 bits) | \(-2^{31}\) | \(2^{31}-1\) |
i64 | 8 bytes (64 bits) | \(-2^{63}\) | \(2^{63}-1\) |
i128 | 16 bytes (128 bits) | \(-2^{127}\) | \(2^{127}-1\) |
Float types represent non integer numerals (both positive and negative) following the IEEE 754 standard. Float numbers are declared with the letter f. The only possible sizes are 32 or 64 bits. Thus the two possible types are f32 (single precision) and f64 (double precision).
| Type | Byte Size (bit) | Min Value (Normalized) | Min Value (Subnormal) | Max Value |
|---|---|---|---|---|
f32 | 4 byte (32 bits) | \(1.0×2^{−126}\) | \(2^{−149}\) | \((2−2^{−23})×2^{127}\) |
f64 | 8 bytes (64 bits) | \(1.0×2^{−1022}\) | \(2^{−1074}\) | \((2−2^{−52})×2^{1023}\) |
#![allow(unused)]
fn main() {
let x: f32 = 10.5;
let y: f64 = 10.5f;
}
As seen in the example both the declarations are valid.
The usize type is an unsigned integer type with the same number of bits as the platform’s pointer type. It can represent every memory address in the process.
The isize type is a signed two’s complement integer type with the same number of bits as the platform’s pointer type. The theoretical upper bound on object and array size is the maximum isize value. Thus isize can be used to calculate differences between pointers into an object or array and can address every byte within an object along with one byte past the end.
Both
usizeandisizeare at least 16 bit lenght.
Character
A character represents a Unicode scalar value. Characters are declared as char and the declared value must be enclosed in single quotes. Every character is stored in 4 bytes (32 bit), thus a sort of alias to u32 type.
#![allow(unused)]
fn main() {
let c: char = 'a';
let emoji: char = '😀';
let unicode: char = '\u{1F600}';
}
Pointer types
Note
This is an example note.
Compound data types
The following types still have a static (immutable) amount of space allocated but it depends both on the types and the amount of values to store.
Textual types
Textual types represents strings, thus sequences of characters.
Strings are declared as strand the value enclosed in double quotes.
#![allow(unused)]
fn main() {
let say: str = "Hello";
}
C-strings are a sequence of characters (non unicode encoded) terminated by a null character (‘\0’). They are identified by prefixing the literal declaration with the qualifier @c.
#![allow(unused)]
fn main() {
let say: str = @c"Hello";
}
Array types
Arrays represent 1D homogeneous product types. They are a statically sized, contiguous blocks of memory containing elements of a single type T.
The syntax for an array type is [T; SIZE].
#![allow(unused)]
fn main() {
let arr: [i32; 3] = [1, 2, 3];
}
Tensor types
Tensors represent multi-dimensional (nD) homogeneous product types. Like arrays, they are statically sized collections of a single type T, but structured across multiple dimensions.
The syntax for a tensor type separates the dimensions with commas: [T; SIZE1, SIZE2, ...].
#![allow(unused)]
fn main() {
let matrix: [f32; 2, 2] = [[1.0, 0.0], [0.0, 1.0]];
}
Note
Compound types like arrays and tensors are not “objects”. Dynamic, heap-allocated collections (such as vectors or dynamic tensors) are managed through the standard library or via gradual typing features, rather than being built into the static compound type syntax.
Tuple types
Tuples are Cartesian product types. They are ordered, statically sized, heterogeneous collections of values where each element can be of a different type.
The syntax for a tuple type uses parentheses () containing a comma-separated list of types: (T, U, V).
#![allow(unused)]
fn main() {
let record: (i32, f64, str) = (42, 3.14, "hello");
}
Struct types
Structs are heterogeneous product of other types (called fields). Structs must be declarated with a name to refer to and the type and the name for eah of it’s fields.
#![allow(unused)]
fn main() {
struct AName {
// fields of AName
x: i32,
y: f64,
}
let aStruct = AName ( x: 15, y: 10.4 );
}
Structs fields can be accessed with . followed by the field name.
#![allow(unused)]
fn main() {
let aVar : i32 = aStruct.x; // 15
}
Unit-like structs are structs with no fields. Those structs can be initialized with only the name.
#![allow(unused)]
fn main() {
struct AStruct{} // Unit-like struct declaration
let a: AStruct; // AStruct variable initialization
let b: AStruct = AStruct{}; // Equivalent initialization
}
Enum types
Enum is a type which defines a new enumerated type domain. Each Enum is declared with a name (like for structs) and the allowed values. Enum constructors (or variants) are assignable to variables using the Enum name as the type of the variable.
Each variant can be declared with just name to refer to (Unit-like) or have the same syntax of structs, tuple or unions.
#![allow(unused)]
fn main() {
enum Animals{
Dog( str, i32 ),
Cat{ name: str, age: i32 },
Spider{ eyes: i32, poisonous: bool },
Reptile,
}
let a: Animal = Animal::Spider{ eyes: 8, poisonous: false };
let b: Animal = Animal::Reptile;
}
Variants defined inside Enum declaration cannot be used as a type specifier.
#![allow(unused)]
fn main() {
let b: Cat = Animals::Cat{..} // Compiler error. Cat not defined
let c: Animals::Cat = Animals::Cat{..} // Another compiler error
}
A constructor with no fields is called Unit-Like. When all the constructors in an enum are Unit-Like, then the enum is called Unit-Only Enum (or Field-less).
#![allow(unused)]
fn main() {
enum Balls{
Tennis,
Golf,
Soccer,
}
let a: Balls = Balls::Tennis
}
Each Enum instance has an associated dicriminant, an integer (isize) that determines which variant of the enum it holds. A discriminant value can be assigned to only one variant and a variant can have only one discriminant.
Discriminants can be manually assigned in Enum declaration as it follows:
#![allow(unused)]
fn main() {
enum Balls{
Tennis = 4,
Golf = 1,
Soccer = 2,
}
}
Non specified discriminant are automatically assigned as the discriminant of the previous constructor in the declaration increased by 1. (If it’s the first constructor then it’s set to 0)
#![allow(unused)]
fn main() {
enum Balls{
Tennis, // Unspecified discriminant for first variant. set to 0
Golf = 10,
Soccer, // Discriminant will be 11
}
}
Discriminant of a variant can be accessed casting the enum to an isize.
#![allow(unused)]
fn main() {
let a: Balls = Balls::Golf;
let discr: isize = a as isize // discr contains 10
}
Static types layout
Dinamically sized types
Union types
Note
This is an example note.
Union types (also known as sum types) can store different types of values but only one at a time. Unions are declared similar to structs but with use of the union keyword (comma separated).
#![allow(unused)]
fn main() {
union MyUnion {
f1: u32,
f2: f32,
}
}
An example of two identically declared Unions with differently stored values.
View types
Statements and expressions
Eter is an expression-oriented language, meaning that most semantic constructs within the language evaluate to a value. To understand the syntactic and semantic structure of a program, it is fundamental to distinguish between statements and expressions:
- Expressions are combinations of variables, literals, operators, and function calls that are evaluated by the compiler to compute and return a resulting value. Every expression has a well-defined type. Because they yield values, expressions can be arbitrarily nested and composed to build more complex logic.
- Statements are instructions that dictate the control flow and state mutations of a program. They do not evaluate to a usable value (conceptually, they evaluate to the unit type
unit). Their primary purpose is to execute side effects, such as introducing new bindings into a scope, modifying existing memory locations, or defining items.
In short: expressions compute values, while statements perform actions.
Expressions and statements are intrinsically linked: you can convert an expression into an expression statement by appending a semicolon ;. This forces the compiler to evaluate the expression solely for its side effects (e.g., executing a function call) while explicitly discarding any resulting value.
Statements
Statements are executed in sequence within a block. Eter supports several types of statements, including item declarations, assignments, and expression statements.
Statement terminator
Statements are generally terminated by a semicolon ;. The semicolon indicates the end of a statement and separates it from the next one.
Note
Some statements, such as those ending with a block expression (e.g.,
if,while,match), do not require a trailing semicolon unless they are part of a larger expression or assignment.
Assignment statements
An assignment statement evaluates an expression and binds its value to a memory location represented by a place expression (such as a variable, a struct field, or an array index).
The syntax for an assignment statement uses the = operator or a compound assignment operator (e.g., +=, -=, *=).
| Type | Example | Description |
|---|---|---|
| Basic Assignment | x = 5; | Assigns the value 5 to the variable x. |
| Compound Assignment | y += 2; | Adds 2 to y and assigns the result back to y. |
| Field Assignment | point.x = 10; | Assigns 10 to the field x of point. |
Assignment statements perform an action and do not evaluate to a value, meaning they cannot be chained like a = b = c.
Item declaration statements (let)
Item declaration statements introduce new items into the current scope. The most common item declaration within a block is the let statement, which binds a new local variable by always specifying its name and type. Variables are immutable by default unless marked with the mut keyword (more in the Memory Model Chapter).
| Declaration | Description | Example |
|---|---|---|
| Variable Binding | Binds a value to a new local variable, with a required type annotation. | let name: str = "Alice"; |
| Mutable Binding | Binds a value to a mutable local variable. | let mut count: i32 = 0; |
Nested Items and Scoping
Items (such as nested let variables) can be declared inside any block scope. This allows you to restrict the visibility of an item strictly to the block it was declared in, keeping the outer namespace clean.
Nested items can be declared inside regular function blocks and unnamed (anonymous) scopes { ... } blocks. They are particularly useful inside functional blocks like if or if-else expressions to encapsulate temporary logic.
fn main() {
let outer_val: i32 = 10;
let condition: bool = true;
// 1. Functional Block Scope (if-else)
// Useful for isolating variables or helper logic
// that is only needed for a specific branch.
if condition {
let inner_val: i32 = 20;
let local_mul: i32 = 5;
let result: i32 = inner_val * local_mul;
do_something(result);
} else {
let fallback_val: i32 = 0;
do_something(fallback_val);
}
// Error! `inner_val`, `local_mul`, and `fallback_val`
// are out of scope and cannot be accessed here.
}
Expression statements
An expression statement is an expression that is evaluated for its side effects, followed by a semicolon. The value produced by the expression is discarded.
Common examples include function calls.
| Type | Example | Description |
|---|---|---|
| Function Call | do_something("Hello"); | Executes the function and discards the return value. |
When a block-based expression (such as if, match, or a simple { ... } block) is used as an expression statement, the trailing semicolon is optional.
Expressions
An expression evaluates to a value and can be used in most places where a value is expected. Expressions can be nested and combined.
Literal expressions
A literal expression consists of a literal token and evaluates to the value represented by that token.
#![allow(unused)]
fn main() {
let age: i32 = 42; // Integer literal
let name: str = "Alice"; // String literal
let is_valid: bool = true; // Boolean literal
let letter: char = 'A'; // Character literal
}
Path expressions (::)
A path expression refers to an item, variable, or constant in the current scope or another module using the path separator ::. You can also use paths to explicitly refer to the current module (self) or the parent module (super).
#![allow(unused)]
fn main() {
let max_val: u32 = u32::MAX; // Fully qualified path to a constant
let math_pi: f64 = math::PI; // Path to a module item
// Accessing an item within the current actual scope
let current_item: i32 = self::helper_function();
}
Block expressions
A block expression is a sequence of statements enclosed in braces {}. The value of a block expression is the value of its final expression (the one without a trailing semicolon). If the block ends with a statement (with a semicolon), it evaluates to unit.
#![allow(unused)]
fn main() {
let y: i32 = {
let x: i32 = 5;
ret x + 1;
}; // y is now 6
}
Operator expressions
Operator expressions apply unary or binary operators to operands.
#![allow(unused)]
fn main() {
let sum: i32 = 10 + 20; // Binary arithmetic operator
let is_false: bool = !true; // Unary logical NOT operator
let flag: bool = (a == b); // Binary comparison operator
}
Grouped expressions
Parentheses () can be used to explicitly group expressions and control the order of evaluation, overriding default operator precedence.
#![allow(unused)]
fn main() {
let result: i32 = (2 + 3) * 4; // Evaluates to 20 instead of 14
}
Access expressions
Access expressions allow you to retrieve specific elements from compound types like arrays, tuples, and structs. Depending on the underlying type, the syntax to access an element varies.
Array and index expressions
Array expressions create fixed-size collections of elements. Index expressions retrieve elements from an array or slice using brackets []. Indexing is always zero-based.
#![allow(unused)]
fn main() {
let a: [i32; 3] = [1, 2, 3]; // Array expression (list of elements)
let zeros: [i32; 5] = [0; 5]; // Array expression (repeated value: [0, 0, 0, 0, 0])
let first: i32 = a[0]; // Index expression (accessing the first element)
}
Tensor and index expressions
Tensor expressions create fixed-size, multi-dimensional collections of homogeneous elements (nD tensors). Tensor literal expressions use nested arrays.
#![allow(unused)]
fn main() {
let t: [i32; 2, 2] = [[1, 2], [3, 4]]; // Tensor expression (2x2 matrix)
let element: i32 = t[0][1]; // Index expression (accessing the element at row 0, column 1, which is 2)
}
Tuple and index expressions
Tuple expressions create ordered, fixed-size, heterogeneous collections. Tuple elements are accessed using dot . notation followed by a literal integer index.
#![allow(unused)]
fn main() {
let point: (i32, i32, str) = (10, 20, "label"); // Tuple expression
let x: i32 = point.0; // Tuple index expression (gets 10)
let desc: str = point.2; // Tuple index expression (gets "label")
}
Struct expressions and Field access
Struct expressions create instances of user-defined struct types. They specify the name of the struct and provide values for its fields. Field access expressions retrieve the value of a specific named field from a struct or union using the dot . operator.
#![allow(unused)]
fn main() {
// Struct instantiation expression
let p: Point = Point { x: 10, y: 20 };
// Field access expression
let my_x: i32 = p.x; // Accesses the 'x' field of the struct 'p'
}
Call expressions
Call expressions invoke functions or closures. They consist of an expression that evaluates to a callable entity, followed by a parenthesized list of arguments.
#![allow(unused)]
fn main() {
let result: i32 = add(5, 3); // Function call
}
If expressions
An if expression evaluates a boolean condition and executes the corresponding block. If an else branch is provided, the if expression can evaluate to a value, provided both branches return the same type.
#![allow(unused)]
fn main() {
let condition: bool = true;
// if used as an expression to assign a value
let result: str = if condition {
"Success"
} else {
"Failure"
};
// if used for side effects (evaluates to `unit`)
if result == "Success" {
do_something("Everything is fine");
}
}
Loop expressions
Loop expressions are used to execute a block of code multiple times. Because they are expressions, they can optionally evaluate to a value (e.g., by using the break keyword).
Eter supports both for and while loops.
#### While loops
The simple while loop continues to execute as long as its condition is true.
Consider the following example, which decrements a variable until it reaches zero:
#![allow(unused)]
fn main() {
// while loop
let mut n: i32 = 5;
while n > 0 {
n -= 1;
}
}
Infinite loops can be created using while true or the loop keyword, which will continue indefinitely until explicitly broken out of.
For example:
#![allow(unused)]
fn main() {
// infinite loop using `while`
while true {
do_something();
}
}
Since they are expressions, while loops can also be used to compute a value by using the break statement to exit the loop and return a value.
For example:
#![allow(unused)]
fn main() {
// loop expression that evaluates to a value using `break`
let mut counter: i32 = 0;
let final_value: i32 = while true {
counter += 1;
if counter == 10 {
break counter * 2; // The value of the loop expression will be 20 when it breaks
}
};
}
For loops
Warning
Future development may introduce loop constructs, such as
for inloops for iterating over collections. For instance, afor inloop might look like this:#![allow(unused)] fn main() { let numbers: [i32; 5] = [1, 2, 3, 4, 5]; for num in numbers { do_something(num); } }
Match expressions
A match expression provides pattern matching. It compares a value against a series of patterns and executes the block corresponding to the first matching pattern. Like if expressions, match blocks can evaluate to a value if all branches resolve to the same type.
Patterns can include literals, variables, and the catch-all wildcard _.
Crucially, because each branch in a match must evaluate as an expression to return a value, you generally do not use a semicolon to terminate the branch’s expression. Instead, you use a comma , to separate and define the “next” pattern in the sequence.
#![allow(unused)]
fn main() {
let status_code: i32 = 404;
let message: str = match status_code {
200 => "OK", // Notice the comma instead of a semicolon
404 => "Not Found",
500 => "Internal Server Error",
// The underscore `_` acts as a wildcard, catching any unhandled values
_ => "Unknown Error",
};
}
You can also use patterns to destructure types or bind variables:
#![allow(unused)]
fn main() {
let coords: (i32, i32) = (0, 10);
match coords {
(0, 0) => do_something("Origin"),
(x, 0) => do_something("On the X axis"),
(0, y) => do_something("On the Y axis"),
(x, y) => do_something("Somewhere else"),
}
}
Return expressions
A ret expression immediately terminates the current function or closure and evaluates to a value that is passed back to the caller. A ret expression without a value implies returning unit.
#![allow(unused)]
fn main() {
fn get_positive(val: i32) -> i32 {
if val < 0 {
ret 0; // Early return expression
}
ret val;
}
}
The implicit return in Eter is allowed only when there is a single expression within a scope (e.g., a function body or a block). If there are multiple statements, an explicit ret is required to indicate the return value.
For instance, in the following function, the implicit return is not allowed because there are multiple statements:
#![allow(unused)]
fn main() {
fn compute_value(x: i32) -> i32 {
let intermediate: i32 = x * 2; // Statement (requires a semicolon)
ret intermediate + 1; // Explicit return expression
}
}
While in this function, the implicit return is allowed because there is only a single expression:
#![allow(unused)]
fn main() {
fn compute_value(x: i32) -> i32 {
x * 2 + 1 // Single expression (no semicolon, implicit return)
}
}
Note that, this extends to block expressions as well. If a block contains multiple statements, an explicit ret is required to return a value from that block. However, if the block consists of a single expression, it can implicitly return its value without needing ret.
#![allow(unused)]
fn main() {
let result: i32 = {
let intermediate: i32 = 5; // Statement (requires a semicolon)
ret intermediate + 10; // Explicit return expression
};
let simple_result: i32 = {
5 + 10 // Single expression (no semicolon, implicit return)
};
}
This feature is vital for writing concise and readable code, especially in cases where match and if expressions are used to compute values based on conditions.
For example, in a match expression, with out the implicit return, you would need to write:
#![allow(unused)]
fn main() {
let status_code: i32 = 200;
let message: str = match status_code {
200 => { ret "OK"; }, // Explicit return with `ret`
404 => { ret "Not Found"; },
500 => { ret "Internal Server Error"; },
_ => { ret "Unknown Error"; },
};
}
Underscore expressions
The underscore _ can be used as an expression pattern to explicitly discard a value. This is useful when calling a function for its side effects, but explicitly acknowledging that you are choosing not to use its return value. It is also heavily used in match blocks as a wildcard (as shown above).
#![allow(unused)]
fn main() {
// Discard the result of a function that returns a value
let _: i32 = compute_heavy_task();
}
Qualifiers
The language uses a system of annotqualifiers) to explicitly define the memory hierarchy and movement of data across different hardware components. This provides a complete mental model for data management, from CPU RAM to GPU SRAM.
@host
- Hardware: CPU RAM
- Typical Usage: Dataset loading and heavy preprocessing tasks (Pandas-style). This is standard, pageable system memory.
@pinned
- Hardware: CPU RAM (Page-locked / Pinned)
- Typical Usage: Transit buffers used to stream data to GPUs at high bandwidth (e.g., 32GB/s+). Since it cannot be paged out to disk, it allows for faster DMA transfers to the device.
@global
- Hardware: GPU VRAM
- Typical Usage: Storage of model weights, biases, and gradients on a single GPU board. This is the main high-capacity memory on the device.
@sharded
- Hardware: Cluster VRAM (Multi-GPU)
- Typical Usage: Distribution of giant models (e.g., Llama 405B) across 8+ GPUs. It abstracts the partitioned memory across a cluster of devices.
@shared
- Hardware: GPU SRAM (Shared Memory)
- Typical Usage: Ultra high-speed computation inside a kernel (e.g., Tiling). This is the small, fast, on-chip memory shared by threads within the same block.
Memory model
Mutable Value Semantics (MVS)
Immutability
Mutability
Ownership
Use of Pointers (unsafe scopes)
let ptr<i32>let mut ptr<i32>let ptr<mut i32>let mut ptr<mut i32>
Namespaces
Type namespace
Value namespace
Scopes
Unsafety
Unsafe scopes
unsafe {…}
Unsafe functions
unsafe fn foo() {…}