The Koral Programming Language

May 26, 2026 · View on GitHub

Koral is an open-source programming language focused on performance, readability, and practical cross-platform development.

Through carefully designed syntax rules, this language can effectively reduce reading and writing burden, allowing you to put your real attention on solving problems.

Specification note:

  • This document is the user-facing language reference.
  • For grammar-sensitive questions, docs/grammar.bnf and current compiler behavior are authoritative.
  • If examples in this document and compiler behavior disagree, treat compiler behavior as the source of truth and update the documentation accordingly.

Key Features

  • Modern, easy-to-scan syntax with optional semicolons and expression-oriented control flow: if, when, and blocks produce values, while while and for remain statement-only.
  • Automatic memory management based on reference counting, ownership analysis, and escape analysis.
  • Generics with trait constraints and monomorphization for zero-cost abstraction.
  • Algebraic data types (structs and enums) with exhaustive pattern matching.
  • Trait-based polymorphism with trait objects for runtime dispatch.
  • First-class functions, lambdas, and closures.
  • Multi-paradigm programming (combining functional and imperative).
  • Module system with access control (public / protected public / protected / private).
  • Foreign function interface (FFI) for seamless C interop.
  • C backend for broad platform compatibility.

Installation and Usage

Koral currently compiles to C and invokes clang in the backend, so clang must be available in PATH.

Compilation and Execution

You can compile either a single source file directly or a manifest-declared module graph.

  1. Build a single file:
    koralc build hello.koral
    
  2. Build a manifest-declared target module.
    koralc build --package-config koral.json --target-module app::main
    
  3. Type-check only:
    koralc check --package-config koral.json --target-module app::main
    
  4. Compile and run: Use the run command to compile and execute in one step.
    koralc run --package-config koral.json --target-module app::main
    
  5. Emit C only: Use emit-c to generate C source.
    koralc emit-c --package-config 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
  • --deps-root <path>: dependency root for manifest-driven builds
  • --std-config <path>: explicit std manifest path
  • --no-std: compile without loading modules declared by std/koral.json
  • -m / -m=<N>: print escape-analysis diagnostics

Basic Syntax

Basic Statements and Semicolons

In Koral, statements are the smallest unit of composition.

Semicolon insertion follows these rules:

  • A statement may end with an explicit semicolon ;.
  • A newline may terminate a statement when the parser is not in a continuation context.
  • Newlines inside (), [], and {} do not terminate the surrounding statement.
  • Blank lines and comments break continuation.
let a = 0;
let b = 1;

// Ends with a newline, semicolon omitted
let c = { 
    1 + 1 
} 

// Ends with a newline, semicolon omitted
let d = 1
let e = 2

Entry Function

Every executable program needs an entry point. In Koral, this entry point is the main function. A typical main function declaration is as follows.

let main() Void = {}

Here we declare a function named main. The right side of = is the function body, {} represents an empty block expression, returning Void.

The main function can also accept parameters (command line arguments) and return an integer (status code), but this depends on the specific runtime environment support. More details about functions will be explained in later chapters.

Display Information

The standard library provides the println function to print a line of text to the standard output.

let main() Void = println("Hello, world!")

Now try to execute this program, and we can see Hello, world! displayed on the console.

Comments

Comments are parts of the code ignored by the compiler, used to provide explanations to people reading the code.

// This is a single-line comment, starting from double slashes to the end of the line

/*
    This is a block comment.
    It can span multiple lines.
    /* Koral supports nested block comments */
*/

Variables

Koral's variables use binding semantics, equivalent to binding a variable name and a value together. For safety reasons, variables are immutable by default, but we also provide mutable variables.

Read-only Variables

In Koral, read-only variables are declared using the let keyword, following the principle of declaration before use.

Koral ensures type safety through static typing. Variable bindings can explicitly annotate types at declaration. When there is enough information in the context, we can also omit the type, and the compiler will infer the variable's type.

let a Int = 5   // Explicit type annotation
let b = 123     // Automatic type inference

Once a read-only variable is declared, its value cannot be changed within the current scope.

let a = 5
a = 6 // Error

Mutable Variables

If we need a variable that can be reassigned, we can use a mutable variable declaration with let mut.

let mut a Int = 5   // Explicit type annotation
let mut b = 123     // Automatic type inference

Pair Destructuring

When the right-hand side expression is a Pair, you can use parenthesized syntax to bind each element to a separate variable. Each binding position supports _ (discard), mut (mutable), and an optional type annotation.

let (a, b) = (1, 2)                  // Type inference
let (c Int, d String) = (3, "hello")  // Explicit type annotations
let (mut e, f) = (10, 20)             // Mutable binding
let (_, g) = (1, 2)                   // Discard first element

The compiler moves fields directly from the Pair value into the target variables, avoiding unnecessary copies and drop overhead.

Reference Creation Rules (.ref / box)

