Introduction to Risk

Risk is a systems-level programming language with a static type system, explicit memory management, and a rather convenient set of built-in functions (I hope it is). It suits domains where predictable performance, precise resource control, and low-level memory access are important constraints.

Scope of this documentation Risk has two development branches each with a compiler. riskc and riskavr (For Avr chips). There are many major differences between the two. For example, riskc has no issue allocating on heap, while riskavr with its target chips will either return a linker error or crash with heap. This documentation is only for riskc

Risk source files use the .risk extension (Yeah, kinda lazy when it comes to finding names, you will soon understand why). I designed the language so that every variable's type is known at compile time and every allocation is visible in the source code, meaning there is no hidden garbage collector and implicit boxing. :) At the same time, Risk comes with a "rather convenient" (again) standard library covering I/O, string manipulation, file access, math, randomness, and timing.

Design philosophy Risk aims to be explicit (type annotations on variable declarations, well specified casts, clearly stated pointer operations) without being too verbose, so reading a piece of Risk code tells you exactly what is happening in memory, and if your code screws up, you know i ain't doing anything in background. Your code looks like you, Period.

Primitive Types

Risk's type system has a set of well-defined platform-independent width primitive types covering integers, floats, booleans, characters, strings, and raw memory primitives.

Integer Types

Risk provides signed and unsigned integers in 8, 16, 32, and 64-bit widths. The default integer type is i32. Unsigned variants are available at all widths and are named with a u prefix. Integer literals can be written in decimal, hexadecimal (0x prefix), or binary (0b prefix).

TypeWidthRangeDescription
i8 8-bit −128 to 127 Signed 8-bit integer
i1616-bit −32,768 to 32,767 Signed 16-bit integer
i3232-bit −2,147,483,648 to 2,147,483,647 Signed 32-bit integer (default)
i6464-bit −9.2×10¹⁸ to 9.2×10¹⁸ Signed 64-bit integer
u8 8-bit 0 to 255 Unsigned 8-bit integer
u1616-bit 0 to 65,535 Unsigned 16-bit integer
u3232-bit 0 to 4,294,967,295 Unsigned 32-bit integer
u6464-bit 0 to 1.8×10¹⁹ Unsigned 64-bit integer
// Variable declarations with explicit integer types
a: i32 = -42;
b: u8  = 255;
c: i64 = 9259034215553;
d: u32 = 0xFF;     // hexadecimal literal
e: u8  = 0b011;    // binary literal

Floating-Point Types

Risk supports IEEE 754 single-precision (f32) and double-precision (f64) floating-point numbers. The default float type is f64. The special values INF and NAN are recognized as built-in literals for positive infinity and not-a-number respectively.

TypeWidthPrecisionDescription
f3232-bit~7 decimal digits Single-precision IEEE 754 float
f6464-bit~15 decimal digitsDouble-precision IEEE 754 float (default)
pi:  f64 = 3.141592;
e:   f32 = 2.7;
inf: f32 = INF;     // positive infinity
nan: f64 = NAN;     // not-a-number

Boolean

The bool type holds exactly two values: true or false. Boolean expressions can be formed using the comparison and logical operators and are the condition type required by all control flow constructs.

t:      bool = true;
f:      bool = false;
result: bool = (10 > 5) && !f;  // true

Character

The char type holds a single Unicode scalar value and is always 4 bytes wide, allowing the full Unicode range to be represented without multi-char sequences. Character literals are enclosed in single quotes. Standard escape sequences such as '\n', '\t', and '\\' are supported.

c:       char = 'A';
dollar:  char = '$';
newline: char = '\n';

String

The string type represents a sequence of characters enclosed in double quotes. Strings in Risk are owned values — they carry their own storage and do not require the programmer to manage a separate buffer manually for typical use.

s: string = "Hello World!";

Pointers

Pointers in Risk are declared with the ptr<T> generic syntax. They hold the raw memory address of a value of type T. The address-of operator & takes the address of a variable, and the dereference operator * reads or writes through the pointer.

Constness can be applied to either the pointer itself, the data it points to, or both, following a straightforward read-right-to-left rule:

POINTER mutable POINTER const DATA mutable DATA const ptr<T> ptrX 42 can reseat ✓ can write ✓ const ptr<T> ptrX LOCK 42 can't reseat ✗ can write ✓ ptr<const T> ptrY 14 LOCK can reseat ✓ can't write ✗ const ptr<const T> ptrY LOCK 14 LOCK can't reseat ✗ can't write ✗
x:          i32            = 42;
ptrX:       ptr<i32>       = &x;
*ptrX = 5; // dereference and write

y:          const i32       = 14;
ptrY:       ptr<const i32> = &y;
// *ptrY = 10;  WRONG — cannot write through a pointer-to-const

constPtrX:  const ptr<i32>       = &x;
// constPtrX can modify what it points to, but cannot be reseated

constPtrY:  const ptr<const i32> = &y;
// fully immutable — neither pointer nor data may change

References

References, declared with ref<T>, are non-nullable aliases to an existing variable. Unlike pointers, a reference cannot be declared without immediately binding it to a referend, and it cannot hold a null value. Reading a reference yields the referred value directly; writing through a reference modifies the original variable.

Non-nullable rule A reference must be initialized at the point of declaration. Declaring refA: ref<i8>; without a referend, or assigning null to it, is a compile-time error.
ptr<T> — Pointer STACK ptrX 0x7ffc… HEAP x : i32 42 can point elsewhere ref<T> — Reference STACK a : i8 5 refA 5 alias — same location ✗ null not allowed • ✗ cannot be reseated
a:    i8        = 5;
// refA: ref<i8>;        WRONG — references must be immediately bound
// refA: ref<i8> = null; WRONG — references cannot be null

refA: ref<i8> = a;
println(refA); // prints 5 — the actual value of a
refA = 77;     // writes through reference; a is now 77

Compound Types

Arrays

Risk provides two distinct array kinds: a compile-time-fixed static array and a heap-allocated dynamic array. Both use the Array<T> syntax, differentiated by whether a size argument is supplied.

