R'

A subset of Radiance
Alexis Sellier
March 29, 2025

R’, pronounced “R prime” is a lower level unsafe subset of the Radiance programming language, that serves as the foundation for building the Radiant operating system and bootstrapping the full Radiance language. By providing direct memory access, pointer arithmetic, and hardware control capabilities, R’ enables the construction of essential system components like the kernel, device drivers, and runtime environment, on top of which Radiance applications can be built.

The R’ subset will therefore be designed and implemented first, after which the full language will be built using R’ as the implementation language.

Status

This document is incomplete. Sections on the following topics are missing:

Compared to Radiance

Compared to Radiance, R’ has:

This keeps the language small and simpler to reason about.

Basic syntax

R’ programs consist of statements that end with semicolons. Blocks of code are enclosed in curly braces.

// This is a single-line comment.
let x: i32 = 42;

if (x > 0) {
    x = x * 2;
}
default fn main() -> i32 {
    std::io::print("Hello World!");

    return 0;
}

Primitive types

The set of primitive types is the following.

u8 u16 u32 i8 i16 i32 bool char str

Note that there are no floats or 64-bit types in R’.

Literals

Booleans – bool.

true false

Integers – i8 i16 i32 u8 u16 u32.

8 41 -9 48193 0

Characters – char.

'a' 'z', '\n'

Strings – str.

"Hello World!\n"

Nil. Used when there is an absence of value.

nil

Bindings

R’ has immutable let and mutable mut bindings.

// Immutable binding.
let f: u8 = 42;

// Mutable binding.
mut g: u8 = 24;

Values can be re-assigned if declared with mut.

mut x: u8 = 16;

x = x * 2;
x = x + 1;

Bindings must always include a type annotation separated by a : from the identifier. Note that assignment is not an expression, unlike in C.

Operators

R’ has arithmetic operators:

+ - * / %

Comparison operators, that yield bool:

== != < > <= >=

And logical operators that take boolean operands and yield a bool.

and or xor not

Comments

As seen above, comments start with // and go until the end of the line.

// This is a comment.
// The comment continues on the next line.

If statements

if (x > 0) {
    // Code executed if `x > 0`.
} else if (x < 0) {
    // Code executed if `x < 0`.
} else {
    // Code executed if `x == 0`.
}

Match statements

Match statements work similarly to other languages, and unlike C’s switch, there is no fallthrough.

match (x) {
    case 1 => {
        // `x` is `1`.
    },
    case 2, 3 => {
        // `x` is `2` or `3`.
    }
    else => {
        // `x` is another value.
    }
}

Loops

The simplest loop is the unconditional loop, introduced with the loop keyword. The break statement can be used to exit a loop, and continue can be used to jump back to the beginning of the loop.

mut i: i32 = 0;

loop {
    i = i + 1;

    if (i > 8) {
        break; // Exit the loop.
    }

    if (i % 2 == 0) {
        continue; // Skip to next iteration.
    }
}

While loops

While loops specify a condition under which the loop should continue.

mut i: i32 = 0;

while (i < 8) {
    i = i + 1;
}

For loops

For loops are used for iterating over a collection.

let numbers: [i32; 5] = [1, 2, 3, 4, 5];

for (let num in numbers) {
    // `num` takes each value in the array.
}

Functions

Functions are defined using the fn keyword.

// Function with no parameters and a `i32` return type.
fn answer() -> i32 {
    return 42;
}

// Function with parameters.
fn add(a: i32, b: i32) -> i32 {
    return a + b;
}

// Function with no return value.
fn processOp(op: u32) {
    // An optional `return` statement with no value can be used
    // to return early.
    return;
}

Functions are called using () syntax.

let x: i32 = answer();  // `42`
let y: i32 = add(3, 2); // `5`
processOp(0);           // Does not return a value. Cannot be assigned.

Argument labels can be optionally used for any of the arguments, as long as they are in the correct position.

// These are all equivalent.
add(a: 3, b: 2);
add(a: 3, 2);
add(3, b: 2);

Arrays

Arrays are fixed-size collections of elements of the same type.

array type = "[" element type ";" array length "]"
// Declaration and initialization.
mut numbers: [i32; 5] = [1, 2, 3, 4, 5];

// Accessing elements (zero-indexed).
let first: i32 = numbers[0]; // 1
let last: i32 = numbers[4];  // 5

// Array elements can be assigned if the array is declared as mutable.
numbers[2] = 42;

// Arrays can have their length taken.
numbers.length // 5

Arrays are passed and returned by value from functions.

Pointers

Pointers provide a way to refer to a value without copying it.

let x: i32 = 42;
let p: *i32 = &x; // `p` is a pointer to `x`.

