Skip to content
You are reading next, which describes the unreleased compiler. Khora is unstable before v1: syntax, standard-library APIs and behavior may change. The current release is documented under v0.4.

Modules and packages

A Khora package groups source, dependencies and compiler configuration behind a khora.toml manifest. Modules give stable paths and visibility to the declarations inside it.

The syntax of module, import, pub and const is in Declarations. This page is what surrounds it: the manifest, the programs a package builds, dependency resolution, and where the boundary of a package falls.

There is no prelude. Every std::core name a file writes — List, Dict, Option, Result, print, Show — needs an import, including the ones that look built in; only Int, Float, Bool, Char, String and the fixed-width numerics are the language’s own. Names that need no import states the rule. Worth knowing before the first build, because the error arrives as a surprise about [a, b, c] rather than about imports.

A minimal package:

[package]
name = "orders"
version = "0.1.0"
[toolchain]
version = "0.4.0"

Source lives under src/. The manifest documents every table; your first Khora project builds one end to end.

Khora spells compile-time names and runtime values differently, and the difference is load-bearing when reading unfamiliar code:

import http::{Response};
let response = Response::text(200, "ok");
print(Int::to_string(response.status));
  • :: walks types, traits, constructors and associated items;
  • . projects a field, or calls behavior on a runtime value.

So Response::text is visibly not response.status.

:: does not walk modules at a use site. A module path is only ever written in an import; it cannot prefix an expression. Both of these are refused with cannot resolve ... in this scope:

http::Response::text(200, "ok") // refused
app::helper::greeting() // refused

A trait name is not a module name, so this rule says nothing about Functor::map(xs, f). That is an imported trait followed by one of its methods — the first bullet above, the same shape as Response::text — and it is how a call is disambiguated when a type implements two traits that declare the same method. See Traits.

An import brings names in unqualified, and helper::greeting() does not resolve however helper’s names were imported. Every name a module uses is imported unqualified, so two modules that export the same name are separated with as — import app::money::{add as money_add}; — rather than by qualifying the call. Importing both under one name is an error that names the two imports.

Importing a function that returns a record is not enough to read that record’s fields. The type has to be in scope as well:

// Not enough: `origin()` is callable, but `.x` is not readable.
import shapes::{origin};
// Both.
import shapes::{Point, origin};

A field is looked up on a type, and a type the file cannot name is one it knows nothing about, its fields included. The compiler says so directly — Point is not in scope here, so nothing is known about its fields — but importing the pair from the start is easier.

The same applies to a trait’s methods and to an effect’s operations. A capability can arrive in a function without its type being named anywhere, and calling an operation on it needs the effect imported.

src/main.kh is the package’s program. A package that needs more than one — a migration, a backfill, a one-off report — puts each in its own file under src/bin/:

myapp/
khora.toml
src/
main.kh -> build/myapp.exe
shared.kh the modules both use
bin/
backfill.kh -> build/backfill.exe
report.kh -> build/report.exe

khora build . builds all of them. Each is its own compilation: it gets the package’s modules and not the other programs, which is what stops two main functions from meeting. A program is named after its file, so src/bin/backfill.kh is build/backfill.exe.

One file per program. A program needing several modules of its own is a package, and that is the shape to reach for rather than a directory inside src/bin.

src/main.kh is one of “the other programs”. A shared module under src/ may not import from it once the package has anything in src/bin/, because each src/bin program is compiled without src/main.kh and the import has nothing to resolve against:

error: cannot find module `myapp.main`
error: cannot find `answer` in this scope

Only khora build reports this — check, test and run all pass, because they compile the package’s own program where src/main.kh is present. Keep anything shared in its own module (src/shared.kh) and leave src/main.kh holding only main.

khora run . runs the package’s own program. To run one of the others, name it:

Terminal window
khora run src/bin/backfill.kh

A package with no src/main.kh has no default program, and khora run . says which ones it does have rather than reporting that it has none.

Package dependencies are source dependencies. Khora fetches and compiles the package’s source; there are no binary artifacts to publish or to trust.

[dependencies]
# From a git repository, at a version-matched, immutable tag.
postgres = { git = "https://github.com/codyspate/khoralang", rev = "v0.4.0", subdir = "packages/postgres" }
# From a directory on this machine, for a package you are also editing.
shared = { path = "../shared" }