Static Array — Array<T, N>

A static array has a fixed element count N that must be known at compile time. The array's memory is sized exactly to hold N elements of type T and cannot grow or shrink at runtime. Push and pop operations are not available. Accessing an out-of-bounds index is a compile-time or runtime error.

// arr: Array<i32, 5> = [];  WRONG — static arrays must be fully initialized
arr: Array<i32, 5> = [1, 2, 3, 4, 5];
// arr.push(6);              WRONG — no push on static arrays

len:  i32 = length(arr); // 5
arr[0] = 5;               // index-based write
// println(arr[5]);          WRONG — out-of-bounds access

Dynamic Array — Array<T>

A dynamic array is a heap-allocated, growable contiguous buffer. It starts empty and can be extended or reduced at runtime through a set of methods. Elements are accessed by index the same way as static arrays.

Array<i32, 5> — Static STACK — fixed at compile time 1 2 3 4 5 [0] [1] [2] [3] [4] N = 5, size fixed forever no .push() — no .pop() out-of-bounds [5] → error Array<i32> — Dynamic STACK — descriptor ptr → heap 0x… len 3 cap 6 HEAP — contiguous buffer 1 2 3 len = 3 (used) cap = 6 (allocated)
MethodDescription
.push(v) Appends v to the end.
.pop() Removes and discards the last element.
.push_front(v) Inserts v at index 0.
.pop_front() Removes the element at index 0.
.insert(i, v) Inserts v before index i.
.remove_at(i) Removes the element at index i.
v: Array<i32> = [];

v.push(6);
v.push(1); v.push(2); v.push(3);
v.push_front(0);     // inserts 0 at the front
v.insert(2, 12);    // inserts 12 at index 2
v.pop();            // removes last element
v.pop_front();      // removes first element
v.remove_at(2);    // removes element at index 2

len: i32 = length(v);
println(v[0]);

HashMap — Hashmap<Ta, Tb>

A Hashmap<Ta, Tb> maps keys of type Ta to values of type Tb using a hash table. It is declared with an empty brace literal {} and entries are read and written using bracket-index syntax identical to array access.

score: Hashmap<string, i32> = {};
score["alice"] = 100;
println(score["alice"]); // 100

Control Flow

Block Expressions

Any pair of braces { } introduces a new scope in Risk. Variables declared inside a block are destroyed when the block exits. Blocks can appear anywhere a statement is valid — including as the bodies of functions, loops, and branches, or as standalone scope delimiters for lifetime management.

func main() -> i32 {
    // outer scope
    {
        // inner scope — variables here do not leak out
    }
    return 0;
}

If/ else if /else

Conditional branching uses the familiar if / else if / else chain. The condition is a boolean expression enclosed in parentheses. Each branch body is wrapped in braces. Multiple else if clauses can be chained before an optional else fallthrough.

x: i32 = 16;
if(x > 0) {
    println("positive");
} else if(x < 0) {
    println("negative");
} else {
    println("zero");
}

Switch Statement

The switch statement dispatches on an integer or comparable value. Each case label is followed by a braced body. A break at the end of each case prevents fall-through to the next. A default case catches all values not matched by an explicit label.

switch(val) {
    case 1 { println(1); break; }
    case 2 { println(2); break; }
    case 3 { println(3); break; }
    default { println("unhandled"); }
}

For Loop

The for loop uses a C-style three-clause header: initializer, condition, and post-iteration expression. The loop variable is scoped to the loop body. break exits the loop immediately; continue skips to the next iteration.

for(i: i32 = 0; i < 5; i++) {
    if (i == 4) break;     // exits loop
    if (i == 2) continue;  // skips iteration
    println(i);
}

While Loop

The while loop evaluates a boolean condition before each iteration and repeats the body for as long as it holds true.

while(i < 10) {
    // body
}

Functions

Declaration & Definition

In Risk, functions are declared and defined in a single step using the func keyword. There is no separate forward-declaration or prototype step. The return type follows a -> arrow after the parameter list. Functions with no meaningful return value use void.

i
Entry Point Risk has an entry point function called main.
// SYNTAX: func name(param: Type, ...) -> ReturnType { body }

func add(a: i32, b: i32) -> i32 {
    return a + b;
}

res: i32 = add(5, 2); // 7

Function Pointers

A function pointer stores the address of a function with a specific signature and can be called indirectly through the pointer variable. The type of a function pointer is written as func(ParamTypes...) -> ReturnType. Function pointers can be passed as arguments to other functions, enabling higher-order patterns without closures.

func times2(x: i32) -> i32 {
    return x * 2;
}

func applyFn(f: func(i32) -> i32, v: i32) -> i32 {
    return f(v);
}

fp: func(i32) -> i32 = times2;

println(fp(5));              // 10
println(applyFn(times2, 5)); // 10
Direct call via function pointer variable function definition times2 (i32) → i32 stores addr func pointer var fp func(i32) → i32 fp(5) result 10 Higher-order call — passing a function as an argument function times2 passed as f argument v = 5 higher-order function applyFn (f, v) → calls f(v) f is func(i32)→i32 result 10

Type Aliases

The typealias keyword creates a named synonym for any type, including function pointer types. Aliases are interchangeable with the types they alias and are particularly useful for giving long function-pointer signatures readable names.

typealias CallbackI8ToStr = func(i8) -> string;

x: CallbackI8ToStr = someValidFunc; // cleaner than the full signature

typealias str = string;

y: str;     // valid
z: string; // also valid — aliases are purely syntactic

Lambda / Closure Functions

Risk supports anonymous function literals (lambdas). A lambda is defined inline with a capture clause ((...) for capturing the environment) followed by a parameter list in brackets and a braced body. Lambdas can be stored in function-pointer variables and called by name.

// No-parameter lambda
greet: func() -> void = (...)[]{
    println("Hello World!!!");
};
greet();

// Lambda with parameters
add: func(i32, i32) -> i32 = (...)[a: i32, b: i32] {
    return a + b;
};
res: i32 = add(1, 2); // 3

