Language Specification
Philosophy
Section titled “Philosophy”- Go’s simplicity meets systems-level control
- Target audience: Go developers who want more control over memory and performance
- Core differentiator: simplicity
- Lower-level than high-level: suitable for OS/kernels and very fast web applications
Memory Model
Section titled “Memory Model”Run uses generational references (inspired by Vale) for memory safety without a borrow checker or garbage collector.
- Every allocation carries a generation number
- Non-owning references store a remembered generation
- On dereference, a runtime generation check verifies the object is still alive
- Owning references auto-free when they go out of scope (deterministic destruction)
- Default global allocator; collection allocations can optionally specify a custom allocator
- No borrow checker, no GC, no reference counting
Pointer Types
Section titled “Pointer Types”&T— read/write pointer (default, Go-like semantics)@T— read-only pointer (compiler-enforced immutability on pointee)
Allocation Expressions
Section titled “Allocation Expressions”Run provides a built-in alloc expression for collection and channel allocation:
s := alloc([]int, 64)m := alloc(map[string]string, 32)c := alloc(chan[int])Valid allocation targets are:
- slices (
[]T) - maps (
map[K]V) - channels (
chan Torchan[T])
alloc arguments:
alloc(type)— use type defaultsalloc(type, capacity)— set initial capacity/bufferalloc(type, capacity, allocator: expr)— custom allocatoralloc(type, allocator: expr)— custom allocator with default capacity
Default behavior when capacity is omitted:
- slice: empty slice with capacity 0 (grows on append)
- map: map with runtime default buckets
- channel: unbuffered channel
Custom allocators in alloc are named via allocator: for readability and to avoid positional ambiguity.
Variables
Section titled “Variables”var a int // mutable, zero-initializedvar a int = 32 // mutable, explicit type with initializationa := 32 // mutable, short declaration with type inferencelet a int = 32 // immutable, explicit typelet a = compute() // immutable, type inferencevar— mutable binding, can be reassignedlet— immutable binding, must be initialized, cannot be reassigned (compiler-enforced):=— short declaration (mutable, equivalent tovarwith type inference)
Functions
Section titled “Functions”pub fun add(a int, b int) int { return a + b}
fun private_helper(x int) int { return x * 2}- Zig-style signature: return type after parameters, no arrow
pubkeyword for public visibility, private by default- Full closures supported:
fun(x int) int { return x + 1 }
Methods (Go-style Receivers)
Section titled “Methods (Go-style Receivers)”Methods are functions with a receiver parameter, declared outside the struct:
fun (name ReceiverType) method_name(params) return_type { body }The receiver appears in parentheses between fun and the method name — identical to Go’s
method declaration syntax. Methods are not defined inside the struct body; the struct
contains only data.
pub type Point struct { x f64 y f64}
// Read-only receiver — cannot modify pfun (p @Point) length() f64 { return math.sqrt(p.x * p.x + p.y * p.y)}
// Read/write receiver — can modify pfun (p &Point) translate(dx f64, dy f64) { p.x = p.x + dx p.y = p.y + dy}Receiver types:
&T— read/write pointer receiver. The method can read and modify the struct.@T— read-only pointer receiver. Compiler-enforced immutability on the receiver.T— value receiver. The method receives a copy of the struct. Useful for small types where copying is cheaper than pointer indirection.
Methods can be made public with pub:
pub fun (p @Point) distance(other @Point) f64 { dx := p.x - other.x dy := p.y - other.y return math.sqrt(dx * dx + dy * dy)}The colon between receiver name and type is optional: (p &Point) and (p: &Point) are
both valid.
Multiple Return Values
Section titled “Multiple Return Values”Functions can return multiple values using anonymous structs (Zig-style):
fun divmod(a int, b int) struct { quotient int, remainder int } { return .{ quotient: a / b, remainder: a % b }}
// Caller accesses fields on the result:result := divmod(10, 3)result.quotient // 3result.remainder // 1- Anonymous struct types can be used anywhere a type is expected
- Anonymous struct literals use
.{ field: value }syntax - Fields can be separated by commas or newlines
- Works with error unions:
fun parse(s string) !struct { value int, rest string } - Works with pointers:
fun make() &struct { x int, y int }
Error Handling
Section titled “Error Handling”Zig-style error unions. A function that can fail returns !T:
fun readFile(path string) !string { // returns string on success, error on failure}
// Caller handles with try:content := try readFile("config.txt")
// Or with switch:switch readFile("config.txt") { .ok(content) :: use(content), .err(e) :: log(e),}A function that returns nothing but can fail uses bare !:
fun save(path string) ! { try writeFile(path, data)}
// Caller:try save("output.txt")- Error sets are inferred by the compiler
- No generics needed —
!Tis a built-in language construct - Bare
!is equivalent to an error union with a void success type
Error Context
Section titled “Error Context”When propagating errors with try, you can attach context using :::
content := try readFile(path) :: "loading config"If the expression returns an error, the context string is attached to the error before it propagates. This builds a chain of context as errors bubble up through the call stack, making it easy to trace the origin of failures.
Plain try (without context) still propagates errors unchanged.
Type System
Section titled “Type System”Primitive Types
Section titled “Primitive Types”- Integers:
int,uint,i8,i16,i32,i64,u32,u64,byte - Floats:
f32,f64 - Boolean:
bool - String:
string— UTF-8 byte slice
Strings
Section titled “Strings”- UTF-8 encoded byte slices
- Default iteration yields characters (unicode codepoints):
for c in s { }— iterate over charactersfor b in s.bytes { }— iterate over raw bytes
Structs
Section titled “Structs”pub type Point struct { x f64 y f64}- Type declarations start with the
typekeyword,pubmodifier for exported types - Structs contain only data — no methods inside the body
- Methods are declared outside with a Go-style receiver (see Methods under Functions)
Interfaces (Explicit)
Section titled “Interfaces (Explicit)”pub type Stringer interface { fun to_string() string}
pub type Point struct { implements( Stringer )
x f64 y f64}
fun (p @Point) to_string() string { return fmt.sprintf("(%f, %f)", p.x, p.y)}interfacedefines a set of method signatures (no receiver in signatures)- Structs declare which interfaces they implement via an
implementsblock - Method implementations remain outside the struct with a receiver (Go-style)
- No operator overloading
Sum Types / Tagged Unions
Section titled “Sum Types / Tagged Unions”type State = .loading | .ready(Data) | .error(string)
switch state { .loading :: show_spinner(), .ready(data) :: render(data), .error(msg) :: show_error(msg),}- First-class pattern matching via
switch
Nullable Types
Section titled “Nullable Types”var x int? = nullvar y int? = 42
switch x { .some(val) :: use(val), .null :: handle_missing(),}- Compile-time null safety (Kotlin-style)
Type?denotes a nullable type- Must handle null explicitly before use
Newtype
Section titled “Newtype”type UserID = int // distinct type, not an aliastype Email = string- Creates a new type that is not interchangeable with the underlying type
Control Flow
Section titled “Control Flow”For (unified loop)
Section titled “For (unified loop)”for { } // infinite loopfor condition { } // while loopfor i in 0..10 { } // range iterationfor item in collection { } // iteratorfor i, item in collection { } // index + valuebreakandcontinuesupported
Switch (pattern matching)
Section titled “Switch (pattern matching)”switch value { 1 :: do_one(), 2, 3 :: do_two_or_three(), .variant(x) :: use(x), _ :: default(),}- No fallthrough
- Exhaustive matching on sum types
fun process() ! { file := try os.open("data.txt") defer file.close()
// file.close() runs when function exits}- Go-style defer for cleanup
Concurrency
Section titled “Concurrency”Green Threads
Section titled “Green Threads”run my_function()run fun() { do_work() }runspawns a green thread (goroutine-style)- Lightweight, multiplexed onto OS threads by runtime
Channels
Section titled “Channels”var ch chan intch := alloc(chan[int]) // unbufferedch := alloc(chan[int], 100) // buffered
ch <- 42 // sendval := <-ch // receiveThe unsafe Package
Section titled “The unsafe Package”The unsafe package is a standard library package providing low-level operations
that bypass Run’s safety guarantees. Like Go’s import "unsafe", its presence in
a file’s imports is the signal that dangerous operations are in use.
use "unsafe"
var p unsafe.Pointer = unsafe.ptr(&x) // raw pointervar n int = unsafe.sizeof(MyStruct) // type size in bytesvar off int = unsafe.offsetof(MyStruct, "field") // field byte offsetunsafe.Pointer— raw pointer type, convertible to/from any&Tor@Tunsafe.ptr(p)— convert a typed pointer tounsafe.Pointerunsafe.cast(&T, p)— convertunsafe.Pointerback to a typed pointerunsafe.sizeof(T)— size of typeTin bytesunsafe.alignof(T)— alignment of typeTunsafe.offsetof(T, field)— byte offset of a field within a structunsafe.slice(p, len)— create a slice from a raw pointer and length
No special keyword or block syntax — use "unsafe" is a regular use statement and
grep "unsafe" finds every file that uses low-level operations.
Assembly Language
Section titled “Assembly Language”Run provides a universal, portable assembly language for very low-level optimizations — similar
to Go’s Plan9 assembly. This gives developers an escape hatch below unsafe for performance-critical
code paths without sacrificing portability.
Inline Assembly
Section titled “Inline Assembly”Inline assembly blocks can appear inside any function using the asm keyword:
fun fast_add(a u64, b u64) u64 { return asm(a -> r0, b -> r1) u64 { add r0, r0, r1 }}asm(inputs) return_type { instructions }— inline assembly expression- Inputs:
expr -> registerbinds a Run expression to an abstract register using the->(arrow right) operator - Return type: the type of the value produced (read from
r0by convention); optional for void assembly - Clobber list:
asm(inputs; clobber: r2, r3, memory) { ... }declares side effects — the;separates inputs from the clobber clause - No-input form:
asm() { instructions }for assembly with no inputs or outputs - Platform conditionals: Inside the assembly body,
#platform_name { ... }selects instructions for a specific target (e.g.,#x86_64,#arm64). The#token introduces the platform selector
Abstract Register Model
Section titled “Abstract Register Model”Run assembly uses abstract register names that map to platform registers at compile time:
| Abstract | x86-64 (System V) | ARM64 (AAPCS) |
|---|---|---|
r0–r15 | rax, rbx, rcx, … | x0–x15 |
f0–f15 | xmm0–xmm15 | v0–v15 (scalar) |
sp | rsp | sp |
fp | rbp | x29 |
This allows writing assembly that is structurally portable while still mapping to efficient native instructions. For platform-specific instructions, use conditional sections:
asm(data -> r0) { #x86_64 { popcnt r0, r0 } #arm64 { cnt v0.8b, v0.8b addv b0, v0.8b fmov r0, s0 }}External Assembly Files
Section titled “External Assembly Files”For larger assembly routines, use external .rasm files with platform suffixes:
fast_math.rasm— portable assembly (abstract registers only)fast_math_amd64.rasm— x86-64 specificfast_math_arm64.rasm— ARM64 specific
The build system selects the correct file based on the target architecture. If a platform-specific file exists, it takes priority over the portable version.
pub fun simd_dot_product(a @[]f32, b @[]f32, len int) f32 { // x86-64 native assembly using real register names vxorps ymm0, ymm0, ymm0 // ...}External assembly functions are callable from Run code like any other function.
Implementation
Section titled “Implementation”Inline assembly lowers to GCC/Clang __asm__ blocks in the C codegen backend.
External .rasm files are assembled into .S files and compiled alongside the
generated C code. The runtime already uses this pattern for context switching
(run_context_amd64.S, run_context_arm64.S).
SIMD Types and Operations
Section titled “SIMD Types and Operations”Run provides first-class SIMD vector and mask types as native primitives. The
simd.* namespace is compiler-recognized in this release, so these operations
do not rely on generic library overloading.
Scalar and Vector Types
Section titled “Scalar and Vector Types”Lane access for integer vectors uses the scalar primitives i8, i16, and
i32.
128-bit vectors:
v4f32— 4 ×f32(SSE / NEON)v2f64— 2 ×f64v4i32— 4 ×i32v8i16— 8 ×i16v16i8— 16 ×i8
256-bit vectors (x86-64 AVX):
v8f32— 8 ×f32v4f64— 4 ×f64v8i32— 8 ×i32v16i16— 16 ×i16v32i8— 32 ×i8
Mask types:
v2boolv4boolv8boolv16boolv32bool
Operations
Section titled “Operations”SIMD vectors support literal syntax, element-wise arithmetic, comparisons, and lane access:
a := v4f32{ 1.0, 2.0, 3.0, 4.0 }b := v4f32{ 5.0, 6.0, 7.0, 8.0 }
c := a + b // element-wise add: { 6.0, 8.0, 10.0, 12.0 }d := a * b // element-wise mul: { 5.0, 12.0, 21.0, 32.0 }mask := c > a // comparison result: v4bool
x := a[0] // read lanea[2] = 9.0 // write lane on a mutable local/parameterComparisons on matching vector types produce the mask type with the same lane count. Lane indexing returns the corresponding scalar element type.
Builtins
Section titled “Builtins”The compiler recognizes these simd.* builtins:
simd.hadd(v)— horizontal reduction to the scalar lane typesimd.dot(a, b)— dot product of matching vector typessimd.shuffle(v, idx0, ..., idxN)— lane permutation; indices must be integer literals, and the call must provide exactly one index per lanesimd.min(a, b),simd.max(a, b)— element-wise minimum/maximumsimd.select(mask, a, b)— choose lanes fromaorbusing the matching mask typesimd.load(ptr)— aligned load from a pointer-to-vectorsimd.loadUnaligned(ptr)— unaligned load from a pointer-to-vectorsimd.store(ptr, v)— aligned store through a mutable pointer-to-vectorsimd.width()— available fast-path width in the compiled binary (256with AVX-enabled builds,128with SSE/NEON builds, otherwise0)
Memory and Alignment
Section titled “Memory and Alignment”simd.load, simd.loadUnaligned, and simd.store operate on pointers to the
vector type itself, not pointers to slices or arrays:
var data = v4f32{ 1.0, 2.0, 3.0, 4.0 }let ptr = &data
let loaded = simd.load(ptr)simd.store(ptr, loaded)SIMD types are automatically aligned to their natural boundary:
- 128-bit types: 16-byte aligned
- 256-bit types: 32-byte aligned
The allocator respects SIMD alignment for heap allocations, stack locals are
emitted with matching alignment, and unsafe.alignof(T) reports the correct
SIMD alignment for these types.
Platform Mapping
Section titled “Platform Mapping”SIMD operations lower to C compiler intrinsics in the codegen backend:
- x86-64: SSE/AVX intrinsics (
_mm_add_ps,_mm256_mul_ps, etc.) via<immintrin.h> - ARM64: NEON intrinsics (
vaddq_f32,vmulq_f32, etc.) via<arm_neon.h>
On platforms without a matching hardware fast path, the compiler emits scalar fallback helpers so the same source still compiles and runs.
SIMD types do not require the unsafe package — they are safe, first-class types.
For operations not covered by the built-in functions, use inline assembly (see Assembly Language).
Standard Library: simd Package
Section titled “Standard Library: simd Package”The simd namespace exposes the compiler-recognized builtins listed above. The
older per-type helper names such as sum_f32 and blend_f32 are not part of
this API.
NUMA Awareness
Section titled “NUMA Awareness”Run provides tools for building NUMA-friendly applications. On multi-socket systems, memory locality and thread placement significantly impact performance. Run exposes NUMA topology through the runtime and integrates it with the scheduler and allocator.
Topology Discovery
Section titled “Topology Discovery”use "runtime/numa"
nodes := numa.nodeCount() // number of NUMA nodescurrent := numa.currentNode() // node the current green thread is oncpus := numa.cpus_on_node(0) // CPU IDs belonging to node 0dist := numa.distance(0, 1) // relative distance between nodesThe runtime discovers NUMA topology at startup:
- Linux: reads
/sys/devices/system/node/or useslibnuma - Windows:
GetNumaProcessorNodeEx,GetNumaAvailableMemoryNode - macOS/Apple Silicon: UMA (single node) — NUMA APIs return trivial values
NUMA-Aware Allocation
Section titled “NUMA-Aware Allocation”NUMA-local allocators can be passed to alloc() using Run’s existing custom allocator support:
use "runtime/numa"
// Create an allocator that allocates on a specific NUMA nodenodeAlloc := numa.allocator(node: 0)
// Use it with allocdata := alloc([]f32, 1024, allocator: nodeAlloc)The runtime’s per-P slab caches automatically allocate from the NUMA node their bound OS thread is running on. For most applications, the default allocator already provides good NUMA locality without explicit configuration.
Thread Affinity
Section titled “Thread Affinity”Green threads can be pinned to specific NUMA nodes:
use "runtime/numa"
// Spawn a green thread on a specific NUMA noderun(node: 0) process_local_data(data)
// Pin the current green threadnuma.pin(node: 1)Scheduler Integration
Section titled “Scheduler Integration”The GMP scheduler is NUMA-aware:
- Processors (P) are assigned to NUMA nodes
- Work stealing prefers same-NUMA-node Ps before cross-node Ps
- OS threads (M) are pinned to CPUs on their P’s NUMA node
- The
G.last_paffinity hint prefers same-NUMA-node Ps for rescheduling
This means green threads naturally stay on the NUMA node where their data lives, minimizing cross-node memory traffic without explicit management in most cases.
Platform Support
Section titled “Platform Support”| Feature | Linux | Windows | macOS |
|---|---|---|---|
| Topology discovery | /sys/ + libnuma | GetNumaProcessorNodeEx | UMA (trivial) |
| NUMA-local alloc | mbind() / VirtualAllocExNuma | VirtualAllocExNuma | Default alloc |
| Thread affinity | pthread_setaffinity_np | SetThreadAffinityMask | Default scheduling |
Visibility and Modules
Section titled “Visibility and Modules”pubkeyword marks items as public; everything is private by default- File = module, directory = package (Go-style)
- No semicolons; statements are newline-terminated
pub type Vec3 struct { x f64, y f64, z f64 }
// main.runuse "math"v := math.Vec3{ x: 1.0, y: 2.0, z: 3.0 }No Generics
Section titled “No Generics”Deliberate choice for simplicity. Built-in types (slices, channels, maps) have language-level support without requiring user-facing generics.
Compilation
Section titled “Compilation”- Compiler written in Zig
- Native codegen via Zig’s own backend (no LLVM dependency)
- File extension:
.run
Standard Library (Go-level comprehensive)
Section titled “Standard Library (Go-level comprehensive)”io— readers, writers, buffered I/Oos— file system, processes, environmentnet— TCP/UDP sockets, DNShttp— HTTP server and clientjson— JSON encoding/decodingcrypto— hashing, encryption, TLSfmt— string formattingstrings— string manipulationbytes— byte slice utilitiesmath— math functionssync— mutexes, atomics, wait groupsunsafe— raw pointers, type layout, pointer arithmetictesting— test frameworktime— time, duration, timerslog— structured logging