The Koral Programming Language
May 25, 2026 · View on GitHub
Koral is an experimental compiled language that combines Go's aggressive escape analysis with Swift's Automatic Reference Counting (ARC). It targets C to deliver predictable, high-performance memory management without a garbage collector, while keeping the syntax clean and its core control flow expression-oriented.
This repository contains the compiler, standard library, formatter, language documentation, and sample projects.
Status: Koral is in an experimental stage and is not yet production-ready.
Reference note:
README.mdis a high-level overview, not the canonical grammar document.- For syntax-sensitive details, use
docs/grammar.bnfand current compiler behavior as the source of truth. - If this README disagrees with the compiler, prefer compiler behavior and update the README.
The Core Idea: ARC + Escape Analysis
Most compiled languages make you choose: either you get high-level ergonomics with a tracing garbage collector, or you get manual control with verbose syntax. Koral offers a middle ground:
- Escape Analysis First: Every allocation is analyzed at compile time. If the compiler can prove that an object does not escape its current scope, it is allocated on the stack. Stack allocation is practically free and completely bypasses ARC overhead.
- ARC for the Rest: If an object does escape, it is allocated on the heap and managed via Automatic Reference Counting. This provides predictable, pause-free performance.
Because Koral compiles to C, stack allocations become standard C local variables. The backend compiler can heavily optimize them, often keeping them entirely in CPU registers and optimizing away reference counting operations for local data.
// The compiler sees this doesn't escape.
// It's allocated on the stack. No ARC overhead.
let local_point = Point(1, 2)
// box(...) creates an owned escaping mutable reference.
let heap_point = box(Point(3, 4))
// The 'ref' keyword borrows from an existing lvalue.
// Result mutability depends on the source: let mut → mut ref, let → ref.
let mut local_point2 = Point(3, 4)
let heap_point_ref = local_point2.ref // mut ref (from let mut)
// Bumping the refcount, no deep copy
let shared_point = heap_point
Language Highlights
- No GC, No Manual
free: Automatic memory management based on reference counting and escape analysis. - Expression-Oriented Control Flow:
if,when, and blocks produce values;whileandforkeep the same surface style but remain statement-only. - Zero-Cost Abstractions: Generics with trait constraints and monomorphization.
- Algebraic Data Types: Structs and enums with exhaustive pattern matching.
- C Interop: Foreign function interface (FFI) and a C backend for broad platform compatibility.
Syntax Quick Tour
Expression-oriented core control flow
let sign = if x > 0 then 1 else if x < 0 then -1 else 0
let label = when status in {
.Active then "running",
.Paused(reason) then "paused: " + reason,
.Stopped then "done",
}
Blocks are also expressions, so branch bodies can stay local instead of forcing helper functions. When a branch body uses a block, that block still defaults to Void; use yield to produce the enclosing if or when expression's value from inside the block.
let label = if score >= 90 then {
if score == 100 then {
yield "perfect"
}
yield "A"
} else {
yield "other"
}
while and for intentionally keep the same ... then ... surface shape, but they are statements rather than value-producing expressions.
Pattern matching built into if and while
Rules:
ismay destructure directly insideifandwhileconditions.- Bound names from an earlier
isclause remain visible to laterandclauses. - Condition chains evaluate left-to-right with normal short-circuit behavior.
if config.get("port") is .Some(v) then start_server(v)
while iter.next() is .Some(item) then process(item)
You can chain multiple condition clauses with and.
Each clause runs only if previous clauses succeed, and bindings from earlier is clauses are visible to later clauses.
if load() is .Some(a) and parse(a) is .Ok(b) and b.is_valid() then use(b)
while source.next() is .Some(raw) and decode(raw) is .Ok(msg) then handle(msg)
Pattern combinators: or, and, not
when temperature in {
> 0 and < 100 then "liquid",
<= 0 then "solid",
>= 100 then "gas",
}
or else / and then / or return — Error flow as keywords
let port = config.get("port") or else 8080
let name = user and then it.profile and then it.display_name or else "anonymous"
let read_config(path String) Result[Config] = {
let text = read_text_file(path) or return
let parsed = parse_json(text) or return
return .Ok(parsed)
}
Generics
let nums = List[Int].new()
let scores = Dict[String, Int].new()
let max[T Ord](a T, b T) T = if a > b then a else b
Traits and given blocks
trait Greet {
greet(self ref) String
}
type Bot(name String)
given Bot as Greet {
greet(self ref) String = "beep boop, I'm " + self.name
}
let g ref Greet = box(Bot("K-9")) // trait object
Algebraic data types with implicit member syntax
Rules:
.Member(...)requires an expected type from context.- It may construct enum cases or call static methods.
- If the expected type is not known, the expression is rejected.
type Result[T Any] {
Ok(value T),
Error(error ref Error),
}
let parse_int(s String) Result[Int] =
if s == "42" then .Ok(42) else .Error(box("bad input"))
Lazy streams
let result = list.iterator()
.filter((x) -> x > 0)
.map((x) -> x * 2)
.take(10)
.fold(0, (acc, x) -> acc + x)
Language Capabilities
Type System
- Primitive types:
Bool,Int,UInt,Int8–Int64,UInt8–UInt64,Float32,Float64,Never - Structs (product types):
type Point(x Int, y Int) - Enums (sum types / tagged enums):
type Shape { Circle(r Float64), Rectangle(w Float64, h Float64) } - Type aliases:
type Name = TargetType - Generic types and functions:
Type[T],func[T Constraint](...) - Function types:
Func[Int, Int, Int]—(Int, Int) -> Int - Reference types:
ref(read-only),mut ref(mutable),ptr(read-only),mut ptr(mutable),weakref(read-only weak),mut weakref(mutable weak)
Control Flow
if / then / elseexpressions (with pattern matching viais)whilestatements (with pattern matching viais)forstatements over anyIterablewhenexpressions for exhaustive pattern matchingfinallyfor deterministic cleanupbreak,continue,returnyieldinsideif/whenbranch bodies for branch values and early branch exit
Pattern Matching
- Wildcard (
_), literal, variable binding, comparison (> n,<= n) - Struct/Pair/Enum destructuring (including nested)
- Logical patterns:
or,and,not
Traits and Generics
- Trait definitions with inheritance:
trait Ord Eq { ... } - Generic trait declarations use postfix type parameters:
trait Iterator[T Any] { ... } - Implementations via
givenblocks - Trait objects for runtime polymorphism:
ref Greet,mut ref Greet - Operator overloading through algebraic traits (
Add,Sub,Mul,Div,Index, etc.)
Functions and Lambdas
- Top-level and generic functions
- Named parameters:
let connect(host: String, port: Int) = ...called asconnect(host: "localhost", port: 8080) - Lambda expressions:
(x Int) Int -> x * 2 - Closures with captured variables
- Literals: strings use
"..."; rune literals use'...'(defaultRune, can infer toUInt8in explicit byte context) - Duration suffix literals:
10s,250ms,30min,2h,150us,42ns - Pair literal:
(a, b)(equivalent toPair(a, b)) - Pair destructuring:
let (a, b) = pair(binds Pair fields to separate variables) - Collection literals:
- List:
[1, 2, 3](defaults toList[T]when no explicit type context exists) - Set:
let s Set[Int] = [1, 2, 3] - Dict:
["k": 1, "v": 2] - Empty literal
[]requires explicit type context (e.g.let xs List[Int] = [])
- List:
- String interpolation:
"value = \(x)" - Multiline string literals:
"""..."""with Swift-style indentation stripping
Memory Management
- Automatic reference counting with copy-on-write semantics
- Escape analysis for stack vs. heap allocation decisions
- Weak references (
weakref/mut weakref) for breaking reference cycles finallyfor deterministic resource cleanup
Reference creation rules:
.refresult type depends on the source's mutability:let mutbinding →mut ref T,letbinding →ref T, mutable path →mut ref T.mut ref Timplicitly converts toref T(widening). The reverse is not allowed..refon rvalues is rejected by the compiler.- Method/subscript receiver adjustment is a special case: a call whose receiver is declared as
self refmay use an rvalue receiver. The compiler materializes a stable temporary for the duration of that call. - This receiver-only rule does not make
expr.refon rvalues legal. self mut refstill requires a writable lvalue receiver; rvalues are rejected.- Calling a
self refmethod on an rvalue can introduce hidden retain/allocation cost due to temporary materialization. - Trait objects follow the same mutability split as ordinary refs:
ref Traitcan call onlyself refrequirements, whilemut ref Traitcan call bothself mut refandself refrequirements. ref Tis read-only:.valread only.mut ref Tsupports.valread and.val = exprassignment.ptr Tis read-only:.valread only.mut ptr Tsupports.valread,.val = expr, andp[i] = expr.- Use
box(expr)for owned escaping references from literals/temporaries — returnsmut ref T. boxforms the escaping reference directly from its parameter local; once that reference escapes, cleanup transfers to the ref owner instead of dropping the local again.- Ordinary parameter
mutis only local binding mutability inside the function body. It is not part of the function signature and is ignored for trait/given matching. Dropusesdrop(source mut ptr Self) Void. It is a compiler-only destructor entry, andDropimplementations are allowed on types with composite fields.
Weak reference rules:
.weakrefon aref Tproducesweakref T; on amut ref Tproducesmut weakref T. It is only valid on ref types..to_ref()on aweakref TreturnsOption[ref T]; on amut weakref TreturnsOption[mut ref T].mut weakref Timplicitly converts toweakref T(widening).
let strong mut ref Int = box(42)
let weak mut weakref Int = strong.weakref // mut ref → mut weakref
when weak.to_ref() in {
.Some(r) then println(r.val),
.None then println("expired"),
}
Module System
Module rules summary:
-
using "path"merges another file into the current module scope. -
using module::path { Symbol, Other as Alias }imports explicit symbols visible to the importing file:publicfrom any package, plusprotected publicwhen importing from the same package. -
using module::path { .. }imports all symbols visible to the importing file from that module, and..must be the only item. -
Module imports bind symbols only; they do not bind a module name or namespace. Use
Symbol, notmodule.Symbol. -
Entry file basenames must match
[a-z][a-z0-9_]*. -
File merge (
using "file_name"/using "./helpers"/using "../shared/format") is resolved relative to the current file directory -
Modules are declared in
koral.json;stdmodules are declared instd/koral.json -
Top-level manifest
entryis the default target module name (for exampleapp::main), not a source file path -
Per-module dependency edges use
requires; non-stdpackages do not need to liststdmanually -
Imports are file-local bindings and never re-export automatically
-
Access control:
public,protected public(same-package),protected(same module, default for top-level declarations),private -
Direct
Type(...)construction requires constructor field visibility at call site; non-public fields should be initialized via public factory methods -
Module entry file basename must match
[a-z][a-z0-9_]* -
String in
using "file"is the literal file name (no case conversion); file is resolved relative to the current file's directory -
Type aliases must start with an uppercase letter (
type Name = ...)
FFI
foreign letfor binding C functionsforeign typefor opaque or layout-compatible C types- Native library linking is configured in
koral.json/std/koral.jsonvialinks, not via source syntax
Standard Library Overview
The standard library (std/) ships with the compiler and is loaded automatically unless --no-std is specified.
Commonly used pieces:
- Core types:
Int,Float64,String,Rune,Bool - Collections:
List[T],Dict[K, V],Set[T] - Error flow:
Option[T],Result[T],or else,and then,or return - Runtime and system modules:
Io,Os,Proc,Time,Async,Sync,Net - Utility modules:
Math,Rand,Text,Container
Minimal examples:
let nums List[Int] = [1, 2, 3]
let scores Dict[String, Int] = ["alice": 10, "bob": 8]
let port = Option[Int].Some(8080) or else 80
let doubled = Option[Int].Some(21) and then it * 2
let parse_port(text String) Result[Int] = {
let port = parse_int(text) or return
return .Ok(port)
}
let ok = Result[Int].Ok(42)
let err = Result[Int].Error(box("failed"))
For full module-by-module API documentation, see docs/std/.
Repository Layout
compiler/— Swift compiler project (koralc,KoralCompiler)bootstrap/— self-hosting compiler implementation and bootstrap testsstd/— standard library modules and runtime C filesdocs/— language and developer documentationtoolchain/fmt/— formatter implementation and testssamples/— example projectstest/— ad-hoc language playground and cases
Prerequisites
- Swift toolchain (for building
koralc) - A C compiler in
PATH(clangrecommended)
On Windows, ensure clang.exe is available from terminal:
clang --version
Build from Source
cd compiler
swift build -c debug
Run the Compiler
# Build a single source file directly
swift run koralc build hello.koral
# Build a manifest-declared target module
swift run koralc build --package-config path/to/koral.json --target-module app::main
# Build and run
swift run koralc run --package-config path/to/koral.json --target-module app::main
# Emit C only
swift run koralc emit-c --package-config path/to/koral.json --target-module app::main -o out
Common Options
-o, --output <dir>: output directory--package-config <path>: build from a package manifest--target-module <name>: choose the manifest target module, for exampleapp::main--deps-root <path>: dependency root directory for manifest-driven builds--std-config <path>: explicit std manifest path--no-std: compile without loading the std manifest-m/-m=<N>: print escape analysis diagnostics (Go-style;-m -mor higher level currently same output)
Test
The only supported test entry lives under tests/.
cd compiler
swift build -c debug
cd ..
compiler/.build/debug/koralc build --package-config tests/compiler-runner/koral.json --target-module compiler_runner -o bin/compiler-test-runner
./bin/compiler-test-runner/compiler_runner.exe --compiler swift --swift-koralc compiler/.build/debug/koralc.exe -j=8
See tests/README.md for bootstrap mode, custom compiler mode, and other runner flags.
Documentation
Standard Library Resolution (KORAL_HOME)
If koralc cannot find std/std.koral / std/koral.json due to your working directory, set KORAL_HOME to the repository root.
# macOS / Linux
export KORAL_HOME=/path/to/koral
# Windows PowerShell
$env:KORAL_HOME = "C:\path\to\koral"
Contributing
Issues and pull requests are welcome. If you change parser/type-checker/codegen behavior, please add or update integration test cases under tests/compiler-cases/.