Structures

Block Structures

In Risk, a block is a composite data type that groups a contiguous list of named fields, each of which can be a primitive, another block, or even a function pointer. Blocks are analogous to structs in C. Fields can have optional default values that are used when the field is not explicitly provided during initialization.

Block Declaration

/* SYNTAX:
   block Name {
       fieldName: Type;
       fieldName: Type = defaultValue;
   } */

block Person {
    name:        string;
    age:         i32;
    dateOfBirth: Date;             // nested block
    print:       func(p: Person) -> void; // function pointer field
    male:        bool;
    occupation:  string = "Student"; // default value
}

Field Assignment — Three Styles

Risk supports three distinct ways to initialize a block instance, suited to different use cases:

explicitly set using default ① Manual set each field by dot notation age = 30 name = "Sponge bob" active = false all fields touched explicitly ② Named only override what you need age → default (18) name = "Bob" active → default (true) grayed = untouched defaults ③ Positional all values in declared order 21 ← age "Root" ← name false ← active ⚠ wrong order = silent bug

1. Manual field assignment — Declare the variable first, then set each field individually using dot notation. Fields with default values are already populated; only fields you care about need to be touched.

u1: User;
u1.age    = 30;
u1.name   = "Sponge bob";
u1.active = false;

2. Named field assignment — Initialize specific fields by name in a brace literal. Any field not mentioned keeps its declared default value. This is the most explicit and readable style when only a subset of fields differs from defaults.

u2: User = {name = "Bob"};            // age and active use defaults
u3: User = {age = 21, name = "Robot"}; // active uses default

3. Positional field assignment — Provide all field values in the exact order they were declared. This is the most compact style but requires knowing the declaration order precisely.

Order matters Positional initialization is sensitive to declaration order. Swapping values for fields of different types may silently compile and produce wrong results.
block User {
    age:    i32    = 18;
    name:   string;
    active: bool   = true;
}

u4: User = {21, "Root", false}; // VALID — matches declaration order
// u4: User = {21, false, "Root"}; // BAD — wrong order

Opts (Enumerations)

An opts declaration defines a named set of mutually exclusive symbolic constants, each backed by an integer value. The name "opts" reflects their purpose: they describe the available options that a variable of that type can take — a more intuitive framing than the traditional "enumeration". Each variant is referenced by the type name followed by a dot, yielding the underlying integer value. This makes opts suitable for state machines, status codes, direction flags, and anywhere a value must belong to a closed, well-named set.

Variant values can be assigned explicitly. If a variant has no explicit value, it automatically takes the value of the previous variant plus one, beginning from the last explicitly set value. This allows sparse or semantically grouped numbering without listing every value manually.

opts Direction — auto-increment from explicit anchor North 0 explicit = 0 +1 East 1 auto = 0+1 +1 South 2 auto = 1+1 +1 West 3 auto = 2+1 explicit anchor auto-incremented
// Basic opts with explicit values
opts Color {
    Red    = 0,
    Green  = 1,
    Blue   = 2,
    Yellow = 3
}

// Semantically grouped status codes
opts Status {
    Pending    = 100,
    InProgress = 200,
    Completed  = 300,
    Failed     = 400
}

// Auto-increment: East=1, South=2, West=3
opts Direction {
    North = 0,
    East,
    South,
    West
}

// Usage — opts resolve to their underlying integer
c: i32 = Color.Red;  // 0
println(c);

Operators

Arithmetic

OperatorOperationExample
+ Addition r = 5 + 3; // 8
- Subtraction r = 10 - 4; // 6
* Multiplication r = 6 * 7; // 42
/ Division (truncating) r = 10 / 3; // 3
+= Add-assign r += 5;
-= Subtract-assign r -= 5;
*= Multiply-assign r *= 5;
/= Divide-assign r /= 5;
Integer division in Risk truncates toward zero — dividing two integers always yields an integer. To get a floating-point result, cast at least one operand to a float type first.

Increment / Decrement

Both prefix and postfix forms of ++ and -- are supported. The postfix form returns the value before modification; the prefix form modifies the variable first and returns the new value.

x: i32 = 10;
println(x++); // prints 10, x becomes 11
println(++x); // x becomes 12, prints 12
println(x--); // prints 12, x becomes 11
println(--x); // x becomes 10, prints 10

Comparison

OperatorMeaningExample
== Equal to 5 == 5 → true
!= Not equal to 5 != 3 → true
< Less than 3 < 10 → true
<=Less than or equal 5 <= 5 → true
> Greater than 10 > 3 → true
>=Greater than or equal5 >= 6 → false

Logical

Logical operators act on bool values and produce a bool result. && and || are short-circuit evaluating — the right operand is only evaluated if needed.

A: bool = true;
B: bool = false;

A && B  // AND  → false
A || B  // OR   → true
!A      // NOT  → false

Bitwise

OpOperationExample
& Bitwise AND 0b1100 & 0b1010 = 0b1000
| Bitwise OR 0b1100 | 0b1010 = 0b1110
^ Bitwise XOR 0b1100 ^ 0b1010 = 0b0110
! Bitwise NOT !0b1100u8 = 0b0011
<<Left shift 1u32 << 4 = 16
>>Right shift 128u32 >> 3 = 16

Compound bitwise-assignment operators are supported: &=, |=, ^=, <<=, >>=.

a: u8 = 0b11001010; // 202
b: u8 = 0b10100101; // 165

a &= b; // bitwise AND-assign
a |= b; // bitwise OR-assign
a ^= b; // bitwise XOR-assign

Casting

Safe Cast — as T and cast<T>

The as keyword and the cast<T> function both perform explicit, well-defined numeric conversions. If the value being cast does not fit in the target type, truncation occurs — the operation completes without a runtime panic or exception. This makes the programmer explicitly responsible for ensuring values are in range.

x: i64 = 1000;
y: i32 = x as i32;       // safe cast — truncates if value overflows
z: f64 = cast<f64>(x);   // integer to float

Unsafe Cast — unsafe_cast<T>

