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.
Package manifest
Section titled “Package manifest”A minimal package:
[package]name = "orders"version = "0.1.0"
[toolchain]version = "0.2.0"Source lives under src/. The manifest documents
every table; your first Khora project
builds one end to end.
Paths versus fields
Section titled “Paths versus fields”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 behaviour 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") // refusedapp::helper::greeting() // refusedA 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.
A glob import does not change that either: import app::helper::*; brings the
names in unqualified, and helper::greeting() still does not resolve. 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.
A field needs its type imported
Section titled “A field needs its type imported”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.
A package’s other programs
Section titled “A package’s other programs”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.exekhora 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 scopeOnly 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:
khora run src/bin/backfill.khA 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.
Dependencies
Section titled “Dependencies”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 branch, tag or commit.postgres = { git = "https://github.com/codyspate/khoralang", rev = "main", subdir = "packages/postgres" }
# From a directory on this machine, for a package you are also editing.shared = { path = "../shared" }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:
khora install https://github.com/codyspate/khoralang --subdir packages/postgresThat 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.
The lockfile
Section titled “The lockfile”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 — so rev = "main" is a convenience at the moment the dependency is
added, not a moving target afterwards.
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.
Publishing a package
Section titled “Publishing a package”A package is consumable when its manifest says so:
[package]name = "postgres"version = "0.1.0"publish = trueAbsent 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.
Writing a library
Section titled “Writing a library”khora new semver --libwrites src/lib.kh instead of src/main.kh, sets publish = true, and declares a module:
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};The scaffold in 0.2 writes module semver::lib, which makes consumers
write import semver::lib::{..} — naming a file layout that is the library’s
business rather than theirs. Editing src/lib.kh to say module semver is
accepted and gives the shorter import; later toolchains scaffold that form.
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" }Toolchain pinning
Section titled “Toolchain pinning”Every project says which compiler builds it, and the field is required:
[toolchain]version = "0.2.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.
Package boundaries
Section titled “Package boundaries”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.
Native binary boundaries
Section titled “Native binary boundaries”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.2.0built frombd964738afb6on 2026-10-17 20:41ZVersioningSecurityContributingReleasesSource