The Koral Programming Language
Koral is an experimental compiled language that uses a simplified type system (type / type mutable). It targets C to deliver predictable, high-performance compilation without a garbage collector, while keeping the syntax clean and its core control flow expression-oriented.
This repository contains two compiler implementations — compiler/, the self-hosting primary implementation, and compiler-reference/, the frozen Swift compiler serving as reference oracle, build seed, and backup — plus the 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/guide/grammar.bnftogether with the language reference indocs/guide/document.mdanddocs/guide/document-zh.md. - When implementation and docs drift, resolve the mismatch by updating the implementation and/or the documents so they converge.
The Core Idea: type / type mutable
Koral's nominal types come in exactly two forms, chosen at the declaration site:
type— a shallowly immutable nominal type. Fields cannot be mutated after construction, values have no identity, and the compiler decides the layout. Value semantics is not promised: copies may share backing storage, which is unobservable precisely because the type is shallowly immutable. A type that implementsDropis the exception — its destructor runs once per object, so the sharing is observable and the type is treated liketype mutableon that axis.type mutable— a shared object type. Assignment and argument passing hand out handles to the same object. Fields are immutable by default; only explicitly declaredmutablefields can be modified in place.
// Shallowly immutable — compiler decides layout.
type Point(x Int, y Int);
// Mutable shared object.
type mutable Counter(mutable count Int);
let p = Point(1, 2);
let c = Counter(0);
c.count = c.count + 1; // in-place mutation through shared reference
Language Highlights
- No GC, No Manual
free: Automatic memory management based ontype/type mutablesemantics. - Expression-Oriented Control Flow:
if,when,while, andforshare the same expression surface syntax;ifandwhenmay produce branch values, whilewhileandforalways produceVoid. - 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. A block may end with a final expression without a trailing semicolon; that final expression becomes the block's value. If the block has no final expression, or the last expression ends with a semicolon, the block evaluates to Void.
let label = if score >= 90 then {
if score == 100 then {
"perfect"
} else {
"A"
}
} else {
"other"
};
while and for intentionally keep the same ... then ... surface shape and are ordinary expressions whose result type is Void.
let loop_value = while ready() then {
process_next();
};
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 maxT Ord T = if a > b then a else b;
Traits and given blocks
trait Greet {
greet(self) String;
};
type Bot(name String);
given Bot as Greet {
greet(self) String = "beep boop, I'm " + self.name;
};
let g Greet = Bot("K-9"); // trait object
// Fully qualified call (Rust-style <Type as Trait>::method) selects the
// trait's method explicitly. The receiver of an instance method is the first
// argument, so instance and static trait methods share one form.
let bot = Bot("R2");
println(Bot(Greet).greet(bot));
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 Error),
};
let parse_int(s String) Result[Int] =
if s == "42" then .Ok(42) else .Error("bad input");
Lazy streams
Lazy pipelines enter through lazy() and stay on LazyView. Nothing lazy is
spelled on the container: dict.take(key), string.find(pat), list.first()
and friends stay the container's own method, and names like map / filter /
to_list / collect on a container are kept free for a future eager API.
A chain is one expression — the calls are joined by the leading ., and the
statement ends at the single trailing ;. Do not put ; between the calls;
that would terminate the statement and leave the next line as a bare implicit
member expression.
let result = list.lazy()
.filter((x) => x > 0)
.map((x) => x * 2)
.take(10)
.fold(0, (acc, x) => acc + x);
lazy() is a door, not a wrapper — list.lazy().lazy() is list.lazy(), and
an adapter returns another LazyView, so a chain never repeats it. Every
terminal builds a fresh cursor and drops it, so asking twice gives the same
answer and the source is never spent. A cursor (list.iterator()) is
one-shot and has only next; to run a pipeline over one, collect it first.
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],funcT Constraint - Function types:
(Int, Int) -> Int - Type mutability:
type(shallowly immutable, no identity, no promise of value semantics),type mutable(shared object semantics)
Control Flow
if / then / elseexpressions (with pattern matching viais)whileexpressions (with pattern matching viais)forexpressions over anyIterablewhenexpressions for exhaustive pattern matchingdeferfor deterministic cleanupbreak,continue,return- Value-producing
if/when: the branch's final expression becomes the branch value
Pattern Matching
- Wildcard (
_), literal (1,1.5,"s",'a',true), variable binding, comparison (> n,<= n,> 1.5,>= 'a') - Struct/Enum destructuring and tuple destructuring (including nested)
- Logical patterns:
or,and,not - Arm guards:
pat if cond then body— the pattern's bindings are visible in the guard; a guarded arm covers nothing for exhaustiveness
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:
Greet - Fully qualified calls:
Type(Trait).method(receiver, ...)— Rust-style qualified path, with the receiver as the first argument - Operator overloading through algebraic traits (
Add,Sub,Neg,Mul,Div,Rem,Eq,Ord)
Functions and Lambdas
- Top-level and generic functions
- Call labels and defaults: the declaration fixes the call shape with no optional-label form — a positional parameter (
name Type) is passed by position and never by label, a named parameter (name: Type) must be passed by label, and only named parameters may declare defaults (name: Int = 1). Positional parameters must come before named ones, at declarations and at call sites. Constructors, free functions, methods and static methods all follow the same rules - 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,150us,42ns - Tuple destructuring:
let (a, b, c) = sbinds a struct's fields by position (any field count;Pairis just a two-field struct). There is no tuple literal — build aPairwith its constructor. - Collection literals:
[1, 2, 3] (defaults to List[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] = [])
- String interpolation:
"value = \(x)" - Multiline string literals:
"""..."""with Swift-style indentation stripping
Memory Management
typevalues are immutable. The compiler decides the internal layout.type mutablevalues are mutable shared objects.deferfor deterministic resource cleanup.
// Immutable value — compiler decides representation.
let p = Point(1, 2);
// Mutable shared object.
type mutable Counter(mutable count Int);
let c = Counter(0);
c.count = c.count + 1;
Module System
Module rules summary:
usinghas one form; the specifier is a string, and its shape decides what it means: starting with./or../is a file merge, anything else is a module import.- File merge (
using "./helpers.koral";/using "../shared/format.koral";) resolves against the current file's directory, must end in.koral, and merges the target's top-level definitions into the current module. It takes no part in package/module resolution and may not be combined with{ ... }. - Module import (
using "std/io";/using "std/io" { Reader, Writer };/using "std/io" { Reader as IoReader };) binds symbols only.{ ... }omitted means every visible member; if written, it may not be empty —{ .. }and{ * }are no longer spellings of anything. - Module imports do not bind a module name or namespace. Use
Symbol, notmodule.Symbol;asrenames one imported symbol, never the module. - Imported names are file-local bindings and are never re-exported; a name collision is an error and
asdisambiguates. A spelling has one owner — an import colliding with a top-level declaration here, with another import of a different declaration, or with a prelude name (List,Eq, …) is an error; the same declaration arriving twice is not, and a localletinside a function may still shadow. - Modules are declared in
koral.json;stdmodules are declared instd/koral.json.modulesmaps a package-internal name (.,conn,compiler/parser) to{ entry, links }, andentryis the entry file relative to the package root. - The module graph is the union of the modules'
usingstatements. There is norequiresand no top-levelentry— the default build target is the main module. - Module full names are
package[/subpath]; a package name is a single identifier segment./separates subpaths. - The
stdmain module is the prelude and is in scope without being named; every other module needs an explicit import. - Imports are file-local bindings and never re-export automatically
- Access control:
public,package_private(same-package),module_private(same module, default for top-level declarations),file_private - Direct
Type(...)construction requires constructor field visibility at call site; non-public fields should be initialized via public factory methods - A module entry file's stem must start with a lowercase letter and continue with lowercase letters, digits or
_ - Type aliases must start with an uppercase letter (
type Name = ...)
FFI
Declaration qualifiers sit immediately after the keyword — the slot type mutable and let mutable occupy. Access modifiers are the only prefix modifiers: they say who can see a declaration, not what kind of thing it is.
let foreignfor binding C functionstype foreignfor opaque or layout-compatible C typeslet intrinsic/type intrinsicfor declarations built into the compiler (standard library only)- Native library linking is configured in
koral.json/std/koral.jsonvialinks, not via source syntax - Raw pointers:
T(read-only),mutable T(read-write); formed with&/&mutable - Weak references:
?T(requiresmutableconstraint);?T(x)constructs,?exprupgrades toOption[T]
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
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 = Int.parse(text) or return;
return .Ok(port);
};
let ok = Result[Int].Ok(42);
let err = Result[Int].Error("failed");
Repository layout
Two compiler implementations live here, and they are not equal:
compiler/— the primary compiler implementation, written in Koral and self-hosting. This is what you develop.compiler-reference/— the frozen Swift compiler, kept as the reference oracle (the differential gate compares the two), the build seed (it buildscompiler/from source), and a backup.
std/— standard library sources and runtime C filestests/— shared integration cases and the shared test runnertoolchain/—koralbuild tool,koral-syntax(the shared Koral parser/printer),koralfmtformatter, std API doc generator, VS Code extensionsamples/— sample programsdocs/— documentation, split by audience intoguide/,api/,design/,implementation/. Start atdocs/README.md.
Documentation
Everything under docs/ is organised by audience — see the documentation map for where each kind of document lives.
- Language Guide (English)
- 语言文档(中文)
- Grammar (BNF) — normative
- Standard Library API Docs — generated
- Design Documents
- Compiler Developer Guide