1. File structure and comments
KernRift source files use the .kr extension. One file is one module. A program starts at fn main() (unless you pass --freestanding).
// Line comment
/* Block comment.
Can span multiple lines. */
fn main() {
println("Hello, KernRift!")
exit(0)
}
Semicolons are accepted but not required — useful when you want to put multiple statements on one line.
2. Types
KernRift is a systems language with fixed-width integer types. Every scalar is stored as a 64-bit word in variable slots; the specific width matters for pointer load/store and for struct field layout.
| Type | Width | Aliases | Notes |
|---|---|---|---|
uint8 | 1 B | u8, byte | Unsigned byte |
uint16 | 2 B | u16 | Unsigned 16-bit |
uint32 | 4 B | u32 | Unsigned 32-bit |
uint64 | 8 B | u64, addr | Unsigned 64-bit, pointer-sized |
int8 | 1 B | i8 | Signed byte |
int16 | 2 B | i16 | Signed 16-bit |
int32 | 4 B | i32 | Signed 32-bit |
int64 | 8 B | i64 | Signed 64-bit |
f16 | 2 B | IEEE 754 half-precision (storage only on ARM64) | |
f32 | 4 B | float | IEEE 754 single-precision; literal suffix 1.5f |
f64 | 8 B | double | IEEE 754 double-precision; default for unsuffixed float literals |
bool | 1 B | true / false, strict since v2.8.3 (no int → bool coercion) | |
char | 1 B | Holds a character literal 'A', '\n', … — strict since v2.8.3 |
The short aliases (u8, i64, byte, addr) are exact synonyms for the long form — use whichever you prefer. Floating-point types f16 (storage only), f32, and f64 are supported with full arithmetic, comparisons, conversions, and a math library (std/math_float.kr). if/while still accept integer conditions (0 is false, non-zero is true), so stdlib predicates that return u64 compose naturally.
Literals
- Decimal:
42,1000000 - Hex:
0x1000,0xDEADBEEF - Float:
1.5,-3.14,2e10,1.5f(f32 suffix) - Bool:
true,false - String:
"hello"with\n,\t,\\,\",\0escapes - Character:
'A','\n','\t','\r','\0','\\','\''— evaluates to the byte value - f-string:
f"x = {x}, pi ≈ {3.14}"—{expr}interpolates with type-directed formatting,{{/}}escape braces
3. Variables and assignment
u64 x = 42
u32 count = 0
u8 byte = 0xFF
count = count + 1
The type precedes the name. Uninitialized declarations hold undefined contents.
Compound assignment
| Operator | Meaning |
|---|---|
+= -= | add, subtract |
*= /= %= | multiply, divide, remainder |
&= |= ^= | bitwise AND, OR, XOR |
<<= >>= | shift left, shift right |
4. Operators
Expressions use a Pratt parser. Precedence from tightest to loosest:
| Prec. | Operators | Notes |
|---|---|---|
| 110 (prefix) | ! ~ - | Logical not, bitwise not, negation |
| 100 | * / % | Multiply, divide, remainder |
| 90 | + - | Add, subtract |
| 80 | << >> | Shift |
| 70 | < <= > >= | Unsigned comparison |
| 60 | == != | Equality |
| 50 | & | Bitwise AND |
| 40 | ^ | Bitwise XOR |
| 30 | | | Bitwise OR |
| 20 | && | Logical AND |
| 10 | || | Logical OR |
<, <=, >, >= are unsigned. For signed comparisons, use signed_lt, signed_gt, signed_le, signed_ge.5. Control flow
if / else
if x > 10 {
println("big")
} else {
println("small")
}
Parentheses around the condition are optional. else if is chained via a nested else { if ... }.
while
u64 i = 0
while i < 10 {
println(i)
i = i + 1
}
for (range)
for i in 0..n {
println(i)
}
0..n is exclusive — i takes values 0, 1, ..., n-1. There is no inclusive ..= form; use 0..n+1 when you need it.
break, continue, match, return
while true {
if done { break }
if skip { continue }
}
match opcode {
1 => { do_add() }
2 => { do_sub() }
3 => { do_mul() }
}
fn get_val() -> u64 {
return 42
}
6. Functions
fn add(u64 a, u64 b) -> u64 {
return a + b
}
fn greet(u64 name) {
print("Hello, ")
print_str(name)
println("!")
}
The return type after -> is optional; omitting it means the function returns void. Parameters are TYPE name. Recursion and mutual recursion are supported — function order within a file doesn't matter.
Up to 8 arguments pass in registers (6 on Windows x64). Functions with more arguments pass the overflow on the stack.
7. Structs, methods, and enums
Structs
Field layout is packed — no alignment padding. Fields are stored in declaration order.
struct Point {
u64 x
u64 y
}
Point p
p.x = 10
p.y = 20
println(p.x)
Methods
fn Point.sum(Point self) -> u64 {
return self.x + self.y
}
fn main() {
Point p
p.x = 10
p.y = 20
println(p.sum()) // 30
exit(0)
}
Methods receive self as a reference to the caller's struct. Field reads and writes through self.field work normally.
Enums
enum Color {
Red = 0
Green = 1
Blue = 2
}
Each variant is a named integer constant accessible as Color.Red, usable wherever an integer is expected.
8. Arrays
Local arrays
u8[256] buffer
buffer[0] = 0xAA
u64 b = buffer[0]
Local arrays are allocated on the stack. The variable holds a pointer to the first element, so buffer alone evaluates to the base address. Indexing is unchecked.
Static arrays
static u8[1024] message_buf
fn main() {
message_buf[0] = 72 // 'H'
message_buf[1] = 105 // 'i'
message_buf[2] = 0
print_str(message_buf)
exit(0)
}
Static arrays get storage in the data section. They're zero-initialized by the loader.
Struct arrays
struct Point { u64 x; u64 y }
fn main() {
Point[10] pts
pts[0].x = 1
pts[0].y = 2
pts[5].x = 50
println(pts[5].x)
exit(0)
}
Element indexing uses the struct's full size as stride. pts[i].field is a first-class syntax that reads and writes the field at the correct offset within element i.
9. Slice parameters
A slice parameter [TYPE] name is sugar for a fat pointer: a (ptr, len) pair passed as two separate arguments. Inside the function, data.len reads the length and data is a plain pointer for indexing.
fn sum_bytes([u8] data) -> u64 {
u64 total = 0
u64 i = 0
u64 n = data.len
while i < n {
total = total + load8(data + i)
i = i + 1
}
return total
}
fn main() {
u8[6] buf
buf[0] = 10
buf[1] = 20
buf[2] = 30
// Caller passes (pointer, length) — two arguments
println(sum_bytes(buf, 3))
exit(0)
}
This is the classic C (ptr, len) pattern with a nicer symbolic name for the length inside the callee.
10. Static variables and constants
static u64 counter = 0
static u64 gpio_base = 0x3F200000
const u64 BAUD = 115200
const u64 UART_BASE = 0x3F201000
static variables live in the data section for the lifetime of the program. const creates a compile-time integer constant that is inlined at use sites — there is no runtime storage.
11. Pointer operations
KernRift has no dedicated pointer type. Addresses are just u64 values. To read or write memory at an address, use the pointer built-ins.
The easy way
u64 v = load64(addr) // read 64 bits
u32 x = load32(addr) // read 32 bits
u16 h = load16(addr) // read 16 bits
u8 b = load8(addr) // read one byte
store64(addr, 0xDEADBEEF) // write 64 bits
store32(addr, 0x1234) // write 32 bits
store16(addr, 0x5678) // write 16 bits
store8(addr, 0xAA) // write 1 byte
The load builtins zero-extend the read into a full u64. The store builtins write exactly the specified width.
The verbose way (unsafe blocks)
u64 val = 0
unsafe { *(addr as u32) -> val } // load
unsafe { *(addr as u8) = some_byte } // store
The cast type determines access width. Supported cast types: u8, u16, u32, u64, i8, i16, i32, i64. The load*/store* builtins compile to identical code and are usually cleaner.
12. Volatile and atomic
Volatile: MMIO-safe loads and stores
For memory-mapped I/O, the compiler must not reorder or elide the access, and the operation must complete before anything after it.
u32 v = vload32(mmio_addr) // volatile load, barrier after
vstore32(mmio_addr, 0x01) // volatile store, barrier before
All widths are available: vload8, vload16, vload32, vload64, vstore8..vstore64. The barrier emitted is:
- x86_64:
mfence(full memory fence) - ARM64:
DSB SY(data synchronization barrier — waits for completion, not just ordering)
volatile { *(addr as u32) = val } is the equivalent block form and does the same thing.
Atomic operations
u64 v = atomic_load(addr)
atomic_store(addr, v)
u64 ok = atomic_cas(addr, expected, desired)
u64 old = atomic_add(addr, delta) // returns previous value
u64 old = atomic_sub(addr, delta)
u64 old = atomic_and(addr, mask)
u64 old = atomic_or(addr, mask)
u64 old = atomic_xor(addr, mask)
These compile to LOCK-prefixed instructions on x86_64 and LDXR/STXR exclusive pairs on ARM64. atomic_cas returns 1 on success, 0 on failure.
13. Device blocks (MMIO)
For driver code, a device block describes a hardware register set at a fixed base address. Field reads and writes compile directly to volatile loads and stores of the right width, with the proper memory barriers.
device UART0 at 0x3F201000 {
Data at 0x00 : u32
Flag at 0x18 : u32
IBRD at 0x24 : u32
FBRD at 0x28 : u32
LCRH at 0x2C : u32
Ctrl at 0x30 : u32 rw
}
fn putc(u8 c) {
// Spin until TX FIFO has room
while (UART0.Flag & 0x20) != 0 { }
UART0.Data = c
}
Syntax:
device NAME at ADDR { ... }declares a device rooted atADDR.FIELD at OFFSET : TYPE [rw|ro|wo]declares a register. The access specifier is currently optional and parsed-but-ignored — future versions will enforce it.- Supported field types:
u8,u16,u32,u64(and signed variants).
Device blocks sit on top of the volatile builtins — there is no hidden mechanism, just a convenient named-register syntax.
14. Inline assembly
The asm keyword emits raw machine instructions at the call site.
asm("nop")
asm("cli")
asm("sti")
asm {
"cli";
"mov rax, cr0";
"sti"
}
// Raw hex bytes when the assembler doesn't recognize a mnemonic
asm("0x0F 0x01 0xD9") // vmmcall (x86_64)
asm("0xD503201F") // nop (ARM64)
Supported instructions
x86_64: nop, ret, hlt, int3, iretq, cli, sti, cpuid, rdmsr, wrmsr, lgdt [rax], lidt [rax], invlpg [rax], ltr ax, swapgs, control-register moves (mov cr0, rax, etc.), port I/O (in al, dx, out dx, al, wide variants).
ARM64: nop, ret, eret, wfi, wfe, sev, barriers (isb, dsb sy/ish, dmb sy/ish), svc #N, and mrs/msr for 20+ system registers including SCTLR_EL1, VBAR_EL1, TCR_EL1, MAIR_EL1, MPIDR_EL1, CurrentEL.
15. Imports
import "std/io.kr"
import "std/string.kr"
import "utils.kr"
Import paths are resolved relative to the importing file, then under ~/.local/share/kernrift/ (or %LOCALAPPDATA%\KernRift\share\ on Windows). Circular imports are detected and rejected.
16. Built-in functions
All of these are compiler intrinsics — no runtime library, no imports needed.
I/O
| Function | Description |
|---|---|
print(a, b, …) | Typed, variadic (v2.8.3). Each arg is formatted by its static type: string literals emit as-is, integers decimal, floats via fmt_f64/fmt_f32, bools as true/false, chars as their byte. Space-separated, no trailing newline. |
println(a, b, …) | Same, plus a newline. |
print_str(s) | Print a null-terminated string from a pointer variable (e.g. return of int_to_str, fmt_hex). |
println_str(s) | Same, plus a newline. |
| f-strings | f"x = {x}, pi ≈ {3.14}" — {expr} interpolates with the same type-directed formatter print uses; {{/}} escape braces. |
write(fd, buf, len) | Write len bytes from buf to file descriptor fd. |
file_open(path, flags) | Open a file. Returns a descriptor. |
file_read(fd, buf, len) | Read up to len bytes. |
file_write(fd, buf, len) | Write len bytes. |
file_close(fd) | Close a descriptor. |
file_size(fd) | Size of an open file. |
print(variable) and println(variable) format the variable as a decimal integer. If you want to print a string that lives in a variable (e.g. the return value of int_to_str), use print_str/println_str instead.Memory
| Function | Description |
|---|---|
alloc(size) | Heap-allocate size bytes. Returns a pointer. |
dealloc(ptr) | Free a previously allocated block. |
memcpy(dst, src, len) | Copy len bytes. |
memset(dst, val, len) | Fill len bytes with val. |
str_len(s) | Length of a null-terminated string. |
str_eq(a, b) | 1 if two null-terminated strings are equal, 0 otherwise. |
Pointer load/store
| Function | Description |
|---|---|
load8/16/32/64(addr) | Read a value of the given width, zero-extended to u64. |
store8/16/32/64(addr, val) | Write a value of the given width. |
vload8/16/32/64(addr) | Volatile load with barrier — for MMIO. |
vstore8/16/32/64(addr, val) | Volatile store with barrier — for MMIO. |
Atomic
| Function | Description |
|---|---|
atomic_load(ptr) | Sequentially-consistent load. |
atomic_store(ptr, val) | Sequentially-consistent store. |
atomic_cas(ptr, exp, new) | Compare-and-swap. Returns 1 on success. |
atomic_add/sub/and/or/xor(ptr, val) | Read-modify-write, returns old value. |
Bit manipulation
| Function | Description |
|---|---|
bit_get(v, n) | Bit n of v (0 or 1). |
bit_set(v, n) | Return v with bit n set. |
bit_clear(v, n) | Return v with bit n cleared. |
bit_range(v, start, width) | Extract width bits starting at start. |
bit_insert(v, start, width, bits) | Insert bits into v at position start. |
Signed comparison
The normal <, <=, >, >= operators are unsigned. For signed comparisons:
signed_lt(a, b) signed_gt(a, b)
signed_le(a, b) signed_ge(a, b)
Platform and process
| Function | Description |
|---|---|
exit(code) | Terminate the process with an exit code. |
get_target_os() | Host OS: 0=Linux, 1=macOS, 2=Windows, 3=Android. |
get_arch_id() | Host arch: 1=x86_64, 2=ARM64. |
exec_process(path) | Spawn and wait for a process. Returns exit code. |
set_executable(path) | chmod +x equivalent. |
get_module_path(buf, size) | Write the current binary's path into buf. |
fmt_uint(buf, val) | Format val as decimal into buf. Returns length. |
syscall_raw(nr, a1..a6) | Raw syscall with up to 6 arguments. |
Function pointers
| Function | Description |
|---|---|
fn_addr(name) | Get the address of a named function. |
call_ptr(addr, ...) | Call a function by address with any number of arguments. |
17. Annotations
Annotations appear immediately before a function or struct declaration.
| Annotation | Effect |
|---|---|
@export | Mark a function for inclusion in the output symbol table. |
@naked | Emit the function with no prologue/epilogue. Use for interrupt handlers and low-level entry points that manage their own stack. |
@noreturn | Mark a function that never returns (e.g. panic, infinite loops). Omits the epilogue. |
@packed | Accepted on struct declarations. KernRift structs are already packed (no alignment padding), so this annotation currently documents intent. |
@section("name") | Record a linker section name. Used with --emit=obj output. |
@naked fn isr() {
asm { "cli"; "nop"; "iretq" }
}
@noreturn fn panic() {
write(2, "panic\n", 6)
while true { asm("hlt") }
}
18. Compiler CLI
# Default: fat binary (8 platform slices, BCJ+LZ-Rift-compressed)
$ krc hello.kr -o hello.krbo
$ kr hello.krbo
# Single architecture — native ELF
$ krc hello.kr --arch=x86_64 -o hello
$ krc hello.kr --arch=arm64 -o hello
# Cross-format output
$ krc hello.kr --emit=pe -o hello.exe
$ krc hello.kr --emit=macho -o hello
$ krc hello.kr --emit=android -o hello # ARM64 PIE ELF
$ krc hello.kr --emit=obj -o hello.o # ELF relocatable
$ krc hello.kr --emit=asm -o hello.s # disassembled listing
# Freestanding: no main trampoline, no auto-exit
$ krc --freestanding kernel.kr -o kernel.elf
# Backend selection (default is the SSA IR backend with graph-coloring regalloc)
$ krc hello.kr --legacy -o hello # bypass IR, use the original direct codegen
$ krc hello.kr --emit=ir # dump IR in human-readable form
# Tools
$ krc check module.kr # semantic checks only
$ krc fmt module.kr # auto-format in place
$ krc --version
$ krc --help
kr runner
$ kr program.krbo # extracts the slice for the current host and runs it
$ kr --version
$ kr --help
19. Freestanding mode
krc --freestanding produces a binary suitable for bare-metal environments:
- No
_starttrampoline. - No automatic
exit(0)at the end ofmain. - No OS-specific syscall wrappers injected.
$ krc --freestanding --arch=arm64 kernel.kr -o kernel.elf
Use this for kernel entry points, bootloaders, and embedded firmware. The programmer is responsible for setting up the stack, calling into main, and handling any return.
Stack size warnings
The compiler prints a warning to stderr when a function's stack frame exceeds 4096 bytes — catches accidental large local arrays that could overflow a kernel stack.
20. Binary formats
| Format | Produced by | Use |
|---|---|---|
.krbo fat binary | default (no --arch) | Cross-platform distribution — kr picks the right slice |
| ELF executable | --arch=x86_64/--arch=arm64 on Linux | Native Linux binary |
| ELF relocatable | --emit=obj | Link into an external object (.o) |
| Mach-O | --emit=macho | macOS executable |
| PE | --emit=pe | Windows .exe |
| Android PIE ELF | --emit=android | Android ARM64 |
| Assembly listing | --emit=asm | Human-readable disassembly with labels |
A .krbo fat binary packs up to 8 platform slices (Linux x86_64, Linux ARM64, Windows x86_64, Windows ARM64, macOS x86_64, macOS ARM64, Android ARM64, Android x86_64), each BCJ+LZ-Rift compressed. The kr runner extracts and executes the slice matching the current host at startup.
See the examples/ directory for runnable programs demonstrating every feature above.