Skip to content

Declarations

This page lists the declaration forms accepted at module scope. Local bindings and expression forms are covered in Expressions.

module app::users;

A module path uses :: between segments. The module declaration belongs at the beginning of the source file.

Selected names:

import std::core::{List, Option, print};

Aliased name:

import app::storage::{User as StoredUser};

Grouped imports allow a trailing comma.

There is no glob import. import app::prelude::*; is a syntax error: name what the file uses, as in import app::prelude::{A, B};, so every name a module uses can be found on one of its import lines. A name imported twice from two places is an error that names both imports; rename one with as.

The braces are not optional, even for one name. import std::core::print; is a parse error — expected ::{…} after the module path. Write import std::core::{print};.

There is no prelude of standard-library items: Option, Result, List, Dict, Show, Eq, print and everything else in std::core has to be named in an import wherever it is written. let x: Option<Int> = Option::None; needs Option; a module that imports nothing gets cannot find type `Option` in this scope.

Written is the whole of the rule. A trait’s methods resolve on any type that implements it whether or not the trait is imported, because the implementation belongs to the type and value.method(..) asks the type. 42.show() compiles with no import of Show, left == right with no import of Eq, and "${n}" needs neither. Show is imported when the word Show appears — a bound, a derive(Show) clause, a trait-qualified call — and not otherwise. See Traits.

What resolves without an import is the set of built-in type names, which belong to the language rather than to a module:

Bool Char Float Int String Unit Never Ptr
I8 I16 I32 I64 U8 U16 U32 U64

So let n: Int = 1; and let ok: Bool = true; compile in a module that imports nothing, while let x: Option<Int> = Option::None; does not. F32, F64, I128 and U128 are not among them and are not types Khora has.

pub makes a declaration visible outside its declaring module:

pub fn parse(input: String) -> Value {
// ...
}

Without pub, a top-level declaration is module-private. Current Khora uses pub; export is not the public-visibility keyword.

const NAME = "khora";
pub const DEFAULT_PORT: Int = 8080;

General form:

pub? const Pattern (: Type)? = Expr ;

A module-level binding is a const, not a let. const mut is invalid; Khora has no mutable global binding.

Reach for const for a genuine constant — a limit, a protocol value, a fixed default — and for let for a value computed while a function runs.

Named definition over an existing type, and record definition:

pub type UserId = pub Int;
pub type User = {
pub id: UserId,
pub name: String,
};

A field is private to the declaring module unless it is marked pub, and a wrapper’s value is private unless the declaration writes = pub. Field visibility has the whole rule.

Variant type:

pub type Result<A, E> =
| Ok(value: A)
| Err(error: E);

pub type UserId = pub Int; is a new type, not another spelling of Int. Khora has no transparent alias: an Int is not accepted where a UserId is wanted, and takes(5) on fn takes(u: UserId) reports expected UserId, found Int``.

Opaque declaration:

pub type Handle;

General form:

derive(...)? pub? type Name<TypeParams>? (= pub? TypeDefinition)? ;

The pub before type exports the declaration. The one after = opens a wrapper’s value to other modules, and is refused before a record or a variant: a record opens its fields one at a time, and a case’s payload is always public.

A derive clause appears immediately before the type it applies to:

derive(Eq, Ord, Show, Hash)
pub type Point = {
pub x: Int,
pub y: Int,
};

The syntax accepts a comma-separated list of trait names and an optional trailing comma. The compiler-supported derivable traits are Eq, Ord, Show, Hash, Decode and Encode.

The trait must be in scope, so derive(Show) needs Show imported from std::core. Derive where the implementation follows from the fields, and write an impl where the behavior is a domain decision rather than a structural consequence of the data.

derive(Decode) reads the /// above the type and above each record field into the schema’s descriptions, so a JSON Schema rendered from the type says what the comment says. The comment is the description; there is nothing to write a second time.

A field’s type decides whether the derive is available at all, and a missing impl is sometimes the point:

  • List<A> has Show and Eq when A does, so a record holding a list derives both, and Decode/Encode from std::schema on the same terms.
  • Redacted<A> has Show — it prints <redacted> — and Decode, and deliberately no Encode. A record holding a secret stays printable and readable, and refuses to serialize, so the build stops rather than the payload leaking. It has no Eq either: comparing two secrets byte by byte is how a timing side channel gets written by somebody who was not writing one.

Definition:

pub fn add(left: Int, right: Int) -> Int {
left + right
}

Generic and effectful definition:

pub fn load<A>(id: Id) -> A
with { store: Store }
raises StoreError
{
store.load(id)!
}

Signature without a body:

fn intrinsic(value: Int) -> Int;

General form:

pub? fn Name<TypeParams>? (Params) (-> Type)? (with Row)? (raises Type)? (Block | ;)

There is no = between a function signature and its block body, and a function definition has no semicolon after the block.

A signature has at most one with clause and one raises clause, and with comes first. A second clause of either kind, or raises before with, is a syntax error: put every capability in one row (with { a: A, b: B }) and every error in one union (raises A + B). A function type follows the same rule.

The clauses are read in order. pub fn load<A>(id: Id) -> A with { store: Store } raises StoreError says: given an Id it produces an A, it requires a capability named store implementing Store, and it may fail with StoreError. The two rows are independent dimensions of the type — see Capabilities and Failures.

A public signature states both. A private helper can usually let the compiler infer them, and inference is why a row variable appears in a signature nobody wrote one into.

Declare a C symbol with contextual extern before fn:

extern fn strlen(ptr: Ptr) -> U64;

A public external function can form part of a generated C-compatible library surface:

pub extern fn khora_add(left: Int, right: Int) -> Int;

See FFI for ABI rules and supported boundary types.

pub effect Store {
load: Id -> User raises StoreError,
save: User -> () raises StoreError,
}

General form:

pub? effect Name<TypeParams>? {
operation: FunctionType,
...
}

Effect members are named function types separated by commas.

pub context Production {
scope: Scope::root(),
db: postgres_db()!,
store: sql_store(),
}

General form:

pub? context Name {
label: Expr,
...
}

Bindings are evaluated in order; a later binding may use capabilities introduced above it.

pub trait Named {
fn name(self) -> String;
}

With supertraits and an associated type:

pub trait Stored: Eq + Show {
type Id;
fn id(self) -> Self::Id;
}

Trait functions may be signatures or have default bodies.

Trait implementation:

impl Named for User {
fn name(self) -> String {
self.name
}
}

Inherent implementation:

impl User {
pub fn display_name(self) -> String {
self.name
}
}

Generic implementations may put type parameters after impl:

impl<A: Show> Show for Box<A> {
fn show(self) -> String {
Show::show(self.value)
}
}

Inside a trait:

type Item;
type Item: Show;

Inside an implementation:

type Item = User;
test "addition works" {
assert(add(20, 22) == 42);
}

General form:

test "name" Block

assert is an ordinary std::core function and has to be imported — import std::core::{assert}; — despite reading like a keyword. It is also only legal inside a test block; elsewhere the compiler refuses it:

error: `assert` is only allowed inside a `test` block; elsewhere, `raise` says
the same thing and says where it goes

That check runs at khora check, so the editor underlines it as you type. khora build keeps the same refusal as a backstop, and the two agree.

bench "parse payload" {
parse_payload(fixture)!;
}

General form:

bench "name" Block

test, bench, context, extern, and derive are contextual keywords in their declaration positions rather than globally reserved identifiers.

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