unsafe_cast<T> reinterprets the raw bit pattern of a value as a completely different type without any conversion arithmetic. The bit width of source and target must match. This is the equivalent of C's type-punning via a union and bypasses all type safety guarantees. It is intended for low-level work such as inspecting the IEEE 754 representation of a float, or interfacing with hardware registers.

Use with caution unsafe_cast gives you the raw bits with no interpretation. The result is only meaningful if you know exactly what the bit pattern represents in the target type. Misuse leads to undefined behavior.
x: char = 'A';
b: i32  = unsafe_cast<i32>(x);
println(b); // prints 65 (ASCII value of 'A')

The register Keyword

Not a compiler hint Risk does not borrow the register keyword from C as a hint to keep a variable in a CPU register for performance. In Risk, register is exclusively for declaring memory-mapped I/O registers at specific hardware addresses.

Memory-mapped I/O registers are declared by specifying the hardware address in parentheses after the type. This binds the variable name directly to that physical address, so any read or write to the variable goes straight to the hardware peripheral — no indirection required. Bitwise operators are the standard way to manipulate individual bits within a register.

// SYNTAX: name: register<T>(address) = value;

PORTA: register<u8>(0x05);
PORTA |= (1 << 0x5); // set bit 5 of PORTA

Modules & Imports

Risk has a straightforward module system: one file, one module. If you've written some code in another .risk file, you pull it in with import. No package manifests, no magic resolution — just a path.

Basic Import

To import another file, use import followed by the file path in double quotes. All public symbols from that file become available in the current scope, qualified by the file's base name.

import "file.risk";     // import a local file
import "utils/math.risk"; // subdirectories work too

Aliased Import

You can bind an import to a shorter alias using as. After that, every symbol from that file is accessed through the alias. This keeps call sites clean and avoids name collisions when pulling in multiple files.

import "file.risk" as f;

f.functionA();          // call functionA from file.risk
x: i32 = f.compute(10); // use any exported symbol through the alias

Standard Library Imports

The built-in standard library functions (I/O, math, strings, etc.) are available without any import — they're just there. However, Risk will eventually support installable standard library packages using the @ prefix syntax, for example:

import @"tui/tui.risk";            // future: named standard library package
import @"tui/tui.risk" as tui;    // aliased, so you call tui.draw(...) etc.
Standard library packages are not yet available The @"..." import syntax is reserved for upcoming standard library packages. Only regular file imports ("path/to/file.risk") are supported in the current release.

Summary

SyntaxDescription
import "file.risk";Import a local file; symbols accessed by file base name.
import "file.risk" as f;Import and bind to alias f; call as f.symbol.
import @"pkg/pkg.risk";Future: named standard library package.
import @"pkg/pkg.risk" as p;Future: named standard library package with alias.

Built-in Functions & Standard Library

Risk ships a standard library of built-in functions organized into functional groups. All built-ins are available without any import statement.

I/O

SignatureDescriptionExample
print(v: any): void Prints any value to stdout without a trailing newline. print("Hello "); print(42);
println(v: any): void Prints any value to stdout followed by a newline. println("Hello, World!");
rprint(v: any): void Like print but interprets ANSI escape sequences. rprint("\x1b[31mRed\x1b[0m");
rprintln(v: any): void Like rprint but appends a newline. rprintln("\x1b[1mBold\x1b[0m");
input(): string Reads one line from stdin, strips the trailing newline. Returns an empty string on EOF. let name = input();
clrscr(): void Clears the terminal screen. clrscr();
flush_stdout(): void Flushes the stdout buffer, forcing all pending output to appear in the terminal immediately. Required after print calls in TUI or game loops where you need the full frame to land at once rather than trickle out across OS scheduling windows. print("frame..."); flush_stdout();

Type Conversion

SignatureDescriptionExample
toInt(s: string): i32 Parses a string to an integer. Handles optional +/- and leading whitespace. Returns 0 on failure. toInt("42"); // 42
toFloat(s: string): f64 Parses a string to a float. Returns 0.0 on failure. toFloat("3.14"); // 3.14
toBool(s: string): bool Interprets "true", "yes", "on", "1", "t", "y" (case-insensitive) as true. All else is false. toBool("yes"); // true
toStr(v: any): string Converts any value to its string representation. toStr(99); // "99"

General

SignatureDescriptionExample
length(v: any): i32Returns the number of elements in an array, or the byte count of a string.length([1,2,3]); // 3

Math

SignatureDescriptionExample
abs(x: f64): f64 Absolute value. abs(-5.0); // 5.0
pow(base: f64, exp: f64): f64 base raised to exp. pow(2.0, 8.0); // 256.0
sqrt(x: f64): f64 Square root. sqrt(9.0); // 3.0
sq(x: f64): f64 x squared (x * x). sq(4.0); // 16.0
cbrt(x: f64): f64 Cube root. cbrt(27.0); // 3.0
mod(x: f64, y: f64): f64 Floating-point remainder of x / y. mod(10.0, 3.0); // 1.0
max(a: f64, b: f64): f64 Larger of a and b. max(3.0, 7.0); // 7.0
min(a: f64, b: f64): f64 Smaller of a and b. min(3.0, 7.0); // 3.0
floor(x: f64): f64 Round down to nearest integer. floor(2.9); // 2.0
ceil(x: f64): f64 Round up to nearest integer. ceil(2.1); // 3.0
round(x: f64): f64 Round to nearest integer (half-away from zero). round(2.5); // 3.0
exp(x: f64): f64 e raised to x. exp(1.0); // ~2.718
exp2(x: f64): f64 2 raised to x. exp2(10.0); // 1024.0
log(x: f64): f64 Natural logarithm (base e). log(2.718); // ~1.0
log10(x: f64): f64 Base-10 logarithm. log10(1000.0); // 3.0
log2(x: f64): f64 Base-2 logarithm. log2(1024.0); // 10.0
sin(x: f64): f64 Sine of x (radians). sin(0.0); // 0.0
cos(x: f64): f64 Cosine of x (radians). cos(0.0); // 1.0
tan(x: f64): f64 Tangent of x (radians). tan(degToRad(45.0, 0.0)); // ~1.0
asin(x: f64): f64 Arcsine of x. Domain: [−1, 1]. Result in radians. asin(1.0); // ~1.5708
acos(x: f64): f64 Arccosine of x. Domain: [−1, 1]. Result in radians. acos(1.0); // 0.0
atan(x: f64): f64 Arctangent of x. Result in radians. atan(1.0); // ~0.7854
atan2(y: f64, x: f64): f64 Arctangent of y/x using sign of both to determine quadrant. Result in radians.atan2(1.0, 1.0); // ~0.7854
sinh(x: f64): f64 Hyperbolic sine. sinh(1.0); // ~1.1752
cosh(x: f64): f64 Hyperbolic cosine. cosh(0.0); // 1.0
tanh(x: f64): f64 Hyperbolic tangent. tanh(1.0); // ~0.7616
degToRad(deg: f64, _: f64): f64 Converts degrees to radians: deg × (π / 180). degToRad(180.0, 0.0); // ~3.14159
radToDeg(rad: f64, _: f64): f64 Converts radians to degrees: rad × (180 / π). radToDeg(3.14159, 0.0); // ~180.0