The example above is for the published 0.4.0 toolchain. For its published candidate, pin [toolchain] to 0.4.0-rc.2 and the dependency’s rev to v0.4.0-rc.2; neither tag can be fetched before that release is published. subdir is for a repository holding more than one package — a git URL names a repository, and the two are the same thing only in the simplest layout.

There is no registry yet, so version = "..." has nothing to resolve against. Use git for anything you did not write and path for anything you did.

khora build resolves what it needs, so there is no fetch step to remember. To add a dependency without editing the manifest by hand:

Terminal window
khora install https://github.com/codyspate/khoralang --rev v0.4.0 --subdir packages/postgres

For the published candidate, use --rev v0.4.0-rc.2 with its [toolchain] version = "0.4.0-rc.2" pin. Omitting --rev chooses moving main, which can resolve to a package newer than your compiler even though the resulting lockfile freezes that commit.

That finds out the package’s real name and whether it offers itself at all before writing the entry — two things a line typed into [dependencies] cannot check. --rev takes a branch, tag or commit and defaults to main. With no URL, khora install fetches and locks whatever the manifest already declares, which is the command to run after cloning a project.

khora why <package> explains what pulled something in, and khora graph draws the whole thing.

Resolution writes khora.lock, and a locked build never asks the network again:

[[package]]
name = "postgres"
source = "git"
url = "https://github.com/codyspate/khoralang"
revision = "0fcf6d65a2cf8c1b1636da586ed47839152c315c"
path = "packages/postgres"
checksum = "001bf5bf28448ba94bd6c08d2a8a3c55535692be5c61113fbfd94df74ff1ff55"

A branch name resolves to the commit it pointed at, and the commit is what is recorded. That makes a lockfile reproducible after resolution, but rev = "main" may already select an incompatible package when first added. Use a release tag matching the project’s toolchain pin for packages in Khora’s repository.

The checksum is verified, not merely recorded. Every resolution hashes what arrived and compares it against the lockfile. If the same commit id ever produces different bytes, the build stops and says so rather than compiling what turned up.

Commit the lockfile for applications and anywhere else reproducible builds matter, and review a lockfile change like any other dependency change.

A package is consumable when its manifest says so:

[package]
name = "postgres"
version = "0.1.0"
publish = true

Absent means no. Publishing here is passive — a pushed repository is already fetchable — so the marker records an intention rather than granting a permission. What it prevents is depending on somebody’s application, or their unfinished experiment, because it happened to sit in a repository you fetched.

publish = true is checked for a git dependency and not for a path one. A path dependency is a directory you already chose to point at, usually inside your own workspace, and refusing it would only mean editing a manifest you control to say what you have already said by writing the path.

khora new semver --lib

writes src/lib.kh instead of src/main.kh, sets publish = true, and declares the module as the package’s own name:

module semver;
/// A version, as its three numbers.
pub type Version = { major: Int, minor: Int, patch: Int };
/// The version `major.minor.patch`.
pub fn of(major: Int, minor: Int, patch: Int) -> Version {
{ major: major, minor: minor, patch: patch }
}

That module path is what a consumer types. module semver gives them:

import semver::{Version, parse};

A library whose lib.kh says module semver::lib is accepted and works, but everybody depending on it has to write import semver::lib::{..} — naming a file layout that is the library’s business rather than theirs. The scaffold writes the bare form for that reason.

khora build has nothing to build for a library: a library is compiled as part of whatever depends on it, so there is no executable to produce and the command says so. khora check and khora test are the two that apply.

Consuming it from a sibling directory needs a path and nothing else:

[dependencies]
semver = { path = "../semver" }

Every project says which compiler builds it, and the field is required:

[toolchain]
version = "0.4.0"

The pin takes precedence over the machine default, and a pinned version that is not installed stops the command rather than building with a different compiler. In a workspace it belongs at the root, where members inherit it.

latest and latest.rc are also accepted, and mean the newest toolchain installed on this machine — not reproducible, and useful while testing. The manifest has the detail; Installation covers toolchain management.

Expose the domain types and operations callers need, and keep helpers private. Capabilities are useful at a package boundary because a signature can state the external authority the package needs without prescribing how an application supplies it.

Adding a required capability or a recoverable failure to a public function changes what callers must provide or handle, so both belong to the package’s public compatibility surface. See Compatibility and stability.

Khora packages distribute source and are compiled as part of the consuming program. Whole-program monomorphization and native optimization mean Khora defines no stable Khora-to-Khora binary ABI for separately compiled package binaries.

Where a stable C-compatible boundary is required, use extern fn and the library build flow in FFI.

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