Declarations
This page lists the declaration forms accepted at module scope. Local bindings and expression forms are covered in Expressions.
Module declaration
Section titled “Module declaration”module app::users;A module path uses :: between segments. The module declaration belongs at the beginning of the source file.
Imports
Section titled “Imports”Selected names:
import std::core::{List, Option, print};Aliased name:
import app::storage::{User as StoredUser};Glob import:
import app::prelude::*;Grouped imports allow a trailing comma.
The braces are not optional, even for one name. import std::core::print;
is a parse error — expected ::{…}or::* after the module path. Write
import std::core::{print};.
Names that need no import
Section titled “Names that need no import”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 PtrI8 I16 I32 I64 U8 U16 U32 U64So 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.
Visibility
Section titled “Visibility”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.
Constants
Section titled “Constants”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.
Type declarations
Section titled “Type declarations”Named definition over an existing type, and record definition:
pub type UserId = Int;
pub type User = { id: UserId, name: String,};Variant type:
pub type Result<A, E> = | Ok(value: A) | Err(error: E);pub type UserId = 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>? (= TypeDefinition)? ;derive(...)
Section titled “derive(...)”A derive clause appears immediately before the type it applies to:
derive(Eq, Ord, Show, Hash)pub type Point = { x: Int, 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 behaviour 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>hasShowandEqwhenAdoes, so a record holding a list derives both, andDecode/Encodefromstd::schemaon the same terms.Redacted<A>hasShow— it prints<redacted>— andDecode, and deliberately noEncode. A record holding a secret stays printable and readable, and refuses to serialise, so the build stops rather than the payload leaking. It has noEqeither: comparing two secrets byte by byte is how a timing side channel gets written by somebody who was not writing one.
Function declarations
Section titled “Function declarations”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)? EffectClause* (Block | ;)There is no = between a function signature and its block body, and a function definition has no semicolon after the block.
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.
External C functions
Section titled “External C functions”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.
Effect declarations
Section titled “Effect declarations”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.
Context declarations
Section titled “Context declarations”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.
Trait declarations
Section titled “Trait declarations”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.
Implementations
Section titled “Implementations”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 { self.value.show() }}Associated type definitions
Section titled “Associated type definitions”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" Blockassert 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 goesThat 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.
Benchmarks
Section titled “Benchmarks”bench "parse payload" { parse_payload(fixture)!;}General form:
bench "name" Blocktest, bench, context, extern, and derive are contextual keywords in their declaration positions rather than globally reserved identifiers.
Khora 0.3.0built frombd964738afb6on 2026-10-17 20:41ZVersioningSecurityContributingReleasesSource