Strings — str_*

All string functions return new strings and do not modify the original.

SignatureDescriptionExample
str_length(s): i32 Byte length of s. str_length("hello"); // 5
str_upper(s): string New uppercased copy. str_upper("hello"); // "HELLO"
str_lower(s): string New lowercased copy. str_lower("HELLO"); // "hello"
str_capitalize(s): string First char uppercased, rest lowercased. str_capitalize("hELLO"); // "Hello"
str_substr(s, start, length): string Substring starting at byte start for at most length bytes. str_substr("Hello", 1, 3); // "ell"
str_find(s, substr): i32 Index of first occurrence of substr, or −1. str_find("hello", "ll"); // 2
str_rfind(s, substr): i32 Index of last occurrence of substr, or −1. str_rfind("hello", "l"); // 3
str_concat(s1, s2): string Concatenation of s1 and s2. str_concat("foo", "bar"); // "foobar"
str_replace(s, old, new): string Replaces all occurrences of old with new. str_replace("aXbXc", "X", "-"); // "a-b-c"
str_split(s, delim): Array<string> Splits s on delim, returning an array of parts. str_split("a,b,c", ","); // ["a","b","c"]
str_join(parts, delim): string Joins array of strings with delim between each. str_join(["a","b"], ", "); // "a, b"
str_trim(s): string Strips leading and trailing whitespace. str_trim(" hi "); // "hi"
str_startswith(s, prefix): bool True if s begins with prefix. str_startswith("hello", "he"); // true
str_endswith(s, suffix): bool True if s ends with suffix. str_endswith("hello", "lo"); // true
str_contains(s, substr): bool True if substr appears anywhere in s. str_contains("hello", "ell"); // true
str_to_chars(s): Array<char> Returns an array of the characters in s. str_to_chars("hi"); // ['h','i']
str_repeat(s, count): string s repeated count times. Returns "" for count == 0. str_repeat("ab", 3); // "ababab"
chr(n: i32): string ASCII character for integer n. chr(65); // "A"

File I/O — fio_*

SignatureDescriptionExample
fio_read(path): string Reads entire file contents as a string. fio_read("main.rk");
fio_bufread(path): Array<string> Reads file line by line; one string per line. fio_bufread("data.txt");
fio_write(path, content): bool Writes content to path, overwriting. Returns true on success. fio_write("out.txt", "done");
fio_append(path, content): bool Appends content to path; creates file if absent. Returns true on success.fio_append("log.txt", "line\n");
fio_bufwrite(path, lines): bool Writes array of strings, one per line. Returns true on success. fio_bufwrite("out.txt", lines);
fio_exists(path): bool True if the file exists. fio_exists("cfg.txt");
fio_delete(path): bool Deletes the file. Returns true on success. fio_delete("tmp.txt");
fio_size(path): i64 File size in bytes, or −1 on error. fio_size("data.bin");
fio_copy(src, dst): bool Copies src to dst. Returns true on success. fio_copy("a.txt", "b.txt");
fio_bytesread(path, buffer, count): i64 Reads up to count raw bytes from path into buffer. Returns bytes read. fio_bytesread("img.bin", buf, 512);
fio_byteswrite(path, buffer, count): i64 Writes count raw bytes from buffer to path. Returns bytes written. fio_byteswrite("img.bin", buf, 512);

Random — rand_*

SignatureDescriptionExample
rand_seed(seed: i64): void Seeds the RNG for reproducible sequences. Auto-seeded from system time if not called.rand_seed(42);
rand_int(max: i32): i32 Random integer in [0, max). rand_int(100); // e.g. 57
rand_range(min, max: i32): i32 Random integer in [min, max). rand_range(5, 15);
rand_float(): f64 Random float in [0.0, 1.0). rand_float(); // e.g. 0.4821
rand_float_range(min, max: f64): f64 Random float in [min, max). rand_float_range(1.5, 3.5);

Time — time_*

SignatureDescriptionExample
time_now(): i64 Current Unix timestamp in whole seconds. time_now();
time_millis(): i64 Wall-clock time in milliseconds since the Unix epoch. time_millis();
time_micros(): i64 Wall-clock time in microseconds since the Unix epoch. time_micros();
time_monotonic(): i64 Monotonically increasing nanosecond counter. Use for measuring elapsed durations.time_monotonic();
time_sleep(ms: i32): voidSuspends execution for ms milliseconds. time_sleep(500);

Array Methods

Dynamic arrays expose their operations as method-style calls on the array variable. There is no array_new function — allocate by declaring with an empty literal [] and push elements in, or declare with a literal list.