Pointers can be dereferenced using the * operator:

let y: i32 = *p; // y = 42

The underlying value can be modified via dereferencing:

*p = 100; // `x` is now `100`

Slices

let numbers: [i32; 5] = [0, 1, 2, 3, 4];

// Full slice.
let full: *[i32] = &numbers[..]; // Elements `[0, 1, 2, 3, 4]`

// Partial slice with start and end indices.
let middle: *[i32] = &numbers[1..4]; // Elements `[1, 2, 3]`

// Slice from start to a specific index.
let prefix: *[i32] = &numbers[..3]; // Elements `[0, 1, 2]`

// Slice from a specific index to the end.
let suffix: *[i32] = &numbers[2..]; // Elements `[2, 3, 4]`

// Slice length.
suffix.length // 3

Structs

Structs are custom data types that group related values called fields.

// A struct definition.
struct Point {
    x: i32,
    y: i32,
}

Struct literal syntax allows for struct initialization.

// Creating a struct instance via a struct literal.
mut p: Point = Point { x: 10, y: 20 };

Structs are accessed via the . field access syntax.

p.y = p.x * 2;

Structs can have default field values, making their initialization optional:

struct Size {
    w: u32 = 1, // Set default value to `1`.
    h: u32 = 1, // Set default value to `1`.
}

// `Size { w: 2, h: 1 }`.
let s: Size = Size { w: 2 };

Structs are passed and returned by value from functions.

Enums

Enumerations define a type with a fixed set of possible values, called variants. Unlike C enumerations, they are strongly typed in R’ and can’t be compred to integers without casting.

// Enum definition with three variants.
enum Color {
    red,
    green,
    blue,
}

// Creating enum values.
let c: Color = Color::red;

// Comparing enum values.
if (c == Color::red) {
    // Do something if color is blue.
}

Match statements can be used to match all enum variants:

match (color) {
    case Color::red => ...
    case Color::green => ...
    case Color::blue => ...
}

Unions

A union is a type that can take one of multiple values. It is like an enum with associated data, or a C union with an associated “tag”.

union Response {
    success: Data,
    failure: struct {
        errorCode: u32,
        message: str
    }
    offline,
}

The match statement is useful for handling all union cases:

// Exhaustively match all union variants.
// Binds the associated value to an identifier via `as`.
match (resp) {
    case Response::success as d: Data => {
        // Data value assigned to `d`.
    }
    case Response::failure as f => {
        handleFailure(f.errorCode, f.message);
    }
    case Response::offline => {
        // Handle offline.
    }
}

Constants

Constants are values defined at compile time.

const MAX_ENTITIES: usize = 4096;
const DEFAUL_COLOR: Color = Color::red;

Modules

Modules are used to organize code. They effectively function as both compilation units, and namespaces. Modules can have sub-modules, and expose public functions and constants. There is no module definition syntax: each source file defines a module, named after the file. Sub-modules are defined using directories, hence the following hierarchy:

std/
└─ net/
   └─ tcp.r

Defines the following module hierarchy:

std::net::tcp

Modules control the visibility of their members via the pub keyword, which signifies public visibility. Members not marked as pub are private.

For example, inside std/net/tcp.r we could define a public function as such:

pub fn connect(addr: Ip) {
    ...
}

To use this function, the use keyword is employed with a module name.

use std;
...
std::tcp::connect(addr);

We can directly import sub-modules, which avoids having to repeat the full module path on every call site:

use std::tcp;
...
tcp::connect(addr); // `std` can be omitted, since `tcp` is imported.

Optional types

To deal with the absence of a value, R’ has the concept of optional types, or optionals. The syntax for optionals is ?T, where T is any regular type. For example ?i32 represents an optional i32. Optional types either carry the base value T, or nil.

Given a function lookup that returns an optional Path:

fn lookup(key: u32) -> ?Path {
    ...
}

We can match on the presence of a value with:

if match (lookup(k)) as path {
    // This branch is evaluated if `lookup` returns a value,
    // with `path` bound to it.
} else {
    // This branch is evaluated if `lookup` returns `nil`.
}

We can also loop while a function returns a value, breaking as soon as it returns nil:

while match (nextResult()) as r {
    // Evaluated as long as `r` is not `nil`.
}

Chaining:

// Assigns nil to `value` if it encounters a nil in the chain.
let value: ?str = getObject(name)?.path?.baseName();

Coalescing:

// Assigns `Path::new("/")` if `optionalPath` is nil.
let value: Path = optionalPath ?? Path::new("/");

A regular match statement can also be used:

match (optional) {
    case value => {
        // Handle presence of value.
    }
    else => {
        // Handle absence of value.
    }
}

Naming conventions