Skip to content

Grammar and precedence

This page summarizes the concrete grammar rules that most often matter when reading or writing Khora. Individual reference pages show the complete examples for each construct.

A source file is a sequence of declarations. Common forms are:

module Path ;
import Path::{...} ;
pub? const Pattern (: Type)? = Expr ;
derive(...)? pub? type Name<TypeParams>? (= pub? TypeDef)? ;
pub? fn Name<TypeParams>? (Params) (-> Type)? (with Row)? (raises Type)? (Block | ;)
pub? effect Name<TypeParams>? { Field, ... }
pub? context Name { name: Expr, ... }
pub? trait Name<TypeParams>? (: Bounds)? { TraitItem* }
impl<TypeParams>? Type (for Type)? { TraitItem* }
test "name" Block
bench "name" Block
extern fn name(Params) (-> Type)? (with Row)? ;

Current public visibility is spelled pub. A record field takes it too, before mut: { pub name: String, pub mut count: Int }. A field without it is private to the declaring module; see Field visibility.

Compile-time path:

app::model::User
Result::Ok

Runtime projection:

user.name
response.status

:: and . are intentionally separate tokens with separate roles.

Named/generic type:

User
List<User>

Tuple and unit:

()
(Int, String)

Record:

{ id: Int, name: String }

Record update, in expression position — the base first, then the fields that replace its own:

{ ..base }
{ ..base, id: 7 }
{ ..base, id: 7, name: "Grace" }

Open row:

{ db: Db | 'ef }

Function type:

Request -> Response
with { db: Db }
raises DbError + HttpError

Explicit polymorphism:

forall<A>. A -> A

Variant type:

| None
| Some(value: A)

Ordinary parameter:

<A>

Bound:

<A: Eq + Show>

Const parameter:

<const N: Int>

Row parameter:

<'ef>

Variance:

<+A>
<-A>

These forms may be mixed in one parameter list.

42
"text"
path::name
{}
(a, b)
[a, b]
{ let x = 1; x + 1 }
fn x => x + 1
if condition { a } else { b }
match value { Pattern => Expr, }
while condition { body; }
for pattern in iterable { body; }
loop { body; }
raise error
handler for Effect { operation: fn () => value }
with Context { body; }
break
continue
return value

Starting from an expression, postfix forms bind tightly and may chain:

f(a, b)
value.field
fallible()!
fallible()! catch { Error::Case => recover(), }
operation() with { service: handler }
operation() with Production

Calls, field projection, postfix failure propagation, catch, and postfix with bind more tightly than binary operators.

Single parameter:

fn value => expression

Ignored parameter:

fn _ => expression

Parameter list:

fn (left: Int, right: Int) => left + right

Block body:

fn value => {
let next = value + 1;
next * 2
}

First argument:

value |> f(a)

Explicit insertion point:

value |> f(a, _, b)

A stage may contain one _ placeholder.

Flow lambda:

||> normalize
|> validate!
|> persist!

||> starts a unary lambda; following |> stages belong to that flow expression until grouping ends.

From loosest to tightest:

  1. = — assignment, right-associative
  2. |> — pipeline, left-associative
  3. || — boolean OR
  4. && — boolean AND
  5. == != < > <= >= — comparisons
  6. + -
  7. * / %
  8. prefix - !
  9. postfix call, ., failure !, catch, and with

For example:

x = value + 1 |> double

parses as assigning the result of the full pipeline to x.

These braces are a record because they begin with fields:

{ id: 1, name: "Ada" }

These braces are a block:

{
let id = 1;
id + 1
}

An empty {} is an empty record literal. In control-flow forms such as match, if, while, for, and loop, the grammar already knows when braces introduce the body or arm list.

Pattern (if Expr)? => Expr ,?

Examples:

Result::Ok(value) => value,
value if value > 0 => value,
_ => 0,
_
Literal
identifier
Path
Path(Pattern, ...)
Path { field, field: Pattern, ... }
(Pattern, ...)

See Patterns for binding and exhaustiveness rules.

1_000
0.25
6.02e23
19.99d

The d suffix creates an exact Decimal; a fractional literal without it is Float.

A semicolon terminates declarations such as const, type, imports, and signature-only functions, and turns an expression inside a block into a statement:

let value = compute();
log(value);
value

The last value is the block result because it has no semicolon. A function definition ends with its closing } rather than };.

Khora 0.4.0built frombd964738afb6on 2026-10-17 20:41ZVersioningSecurityContributingReleasesSource