CallDescriptionExample
arr.push(v) Appends v to the end of the array. This is how you grow an array after declaring it as []. nums.push(42);
arr.pop() Removes and returns the last element. last: i32 = nums.pop();
arr.push_front(v) Inserts v at index 0, shifting all existing elements right. O(n). nums.push_front(0);
arr.pop_front() Removes and returns the first element, shifting the rest left. O(n). first: i32 = nums.pop_front();
arr.insert(i, v) Inserts v at index i, shifting elements from i onward right. nums.insert(2, 99);
arr.remove_at(i) Removes the element at index i, shifting the rest left. nums.remove_at(0);
arr.clear() Sets the array length to zero without freeing the underlying buffer. Capacity is retained. nums.clear();
length(arr): i32 Returns the current number of elements. This is a free function, not a method. n: i32 = length(nums);
Pre-allocating a fixed-size array Risk has no array_new(n, default). When you need a pre-filled array of known size (e.g. a game board), declare it as [] and fill it with a while loop:
board: Array<i32> = [];
i: i32 = 0;
while (i < W * H) { board.push(0); i = i + 1; }

Additional String Functions

These functions exist in the runtime but were missing from the main strings table.

SignatureDescriptionExample
str_capitalize(s): string Uppercases the first character and lowercases the rest. str_capitalize("hELLO"); // "Hello"
str_rfind(s, sub): i32 Returns the index of the last occurrence of sub in s, or −1 if not found. str_rfind("abab", "ab"); // 2
str_concat(a, b): string Concatenates two strings and returns the result. str_concat("foo", "bar"); // "foobar"
str_chars(s): Array<string> Splits a string into an array of single-character strings. str_chars("hi"); // ["h","i"]
str_char_at(s, i): string Returns the character at index i as a one-character string. Returns "" if out of bounds. str_char_at("abc", 1); // "b"
str_char_code(s): i32 Returns the ASCII value of the first character of s. str_char_code("A"); // 65
str_from_char_code(n): string Returns a one-character string for the given ASCII value. str_from_char_code(65); // "A"
char_to_str(c: char): string Converts a char value to a string. char_to_str('Z'); // "Z"

TUI & Keyboard

Functions for building full-screen terminal applications. Raw mode disables line-buffering and echo so your program receives keystrokes immediately and characters never appear on screen behind your UI. Always call tui_raw_mode_leave() before exiting, even on error, or the terminal will be left in a broken state.

Terminal Control

SignatureDescription
tui_raw_mode_enter(): void Puts the terminal into raw mode: disables line-buffering, echo, and signal keys (Ctrl-C etc.). Also sets stdout to unbuffered so escape sequences are never split. Call once at TUI startup.
tui_raw_mode_leave(): void Restores the terminal to the state it was in before tui_raw_mode_enter(). Must be called before the program exits.
flush_stdout(): void Flushes the stdout buffer immediately. In a TUI, call this once after all draw calls for a frame so the entire frame reaches the terminal atomically.
clrscr(): void Clears the entire terminal screen and moves the cursor to the top-left.

Keyboard Input

SignatureDescription
kb_read_key(): i32 Blocking read. Waits until a key is pressed and returns its code. Ordinary printable characters and control characters return their ASCII value (0–127). Special keys return values ≥ 1000 (see key code table below). Requires tui_raw_mode_enter() to have been called.
kb_hit(): bool Non-blocking check. Returns true if a key is currently available in the input buffer, false otherwise. Does not consume the key.
kb_read(): string Non-blocking read. Returns the next character as a one-character string, or "" if no key is waiting.
kb_wait(): string Blocking read. Waits for a key and returns it as a one-character string. Does not decode escape sequences — use kb_read_key() for arrow keys and function keys.

Key Code Table

Special key constants returned by kb_read_key():

ConstantValueConstantValue
KEY_UP 1000 KEY_DOWN 1001
KEY_RIGHT 1002 KEY_LEFT 1003
KEY_HOME 1004 KEY_END 1005
KEY_INSERT 1006 KEY_DELETE 1007
KEY_PAGE_UP 1008 KEY_PAGE_DOWN 1009
KEY_F1KEY_F121011–1022KEY_SHIFT_UP/DOWN/RIGHT/LEFT1030–1033
KEY_CTRL_UP/DOWN/RIGHT/LEFT1040–1043KEY_ALT_UP/DOWN/RIGHT/LEFT1050–1053
KEY_ENTER 13 KEY_ESCAPE 27
KEY_BACKSPACE 127 KEY_TAB 9

System — sys_*

Runtime information about the host environment. All functions take no arguments except sys_env.

SignatureDescriptionExample
sys_platform(): string Returns "linux", "macos", "windows", or "unknown". sys_platform(); // "linux"
sys_arch(): string Returns "x86_64", "arm64", "arm", "x86", or "unknown". sys_arch(); // "x86_64"
sys_hostname(): string The machine's hostname. sys_hostname();
sys_username(): string The current user's name from the USER or USERNAME environment variable. sys_username();
sys_cwd(): string The current working directory as an absolute path. sys_cwd();
sys_pid(): i64 The process ID of the running program. sys_pid();
sys_env(name: string): string Returns the value of the named environment variable, or "" if not set. sys_env("HOME");

Memory

Low-level memory operations. These are for advanced use — most programs do not need them.

SignatureDescriptionExample
alloc(n: i32): ptr<u8> Allocates n bytes on the heap and returns a raw pointer. Panics on allocation failure. buf: ptr<u8> = alloc(256);
sizeof(T): i32 Returns the size in bytes of type T at compile time. sizeof(i64); // 8
copy_mem(dst: ptr<u8>, src: ptr<u8>, n: i32): void Copies n bytes from src to dst. Regions must not overlap. copy_mem(dst, src, 64);
set_mem(dst: ptr<u8>, val: i32, n: i32): void Fills n bytes starting at dst with the byte value val. set_mem(buf, 0, 256);
is_inf(x: f64): bool Returns true if x is positive or negative infinity. is_inf(1.0 / 0.0); // true
is_nan(x: f64): bool Returns true if x is NaN. is_nan(NAN); // true
type(v: any): string Returns the type name of v as a string at compile time. type(42); // "i32"

Type Cheat Sheet

