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.
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 mutable reference on the heap.
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
- 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.
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.
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)
when temperature in {
> 0 and < 100 then "liquid",
<= 0 then "solid",
>= 100 then "gas",
}
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)
}
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
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
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"))
let result = list.iterator()
.filter((x) -> x > 0)
.map((x) -> x * 2)
.take(10)
.fold(0, (acc, x) -> acc + x)
- 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)
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
- Wildcard (
_), literal, variable binding, comparison (> n,<= n) - Struct/Pair/Enum destructuring (including nested)
- Logical patterns:
or,and,not
- 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.)
- 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
- 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 heap references from literals/temporaries — returnsmut ref T.
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 rules summary:
-
using "path"merges another file into the current module scope. -
using module::path { Symbol, Other as Alias }imports explicit public symbols from another module. -
using module::path { .. }imports all public symbols from that module, and..must be the only item. -
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 -
Imports are file-local bindings and never re-export automatically
-
Removed forms:
using "file" as Name,using Super..., bare-path imports such asusing Std.Io, alias imports such asusing Std.Io as Io, batch imports such asusing Std.Io.*, andpublic/protected/private using ... -
Access control:
public,protected(default),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 = ...)
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
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/.
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
- Swift toolchain (for building
koralc) - A C compiler in
PATH(clangrecommended)
On Windows, ensure clang.exe is available from terminal:
clang --versioncd compiler
swift build -c debug# 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-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)
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=8See tests/README.md for bootstrap mode, custom compiler mode, and other runner flags.
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"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/.