RISK
Programming Language Reference - Complete Language Documentation
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.
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.
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).
| Type | Width | Range | Description |
|---|---|---|---|
i8 | 8-bit | −128 to 127 | Signed 8-bit integer |
i16 | 16-bit | −32,768 to 32,767 | Signed 16-bit integer |
i32 | 32-bit | −2,147,483,648 to 2,147,483,647 | Signed 32-bit integer (default) |
i64 | 64-bit | −9.2×10¹⁸ to 9.2×10¹⁸ | Signed 64-bit integer |
u8 | 8-bit | 0 to 255 | Unsigned 8-bit integer |
u16 | 16-bit | 0 to 65,535 | Unsigned 16-bit integer |
u32 | 32-bit | 0 to 4,294,967,295 | Unsigned 32-bit integer |
u64 | 64-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.
| Type | Width | Precision | Description |
|---|---|---|---|
f32 | 32-bit | ~7 decimal digits | Single-precision IEEE 754 float |
f64 | 64-bit | ~15 decimal digits | Double-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:
ptr<T>— mutable pointer to mutable dataptr<const T>— mutable pointer to immutable data (cannot write through)const ptr<T>— immutable pointer (cannot be reseated) to mutable dataconst ptr<const T>— fully immutable: neither the pointer nor the data may change
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.
refA: ref<i8>; without a referend, or assigning null to it, is a
compile-time error.
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.
| Method | Description |
|---|---|
.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.
// 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
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:
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.
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.
// 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
| Operator | Operation | Example |
|---|---|---|
+ | 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; |
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
| Operator | Meaning | Example |
|---|---|---|
== | 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 equal | 5 >= 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
| Op | Operation | Example |
|---|---|---|
& | 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.
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
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.
@"..." import syntax is reserved for upcoming standard library packages. Only regular file imports ("path/to/file.risk") are supported in the current release.
Summary
| Syntax | Description |
|---|---|
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
| Signature | Description | Example |
|---|---|---|
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
| Signature | Description | Example |
|---|---|---|
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
| Signature | Description | Example |
|---|---|---|
length(v: any): i32 | Returns the number of elements in an array, or the byte count of a string. | length([1,2,3]); // 3 |
Math
| Signature | Description | Example |
|---|---|---|
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.
| Signature | Description | Example |
|---|---|---|
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_*
| Signature | Description | Example |
|---|---|---|
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_*
| Signature | Description | Example |
|---|---|---|
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_*
| Signature | Description | Example |
|---|---|---|
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): void | Suspends 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.
| Call | Description | Example |
|---|---|---|
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); |
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.
| Signature | Description | Example |
|---|---|---|
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
| Signature | Description |
|---|---|
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
| Signature | Description |
|---|---|
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():
| Constant | Value | Constant | Value |
|---|---|---|---|
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_F1–KEY_F12 | 1011–1022 | KEY_SHIFT_UP/DOWN/RIGHT/LEFT | 1030–1033 |
KEY_CTRL_UP/DOWN/RIGHT/LEFT | 1040–1043 | KEY_ALT_UP/DOWN/RIGHT/LEFT | 1050–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.
| Signature | Description | Example |
|---|---|---|
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.
| Signature | Description | Example |
|---|---|---|
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
| Category | Type(s) | Size | Notes |
|---|---|---|---|
| Signed integers | i8 i16 i32 i64 | 1–8 bytes | Default: i32 |
| Unsigned integers | u8 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.
| Level | Operators | Notes |
|---|---|---|
| 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.
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; }
// 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.