CategoryType(s)SizeNotes
Signed integers i8 i16 i32 i64 1–8 bytes Default: i32
Unsigned integersu8 u16 u32 u64 1–8 bytes u8 = byte
Floats f32 f64 4–8 bytes Default: f64
Boolean bool 1 byte true / false
Character char 4 bytes Unicode scalar value
String string Varies Owned string value
Raw pointer ptr<T> Word size Explicit memory access
Reference ref<T> Word size Non-nullable alias
Static array Array<T, N> N × sizeof(T) Stack-allocated, fixed size
Dynamic array Array<T> Heap Growable contiguous buffer
Hash map Hashmap<K, V> Heap Key-value hash table

Operator Precedence

Higher level numbers bind more loosely. When in doubt, use parentheses.

LevelOperatorsNotes
1 (highest)() [] . :: as Grouping, indexing, path, cast
2 ! - * & Unary operators, dereference, address-of
3 * / % Multiplicative
4 + - Additive
5 << >> Bit shift
6 & Bitwise AND
7 ^ Bitwise XOR
8 | Bitwise OR
9 == != < > <= >= Comparison
10 && Logical AND
11 || Logical OR
12 (lowest)= += -= *= /= %= &= ^= |= <<= >>=Assignment

Examples

A collection of complete, runnable Risk programs — from a terminal UI to file utilities. Each example is self-contained and can be compiled with riskc file.risk.

TUI Explorer

A keyboard-driven terminal browser over a list of famous figures. Navigate with k/j, press Enter to read a bio, any key to go back, q to quit.

func render_browser(names: Array<string>, selected: i32, e: string) -> void {
    clrscr();
    println(e, "[1;36m", "  Figures of History", e, "[0m");
    println(e, "[2m",    "  Select a person",     e, "[0m");
    println("  ──────────────────────────────────");

    i: i32 = 0;
    n: i32 = length(names);
    while (i < n) {
        if (i == selected) {
            println(e, "[7m", "  > ", names[i], e, "[0m");
        } else {
            println("    ", names[i]);
        }
        i = i + 1;
    }
    println("");
    println(e, "[2m", "  k/j navigate   Enter open   q quit", e, "[0m");
    flush_stdout();
}

func render_viewer(name: string, bio: string, e: string) -> void {
    clrscr();
    println(e, "[1;36m", "  ", name, e, "[0m");
    println("  ──────────────────────────────────");
    println("");
    println("  ", bio);
    println("");
    println(e, "[2m", "  any key to go back...", e, "[0m");
    flush_stdout();
}

func main() -> i32 {
    names: Array<string> = [
        "Albert Einstein",
        "Alan Turing",
        "Nikola Tesla",
        "Linus Torvalds",
        "Neil deGrasse Tyson"
    ];

    bios: Array<string> = [
        "Theoretical physicist. Reshaped our understanding of space, time, and gravity.",
        "Mathematician and father of computer science. Cracked Enigma, defined computation.",
        "Inventor and electrical engineer. Gave the world alternating current.",
        "Creator of Linux. Believes in open source, version control, and strong opinions.",
        "Astrophysicist and science communicator. Makes the cosmos feel personal."
    ];

    e:        string = chr(27);
    selected: i32    = 0;
    mode:     i32    = 0;
    viewed:   i32    = 0;

    tui_raw_mode_enter();
    print(e, "[?25l");
    flush_stdout();

    running: bool = true;
    while (running) {
        if (mode == 0) {
            render_browser(names, selected, e);
            key: i32 = kb_read_key();
            n:   i32 = length(names);

            if (key == 1000 || key == 107) {        // up / k
                if (selected > 0) { selected = selected - 1; }
            } else if (key == 1001 || key == 106) {  // down / j
                if (selected < n - 1) { selected = selected + 1; }
            } else if (key == 13) {                   // Enter
                viewed = selected;
                mode   = 1;
            } else if (key == 113 || key == 27) {   // q / Esc
                running = false;
            }
        } else {
            render_viewer(names[viewed], bios[viewed], e);
            kb_read_key();
            mode = 0;
        }
    }

    print(e, "[?25h");
    tui_raw_mode_leave();
    clrscr();
    return 0;
}

Hello World

The classic starting point — print a greeting and exit.

func main() -> i32 {
    println("Hello, World!");
    return 0;
}

Spice it up with a name argument read from the user:

func main() -> i32 {
    print("Enter your name: ");
    flush_stdout();
    name: string = input();
    println("Hello, ", name, "!");
    return 0;
}

Fibonacci

Compute and print the first n Fibonacci numbers iteratively, then also show a recursive version for contrast.

// Iterative — O(n) time, O(1) space
func fib_iter(n: i32) -> void {
    a: i64 = 0;
    b: i64 = 1;
    i: i32 = 0;
    while (i < n) {
        println(a);
        tmp: i64 = a + b;
        a = b;
        b = tmp;
        i = i + 1;
    }
}

// Recursive — clean but exponential; fine for small n
func fib(n: i32) -> i64 {
    if (n <= 1) { return n; }
    return fib(n - 1) + fib(n - 2);
}

func main() -> i32 {
    println("First 10 Fibonacci numbers:");
    fib_iter(10);

    println("");
    println("fib(20) = ", fib(20));   // 6765
    return 0;
}

File I/O

Write a few lines to a file, read them back, then append a timestamp. Demonstrates fio_write, fio_bufread, and fio_append.

func main() -> i32 {
    path: string = "notes.txt";

    // Write initial content (overwrites if exists)
    ok: bool = fio_write(path, "Line one\nLine two\nLine three\n");
    if (!ok) {
        println("Error: could not write file.");
        return 1;
    }

    // Read back line by line
    lines: Array<string> = fio_bufread(path);
    i: i32 = 0;
    n: i32 = length(lines);
    while (i < n) {
        println("  [", i, "] ", lines[i]);
        i = i + 1;
    }

    // Append a timestamp
    stamp: i64    = time_now();
    entry: string = "Written at unix time: ";
    fio_append(path, entry);
    fio_append(path, to_string(stamp));
    fio_append(path, "\n");

    sz: i64 = fio_size(path);
    println("File size: ", sz, " bytes");
    return 0;
}