Koral uses ref and mut ref as managed reference types. ref T is a read-only reference, while mut ref T is a mutable reference that supports .val = expr assignment:

  • x.ref creates a managed reference from an existing lvalue. The result type depends on the source's mutability:
    • let mut binding → .ref produces mut ref T
    • let (immutable) binding → .ref produces ref T
    • Mutable path (e.g. mut ref's mut field) → .ref produces mut ref T
  • mut ref T implicitly converts to ref T (widening). The reverse is not allowed.
  • .ref on rvalues is rejected.
  • To create a managed reference from a temporary/literal, use box(expr), which returns mut ref T.

Receiver adjustment for methods and subscripts has one extra rule:

  • A call whose receiver is declared as self ref may use an rvalue receiver expression. The compiler materializes a stable temporary for the duration of that call.
  • This special case applies only to receiver adjustment. It does not make expr.ref on rvalues legal.
  • self mut ref still requires a writable lvalue receiver; rvalues are rejected.
  • Because self ref on rvalues may materialize temporaries, such calls can introduce hidden retain/allocation cost.
let mut x = 10
let rx mut ref Int = x.ref   // let mut → mut ref T

let y = 10
let ry ref Int = y.ref       // let → ref T

let owned mut ref Int = box(42) // box() returns mut ref T

// let rz = 42.ref           // error: rvalue cannot be borrowed

Assignment

For mutable variables, we can change their value multiple times when needed.

let mut a = 0
a = 1  // Legal
a = 2  // Legal

Block Expressions

In Koral, {} represents a block expression.

Block rules:

  • A block contains zero or more statements.
  • A plain block's default type is Void.
  • return, break, and continue can end the block early and therefore give that block type Never.
  • yield is not a block-return mechanism. It is only valid inside the body of an if or when expression branch.
  • A block ending with return, break, or continue has type Never.
let a Void = {}
let main() Void = {
    let c = 7
    let d = c + 14
    println(((c + 3) * 5 + d / 3).to_string())
}

Identifiers

Identifiers are names given to variables, functions, types, etc. The naming rules are:

  1. Case sensitive. Myname and myname are two different identifiers.
  2. Types and Constructors must start with an uppercase letter (e.g., Int, String, Point).
  3. Variables, Functions, Members must start with a lowercase letter or underscore (e.g., main, println, x).
  4. Other characters in identifiers can be underscores _, letters, or numbers.
  5. Within the same {}, identifiers with the same name cannot be defined repeatedly.
  6. In different {}, identifiers with the same name can be defined, and the language will prioritize the identifier defined in the current scope.

Basic Types

We only need a few simple basic types to carry out most of the work.

Booleans

Booleans refer to logical values, they can only be true or false. The default boolean is Bool type.

let b1 Bool = true
let b2 Bool = false
let isGreater = 5 > 3 // Result is true

Numeric Types

Koral provides rich numeric types to meet different needs. The default integer is Int type, and floating-point numbers use Float64 (64-bit) or Float32 (32-bit).

  • Int: Platform-dependent signed integer (usually 64-bit).
  • UInt: Platform-dependent unsigned integer (usually 64-bit).
  • Int8, Int16, Int32, Int64: Fixed-width signed integers.
  • UInt8, UInt16, UInt32, UInt64: Fixed-width unsigned integers.
  • Float32: 32-bit floating-point number.
  • Float64: 64-bit floating-point number.
let i Int = 3987349
let f Float64 = 3.14
let b UInt8 = 255

Numeric literals support underscores _ as separators for readability:

let million = 1_000_000
let pi = 3.141_592_653

Koral also supports binary, octal, and hexadecimal integer literals using the 0b, 0o, and 0x prefixes respectively:

let bin = 0b1010          // Binary, value is 10
let oct = 0o755           // Octal, value is 493
let hex = 0xFF            // Hexadecimal, value is 255

Non-decimal literals also support underscore separators:

let mask = 0xFF_FF        // Hexadecimal, value is 65535
let flags = 0b1010_0101   // Binary, value is 165

Note: Non-decimal literals only support integers, not floating-point numbers. Hexadecimal letters are case-insensitive (0xABcd is equivalent to 0xabCD).

Floating-point literals also support scientific notation using the e exponent suffix:

let a = 1e3      // 1000.0
let b = 1e-3     // 0.001
let c = 2.5e+2   // 250.0
let d = 1_000e2  // 100000.0

Note: Only lowercase e is supported for exponent notation, consistent with the 0b/0o/0x prefix convention.

Duration literals are supported with integer suffixes:

let a = 10s
let b = 250ms
let c = 30min
let d = 2h
let e = 150us
let f = 42ns

Supported suffixes are h, min, s, ms, us, ns. Duration literals are lowered to Duration.new(seconds: ..., nanoseconds: ...). Negative durations keep unary-minus semantics (for example -5s is parsed as unary - applied to 5s).

Type Casting

Different numeric types require explicit conversion using expr(Type) syntax:

let a Int = 42
let b Float64 = a(Float64)    // Int -> Float64
let c Int32 = a(Int32)        // Int -> Int32
let d UInt8 = 255(UInt8)      // Int -> UInt8

Strings

In Koral, strings are used to represent text data. String type is a UTF-8 encoded character sequence.

String literals use double quotes "" only.

let s1 String = "Hello, world!"

Koral supports string interpolation, allowing expressions to be embedded in strings using \(expr) syntax:

let name = "Koral"
let count = 3
println("Hello, \(name)!")                    // Hello, Koral!
println("Count: \(count)")                    // Count: 3
println("Mixed \(name) has \(count) messages") // Mixed Koral has 3 messages
println("Sum \(1 + (2 * 3))")                 // Sum 7

Escape characters use backslash \:

"\n"        // Newline
"\t"        // Tab
"\r"        // Carriage return
"\v"        // Vertical tab
"\f"        // Form feed
"\0"        // Null character
"\\"        // Backslash
"\""        // Double quote
"\'"        // Single quote
"\x41"      // Hex byte escape: exactly 2 hex digits (0x00–0xFF), e.g. \x41 = 'A'
"\u{41}"    // Unicode scalar escape: 1–6 hex digits, e.g. \u{41} = 'A', \u{1F600} = 😀

Multiline String Literals

Use """ delimiters to write strings that span multiple lines, following the same rules as Swift:

  • The opening """ must be immediately followed by a newline.
  • The closing """ must be on its own line. Its leading whitespace (spaces or tabs) defines the common indentation prefix that is stripped from every content line.
  • Every content line must be indented at least as much as the closing """, otherwise a compile error is reported.
  • The same escape sequences and \(...) interpolation as regular strings are supported.
let message = """
    Hello, Koral!
    Welcome to multiline strings.
    """
// Equivalent to "Hello, Koral!\nWelcome to multiline strings."

let name = "World"
let greeting = """
    Hello, \(name)!
    Have a great day.
    """
// Equivalent to "Hello, World!\nHave a great day."

The indentation of the closing """ determines how much is stripped:

let s = """
        indented content
        second line
    """
// Closing """ is indented 4 spaces, content is indented 8 spaces.
// After stripping 4 spaces: "    indented content\n    second line"

Common String methods:

let s = "Hello, World!"
s.count()                    // 13 - byte length
s.is_empty()                 // false
s.contains("World")          // true
s.starts_with("Hello")       // true
s.ends_with("!")             // true
s.to_ascii_lowercase()       // "hello, world!"
s.to_ascii_uppercase()       // "HELLO, WORLD!"
s.trim_ascii()               // Trim leading/trailing whitespace
s.slice(0..<5)               // "Hello" - slicing
s.find("World")              // Some(7)
s.replace_all("World", "Koral") // "Hello, Koral!"
s.split(",")                 // Split by separator
s.lines()                    // Split by lines

// Join a list of strings
list.join_to_string(", ")   // Join List[String] with separator

Rune Literals

Rune literals use single quotes '' and represent exactly one Unicode scalar value.

let r Rune = 'A'
let nl Rune = '\n'
let smile Rune = '\u{1F600}'

Rune literal typing rules:

  • Default type is Rune.
  • In an explicit UInt8 context, a rune literal can be inferred as byte (UInt8) if it is a single ASCII character.

Collection Literals

Koral supports collection literals for the three built-in collection types: List[T], Set[T], and Dict[K, V].

let a = [1, 2, 3]                    // inferred as List[Int]
let b Set[Int] = [1, 2, 3]           // inferred as Set[Int] from context
let c = ["x": 1, "y": 2]             // inferred as Dict[String, Int]
let empty List[Int] = []             // empty literal requires type context

Rules:

  • [e1, e2, ...] is a collection literal. Without type context, it is inferred as List[T].
  • In Set[T] context, the same syntax is inferred as Set.
  • [k1: v1, k2: v2, ...] is a dict literal and is inferred as Dict[K, V].
  • [] cannot be inferred without type context and must be annotated.
  • Trailing commas are allowed for both collection and dict literals.
  • Collection literals only target built-in List / Set / Dict, not third-party container types.

Reference Types

Reference types are used to refer to another value rather than holding it. This is useful when sharing data or avoiding copying. Koral distinguishes between read-only and mutable references:

  • ref T — read-only reference. Supports .val read but NOT .val = expr assignment.
  • mut ref T — mutable reference. Supports both .val read and .val = expr assignment.
  • mut ref T implicitly converts to ref T (widening). The reverse is not allowed.

Use the .ref postfix expression to create a reference. The result type depends on the source's mutability:

let mut n = 42
let a = n.ref            // let mut → mut ref T
let b = a.val            // Dereference, gets 42
a.val = 100              // Deref assignment (mut ref supports .val = expr)
println(is_unique_mutable(a)) // True only for owning, uniquely-held refs

let m = 42
let c = m.ref            // let → ref T (read-only)
let d = c.val            // Dereference read, gets 42
// c.val = 100           // Error: ref does not support .val assignment

ref and mut ref denote managed reference types. x.ref forms one from an lvalue, with the result mutability determined by the source. The compiler first tries to keep such references in a stack-safe borrowed form; when a reference escapes its scope, it is promoted to a heap-backed reference-counted object. box(expr) returns mut ref T by intentionally producing an escaping managed reference. Conceptually, box is box(mut v T) -> v.ref: the escaping reference takes over ownership of that local storage, so the local is not dropped a second time.

Pointer types follow the same read-only / mutable distinction:

  • ptr T — read-only pointer. Supports .val read but NOT .val assignment or p[i] assignment.
  • mut ptr T — mutable pointer. Supports .val read, .val = expr assignment, p[i] read, and p[i] = expr assignment.
  • mut ptr T implicitly converts to ptr T. The reverse is not allowed.

Weak References

Weak references don't increase the reference count, used to break reference cycles. Like ref/mut ref, weak references also distinguish mutability: weakref T (read-only) and mut weakref T (mutable).

Use the .weakref postfix expression to create a weak reference from a ref type, and the .to_ref() method to attempt upgrading a weak reference back to a strong reference (returns an Option type).

let strong mut ref Int = box(42)

// Mutable path: mut ref → mut weakref → Option[mut ref T]
let weak = strong.weakref              // mut ref T → mut weakref T
let upgraded = weak.to_ref()           // mut weakref T → Option[mut ref T]

// Read-only path: ref → weakref → Option[ref T]
let ro ref Int = strong                // implicit widening
let ro_weak = ro.weakref               // ref T → weakref T
let ro_upgraded = ro_weak.to_ref()     // weakref T → Option[ref T]

Memory Management

Koral aims to provide efficient and safe memory management, combining automatic memory management with manual control.

  • Value Semantics: By default, types in Koral (such as Int, structs) have value semantics. Data is copied during assignment or parameter passing.
  • References: ref and mut ref are Koral's managed reference types. ref T is read-only, mut ref T is mutable. They may be formed from lvalues (with mutability determined by the source) or by creating escaping managed references such as box(expr) (which returns mut ref T). When a local value is promoted into an escaping managed reference, destruction transfers to that reference owner instead of running a second value drop on the source local. Method/subscript receiver adjustment also allows self ref calls on rvalue receivers by materializing a stable temporary for the duration of the call; this receiver-only exception does not make .ref on rvalues legal, and self mut ref still requires a writable lvalue. Koral uses ownership analysis and escape analysis to decide stack-safe borrowing vs heap-backed reference counting, preventing dangling pointers and memory leaks.
  • Move Semantics: For variables that haven't been copied, assignment and parameter passing result in ownership transfer (Move). Once ownership is transferred, the original variable can no longer be used.

Operators

Operators are symbols that tell the compiler to perform specific mathematical or logical operations.

Arithmetic Operators

let a = 4
let b = 2
println( a + b )    // + Add
println( a - b )    // - Subtract
println( a * b )    // * Multiply
println( a / b )    // / Divide
println( a % b )    // % Modulus

Comparison Operators

Comparison operators compare two values. The result is Bool type. Note that not equal is represented by <>.

let a = 4
let b = 2
println( a == b )     // == Equal
println( a <> b )     // <> Not equal 
println( a > b )      // > Greater than
println( a >= b )     // >= Greater than or equal to
println( a < b )      // < Less than
println( a <= b )     // <= Less than or equal to

Logical Operators

Logical operators perform logical operations (AND, OR, NOT) on two Bool type operands.

let a = true
let b = false
println( a and b )       // AND, true only if both are true
println( a or b )        // OR, true if either one is true
println( not a )         // NOT, boolean negation

and and or have short-circuit semantics:

let a = false and f() // f() will not be executed
let b = true or f()   // f() will not be executed

Bitwise Operators

let a = 4
let b = 2
println( a & b )    // Bitwise AND
println( a | b )    // Bitwise OR
println( a ^ b )    // Bitwise XOR
println( ~a )       // Bitwise NOT
println( a << b )   // Left shift
println( a >> b )   // Right shift

Range Operators

Range operators generate a range (Range), commonly used in loops or pattern matching.

1..5     // 1 <= x <= 5 (Closed interval)
1..<5    // 1 <= x < 5  (Right open interval)
1<..5    // 1 < x <= 5  (Left open interval)
1<..<5   // 1 < x < 5   (Open interval)
1..      // 1 <= x      (Right unbounded, inclusive start)
1<..     // 1 < x       (Right unbounded, exclusive start)
..5      // x <= 5      (Left unbounded, inclusive end)
..<5     // x < 5       (Left unbounded, exclusive end)
..       // Full range

Compound Assignment

let mut x = 10
x += 5       // x = x + 5
x -= 2       // x = x - 2
x *= 3       // x = x * 3
x /= 2       // x = x / 2
x %= 4       // x = x % 4

let mut y = 12
y &= 10     // y = y & 10
y |= 1      // y = y | 1
y ^= 15     // y = y ^ 15
y <<= 1     // y = y << 1
y >>= 2     // y = y >> 2

Operator Overloading

Koral supports trait-based operator overloading for arithmetic and comparison operations. Subscripts are built in and are not user-overloadable.

The built-in operator mappings are:

  • + -> Add[R] via add(self, other R) Self
  • - (binary) -> Sub[R] via sub(self, other R) Self
  • - (unary) -> Neg via neg(self) Self
  • * -> Mul[R] via mul(self, other R) Self
  • / -> Div[R] via div(self, other R) Self
  • % -> Rem[R] via rem(self, other R) Self
  • == / <> -> Eq via equals(self, other Self) Bool
  • < / > / <= / >= -> Ord via compare(self, other Self) Int

Bitwise operators (&, |, ^, ~, <<, >>) are currently built-in and are not customized through public operator traits.

type Vec2(x Int, y Int)

given Vec2 as Add[Vec2] {
    add(self, other Vec2) Vec2 = Vec2(self.x + other.x, self.y + other.y)
}

given Vec2 as Neg {
    neg(self) Vec2 = Vec2(-self.x, -self.y)
}

given Vec2 as Eq {
    equals(self, other Vec2) Bool = self.x == other.x and self.y == other.y
}

given Vec2 as Ord {
    compare(self, other Vec2) Int =
        if self.x <> other.x then self.x.compare(other.x) else self.y.compare(other.y)
}

let sum = Vec2(1, 2) + Vec2(3, 4)
let flipped = -sum
let same = sum == Vec2(4, 6)
let ordered = Vec2(1, 0) < Vec2(2, 0)

Builtin subscript rules:

  • value[key] and value[key] = expr are supported only for String, List[T], Deque[T], ptr T, and mut ptr T.
  • String[key] returns a UInt8 byte value. It is read-only and not addressable.
  • List[T] and Deque[T] support value reads, assignment, nested place updates, and explicit/implicit ref / mut ref contexts.
  • ptr T supports reads only. mut ptr T supports both reads and writes.
  • User-defined types cannot implement [] through traits, and generic constraints cannot add subscript capability.
let mut list = [10, 20, 30]
println(list[0])
list[1] = 99

let text = "abc"
let b UInt8 = text[1]

let p mut ptr Int = alloc_memory[Int](2)
p[0] = list[0]
let first = p[0]
dealloc_memory(p)

Value Coalescing and Optional Chaining

Koral provides three special operators for working with Option and Result types:

  • or else: Value coalescing. Returns the right-hand default value when the left side is None or Error.
  • and then: Optional chaining / value transformation. Applies the right-hand transformation when the left side is Some or Ok.
  • or return: Early-return propagation sugar. It unwraps Some / Ok, and on None / Error returns from the enclosing function.

In and then and or else expressions, the keyword it refers to the unwrapped value: for and then, it is the inner Some or Ok value; for or else on a Result, it is the Error value.

let opt = Option[Int].Some(42)
let val = opt or else 0           // 42 (because opt is Some)

let none = Option[Int].None()
let val2 = none or else 0         // 0 (because none is None)

let mapped = opt and then it * 2   // Some(84)

let load_port(path String) Result[Int] = {
    let text = read_text_file(path) or return
    parse_int(text)
}

or return is equivalent to a fixed or else early-return pattern:

  • For Result: expr or return is equivalent to expr or else { return .Error(it) }
  • For Option: expr or return is equivalent to expr or else { return .None() }

It must be used inside a function whose return kind matches the propagated value:

  • Result propagation requires the enclosing function to return Result
  • Option propagation requires the enclosing function to return Option

Operator Precedence

Operator precedence from high to low:

  1. Postfix: calls (), subscripts [], member access ., storage modifiers .ref/.ptr/.val, qualified/generic method suffixes
  2. Prefix: unary -, ~
  3. Multiplication/Division: *, /, %
  4. Addition/Subtraction: +, -
  5. Shift: <<, >>
  6. Comparison: ==, <>, <, >, <=, >=
  7. Range: .., ..<, <.., <..<
  8. Bitwise AND: &
  9. Bitwise XOR: ^
  10. Bitwise OR: |
  11. Pattern test: is, is not
  12. Logical NOT: not
  13. Logical AND: and
  14. Optional chaining: and then
  15. Logical OR: or
  16. Value coalescing: or else
  17. Early-return propagation: or return

Selection Structure

Selection structures are used to judge given conditions and control the flow of the program.

In Koral, selection structures use if syntax. if is followed by a judgment condition. When the condition is true, the then branch is executed. When the condition is false, the else branch is executed.

let main() Void = if 1 == 1 then println("yes") else println("no")

if is also an expression. The then and else branches must be followed by expressions.

let main() Void = println(if 1 == 1 then "yes" else "no")

Since if itself is also an expression, else can naturally be followed by another if expression for chained conditions.

let x = 0
let y = if x > 0 then "bigger" else if x == 0 then "equal" else "less"

When we don't need to handle the else branch, we can omit it, in which case its value is Void.

let main() Void = if 1 == 1 then println("yes")

When an if branch body is a block, that block still defaults to Void. Use yield to produce the value of the enclosing if expression and to exit that branch body early:

let label = if score >= 90 then {
    if score == 100 then yield "perfect"
    yield "A"
} else {
    yield "other"
}

yield inside a statement-form nested if / when still targets the enclosing branch expression. A nested if / when expression creates its own yield target.

if is Pattern Matching

if also supports is pattern matching syntax, allowing you to destructure values in conditions:

let opt = Option[Int].Some(42)
if opt is .Some(v) then {
    println(v)  // 42
} else {
    println("None")
}

Multiple conditions now use standard and / or / not composition. When the left side of an and is an is match with bindings, those bindings are available to later and clauses and to the then branch:

if foo() is .A(x) and bar(x) is .B(y) and y > 0 then {
    println(y)
} else {
    println("no match")
}

Rules for condition composition:

  • Conditions are evaluated left-to-right with normal short-circuiting.
  • Bindings introduced by earlier is clauses are available in later and clauses and in the then branch.
  • Bound is matches are not allowed under or branches or beneath not.

Loop Structure

while Statement

In Koral, loop structures use while syntax. while is followed by a judgment condition. When the condition is true, the following body executes, then control returns to the condition for the next iteration. while is statement-only.

let mut i = 0
while i < 10 then {
    println(i)
    i += 1
}

while is Pattern Matching

while also supports is pattern matching, commonly used for iterator loops:

let mut iter = list.iterator()
while iter.next() is .Some(v) then {
    println(v)
}

while supports the same and-based chaining for bound matches:

while iter.next() is .Some(item) and parse(item) is .Ok(v) then {
    println(v)
}

For while conditions, clauses are also left-to-right and short-circuiting. When a clause fails, the loop terminates.

break and continue

  • break: Exit the loop.
  • continue: Skip the current iteration.
let mut i = 0
while true then {
    if i > 20 then break
    if i % 2 == 0 then { i += 1; continue }
    println(i)
    i += 1
}

for Loop

The for loop is used to traverse any object that implements the iterator interface (such as lists, maps, sets, ranges, etc.).

In each iteration, the next value produced by the iterator will try to match pattern. If the match is successful, the statement body following then is executed. for is statement-only.

let nums List[Int] = [10, 20, 30]
for x in nums then {
    println(x)
}

for i in 0..5 then {
    println(i)
}

finally Statement

The finally statement declares a cleanup expression to be executed when the current block scope exits. The deferred expression runs regardless of whether the scope exits normally or early via return, break, or continue.

When execution takes a Never termination path (for example panic(), abort(), or exit()) and the program terminates immediately, execution of in-scope finally is not guaranteed.

finally is followed by an expression whose return value is discarded.

let main() Void = {
    println("start")
    finally println("cleanup")
    println("work")
    // Output: start, work, cleanup
}

Multiple finally statements in the same scope execute in reverse declaration order (LIFO):

let main() Void = {
    finally println("first")
    finally println("second")
    finally println("third")
    // Output: third, second, first
}

finally binds to the block scope where it is declared, not the function scope. In loops, finally executes at the end of each iteration:

let mut i = 0
while i < 3 then {
    i += 1
    finally println("cleanup")
    println(i)
    // Each iteration outputs: value of i, cleanup
}

The deferred expression can also be a block expression:

finally {
    println("cleaning up")
    close(handle)
}

Restrictions

  • return, break, continue, and yield are not allowed inside a finally expression.
  • Nested finally is not allowed inside a finally expression.
  • finally is not an exception-style stack unwinding mechanism; it is not guaranteed on panic/abort/exit Never termination paths.
  • These restrictions do not cross Lambda boundaries — Lambdas have their own independent scope.

Pattern Matching

Koral has powerful pattern matching capabilities, mainly used through when expressions and the is operator.

when Expression

The when expression allows you to compare a value against a series of patterns and execute corresponding code based on the matching pattern. It is similar to switch statements in other languages, but more powerful. when is also an expression and returns the value of the matching branch.

let x = 5
let result = when x in {
    1 then "one",
    2 then "two",
    _ then "other",
}

Like if, a block branch in when still defaults to Void. Use yield to produce the enclosing when expression's value and to support early exit inside the branch body:

let label = when score in {
    100 then {
        println("bonus")
        yield "perfect"
    },
    >= 90 then {
        if has_curve(score) then yield "A+"
        yield "A"
    },
    _ then { yield "other" },
}

Supported patterns include:

  • Wildcard pattern: _ (matches any value)
  • Literal patterns: 1, "abc", 'a', true
  • Variable binding patterns: x (matches any value and binds to x), mut x (mutable binding)
  • Comparison patterns: > 5, < 0, >= 10, <= -1
  • Struct destructuring patterns: Point(x, y), Rect(Point(a, b), w, h)
  • Pair destructuring pattern: (a, b) (equivalent to Pair(a, b) pattern)
  • Enum case patterns: .Some(v), .None
  • Logical patterns: pattern and pattern, pattern or pattern, not pattern
// Enum type matching
type Shape {
    Circle(radius Float64),
    Rectangle(width Float64, height Float64),
}

let area = when shape in {
    .Circle(r) then 3.14 * r * r,
    .Rectangle(w, h) then w * h,
}

// Comparison patterns
let grade = when score in {
    >= 90 then "A",
    >= 80 then "B",
    >= 70 then "C",
    _ then "F",
}

// Logical patterns
when x in {
    1 or 2 or 3 then println("small"),
    _ then println("big"),
}

// Struct destructuring patterns
type Point(x Int, y Int)
type Rect(origin Point, width Int, height Int)

let p = Point(10, 20)
when p in {
    Point(x, y) then println(x + y),  // 30
}

// Nested struct destructuring
let r = Rect(Point(1, 2), 30, 40)
when r in {
    Rect(Point(a, b), w, h) then println(a + b + w + h),  // 73
}

// Struct destructuring in if...is
if p is Point(x, y) then {
    println(x * y)  // 200
}

// Wildcard and literal field matching
when p in {
    Point(0, y) then println(y),       // Match when first field is 0
    Point(_, y) then println(y),       // Ignore first field
}

// Generic struct destructuring
type Box[T Any](val T)
let b = Box[Int](42)
when b in {
    Box(v) then println(v),  // 42
}

is Operator

The is operator checks whether a value matches a pattern, and the result is always Bool. It is now a general-purpose expression and can appear in let initializers, return expressions, function arguments, and other expression positions.

is not is the negated form and returns the inverse match result.

When used in conditional expressions such as if or while, a successful is match can also bind variables from the pattern into the current scope. Outside those condition contexts, is may only perform a boolean test and may not introduce bindings.

let opt = Option[Int].Some(42)
let has_value = opt is .Some(_)
let is_empty = opt is not .Some(_)

if opt is .Some(v) then {
    println(v)  // 42
}

// Comparison pattern
if score is >= 60 then {
    println("passed")
}

// Standard boolean composition still works in conditions
if opt is .Some(v) and v > 0 then {
    println(v)
}

Functions

Functions are independent blocks of code used to complete specific tasks.

Definition

Functions are defined using the let keyword. The function name is followed by () indicating the parameters, and the return type follows the parentheses. Named functions and methods must spell out the return type explicitly.

The right side of = must declare an expression, and the value of this expression is the return value of the function.

let f1() Int = 1
let f2(a Int) Int = a + 1
let f3(a Int) Int = a + 1

Calling

Use () syntax to call functions:

let a = f1()
let b = f2(1)

Parameters

Parameters are data that the function can receive during execution. Use ParameterName Type to declare parameters.

let add(x Int, y Int) Int = x + y
let a = add(1, 2) // a == 3

Mutable parameters use the mut keyword:

let increment(mut x Int) Int = { x += 1; return x }

For ordinary parameters, mut only makes the local binding writable inside the function body. It is not part of the function signature, does not change the Func[...] type, and is ignored when checking trait/given method compatibility.

Named Parameters

Named parameters follow these rules:

  • A named parameter is declared with name: Type.
  • Callers must supply the label at the call site as name: expr.
  • Each parameter independently chooses named or positional form; mixed signatures are legal.
  • Named arguments remain fixed-order and do not permit reordering.
  • Named parameters do not support default values.
// Mixed positional and named parameters
let create_rect(x Int, y Int, width: Int, height: Int) Rect = todo()
create_rect(10, 20, width: 100, height: 200)

// All named parameters
let connect(host: String, port: Int) Void = todo()
connect(host: "localhost", port: 8080)

Swapping named arguments or omitting labels is a compile error.

Named parameters also apply to structs and enums:

type Button(width: Int, height: Int, label: String)
let b = Button(width: 100, height: 50, label: "OK")

type Shape {
    Circle(radius: Float64),
    Line(start Point, end: Point),  // mixed
}
let s = Shape.Line(Point(0, 0), end: Point(1, 1))

In pattern matching, named parameter positions require name: pattern syntax, keeping construction and destructuring symmetric:

when s in {
    .Circle(radius: r) then println(r),
    .Line(s, end: e) then println(s.x),
}
if b is Button(width: w, height: _, label: l) then println(l)

Trait methods can use named parameters. Implementations must match the trait declaration exactly (same names, same positions):

trait Drawable {
    draw(self ref, at_x: Int, at_y: Int) Void
}
given MyType as Drawable {
    draw(self ref, at_x: Int, at_y: Int) Void = todo()  // must match
}

Function types (Func) do not carry named parameter labels, and lambda parameters always use positional syntax:

let f Func[String, Int, Void] = (host String, port Int) -> {
    connect(host: host, port: port)
}
f("localhost", 8080)

Foreign declarations (foreign let, foreign type) do not support named parameters.

Function Types

In Koral, functions are also a type. Function types are declared using Func[T1, T2, ..., R] syntax, where T1, T2, ... are parameter types and R is the return type.

let sqrt(x Int) Int = x * x          // Func[Int, Int]
let f Func[Int, Int] = sqrt
let a = f(2)                      // a == 4

We can also define function type parameters or return values:

let hello() Void = println("Hello, world!")
let run(f Func[Void]) Void = f()
let toRun() Func[Func[Void], Void] = run

let main() Void = toRun()(hello)

Lambda Expressions

Lambda expressions are very similar to function definitions, except that = is replaced by ->, and there is no function name or let keyword.

let f1(x Int) Int = x + 1            // Func[Int, Int]
let f2 = (x Int) Int -> x + 1        // Func[Int, Int]
let a = f1(1) + f2(1)                // a == 4

When the type of lambda can be inferred from context, parameter types and return type can be omitted:

let f Func[Int, Int] = (x) -> x + 1

Lambda supports multiple forms:

() -> 42                           // No parameters
(x) -> x * 2                      // Single param, type inferred
(x Int) -> x * 2                  // Single param with type
(x, y) -> x + y                   // Multiple params, types inferred
(x Int, y Int) Int -> x + y       // Full type annotations
(x) -> { let y = x * 2; return y + 1 }  // Block body

Closures

Lambda expressions can capture variables from their surrounding scope. This is called a closure.

let make_adder(base Int) Func[Int, Int] = {
    return (x) -> base + x
}

let add10 = make_adder(10)
let result = add10(32)  // result == 42

Capture Rules

Koral only allows capturing immutable variables. Attempting to capture a mutable variable will result in a compile error.

let x = 10
let f = () -> x + 1  // OK: x is immutable

let mut y = 20
let g = () -> y + 1  // Error: cannot capture mutable variable 'y'

Currying

Closures enable currying:

let add Func[Int, Func[Int, Int]] = (x) -> (y) -> x + y

let add10 = add(10)
let result = add10(32)  // result == 42
let sum = add(20)(22)   // sum == 42

Data Types

Data types are data collections composed of a series of data with the same type or different types. It is a composite data type.

Koral provides a powerful type system that allows you to define your own data structures. Use the type keyword to define.

Struct (Product Type)

Structs are used to combine multiple related values together. Each field has a name and a type.

Definition

type Empty()
type Point(x Int, y Int)

Construction

Use () syntax to call the constructor:

let a Point = Point(0, 0)

Using Member Variables

Use . syntax to access member variables:

type Point(x Int, y Int)

let main() Void = {
    let a = Point(64, 128)
    println(a.x)  // 64
    println(a.y)  // 128
}

Mutable Member Variables

Member variables are read-only by default. Use the mut keyword to mark mutable member variables:

type Point(mut x Int, mut y Int)

let main() Void = {
    let a = Point(64, 128)
    a.x = 2  // ok, because x is mut
    a.y = 0  // ok, because y is mut
}

The mutability of member variables follows the type definition, not the instance variable.

Enum (Sum Type)

Enums allow you to define a type that can be one of several different variants. Each variant can carry different types of data.

type Shape {
    Circle(radius Float64),
    Rectangle(width Float64, height Float64),
}

let s = Shape.Circle(1.0)

Using Enum Values

Extract data from enum variants through pattern matching:

let area = when s in {
    .Circle(r) then 3.14 * r * r,
    .Rectangle(w, h) then w * h,
}

Implicit Member Expressions

Implicit member expressions use .memberName(...) syntax.

Rules:

  • They are valid only when the expected type is known from context.
  • They may construct enum cases or call static methods.
  • They require parentheses; bare .Name is not a valid implicit member expression.
  • If the compiler cannot infer the expected type, the expression is rejected.
// Enum construction — omit the Option[Int] prefix
let a Option[Int] = .Some(42)
let b Option[Int] = .None()

// In function arguments
let process(opt Option[Int]) Void = when opt in {
    .Some(v) then println(v.to_string()),
    .None() then println("none"),
}
process(.Some(10))

// In assignments
let mut x Option[Int] = .None()
x = .Some(100)

// In conditional expression branches
let c Option[Int] = if true then .Some(1) else .None()

// Static method calls — omit the List[Int] prefix
let list List[Int] = .new()
let list2 List[Int] = .with_capacity(10)

Type Alias

Type aliases allow you to define a new name for an existing type, improving code readability. Use the type AliasName = TargetType syntax.

type Meters = Int
type Coord = Point
type IntList = List[Int]

Type aliases are fully eliminated at compile time — an alias is completely equivalent to its target type:

type Meters = Int

let distance Meters = 100
let add_meters(a Meters, b Meters) Meters = a + b
let result = add_meters(distance, 50)  // result == 150

Aliases can be chained:

type Meters = Int
type Distance = Meters  // Distance ultimately resolves to Int

Type aliases support access modifiers:

public type Meters = Int       // Public
private type InternalId = Int  // File-scoped only

Restrictions:

  • Type aliases do not support generic parameters (e.g., type Alias[T] = List[T] is invalid), but the target type can be a generic instantiation (e.g., type IntList = List[Int]).
  • Circular references are not allowed (e.g., type A = A).
  • Type alias names must start with an uppercase letter.

Trait and Given

Koral uses Traits to define shared behavior. This is similar to interfaces or type classes in other languages.

Defining Trait

A Trait defines a set of method signatures that any implementing type must provide.

trait Printable {
    to_string(self ref) String
}

Traits support inheritance using parent Trait names:

trait Ord Eq {
    compare(self, other Self) Int
}

Multiple parent Traits are connected with and:

trait MyTrait Eq and Hash {
    my_method(self) Int
}

Implementing Trait (Given)

Use a given Type as Trait { ... } impl block to implement a Trait for a specific type:

trait Eq {
    equals(self, other Self) Bool
}

trait Ord Eq {
    compare(self, other Self) Int
}

type Point(x Int, y Int)

given Point as Eq {
    equals(self, other Point) Bool = self.x == other.x and self.y == other.y
}

given Point as Ord {
    compare(self, other Point) Int = self.x - other.x
}

Notes:

  • given Type as Trait is the explicit conformance entry point.
  • Parent/child traits are implemented level-by-level: implementing Ord does not implicitly implement Eq.

Trait Tool Methods (given Trait)

Koral supports given Trait { ... } for trait tool methods.

Rules:

  • Methods declared inside trait are requirements (used for conformance checks and dynamic dispatch through trait objects).
  • Methods declared inside given Trait are tool methods (ergonomic helpers), and are not requirement witnesses.
  • Tool methods are not merged into a concrete type's inherent method set; they participate in call resolution based on context.

Example:

trait Eq {
    equals(self, other Self) Bool
}

given Eq {
    not_equals(self, other Self) Bool = not self.equals(other)
}

type Num(x Int)

given Num as Eq {
    equals(self, other Num) Bool = self.x == other.x
}

let a = Num(1)
let b = Num(2)
println(a.not_equals(b))

Constrained tool block example:

trait Iterator[T Any] {
    next(self mut ref) Option[T]
}

given[T Ord] Iterator[T] {
    max(self) Option[T] = ...
    min(self) Option[T] = ...
}

// For types implementing Iterator[Int], max/min are available

Dispatch rules:

  • Requirement methods: witness/vtable dispatch in generic and trait-object contexts.
  • Tool methods (given Trait): static dispatch (not virtual dispatch entry points).

Tool methods are available in:

  • Generic constraint contexts (e.g. [T Trait])
  • Trait object contexts
  • Concrete types that explicitly implement the trait

Qualified disambiguation calls

When multiple candidates conflict, use explicit qualified calls:

  • Instance method: object.(TraitName)method(...)
  • Static method: Type.(TraitName)method(value, ...)
  • Generic instance method: object.(TraitName)method[TypeArgs...](...)
  • Generic static method: Type.(TraitName)method[TypeArgs...](value, ...)

For generic methods, the trait qualifier must appear before method type arguments.

Override and conflict rules

  • Tool methods are non-override by default.
  • Inherent type methods win over trait tool methods.
  • If the same method signature appears from multiple trait tool sources, Koral does not choose implicitly.
  • You must disambiguate explicitly (or remove the conflict).
  • given Trait cannot define a method with the same name/signature as a requirement of that trait.

Module boundary rule

Boundary anchoring rules:

  • given Trait { ... } is allowed only within the trait's root module subtree.
  • given Type { ... } is allowed only within the type's root module subtree.
  • given Type as Trait { ... } follows orphan rules: either the type or the trait must be local to the current root module.
  • Cross-crate injection is not allowed.

Extension Methods

The given block can also be used to directly add methods to types:

given Point {
    public distance(self) Float64 = {
        let dx = self.x(Float64)
        let dy = self.y(Float64)
        return dx + dy // ...
    }
    
    // Methods without self are called via type name
    public origin() Point = Point(0, 0)
}

let p = Point.origin()

Standard Library Core Traits

The most commonly used core traits are:

  • Add[R] / Sub[R] / Neg / Mul[R] / Div[R] / Rem[R]: arithmetic operator traits.
  • Eq / Ord: equality and ordering.
  • Hash: hash support for dict/set keys.
  • ToString: conversion to string.
  • Iterator[T]: iteration protocol (next(self mut ref) Option[T]).
  • Error: error message interface (message(self ref) String).
  • Drop: destructor hook (drop(source mut ptr Self) Void).

Arithmetic and comparison operators are lowered to trait methods internally (for example + to Add). Subscripts are resolved by builtin compiler rules instead of public traits.

Drop.drop is a compiler-only destructor entry point. It receives the storage address of an already-owned value as source mut ptr Self; it is not called as an ordinary user method. Drop implementations are allowed to contain composite fields.

Trait Objects

Trait objects are Koral's mechanism for runtime polymorphism (dynamic dispatch). Using the ref TraitName or mut ref TraitName syntax, you can erase any type that implements a Trait into a uniform reference type.

Basic Syntax

Trait-object construction follows these rules:

  • The target type is written as ref TraitName or mut ref TraitName.
  • The source value must implement the trait and be converted in a context expecting that trait-object type.
  • box(...) is the standard way to provide an owned value for this conversion.
trait Drawable {
    draw(self ref) String
    reset(self mut ref) Void
}

type Circle(mut radius Int)
type Square(mut side Int)

given Circle as Drawable {
    public draw(self ref) String = "Drawing circle"
    public reset(self mut ref) Void = {
        self.radius = 0
    }
}
given Square as Drawable {
    public draw(self ref) String = "Drawing square"
    public reset(self mut ref) Void = {
        self.side = 0
    }
}

// Create trait objects
let shape ref Drawable = box(Circle(10))
let mutable_shape mut ref Drawable = box(Square(4))

// Call methods through the trait object (dynamic dispatch)
shape.draw()  // "Drawing circle"
mutable_shape.reset()
mutable_shape.draw()

Dispatch through trait objects respects reference mutability:

  • ref TraitName can call only requirements declared with self ref.
  • mut ref TraitName can call both self mut ref and self ref requirements.
  • ref TraitName cannot call self mut ref requirements.

Object Safety

Only Traits that satisfy the following conditions can be used as trait objects:

  • Methods must not have generic parameters
  • The receiver, if present, must be self ref or self mut ref
  • Self must not appear in method parameters or return types (except within that receiver)
// Object-safe — can be used as a trait object
trait Error {
    message(self ref) String
}

// Not object-safe — cannot be used as a trait object
trait Eq {
    equals(self, other Self) Bool  // Self appears in parameters
}

trait Resettable {
    reset(self) Void  // by-value self is not object-safe
}

Trait objects (ref TraitName, mut ref TraitName) do not support direct .val; use trait methods through dynamic dispatch.

Generics

Generics allow you to write code that applies to multiple types, improving code reusability.

Generic Data Types

Generic data types use TypeName[T Constraint] syntax to define generic parameters:

type Pair[T1 Any, T2 Any](left T1, right T2)

When constructing generic data types, pass actual types in the generic parameter position:

let a1 = Pair[Int, Int](1, 2)
let a2 = Pair[Bool, String](true, "hello")

When the context type is clear, generic type parameters can be omitted:

let a1 = Pair(1, 2)           // Inferred as Pair[Int, Int]
let a2 = Pair(true, "hello")  // Inferred as Pair[Bool, String]

Pair also supports a literal form:

let p1 = (1, 2)               // Equivalent to Pair(1, 2)
let p2 = (true, "hello")     // Equivalent to Pair(true, "hello")

Generic Functions

Generic functions write type parameters after the function name:

let identity[T Any](x T) T = x

println(identity(42))       // 42
println(identity("hello"))  // hello

Generic Constraints

Generic parameters can specify Trait constraints to limit acceptable types:

let max_val[T Ord](a T, b T) T = if a > b then a else b
let contains[T Eq](list List[T], value T) Bool = list.contains(value)

Multiple constraints are connected with and:

let describe[T ToString and Hash](value T) String = value.to_string()

Constraints can also use generic trait forms (for example Iterator[T]):

let consume[I Iterator[Int]](iter I) Void = {}

Generic Methods

given blocks can also define generic methods:

given[T Any] Option[T] {
    public map[U Any](self, f Func[T, U]) Option[U] = self and then f(it)
}

Standard Library Essentials

Use these as the minimal everyday building blocks:

// List
let nums List[Int] = [1, 2, 3]

// Dict
let scores Dict[String, Int] = ["alice": 10, "bob": 8]

// Set
let tags Set[String] = ["koral", "lang"]

// Option + or else / and then
let port = Option[Int].Some(8080) or else 80
let doubled = Option[Int].Some(21) and then it * 2

// or return
let read_number(path String) Result[Int] = {
    let text = read_text_file(path) or return
    parse_int(text)
}

// Result (error side is ref Error)
let ok = Result[Int].Ok(42)
let err = Result[Int].Error(box("failed"))

For complete API reference, see docs under docs/std/.

Module System

Koral provides a powerful module system for organizing code across multiple files and directories.

Module Concepts

A module in Koral is an explicit build unit declared in koral.json (or std/koral.json for the standard library). A module consists of its entry file plus any files merged into it via using "path".

  • Target module: The module selected by --target-module
  • Peer module: Another manifest-declared module in the same package
  • External module: A module coming from std or another package dependency
  • Top-level manifest entry: The default target module name, for example app::main

Entry filename constraints:

  • Module entry file basename must start with a lowercase letter.
  • Remaining characters may only be lowercase letters, digits, or _.
  • Source-level module names come from the manifest and use :: separators (for example, app::models, std::io).

Using Declarations

The using keyword is used for file merge and explicit symbol import. All using declarations must appear at the beginning of a file, before any other declarations.

File Merge

Use string syntax to merge another file into the current module scope:

using "utils"        // Merges utils.koral into current module
using "./helpers"    // Relative paths are allowed
using "../shared/format"

File merge rules:

  1. The path is resolved relative to the current file's directory.
  2. The string names a source file without the .koral suffix.
  3. Relative segments such as . and .. are allowed.
  4. File merge does not create a namespace, alias, or export surface.
  5. Merged files share the same module scope, so protected declarations remain visible across files in that module.

Module Symbol Import

Import visible symbols from another module with explicit braces:

using std::io { Reader }
using std::json { parse, Value }
using std::io { Reader as IoReader, Writer }
using std::io { .. }

Notes:

  1. using module { symbol-list } imports symbols visible to the importing file: public from any package, and protected public when the importer is in the same package.
  2. as applies per imported symbol, not to the module itself.
  3. using module { .. } imports all symbols visible to the importing file, and .. must appear alone.
  4. Imported names are file-local bindings and are not re-exported automatically.
  5. Module legality is checked against manifest requires; the compiler does not infer modules from directory structure.
  6. Non-std packages get std automatically; do not list std manually in application/test package manifests.
  7. A module import never binds the module name as a namespace object. Import Reader with using std::io { Reader }, then write Reader, not std.io.Reader or io.Reader.

Access Modifiers

Koral provides four access levels to control symbol visibility:

ModifierVisibility
publicAccessible from anywhere
protected publicAccessible from any module in the same package
protectedAccessible within the current logical module
privateAccessible only within the same file

protected public is a single composite access modifier. Only the exact order protected public is valid; public protected is invalid.

Package scope follows the manifest graph: the root package, std, and each dependency package are separate package boundaries.

Default Access Levels

DeclarationDefault
Global functions, variables, typesprotected
Struct fieldspublic
Enum constructor fieldspublic
Member functions (in given blocks)protected
Trait methodspublic

Direct struct construction Type(...) is only allowed when all referenced fields are visible at the call site. If a type has inaccessible private/protected/protected public fields, use an exposed public factory method.

Project Structure Example

my_project/
├── koral.json
├── main.koral           # app::main entry
├── utils.koral          # merged into app::main
├── models/
│   ├── models.koral     # app::models entry
│   └── user.koral       # merged into app::models
└── services/
    └── services.koral   # app::services entry
{
  "name": "MyProject",
  "version": "0.1.0",
  "entry": "app::main",
  "modules": {
    "app::main": {
      "entry": "main.koral",
      "requires": ["app::models", "app::services"],
      "links": []
    },
    "app::models": {
      "entry": "models/models.koral",
      "requires": [],
      "links": []
    },
    "app::services": {
      "entry": "services/services.koral",
      "requires": ["app::models"],
      "links": []
    }
  }
}
// main.koral
using "utils"
using app::models { User }
using app::services { authenticate }
using std { .. }

public let main() = {
    let user = User.new("Alice")
    if authenticate(user) then {
        println("Welcome!")
    }
}

FFI (Foreign Function Interface)

Koral supports interoperability with C through the foreign keyword.

Linking External Libraries

Native libraries are declared in package or module links inside koral.json / std/koral.json:

{
  "modules": {
    "app::main": {
      "entry": "main.koral",
      "requires": [],
      "links": ["m"]
    }
  }
}

The compiler adds linker flags from the resolved manifest graph. libc is implicitly linked by default and does not need to be declared.

Foreign Functions

Declare external C functions:

foreign let sin(x Float64) Float64
foreign let exit(code Int) Never
foreign let abort() Never

Foreign Types

Declare external C types:

// Opaque type (no fields)
foreign type CFile {}

// FFI struct with fields (aligned with C layout)
foreign type KoralTimespec(tv_sec Int64, tv_nsec Int64)

Intrinsic

The intrinsic keyword declares types and functions built into the compiler:

public intrinsic type Int
public intrinsic let is_unique_mutable[T Any](r ref T) Bool