Terminal Snake

A fully playable Snake game in the terminal. The snake grows each time it eats food, and the game ends when it hits a wall or itself. Uses raw mode input, ANSI cursor control, and a fixed-size board represented as a flat Array<i32>.

Controls: w a s d or arrow keys. q to quit at any time.

💡
What this exercises Arrays as a 2-D grid, a ring-buffer for the snake body, game-loop timing with time_sleep, non-blocking input with kb_read_key, and ANSI escape sequences for flicker-free rendering.
// Board is W×H cells. Cell values: 0 = empty, 1 = snake, 2 = food.
W:         i32 = 40;
H:         i32 = 20;
MAX_SNAKE: i32 = 800;
TICK_MS:   i32 = 120;

// Move cursor to (x, y) using ANSI escape
func goto_xy(e: string, x: i32, y: i32) -> void {
    print(e, "[", toStr(y + 1), ";", toStr(x + 1), "H");
}

func draw_border(e: string) -> void {
    row: i32 = 0;
    while (row < H) {
        goto_xy(e, 0, row);
        print(e, "[90m", "|", e, "[0m");
        goto_xy(e, W + 1, row);
        print(e, "[90m", "|", e, "[0m");
        row = row + 1;
    }
    col: i32 = 0;
    while (col < W + 2) {
        goto_xy(e, col, 0);
        print(e, "[90m", "-", e, "[0m");
        goto_xy(e, col, H + 1);
        print(e, "[90m", "-", e, "[0m");
        col = col + 1;
    }
}

func draw_cell(e: string, x: i32, y: i32, kind: i32) -> void {
    goto_xy(e, x + 1, y + 1);
    if (kind == 1) {
        print(e, "[32m", "■", e, "[0m");
    }
    if (kind == 2) {
        print(e, "[31m", "●", e, "[0m");
    }
    if (kind == 0) {
        print(" ");
    }
}

func spawn_food(board: Array<i32>, fx: ptr<i32>, fy: ptr<i32>) -> void {
    tries: i32 = 0;
    while (tries < 1000) {
        nx: i32 = rand_int(W);
        ny: i32 = rand_int(H);
        if (board[ny * W + nx] == 0) {
            *fx = nx;
            *fy = ny;
            board[ny * W + nx] = 2;
            return;
        }
        tries = tries + 1;
    }
}

func main() -> i32 {
    e: string = chr(27);

    // Flat board: index = y*W + x
    board: Array<i32> = [];
    i: i32 = 0;
    while (i < W * H) {
        board.push(0);
        i = i + 1;
    }

    // Ring-buffer arrays for snake body (x and y separately)
    sx: Array<i32> = [];
    sy: Array<i32> = [];
    i = 0;
    while (i < MAX_SNAKE) {
        sx.push(0);
        sy.push(0);
        i = i + 1;
    }

    head: i32 = 0;
    tail: i32 = 0;
    slen: i32 = 1;

    sx[0] = W / 2;
    sy[0] = H / 2;
    board[sy[0] * W + sx[0]] = 1;

    dx: i32 = 1;
    dy: i32 = 0;

    fx: i32 = 0;
    fy: i32 = 0;
    spawn_food(board, &fx, &fy);

    score: i32 = 0;

    tui_raw_mode_enter();
    clrscr();
    print(e, "[?25l");
    draw_border(e);
    draw_cell(e, sx[0], sy[0], 1);
    draw_cell(e, fx, fy, 2);
    flush_stdout();

    running: bool = true;
    while (running) {
        time_sleep(TICK_MS);

        key: i32 = -1;
        while (kb_hit()) {
            key = kb_read_key();
        }
        if (key == 119 || key == 1000) {
            if (dy != 1) { dx = 0; dy = -1; }
        } else if (key == 115 || key == 1001) {
            if (dy != -1) { dx = 0; dy = 1; }
        } else if (key == 97 || key == 1003) {
            if (dx != 1) { dx = -1; dy = 0; }
        } else if (key == 100 || key == 1002) {
            if (dx != -1) { dx = 1; dy = 0; }
        } else if (key == 113) {
            running = false;
        }

        nhx: i32 = sx[head] + dx;
        nhy: i32 = sy[head] + dy;

        if (nhx < 0 || nhx >= W || nhy < 0 || nhy >= H) {
            running = false;
        } else if (board[nhy * W + nhx] == 1) {
            running = false;
        } else {
            ate: bool = (board[nhy * W + nhx] == 2);

            head = (head + 1) % MAX_SNAKE;
            sx[head] = nhx;
            sy[head] = nhy;
            board[nhy * W + nhx] = 1;
            draw_cell(e, nhx, nhy, 1);

            if (ate) {
                score = score + 1;
                slen  = slen + 1;
                spawn_food(board, &fx, &fy);
                draw_cell(e, fx, fy, 2);
            } else {
                tx: i32 = sx[tail];
                ty: i32 = sy[tail];
                board[ty * W + tx] = 0;
                draw_cell(e, tx, ty, 0);
                tail = (tail + 1) % MAX_SNAKE;
            }

            goto_xy(e, 0, H + 2);
            print(e, "[2m", "  score: ", e, "[0m", e, "[1m", toStr(score), e, "[0m");
            flush_stdout();
        }
    }

    goto_xy(e, W / 2 - 5, H / 2);
    print(e, "[1;31m", "  GAME OVER  ", e, "[0m");
    goto_xy(e, W / 2 - 6, H / 2 + 1);
    print(e, "[2m", "  final score: ", e, "[0m", toStr(score));
    goto_xy(e, 0, H + 4);
    flush_stdout();

    print(e, "[?25h");
    tui_raw_mode_leave();
    return 0;
}
else if and inline comments Do not place a // comment on the same line as a closing } that is immediately followed by else or else if. The comment token lands between } and else in the token stream and breaks the chain. Put comments on their own line above instead.